create-ponder 0.0.77 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bin/create-ponder.d.ts +3 -0
  3. package/dist/bin/create-ponder.d.ts.map +1 -0
  4. package/dist/bin/create-ponder.js +105 -0
  5. package/dist/bin/create-ponder.js.map +1 -0
  6. package/dist/common.d.ts +23 -0
  7. package/dist/common.d.ts.map +1 -0
  8. package/dist/common.js +11 -0
  9. package/dist/common.js.map +1 -0
  10. package/dist/helpers/getEtherscanChainId.d.ts +6 -0
  11. package/dist/helpers/getEtherscanChainId.d.ts.map +1 -0
  12. package/dist/helpers/getEtherscanChainId.js +75 -0
  13. package/dist/helpers/getEtherscanChainId.js.map +1 -0
  14. package/dist/helpers/getGraphProtocolChainId.d.ts +3 -0
  15. package/dist/helpers/getGraphProtocolChainId.d.ts.map +1 -0
  16. package/dist/helpers/getGraphProtocolChainId.js +42 -0
  17. package/dist/helpers/getGraphProtocolChainId.js.map +1 -0
  18. package/dist/helpers/getPackageManager.d.ts +2 -0
  19. package/dist/helpers/getPackageManager.d.ts.map +1 -0
  20. package/dist/helpers/getPackageManager.js +18 -0
  21. package/dist/helpers/getPackageManager.js.map +1 -0
  22. package/dist/helpers/git.d.ts +2 -0
  23. package/dist/helpers/git.d.ts.map +1 -0
  24. package/dist/helpers/git.js +56 -0
  25. package/dist/helpers/git.js.map +1 -0
  26. package/dist/helpers/validateGraphProtocolSource.d.ts +28 -0
  27. package/dist/helpers/validateGraphProtocolSource.d.ts.map +1 -0
  28. package/dist/helpers/validateGraphProtocolSource.js +8 -0
  29. package/dist/helpers/validateGraphProtocolSource.js.map +1 -0
  30. package/dist/helpers/wait.d.ts +2 -0
  31. package/dist/helpers/wait.d.ts.map +1 -0
  32. package/dist/helpers/wait.js +6 -0
  33. package/dist/helpers/wait.js.map +1 -0
  34. package/dist/index.d.ts +24 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +168 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/templates/basic.d.ts +5 -0
  39. package/dist/templates/basic.d.ts.map +1 -0
  40. package/dist/templates/basic.js +57 -0
  41. package/dist/templates/basic.js.map +1 -0
  42. package/dist/templates/etherscan.d.ts +7 -0
  43. package/dist/templates/etherscan.d.ts.map +1 -0
  44. package/dist/templates/etherscan.js +199 -0
  45. package/dist/templates/etherscan.js.map +1 -0
  46. package/dist/templates/subgraphId.d.ts +6 -0
  47. package/dist/templates/subgraphId.d.ts.map +1 -0
  48. package/dist/templates/subgraphId.js +76 -0
  49. package/dist/templates/subgraphId.js.map +1 -0
  50. package/dist/templates/subgraphRepo.d.ts +6 -0
  51. package/dist/templates/subgraphRepo.d.ts.map +1 -0
  52. package/dist/templates/subgraphRepo.js +86 -0
  53. package/dist/templates/subgraphRepo.js.map +1 -0
  54. package/package.json +15 -11
  55. package/src/bin/create-ponder.ts +127 -0
  56. package/src/common.ts +27 -0
  57. package/src/helpers/getEtherscanChainId.ts +74 -0
  58. package/src/helpers/getGraphProtocolChainId.ts +41 -0
  59. package/src/helpers/getPackageManager.ts +11 -0
  60. package/src/helpers/git.ts +51 -0
  61. package/src/helpers/validateGraphProtocolSource.ts +42 -0
  62. package/src/helpers/wait.ts +2 -0
  63. package/src/index.ts +244 -0
  64. package/src/templates/basic.ts +60 -0
  65. package/src/templates/etherscan.ts +276 -0
  66. package/src/templates/subgraphId.ts +97 -0
  67. package/src/templates/subgraphRepo.ts +116 -0
  68. package/dist/create-ponder.d.ts +0 -1
  69. package/dist/create-ponder.js +0 -937
  70. package/dist/create-ponder.js.map +0 -1
  71. package/dist/create-ponder.mjs +0 -914
  72. package/dist/create-ponder.mjs.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ import type { Abi, AbiEvent } from "abitype";
