Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resilient useABI #116

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ NEXT_PUBLIC_WEB3_URL_PREFIX=https://eth-sepolia.g.alchemy.com/v2/

NEXT_PUBLIC_ALCHEMY_API_KEY="ALCHEMY KEY"
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="YOUR WALLET CONNECT PROJECT ID"
NEXT_PUBLIC_IPFS_ENDPOINT="https://..."
NEXT_PUBLIC_IPFS_API_KEY="..."
NEXT_PUBLIC_IPFS_ENDPOINTS="https://domain1/api,https://domain2/api/v0"
NEXT_PUBLIC_PINATA_JWT="..."
NEXT_PUBLIC_ETHERSCAN_API_KEY="OPTIONAL: ETHERSCAN API"

# PRIVATE (scripts)
Expand All @@ -24,5 +24,5 @@ DEPLOYMENT_WALLET_PRIVATE_KEY="0x..."
DEPLOYMENT_ALCHEMY_API_KEY="..."
DEPLOYMENT_WEB3_ENDPOINT="https://..."

DEPLOYMENT_IPFS_ENDPOINT="https://..."
DEPLOYMENT_IPFS_API_KEY="..."
DEPLOYMENT_APP_NAME="Taiko"
DEPLOYMENT_PINATA_JWT="..."
4 changes: 2 additions & 2 deletions .github/workflows/app-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
NEXT_PUBLIC_WEB3_URL_PREFIX: https://rpc/
NEXT_PUBLIC_ALCHEMY_API_KEY: x
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: x
NEXT_PUBLIC_IPFS_ENDPOINT: https://ipfs/
NEXT_PUBLIC_IPFS_API_KEY: x
NEXT_PUBLIC_IPFS_ENDPOINTS: https://domain1/api,https://domain2/api/v0
NEXT_PUBLIC_PINATA_JWT: x
NEXT_PUBLIC_ETHERSCAN_API_KEY: x
NODE_ENV: production
Binary file modified bun.lockb
Binary file not shown.
4 changes: 2 additions & 2 deletions constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export const PUB_ETHERSCAN_API_KEY = process.env.NEXT_PUBLIC_ETHERSCAN_API_KEY ?

export const PUB_WALLET_CONNECT_PROJECT_ID = process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID ?? "";

export const PUB_IPFS_ENDPOINT = process.env.NEXT_PUBLIC_IPFS_ENDPOINT ?? "";
export const PUB_IPFS_API_KEY = process.env.NEXT_PUBLIC_IPFS_API_KEY ?? "";
export const PUB_IPFS_ENDPOINTS = process.env.NEXT_PUBLIC_IPFS_ENDPOINTS ?? "";
export const PUB_PINATA_JWT = process.env.NEXT_PUBLIC_PINATA_JWT ?? "";

