diff --git a/.changeset/short-pumpkins-wash.md b/.changeset/short-pumpkins-wash.md new file mode 100644 index 000000000..c6e25fdec --- /dev/null +++ b/.changeset/short-pumpkins-wash.md @@ -0,0 +1,5 @@ +--- +"create-ponder": patch +--- + +Added support for explorer.zora.energy links in `create-ponder` etherscan template. diff --git a/packages/create-ponder/src/helpers/getEtherscanChainId.ts b/packages/create-ponder/src/helpers/getEtherscanChainId.ts index 4ec93e4d2..0c9e57d50 100644 --- a/packages/create-ponder/src/helpers/getEtherscanChainId.ts +++ b/packages/create-ponder/src/helpers/getEtherscanChainId.ts @@ -62,6 +62,11 @@ const networkByEtherscanHostname: Record< chainId: 421613, apiUrl: "https://api-goerli.arbiscan.io/api", }, + "explorer.zora.energy": { + name: "zora", + chainId: 7777777, + apiUrl: "https://explorer.zora.energy/api", + }, }; export const getNetworkByEtherscanHostname = (hostname: string) => { diff --git a/packages/create-ponder/src/templates/etherscan.ts b/packages/create-ponder/src/templates/etherscan.ts index 87da21814..05c6ef50f 100644 --- a/packages/create-ponder/src/templates/etherscan.ts +++ b/packages/create-ponder/src/templates/etherscan.ts @@ -28,16 +28,32 @@ export const fromEtherscan = async ({ const { name, chainId, apiUrl } = network; const contractAddress = url.pathname.slice(1).split("/")[1]; - const txHash = await getContractCreationTxn(contractAddress, apiUrl, apiKey); + let blockNumber: number | undefined = undefined; - if (!apiKey) { - console.log("\n(1/2) Waiting 5 seconds for Etherscan API rate limit"); - await wait(5000); + try { + const txHash = await getContractCreationTxn( + contractAddress, + apiUrl, + apiKey + ); + + if (!apiKey) { + console.log("\n(1/n) Waiting 5 seconds for Etherscan API rate limit"); + await wait(5000); + } + const contractCreationBlockNumber = await getTxBlockNumber( + txHash, + apiUrl, + apiKey + ); + + blockNumber = contractCreationBlockNumber; + } catch (error) { + // Do nothing, blockNumber won't be set. } - const blockNumber = await getTxBlockNumber(txHash, apiUrl, apiKey); if (!apiKey) { - console.log("(2/2) Waiting 5 seconds for Etherscan API rate limit"); + console.log("(2/n) Waiting 5 seconds for Etherscan API rate limit"); await wait(5000); } const abis: { abi: string; contractName: string }[] = []; @@ -61,15 +77,15 @@ export const fromEtherscan = async ({ "Detected EIP-1967 proxy, fetching implementation contract ABIs" ); if (!apiKey) { - console.log("(3/X) Waiting 5 seconds for Etherscan API rate limit"); + console.log("(3/n) Waiting 5 seconds for Etherscan API rate limit"); await wait(5000); } - const { implAddresses } = await getProxyImplementationAddresses( + const { implAddresses } = await getProxyImplementationAddresses({ contractAddress, - blockNumber, apiUrl, - apiKey - ); + fromBlock: blockNumber, + apiKey, + }); for (const [index, implAddress] of implAddresses.entries()) { console.log(`Fetching ABI for implementation contract: ${implAddress}`); @@ -138,7 +154,7 @@ export const fromEtherscan = async ({ network: name, abi: abiConfig, address: contractAddress, - startBlock: blockNumber, + startBlock: blockNumber ?? undefined, }, ], }; @@ -220,17 +236,22 @@ const getContractAbiAndName = async ( return { abi, contractName }; }; -const getProxyImplementationAddresses = async ( - contractAddress: string, - fromBlock: number, - apiUrl: string, - apiKey?: string -) => { +const getProxyImplementationAddresses = async ({ + contractAddress, + apiUrl, + fromBlock, + apiKey, +}: { + contractAddress: string; + apiUrl: string; + fromBlock?: number; + apiKey?: string; +}) => { const searchParams = new URLSearchParams({ module: "logs", action: "getLogs", address: contractAddress, - fromBlock: String(fromBlock), + fromBlock: fromBlock ? String(fromBlock) : "0", toBlock: "latest", topic0: "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b", @@ -241,7 +262,7 @@ const getProxyImplementationAddresses = async ( const logs = data.result; const implAddresses = logs.map((log: any) => { - if (log.topics.length === 2) { + if (log.topics[0] && log.topics[1]) { // If there are two topics, this is a compliant EIP-1967 proxy and the address is indexed. return `0x${log.topics[1].slice(26)}`; } else { diff --git a/packages/create-ponder/test/main.test.ts b/packages/create-ponder/test/main.test.ts index 60093d2cf..3d9f22aba 100644 --- a/packages/create-ponder/test/main.test.ts +++ b/packages/create-ponder/test/main.test.ts @@ -217,6 +217,65 @@ describe("create-ponder", () => { expect(JSON.parse(implementationAbiString).length).toBeGreaterThan(0); }); }); + + describe("zora EIP-1967 proxy (ZoraNFTCreatorProxy)", () => { + const rootDir = path.join(tmpDir, randomUUID()); + + beforeAll(async () => { + await run( + { + projectName: "ZoraNFTCreatorProxy", + rootDir, + template: { + kind: TemplateKind.ETHERSCAN, + link: "https://explorer.zora.energy/address/0xA2c2A96A232113Dd4993E8b048EEbc3371AE8d85", + }, + }, + { + installCommand: + 'export npm_config_LOCKFILE=false ; pnpm --silent --filter "." install', + } + ); + }); + + test("creates project files and directories", async () => { + const root = fs.readdirSync(rootDir); + expect(root).toContain(".env.local"); + expect(root).toContain(".gitignore"); + expect(root).toContain("abis"); + expect(root).toContain("generated"); + expect(root).toContain("src"); + expect(root).toContain("node_modules"); + expect(root).toContain("package.json"); + expect(root).toContain("ponder.config.ts"); + expect(root).toContain("schema.graphql"); + expect(root).toContain("tsconfig.json"); + }); + + test("downloads abis", async () => { + const proxyAbiString = fs.readFileSync( + path.join(rootDir, `abis/ZoraNFTCreatorProxy.json`), + { encoding: "utf8" } + ); + expect(JSON.parse(proxyAbiString).length).toBeGreaterThan(0); + + const implementationAbiString = fs.readFileSync( + path.join(rootDir, `abis/ZoraNFTCreatorV1_0xe776.json`), + { encoding: "utf8" } + ); + expect(JSON.parse(implementationAbiString).length).toBeGreaterThan(0); + }); + + test("creates codegen files", async () => { + const generated = fs.readdirSync(path.join(rootDir, "generated")); + expect(generated.sort()).toEqual(["index.ts", "schema.graphql"].sort()); + }); + + test("creates src files", async () => { + const src = fs.readdirSync(path.join(rootDir, "src")); + expect(src.sort()).toEqual(["ZoraNFTCreatorProxy.ts"].sort()); + }); + }); }); describe("from subgraph id", () => {