2
+ import { execSync } from "node:child_process";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import path from "node:path";
5
+ import pico from "picocolors";
6
+ import prettier from "prettier";
7
+
8
+ import { CreatePonderOptions, TemplateKind } from "@/common";
9
+ import { getPackageManager } from "@/helpers/getPackageManager";
10
+ import { tryGitInit } from "@/helpers/git";
11
+ import { fromBasic } from "@/templates/basic";
12
+ import { fromEtherscan } from "@/templates/etherscan";
13
+ import { fromSubgraphId } from "@/templates/subgraphId";
14
+ import { fromSubgraphRepo } from "@/templates/subgraphRepo";
15
+
16
+ export type Network = {
17
+ name: string;
18
+ chainId: number;
19
+ rpcUrl: string;
20
+ };
21
+
22
+ export type Contract = {
23
+ name: string;
24
+ network: string;
25
+ abi: string;
26
+ address: string;
27
+ startBlock?: number;
28
+ };
29
+
30
+ export type PartialConfig = {
31
+ database?: {
32
+ kind: string;
33
+ };
34
+ networks: Network[];
35
+ contracts: Contract[];
36
+ };
37
+
38
+ export const run = async (
39
+ options: CreatePonderOptions,
40
+ overrides: { installCommand?: string } = {}
41
+ ) => {
42
+ const { rootDir } = options;
43
+
44
+ // Create required directories.
45
+ mkdirSync(path.join(rootDir, "abis"), { recursive: true });
46
+ mkdirSync(path.join(rootDir, "src"), { recursive: true });
47
+
48
+ let config: PartialConfig;
49
+
50
+ console.log(
51
+ `\nCreating a new Ponder app in ${pico.bold(pico.green(rootDir))}.`
52
+ );
53
+
54
+ switch (options.template?.kind) {
55
+ case TemplateKind.ETHERSCAN: {
56
+ console.log(`\nUsing ${pico.cyan("Etherscan contract link")} template.`);
57
+ config = await fromEtherscan({
58
+ rootDir,
59
+ etherscanLink: options.template.link,
60
+ etherscanApiKey: options.etherscanApiKey,
61
+ });
62
+ break;
63
+ }
64
+ case TemplateKind.SUBGRAPH_ID: {
65
+ console.log(`\nUsing ${pico.cyan("Subgraph ID")} template.`);
66
+ config = await fromSubgraphId({
67
+ rootDir,
68
+ subgraphId: options.template.id,
69
+ });
70
+ break;
71
+ }
72
+ case TemplateKind.SUBGRAPH_REPO: {
73
+ console.log(`\nUsing ${pico.cyan("Subgraph repository")} template.`);
74
+
75
+ config = fromSubgraphRepo({
76
+ rootDir,
77
+ subgraphPath: options.template.path,
78
+ });
79
+ break;
80
+ }
81
+ default: {
82
+ config = fromBasic({ rootDir });
83
+ break;
84
+ }
85
+ }
86
+
87
+ // Write the handler ts files.
88
+ config.contracts.forEach((contract) => {
89
+ let abi: Abi;
90
+ if (Array.isArray(contract.abi)) {
91
+ // If it's an array of ABIs, use the 2nd one (the implementation ABI).
92
+ const abiString = readFileSync(path.join(rootDir, contract.abi[1]), {
93
+ encoding: "utf-8",
94
+ });
95
+ abi = JSON.parse(abiString);
96
+ } else {
97
+ const abiString = readFileSync(path.join(rootDir, contract.abi), {
98
+ encoding: "utf-8",
99
+ });
100
+ abi = JSON.parse(abiString);
101
+ }
102
+
103
+ const abiEvents = abi.filter(
104
+ (item): item is AbiEvent => item.type === "event"
105
+ );
106
+
107
+ const eventNamesToWrite = abiEvents.map((event) => event.name).slice(0, 2);
108
+
109
+ const handlerFileContents = `
110
+ import { ponder } from '@/generated'
111
+
112
+ ${eventNamesToWrite
113
+ .map(
114
+ (eventName) => `
115
+ ponder.on("${contract.name}:${eventName}", async ({ event, context }) => {
116
+ console.log(event.params)
117
+ })`
118
+ )
119
+ .join("\n")}
120
+ `;
121
+
122
+ writeFileSync(
123
+ path.join(rootDir, `./src/${contract.name}.ts`),
124
+ prettier.format(handlerFileContents, { parser: "typescript" })
125
+ );
126
+ });
127
+
128
+ // Write the ponder.config.ts file.
129
+ const finalConfig = `
130
+ import type { Config } from "@ponder/core";
131
+
132
+ export const config: Config = {
133
+ networks: ${JSON.stringify(config.networks).replaceAll(
134
+ /"process.env.PONDER_RPC_URL_(.*?)"/g,
135
+ "process.env.PONDER_RPC_URL_$1"
136
+ )},
137
+ contracts: ${JSON.stringify(config.contracts)},
138
+ };
139
+ `;
140
+
141
+ writeFileSync(
142
+ path.join(rootDir, "ponder.config.ts"),
143
+ prettier.format(finalConfig, { parser: "babel" })
144
+ );
145
+
146
+ // Write the .env.local file.
147
+ const uniqueChainIds = Array.from(
148
+ new Set(config.networks.map((n) => n.chainId))
149
+ );
150
+ const envLocal = `${uniqueChainIds.map(
151
+ (chainId) => `PONDER_RPC_URL_${chainId}=""\n`
152
+ )}`;
153
+ writeFileSync(path.join(rootDir, ".env.local"), envLocal);
154
+
155
+ // Write the package.json file.
156
+ const packageJson = `
157
+ {
158
+ "private": true,
159
+ "scripts": {
160
+ "dev": "ponder dev",
161
+ "start": "ponder start",
162
+ "codegen": "ponder codegen"
163
+ },
164
+ "dependencies": {
165
+ "@ponder/core": "latest"
166
+ },
167
+ "devDependencies": {
168
+ "@types/node": "^18.11.18",
169
+ "abitype": "^0.8.11",
170
+ "typescript": "^5.1.3",
171
+ "viem": "^1.2.6"
172
+ }
173
+ }
174
+ `;
175
+ writeFileSync(
176
+ path.join(rootDir, "package.json"),
177
+ prettier.format(packageJson, { parser: "json" })
178
+ );
179
+
180
+ // Write the tsconfig.json file.
181
+ const tsConfig = `
182
+ {
183
+ "compilerOptions": {
184
+ "target": "ESNext",
185
+ "module": "ESNext",
186
+ "moduleResolution": "node",
187
+ "resolveJsonModule": true,
188
+ "esModuleInterop": true,
189
+ "strict": true,
190
+ "rootDir": ".",
191
+ "paths": {
192
+ "@/generated": ["./generated/index.ts"]
193
+ }
194
+ },
195
+ "include": ["./**/*.ts"],
196
+ "exclude": ["node_modules"]
197
+ }
198
+ `;
199
+ writeFileSync(
200
+ path.join(rootDir, "tsconfig.json"),
201
+ prettier.format(tsConfig, { parser: "json" })
202
+ );
203
+
204
+ // Write the .gitignore file.
205
+ writeFileSync(
206
+ path.join(rootDir, ".gitignore"),
207
+ `node_modules/\n.DS_Store\n\n.env.local\n.ponder/\ngenerated/`
208
+ );
209
+
210
+ const packageManager = await getPackageManager();
211
+
212
+ // Install packages.
213
+ console.log(pico.bold(`\nInstalling with ${packageManager}.`));
214
+
215
+ const installCommand = overrides.installCommand
216
+ ? overrides.installCommand
217
+ : `${packageManager} ${
218
+ packageManager === "npm" ? "--quiet" : "--silent"
219
+ } install`;
220
+
221
+ execSync(installCommand, {
222
+ cwd: rootDir,
223
+ stdio: "inherit",
224
+ });
225
+
226
+ // Intialize git repository
227
+ process.chdir(rootDir);
228
+ tryGitInit(rootDir);
229
+ console.log(`\nInitialized a git repository.`);
230
+
231
+ // Run codegen.
232
+ const runCommand = `${
233
+ packageManager === "npm" ? `npm --quiet run` : `${packageManager} --silent`
234
+ } codegen`;
235
+ execSync(runCommand, {
236
+ cwd: rootDir,
237
+ stdio: "inherit",
238
+ });
239
+ console.log(`\nGenerated types.`);
240
+
241
+ console.log(
242
+ pico.green("\nSuccess! ") + `Created ${options.projectName} at ${rootDir}`
243
+ );
244
+ };
@@ -0,0 +1,60 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import prettier from "prettier";
4
+
5
+ import type { PartialConfig } from "@/index";
6
+
7
+ export const fromBasic = ({ rootDir }: { rootDir: string }) => {
8
+ const abiFileContents = `[]`;
9
+
10
+ const abiRelativePath = "./abis/ExampleContract.json";
11
+ const abiAbsolutePath = path.join(rootDir, abiRelativePath);
12
+ writeFileSync(abiAbsolutePath, abiFileContents);
13
+
14
+ const schemaGraphqlFileContents = `
15
+ # The entity types defined below map to database tables.
16
+ # The functions you write as event handlers inside the \`src/\` directory are responsible for creating and updating records in those tables.
17
+ # Your schema will be more flexible and powerful if it accurately models the logical relationships in your application's domain.
18
+ # Visit the [documentation](https://ponder.sh/guides/design-your-schema) or the [\`examples/\`](https://github.com/0xOlias/ponder/tree/main/examples) directory for further guidance on designing your schema.
19
+
20
+ type ExampleToken @entity {
21
+ id: String!
22
+ tokenId: Int!
23
+ trait: TokenTrait!
24
+ }
25
+
26
+ enum TokenTrait {
27
+ GOOD
28
+ BAD
29
+ }
30
+ `;
31
+
32
+ // Generate the schema.graphql file.
33
+ const ponderSchemaFilePath = path.join(rootDir, "schema.graphql");
34
+ writeFileSync(
35
+ ponderSchemaFilePath,
36
+ prettier.format(schemaGraphqlFileContents, { parser: "graphql" })
37
+ );
38
+
39
+ // Build the partial ponder config.
40
+ const config: PartialConfig = {
41
+ networks: [
42
+ {
43
+ name: "mainnet",
44
+ chainId: 1,
45
+ rpcUrl: `process.env.PONDER_RPC_URL_1`,
46
+ },
47
+ ],
48
+ contracts: [
49
+ {
50
+ name: "ExampleContract",
51
+ network: "mainnet",
52
+ address: "0x0",
53
+ abi: abiRelativePath,
54
+ startBlock: 1234567,
55
+ },
56
+ ],
57
+ };
58
+
59
+ return config;
60
+ };
@@ -0,0 +1,276 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import fetch from "node-fetch";
5
+ import prettier from "prettier";
6
+ import type { PartialConfig } from "src/index";
7
+
8
+ import { getNetworkByEtherscanHostname } from "@/helpers/getEtherscanChainId";
9
+ import { wait } from "@/helpers/wait";
10
+
11
+ export const fromEtherscan = async ({
12
+ rootDir,
13
+ etherscanLink,
14
+ etherscanApiKey,
15
+ }: {
16
+ rootDir: string;
17
+ etherscanLink: string;
18
+ etherscanApiKey?: string;
19
+ }) => {
20
+ const apiKey = etherscanApiKey || process.env.ETHERSCAN_API_KEY;
21
+
22
+ const url = new URL(etherscanLink);
23
+ const network = getNetworkByEtherscanHostname(url.hostname);
24
+ if (!network) {
25
+ throw new Error(`Unrecognized etherscan hostname: ${url.hostname}`);
26
+ }
27
+
28
+ const { name, chainId, apiUrl } = network;
29
+ const contractAddress = url.pathname.slice(1).split("/")[1];
30
+
31
+ let blockNumber: number | undefined = undefined;
32
+
33
+ try {
34
+ const txHash = await getContractCreationTxn(
35
+ contractAddress,
36
+ apiUrl,
37
+ apiKey
38
+ );
39
+
40
+ if (!apiKey) {
41
+ console.log("\n(1/n) Waiting 5 seconds for Etherscan API rate limit");
42
+ await wait(5000);
43
+ }
44
+ const contractCreationBlockNumber = await getTxBlockNumber(
45
+ txHash,
46
+ apiUrl,
47
+ apiKey
48
+ );
49
+
50
+ blockNumber = contractCreationBlockNumber;
51
+ } catch (error) {
52
+ // Do nothing, blockNumber won't be set.
53
+ }
54
+
55
+ if (!apiKey) {
56
+ console.log("(2/n) Waiting 5 seconds for Etherscan API rate limit");
57
+ await wait(5000);
58
+ }
59
+ const abis: { abi: string; contractName: string }[] = [];
60
+ const { abi, contractName } = await getContractAbiAndName(
61
+ contractAddress,
62
+ apiUrl,
63
+ apiKey
64
+ );
65
+ abis.push({ abi, contractName });
66
+
67
+ // If the contract is an EIP-1967 proxy, get the implementation contract ABIs.
68
+ if (
69
+ (JSON.parse(abi) as any[]).find(
70
+ (item) =>
71
+ item.type === "event" &&
72
+ item.name === "Upgraded" &&
73
+ item.inputs[0].name === "implementation"
74
+ )
75
+ ) {
76
+ console.log(
77
+ "Detected EIP-1967 proxy, fetching implementation contract ABIs"
78
+ );
79
+ if (!apiKey) {
80
+ console.log("(3/n) Waiting 5 seconds for Etherscan API rate limit");
81
+ await wait(5000);
82
+ }
83
+ const { implAddresses } = await getProxyImplementationAddresses({
84
+ contractAddress,
85
+ apiUrl,
86
+ fromBlock: blockNumber,
87
+ apiKey,
88
+ });
89
+
90
+ for (const [index, implAddress] of implAddresses.entries()) {
91
+ console.log(`Fetching ABI for implementation contract: ${implAddress}`);
92
+ if (!apiKey) {
93
+ console.log(
94
+ `(${4 + index}/${
95
+ 4 + implAddresses.length - 1
96
+ }) Waiting 5 seconds for Etherscan API rate limit`
97
+ );
98
+ await wait(5000);
99
+ }
100
+ const { abi, contractName } = await getContractAbiAndName(
101
+ implAddress,
102
+ apiUrl,
103
+ apiKey
104
+ );
105
+
106
+ abis.push({
107
+ abi,
108
+ contractName: `${contractName}_${implAddress.slice(0, 6)}`,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Write ABI files.
114
+ let abiConfig: any;
115
+ abis.forEach(({ abi, contractName }) => {
116
+ const abiRelativePath = `./abis/${contractName}.json`;
117
+ const abiAbsolutePath = path.join(rootDir, abiRelativePath);
118
+ writeFileSync(abiAbsolutePath, prettier.format(abi, { parser: "json" }));
119
+
120
+ if (abis.length === 1) {
121
+ abiConfig = abiRelativePath;
122
+ } else {
123
+ abiConfig ||= [];
124
+ abiConfig.push(abiRelativePath);
125
+ }
126
+ });
127
+
128
+ const schemaGraphqlFileContents = `
129
+ type ExampleEntity @entity {
130
+ id: String!
131
+ name: String!
132
+ }
133
+ `;
134
+
135
+ // Generate the schema.graphql file.
136
+ const ponderSchemaFilePath = path.join(rootDir, "schema.graphql");
137
+ writeFileSync(
138
+ ponderSchemaFilePath,
139
+ prettier.format(schemaGraphqlFileContents, { parser: "graphql" })
140
+ );
141
+
142
+ // Build and return the partial ponder config.
143
+ const config: PartialConfig = {
144
+ networks: [
145
+ {
146
+ name: name,
147
+ chainId: chainId,
148
+ rpcUrl: `process.env.PONDER_RPC_URL_${chainId}`,
149
+ },
150
+ ],
151
+ contracts: [
152
+ {
153
+ name: contractName,
154
+ network: name,
155
+ abi: abiConfig,
156
+ address: contractAddress,
157
+ startBlock: blockNumber ?? undefined,
158
+ },
159
+ ],
160
+ };
161
+
162
+ return config;
163
+ };
164
+
165
+ const fetchEtherscan = async (url: string) => {
166
+ const maxRetries = 5;
167
+ let retryCount = 0;
168
+
169
+ while (retryCount <= maxRetries) {
170
+ try {
171
+ const response = await fetch(url);
172
+ const data = await response.json();
173
+ if (data.status === "0") {
174
+ throw new Error(`Etherscan API error: ${data.result}`);
175
+ }
176
+ return data;
177
+ } catch (error) {
178
+ retryCount++;
179
+ if (retryCount > maxRetries) {
180
+ throw new Error(`Max retries reached: ${(error as Error).message}`);
181
+ }
182
+ await new Promise((resolve) => setTimeout(resolve, 100));
183
+ }
184
+ }
185
+ };
186
+
187
+ const getContractCreationTxn = async (
188
+ contractAddress: string,
189
+ apiUrl: string,
190
+ apiKey?: string
191
+ ) => {
192
+ const searchParams = new URLSearchParams({
193
+ module: "contract",
194
+ action: "getcontractcreation",
195
+ contractaddresses: contractAddress,
196
+ });
197
+ if (apiKey) searchParams.append("apikey", apiKey);
198
+ const data = await fetchEtherscan(`${apiUrl}?${searchParams.toString()}`);
199
+
200
+ return data.result[0].txHash as string;
201
+ };
202
+
203
+ const getTxBlockNumber = async (
204
+ txHash: string,
205
+ apiUrl: string,
206
+ apiKey?: string
207
+ ) => {
208
+ const searchParams = new URLSearchParams({
209
+ module: "proxy",
210
+ action: "eth_getTransactionByHash",
211
+ txhash: txHash,
212
+ });
213
+ if (apiKey) searchParams.append("apikey", apiKey);
214
+ const data = await fetchEtherscan(`${apiUrl}?${searchParams.toString()}`);
215
+
216
+ const hexBlockNumber = data.result.blockNumber as string;
217
+ return parseInt(hexBlockNumber.slice(2), 16);
218
+ };
219
+
220
+ const getContractAbiAndName = async (
221
+ contractAddress: string,
222
+ apiUrl: string,
223
+ apiKey?: string
224
+ ) => {
225
+ const searchParams = new URLSearchParams({
226
+ module: "contract",
227
+ action: "getsourcecode",
228
+ address: contractAddress,
229
+ });
230
+ if (apiKey) searchParams.append("apikey", apiKey);
231
+ const data = await fetchEtherscan(`${apiUrl}?${searchParams.toString()}`);
232
+
233
+ const abi = data.result[0].ABI as string;
234
+ const contractName = data.result[0].ContractName as string;
235
+
236
+ return { abi, contractName };
237
+ };
238
+
239
+ const getProxyImplementationAddresses = async ({
240
+ contractAddress,
241
+ apiUrl,
242
+ fromBlock,
243
+ apiKey,
244
+ }: {
245
+ contractAddress: string;
246
+ apiUrl: string;
247
+ fromBlock?: number;
248
+ apiKey?: string;
249
+ }) => {
250
+ const searchParams = new URLSearchParams({
251
+ module: "logs",
252
+ action: "getLogs",
253
+ address: contractAddress,
254
+ fromBlock: fromBlock ? String(fromBlock) : "0",
255
+ toBlock: "latest",
256
+ topic0:
257
+ "0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
258
+ });
259
+ if (apiKey) searchParams.append("apikey", apiKey);
260
+ const data = await fetchEtherscan(`${apiUrl}?${searchParams.toString()}`);
261
+
262
+ const logs = data.result;
263
+
264
+ const implAddresses = logs.map((log: any) => {
265
+ if (log.topics[0] && log.topics[1]) {
266
+ // If there are two topics, this is a compliant EIP-1967 proxy and the address is indexed.
267
+ return `0x${log.topics[1].slice(26)}`;
268
+ } else {
269
+ // If there's only one topic, this might be a non-compliant proxy and the address is not indexed.
270
+ // USDC is an example of this: https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code#L118
271
+ return `0x${log.data.slice(26)}`;
272
+ }
273
+ }) as string[];
274
+
275
+ return { implAddresses };
276
+ };
@@ -0,0 +1,97 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import fetch from "node-fetch";
4
+ import prettier from "prettier";
5
+ import type { Contract, Network, PartialConfig } from "src/index";
6
+ import { parse } from "yaml";
7
+
8
+ import { getGraphProtocolChainId } from "@/helpers/getGraphProtocolChainId";
9
+ import { validateGraphProtocolSource } from "@/helpers/validateGraphProtocolSource";
10
+
11
+ const fetchIpfsFile = async (cid: string) => {
12
+ const url = `https://ipfs.network.thegraph.com/api/v0/cat?arg=${cid}`;
13
+ const response = await fetch(url);
14
+ const contentRaw = await response.text();
15
+ return contentRaw;
16
+ };
17
+
18
+ export const fromSubgraphId = async ({
19
+ rootDir,
20
+ subgraphId,
21
+ }: {
22
+ rootDir: string;
23
+ subgraphId: string;
24
+ }) => {
25
+ const ponderNetworks: Network[] = [];
26
+ let ponderContracts: Contract[] = [];
27
+
28
+ // Fetch the manifest file.
29
+ const manifestRaw = await fetchIpfsFile(subgraphId);
30
+ const manifest = parse(manifestRaw);
31
+
32
+ // Fetch and write the schema.graphql file.
33
+ const schemaCid = manifest.schema.file["/"].slice(6);
34
+ const schemaRaw = await fetchIpfsFile(schemaCid);
35
+ const schemaCleaned = schemaRaw
36
+ .replaceAll(": ID!", ": String!")
37
+ .replaceAll("BigDecimal", "Float");
38
+ const ponderSchemaFilePath = path.join(rootDir, "schema.graphql");
39
+ writeFileSync(
40
+ ponderSchemaFilePath,
41
+ prettier.format(schemaCleaned, { parser: "graphql" })
42
+ );
43
+
44
+ const dataSources = (manifest.dataSources as unknown[]).map(
45
+ validateGraphProtocolSource
46
+ );
47
+
48
+ // Fetch and write all referenced ABIs.
49
+ const abiFiles = dataSources
50
+ .map((source) => source.mapping.abis)
51
+ .flat()
52
+ .filter(
53
+ (source, idx, arr) => arr.findIndex((s) => s.name === source.name) === idx
54
+ );
55
+ await Promise.all(
56
+ abiFiles.map(async (abi) => {
57
+ const abiContent = await fetchIpfsFile(abi.file["/"].slice(6));
58
+ const abiPath = path.join(rootDir, `./abis/${abi.name}.json`);
59
+ writeFileSync(abiPath, prettier.format(abiContent, { parser: "json" }));
60
+ })
61
+ );
62
+
63
+ // Build the ponder sources.
64
+ ponderContracts = dataSources.map((source) => {
65
+ const network = source.network || "mainnet";
66
+ const chainId = getGraphProtocolChainId(network);
67
+ if (!chainId || chainId === -1) {
68
+ throw new Error(`Unhandled network name: ${network}`);
69
+ }
70
+
71
+ if (!ponderNetworks.map((n) => n.name).includes(network)) {
72
+ ponderNetworks.push({
73
+ name: network,
74
+ chainId: chainId,
75
+ rpcUrl: `process.env.PONDER_RPC_URL_${chainId}`,
76
+ });
77
+ }
78
+
79
+ const abiRelativePath = `./abis/${source.source.abi}.json`;
80
+
81
+ return <Contract>{
82
+ name: source.name,
83
+ network: network,
84
+ address: source.source.address,
85
+ abi: abiRelativePath,
86
+ startBlock: source.source.startBlock,
87
+ };
88
+ });
89
+
90
+ // Build the partial ponder config.
91
+ const config: PartialConfig = {
92
+ networks: ponderNetworks,
93
+ contracts: ponderContracts,
94
+ };
95
+
96
+ return config;
97
+ };