// General
export const PUB_APP_NAME = "Aragonette";
Expand Down
42 changes: 29 additions & 13 deletions hooks/useAbi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ export const useAbi = (contractAddress: Address) => {
error,
} = useQuery<AbiFunction[], Error>({
queryKey: ["abi", resolvedAddress || "", !!publicClient],
queryFn: () => {
queryFn: async () => {
if (!resolvedAddress || !isAddress(resolvedAddress) || !publicClient) {
return Promise.resolve([]);
return [];
} else if (!(await isContract(resolvedAddress, publicClient))) {
return [];
}

const abiLoader = getEtherscanAbiLoader();
return whatsabi
.autoload(resolvedAddress, {
provider: publicClient,
abiLoader,
abiLoader: getEtherscanAbiLoader(),
followProxies: false,
enableExperimentalMetadata: true,
})
Expand All @@ -68,15 +69,7 @@ export const useAbi = (contractAddress: Address) => {
type: item.type,
});
}
functionItems.sort((a, b) => {
const a_RO = ["pure", "view"].includes(a.stateMutability);
const b_RO = ["pure", "view"].includes(b.stateMutability);

if (a_RO === b_RO) return 0;
else if (a_RO) return 1;
else if (b_RO) return -1;
return 0;
});
functionItems.sort(abiSortCallback);
return functionItems;
})
.catch((err) => {
Expand Down Expand Up @@ -125,6 +118,11 @@ function getEtherscanAbiLoader() {
apiKey: PUB_ETHERSCAN_API_KEY,
baseURL: "https://api-sepolia.etherscan.io/api",
});
case "holesky":
return new whatsabi.loaders.EtherscanABILoader({
apiKey: PUB_ETHERSCAN_API_KEY,
baseURL: "https://api-holesky.etherscan.io/api",
});
case "mumbai":
return new whatsabi.loaders.EtherscanABILoader({
apiKey: PUB_ETHERSCAN_API_KEY,
Expand All @@ -134,3 +132,21 @@ function getEtherscanAbiLoader() {
throw new Error("Unknown chain");
}
}

function isContract(address: Address, publicClient: ReturnType<typeof usePublicClient>) {
if (!publicClient) return Promise.reject(new Error("Invalid client"));

return publicClient.getBytecode({ address }).then((bytecode) => {
return bytecode !== undefined && bytecode !== "0x";
});
}

function abiSortCallback(a: AbiFunction, b: AbiFunction) {
const a_RO = ["pure", "view"].includes(a.stateMutability);
const b_RO = ["pure", "view"].includes(b.stateMutability);

if (a_RO === b_RO) return 0;
else if (a_RO) return 1;
else if (b_RO) return -1;
return 0;
}
4 changes: 2 additions & 2 deletions hooks/useMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fetchJsonFromIpfs } from "@/utils/ipfs";
import { fetchIpfsAsJson } from "@/utils/ipfs";
import { JsonValue } from "@/utils/types";
import { useQuery } from "@tanstack/react-query";

Expand All @@ -8,7 +8,7 @@ export function useMetadata<T = JsonValue>(ipfsUri?: string) {
queryFn: () => {
if (!ipfsUri) return Promise.resolve("");

return fetchJsonFromIpfs(ipfsUri);
return fetchIpfsAsJson(ipfsUri);
},
retry: true,
refetchOnMount: false,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"classnames": "^2.5.1",
"dayjs": "^1.11.10",
"dompurify": "^3.0.11",
"ipfs-http-client": "^60.0.1",
"multiformats": "^13.1.3",
"next": "14.1.4",
"react": "^18.2.0",
"react-blockies": "^1.4.1",
Expand Down
19 changes: 5 additions & 14 deletions plugins/dualGovernance/pages/new.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { create } from "ipfs-http-client";
import { Button, IconType, Icon, InputText, TextAreaRichText } from "@aragon/ods";
import React, { useEffect, useState } from "react";
import { uploadToIPFS } from "@/utils/ipfs";
import { uploadToPinata } from "@/utils/ipfs";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { toHex } from "viem";
import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol";
Expand All @@ -13,7 +12,7 @@ import { getPlainText } from "@/utils/html";
import { useRouter } from "next/router";
import { Else, ElseIf, If, Then } from "@/components/if";
import { PleaseWaitSpinner } from "@/components/please-wait";
import { PUB_IPFS_ENDPOINT, PUB_IPFS_API_KEY, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, PUB_CHAIN } from "@/constants";
import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants";
import { ActionCard } from "@/components/actions/action";

enum ActionType {
Expand All @@ -22,11 +21,6 @@ enum ActionType {
Custom,
}

const ipfsClient = create({
url: PUB_IPFS_ENDPOINT,
headers: { "X-API-KEY": PUB_IPFS_API_KEY, Accept: "application/json" },
});

export default function Create() {
const { push } = useRouter();
const [title, setTitle] = useState<string>("");
Expand Down Expand Up @@ -85,7 +79,7 @@ export default function Create() {
});

const plainSummary = getPlainText(summary).trim();
if (!plainSummary.trim())
if (!plainSummary)
return addAlert("Invalid proposal details", {
description: "Please, enter a summary of what the proposal is about",
type: "error",
Expand Down Expand Up @@ -113,17 +107,14 @@ export default function Create() {
}

const proposalMetadataJsonObject = { title, summary };
const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], {
type: "application/json",
});
const ipfsUri = await uploadToPinata(JSON.stringify(proposalMetadataJsonObject));

const ipfsPin = await uploadToIPFS(ipfsClient, blob);
createProposalWrite({
chainId: PUB_CHAIN.id,
abi: OptimisticTokenVotingPluginAbi,
address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS,
functionName: "createProposal",
args: [toHex(ipfsPin), actions, BigInt(0), BigInt(0), BigInt(0)],
args: [toHex(ipfsUri), actions, BigInt(0), BigInt(0), BigInt(0)],
});
};

Expand Down
19 changes: 5 additions & 14 deletions plugins/lockToVote/pages/new.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { create } from "ipfs-http-client";
import { Button, IconType, Icon, InputText, TextAreaRichText } from "@aragon/ods";
import React, { useEffect, useState } from "react";
import { uploadToIPFS } from "@/utils/ipfs";
import { uploadToPinata } from "@/utils/ipfs";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { toHex } from "viem";
import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol";
Expand All @@ -13,7 +12,7 @@ import { getPlainText } from "@/utils/html";
import { useRouter } from "next/router";
import { Else, ElseIf, If, Then } from "@/components/if";
import { PleaseWaitSpinner } from "@/components/please-wait";
import { PUB_IPFS_ENDPOINT, PUB_IPFS_API_KEY, PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants";
import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants";
import { ActionCard } from "@/components/actions/action";

enum ActionType {
Expand All @@ -22,11 +21,6 @@ enum ActionType {
Custom,
}

const ipfsClient = create({
url: PUB_IPFS_ENDPOINT,
headers: { "X-API-KEY": PUB_IPFS_API_KEY, Accept: "application/json" },
});

export default function Create() {
const { push } = useRouter();
const [title, setTitle] = useState<string>("");
Expand Down Expand Up @@ -85,7 +79,7 @@ export default function Create() {
});

const plainSummary = getPlainText(summary).trim();
if (!plainSummary.trim())
if (!plainSummary)
return addAlert("Invalid proposal details", {
description: "Please, enter a summary of what the proposal is about",
type: "error",
Expand Down Expand Up @@ -113,17 +107,14 @@ export default function Create() {
}

const proposalMetadataJsonObject = { title, summary };
const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], {
type: "application/json",
});
const ipfsUri = await uploadToPinata(JSON.stringify(proposalMetadataJsonObject));

const ipfsPin = await uploadToIPFS(ipfsClient, blob);
createProposalWrite({
chainId: PUB_CHAIN.id,
abi: OptimisticTokenVotingPluginAbi,
address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
functionName: "createProposal",
args: [toHex(ipfsPin), actions, BigInt(0), BigInt(0), BigInt(0)],
args: [toHex(ipfsUri), actions, BigInt(0), BigInt(0), BigInt(0)],
});
};

Expand Down
19 changes: 5 additions & 14 deletions plugins/tokenVoting/pages/new.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { create } from "ipfs-http-client";
import { Button, IconType, Icon, InputText, TextAreaRichText } from "@aragon/ods";
import React, { useEffect, useState } from "react";
import { uploadToIPFS } from "@/utils/ipfs";
import { uploadToPinata } from "@/utils/ipfs";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { toHex } from "viem";
import { TokenVotingAbi } from "@/plugins/tokenVoting/artifacts/TokenVoting.sol";
Expand All @@ -13,7 +12,7 @@ import { getPlainText } from "@/utils/html";
import { useRouter } from "next/router";
import { Else, ElseIf, If, Then } from "@/components/if";
import { PleaseWaitSpinner } from "@/components/please-wait";
import { PUB_CHAIN, PUB_IPFS_API_KEY, PUB_IPFS_ENDPOINT, PUB_TOKEN_VOTING_PLUGIN_ADDRESS } from "@/constants";
import { PUB_CHAIN, PUB_TOKEN_VOTING_PLUGIN_ADDRESS } from "@/constants";
import { ActionCard } from "@/components/actions/action";

enum ActionType {
Expand All @@ -22,11 +21,6 @@ enum ActionType {
Custom,
}

const ipfsClient = create({
url: PUB_IPFS_ENDPOINT,
headers: { "X-API-KEY": PUB_IPFS_API_KEY, Accept: "application/json" },
});

export default function Create() {
const { push } = useRouter();
const [title, setTitle] = useState<string>("");
Expand Down Expand Up @@ -86,7 +80,7 @@ export default function Create() {
});

const plainSummary = getPlainText(summary).trim();
if (!plainSummary.trim())
if (!plainSummary)
return addAlert("Invalid proposal details", {
description: "Please, enter a summary of what the proposal is about",
type: "error",
Expand Down Expand Up @@ -114,17 +108,14 @@ export default function Create() {
}

const proposalMetadataJsonObject = { title, summary };
const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], {
type: "application/json",
});
const ipfsUri = await uploadToPinata(JSON.stringify(proposalMetadataJsonObject));

const ipfsPin = await uploadToIPFS(ipfsClient, blob);
createProposalWrite({
chainId: PUB_CHAIN.id,
abi: TokenVotingAbi,
address: PUB_TOKEN_VOTING_PLUGIN_ADDRESS,
functionName: "createProposal",
args: [toHex(ipfsPin), actions, BigInt(0), BigInt(0), BigInt(0), 0, false],
args: [toHex(ipfsUri), actions, BigInt(0), BigInt(0), BigInt(0), 0, false],
});
};

Expand Down
4 changes: 2 additions & 2 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ NEXT_PUBLIC_WEB3_URL_PREFIX=https://eth-${DEPLOYMENT_TARGET_CHAIN_ID}.g.alchemy.

NEXT_PUBLIC_ALCHEMY_API_KEY=<YOUR KEY GOES HERE>
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=<YOUR KEY GOES HERE>
NEXT_PUBLIC_IPFS_ENDPOINT=<YOUR IPFS API URL GOES HERE>
NEXT_PUBLIC_IPFS_API_KEY=<YOUR IPFS API KEY GOES HERE>
NEXT_PUBLIC_IPFS_ENDPOINTS=<YOUR IPFS API URLs GO HERE>
NEXT_PUBLIC_PINATA_JWT=<YOUR IPFS API KEY GOES HERE>
NEXT_PUBLIC_ETHERSCAN_API_KEY=<YOUR API KEY GOES HERE>
`;
console.log(summary);
Expand Down
8 changes: 2 additions & 6 deletions scripts/deploy/5-dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import {
import { type Address, type Hex, type Log, decodeEventLog, toHex } from "viem";
import { deploymentPublicClient as publicClient, deploymentWalletClient as walletClient } from "../lib/util/client";
import { deploymentAccount as account } from "../lib/util/account";
import { uploadToIPFS } from "@/utils/ipfs";
import { deploymentIpfsClient as ipfsClient } from "../lib/util/ipfs";
import { uploadToPinata } from "@/utils/ipfs";
import { ABI as DaoFactoryABI } from "../lib/artifacts/dao-factory";
import { ABI as DaoRegistryABI } from "../lib/artifacts/dao-registry";
import { ABI as PluginSetupProcessorABI } from "../lib/artifacts/plugin-setup-processor";
Expand Down Expand Up @@ -73,11 +72,8 @@ function pinDaoMetadata(): Promise<Hex> {
// },
// ],
};
const blob = new Blob([JSON.stringify(daoMetadata)], {
type: "application/json",
});

return uploadToIPFS(ipfsClient, blob)
return uploadToPinata(JSON.stringify(daoMetadata))
.then((res) => toHex(res))
.catch((err) => {
console.warn("Warning: Could not pin the DAO metadata on IPFS");
Expand Down
33 changes: 26 additions & 7 deletions scripts/lib/util/ipfs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { create } from "ipfs-http-client";
import { getEnv } from "./env";

const IPFS_ENDPOINT = getEnv("DEPLOYMENT_IPFS_ENDPOINT", true) ?? "";
const IPFS_API_KEY = getEnv("DEPLOYMENT_IPFS_API_KEY", true) || "";
const PINATA_JWT = getEnv("DEPLOYMENT_PINATA_JWT", true) || "";
const APP_NAME = getEnv("DEPLOYMENT_APP_NAME", true) || "";

export const deploymentIpfsClient = create({
url: IPFS_ENDPOINT,
headers: { "X-API-KEY": IPFS_API_KEY, Accept: "application/json" },
});
const UPLOAD_FILE_NAME = APP_NAME.toLowerCase().trim().replaceAll(" ", "-") + ".json";

export async function uploadToPinata(strBody: string) {
const blob = new Blob([strBody], { type: "text/plain" });
const file = new File([blob], UPLOAD_FILE_NAME);
const data = new FormData();
data.append("file", file);
data.append("pinataMetadata", JSON.stringify({ name: UPLOAD_FILE_NAME }));
data.append("pinataOptions", JSON.stringify({ cidVersion: 1 }));

const res = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
method: "POST",
headers: {
Authorization: `Bearer ${PINATA_JWT}`,
},
body: data,
});

const resData = await res.json();

if (resData.error) throw new Error("Request failed: " + resData.error);
else if (!resData.IpfsHash) throw new Error("Could not pin the metadata");
return "ipfs://" + resData.IpfsHash;
}
Loading
Loading