Skip to content

Commit 50aed5d

Browse files
Concurrent ABI fetch (#1964)
* fetch rpc/api endpoints concurrently * add timeouts * improve error messages
1 parent 19331a3 commit 50aed5d

File tree

1 file changed

+144
-93
lines changed

1 file changed

+144
-93
lines changed

packages/cli/src/command-helpers/contracts.ts

Lines changed: 144 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,23 @@ import ABI from '../protocols/ethereum/abi.js';
66

77
const logger = debugFactory('graph-cli:contract-service');
88

9+
function withTimeout<T>(promise: Promise<T>, timeoutMs: number = 10_000): Promise<T> {
10+
return Promise.race([
11+
promise,
12+
new Promise<T>((_, reject) =>
13+
setTimeout(() => reject(new Error('Request timed out')), timeoutMs),
14+
),
15+
]);
16+
}
917
export class ContractService {
1018
constructor(private registry: NetworksRegistry) {}
1119

1220
private async fetchFromEtherscan(url: string): Promise<any | null> {
13-
const result = await fetch(url).catch(_error => {
14-
throw new Error(`Contract API is unreachable`);
15-
});
21+
const result = await withTimeout(
22+
fetch(url).catch(_error => {
23+
throw new Error(`Contract API is unreachable`);
24+
}),
25+
);
1626
let json: any = {};
1727

1828
if (result.ok) {
@@ -30,10 +40,13 @@ export class ContractService {
3040
result.url,
3141
json,
3242
);
43+
if (!result.ok) {
44+
throw new Error(`${result.status} ${result.statusText}`);
45+
}
3346
if (json.message) {
34-
throw new Error(`${json.message ?? ''} - ${json.result ?? ''}`);
47+
throw new Error(`${json.message} - ${json.result ?? ''}`);
3548
}
36-
return null;
49+
throw new Error('Empty response');
3750
}
3851

3952
// replace {api_key} with process.env[api_key]
@@ -71,84 +84,107 @@ export class ContractService {
7184

7285
async getABI(ABICtor: typeof ABI, networkId: string, address: string) {
7386
const urls = this.getEtherscanUrls(networkId);
74-
const errors: string[] = [];
7587
if (!urls.length) {
7688
throw new Error(`No contract API available for ${networkId} in the registry`);
7789
}
78-
for (const url of urls) {
79-
try {
80-
const json = await this.fetchFromEtherscan(
81-
`${url}?module=contract&action=getabi&address=${address}`,
82-
);
83-
84-
if (json) {
85-
return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result)));
90+
91+
try {
92+
const result = await Promise.any(
93+
urls.map(url =>
94+
this.fetchFromEtherscan(`${url}?module=contract&action=getabi&address=${address}`).then(
95+
json => {
96+
if (!json?.result) {
97+
throw new Error(`No result: ${JSON.stringify(json ?? {})}`);
98+
}
99+
return new ABICtor('Contract', undefined, immutable.fromJS(JSON.parse(json.result)));
100+
},
101+
),
102+
),
103+
);
104+
return result;
105+
} catch (error) {
106+
if (error instanceof AggregateError) {
107+
for (const err of error.errors) {
108+
logger(`Failed to fetch ABI: ${err}`);
86109
}
87-
throw new Error(`no result: ${JSON.stringify(json)}`);
88-
} catch (error) {
89-
logger(`Failed to fetch from ${url}: ${error}`);
90-
errors.push(String(error));
110+
throw new Error(`Failed to fetch ABI: ${error.errors?.[0] ?? 'no public RPC endpoints'}`);
91111
}
112+
throw error;
92113
}
93-
94-
throw new Error(errors?.[0]);
95114
}
96115

97116
async getStartBlock(networkId: string, address: string): Promise<string> {
98117
const urls = this.getEtherscanUrls(networkId);
99118
if (!urls.length) {
100119
throw new Error(`No contract API available for ${networkId} in the registry`);
101120
}
102-
for (const url of urls) {
103-
try {
104-
const json = await this.fetchFromEtherscan(
105-
`${url}?module=contract&action=getcontractcreation&contractaddresses=${address}`,
106-
);
107-
108-
if (json?.result?.length) {
109-
if (json.result[0]?.blockNumber) {
110-
return json.result[0].blockNumber;
111-
}
112-
const txHash = json.result[0].txHash;
113-
const tx = await this.fetchTransactionByHash(networkId, txHash);
114-
if (!tx?.blockNumber) {
115-
throw new Error(`no blockNumber: ${JSON.stringify(tx)}`);
116-
}
117-
return Number(tx.blockNumber).toString();
121+
122+
try {
123+
const result = await Promise.any(
124+
urls.map(url =>
125+
this.fetchFromEtherscan(
126+
`${url}?module=contract&action=getcontractcreation&contractaddresses=${address}`,
127+
).then(async json => {
128+
if (!json?.result?.length) {
129+
throw new Error(`No result: ${JSON.stringify(json)}`);
130+
}
131+
if (json.result[0]?.blockNumber) {
132+
return json.result[0].blockNumber;
133+
}
134+
const txHash = json.result[0].txHash;
135+
const tx = await this.fetchTransactionByHash(networkId, txHash);
136+
if (!tx?.blockNumber) {
137+
throw new Error(`No block number: ${JSON.stringify(tx)}`);
138+
}
139+
return Number(tx.blockNumber).toString();
140+
}),
141+
),
142+
);
143+
return result;
144+
} catch (error) {
145+
if (error instanceof AggregateError) {
146+
for (const err of error.errors) {
147+
logger(`Failed to fetch start block: ${err}`);
118148
}
119-
throw new Error(`no result: ${JSON.stringify(json)}`);
120-
} catch (error) {
121-
logger(`Failed to fetch start block from ${url}: ${error}`);
149+
throw new Error(`Failed to fetch contract deployment transaction`);
122150
}
151+
throw error;
123152
}
124-
125-
throw new Error(`Failed to fetch deploy contract transaction for ${address}`);
126153
}
127154

128155
async getContractName(networkId: string, address: string): Promise<string> {
129156
const urls = this.getEtherscanUrls(networkId);
130157
if (!urls.length) {
131158
throw new Error(`No contract API available for ${networkId} in the registry`);
132159
}
133-
for (const url of urls) {
134-
try {
135-
const json = await this.fetchFromEtherscan(
136-
`${url}?module=contract&action=getsourcecode&address=${address}`,
137-
);
138-
139-
if (json) {
140-
const { ContractName } = json.result[0];
141-
if (ContractName !== '') {
160+
161+
try {
162+
const result = await Promise.any(
163+
urls.map(url =>
164+
this.fetchFromEtherscan(
165+
`${url}?module=contract&action=getsourcecode&address=${address}`,
166+
).then(json => {
167+
if (!json?.result?.length) {
168+
throw new Error(`No result: ${JSON.stringify(json)}`);
169+
}
170+
const { ContractName } = json.result[0];
171+
if (!ContractName) {
172+
throw new Error('Contract name is empty');
173+
}
142174
return ContractName;
143-
}
175+
}),
176+
),
177+
);
178+
return result;
179+
} catch (error) {
180+
if (error instanceof AggregateError) {
181+
for (const err of error.errors) {
182+
logger(`Failed to fetch contract name: ${err}`);
144183
}
145-
throw new Error(`no result: ${JSON.stringify(json)}`);
146-
} catch (error) {
147-
logger(`Failed to fetch from ${url}: ${error}`);
184+
throw new Error(`Name not found`);
148185
}
186+
throw error;
149187
}
150-
151-
throw new Error(`Failed to fetch contract name for ${address}`);
152188
}
153189

154190
async getFromSourcify(
@@ -168,32 +204,35 @@ export class ContractService {
168204

169205
const chainId = network.caip2Id.split(':')[1];
170206
const url = `https://sourcify.dev/server/v2/contract/${chainId}/${address}?fields=abi,compilation,deployment`;
171-
const resp = await fetch(url).catch(error => {
172-
throw new Error(`Sourcify API is unreachable: ${error}`);
173-
});
207+
const resp = await withTimeout(
208+
fetch(url).catch(error => {
209+
throw new Error(`Sourcify API is unreachable: ${error}`);
210+
}),
211+
);
174212
if (resp.status === 404) throw new Error(`Sourcify API says contract is not verified`);
175213
if (!resp.ok) throw new Error(`Sourcify API returned status ${resp.status}`);
176214
const json: {
177215
abi: any[];
178216
compilation: { name: string };
179217
deployment: { blockNumber: string };
180-
} = await resp.json();
181-
182-
if (json) {
183-
const abi = json.abi;
184-
const contractName = json.compilation?.name;
185-
const blockNumber = json.deployment?.blockNumber;
186-
187-
if (!abi || !contractName || !blockNumber) throw new Error('Contract is missing metadata');
218+
} = await resp.json().catch(error => {
219+
throw new Error(`Invalid Sourcify response: ${error}`);
220+
});
188221

189-
return {
190-
abi: new ABICtor(contractName, undefined, immutable.fromJS(abi)) as ABI,
191-
startBlock: Number(blockNumber).toString(),
192-
name: contractName,
193-
};
222+
if (!json) {
223+
throw new Error(`No result`);
194224
}
225+
const abi = json.abi;
226+
const contractName = json.compilation?.name;
227+
const blockNumber = json.deployment?.blockNumber;
228+
229+
if (!abi || !contractName || !blockNumber) throw new Error('Contract is missing metadata');
195230

196-
throw new Error(`No result: ${JSON.stringify(json)}`);
231+
return {
232+
abi: new ABICtor(contractName, undefined, immutable.fromJS(abi)) as ABI,
233+
startBlock: Number(blockNumber).toString(),
234+
name: contractName,
235+
};
197236
} catch (error) {
198237
logger(`Failed to fetch from Sourcify: ${error}`);
199238
}
@@ -206,29 +245,41 @@ export class ContractService {
206245
if (!urls.length) {
207246
throw new Error(`No JSON-RPC available for ${networkId} in the registry`);
208247
}
209-
for (const url of urls) {
210-
try {
211-
const response = await fetch(url, {
212-
method: 'POST',
213-
headers: { 'Content-Type': 'application/json' },
214-
body: JSON.stringify({
215-
jsonrpc: '2.0',
216-
method: 'eth_getTransactionByHash',
217-
params: [txHash],
218-
id: 1,
219-
}),
220-
});
221248

222-
const json = await response.json();
223-
if (json.result) {
224-
return json.result;
249+
try {
250+
const result = await Promise.any(
251+
urls.map(url =>
252+
withTimeout(
253+
fetch(url, {
254+
method: 'POST',
255+
headers: { 'Content-Type': 'application/json' },
256+
body: JSON.stringify({
257+
jsonrpc: '2.0',
258+
method: 'eth_getTransactionByHash',
259+
params: [txHash],
260+
id: 1,
261+
}),
262+
})
263+
.then(response => response.json())
264+
.then(json => {
265+
if (!json?.result) {
266+
throw new Error(JSON.stringify(json));
267+
}
268+
return json.result;
269+
}),
270+
),
271+
),
272+
);
273+
return result;
274+
} catch (error) {
275+
if (error instanceof AggregateError) {
276+
// All promises were rejected
277+
for (const err of error.errors) {
278+
logger(`Failed to fetch tx ${txHash}: ${err}`);
225279
}
226-
throw new Error(JSON.stringify(json));
227-
} catch (error) {
228-
logger(`Failed to fetch tx ${txHash} from ${url}: ${error}`);
280+
throw new Error(`Failed to fetch transaction ${txHash}`);
229281
}
282+
throw error;
230283
}
231-
232-
throw new Error(`JSON-RPC is unreachable`);
233284
}
234285
}

0 commit comments

Comments
 (0)