@@ -6,13 +6,23 @@ import ABI from '../protocols/ethereum/abi.js';
6
6
7
7
const logger = debugFactory ( 'graph-cli:contract-service' ) ;
8
8
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
+ }
9
17
export class ContractService {
10
18
constructor ( private registry : NetworksRegistry ) { }
11
19
12
20
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
+ ) ;
16
26
let json : any = { } ;
17
27
18
28
if ( result . ok ) {
@@ -30,10 +40,13 @@ export class ContractService {
30
40
result . url ,
31
41
json ,
32
42
) ;
43
+ if ( ! result . ok ) {
44
+ throw new Error ( `${ result . status } ${ result . statusText } ` ) ;
45
+ }
33
46
if ( json . message ) {
34
- throw new Error ( `${ json . message ?? '' } - ${ json . result ?? '' } ` ) ;
47
+ throw new Error ( `${ json . message } - ${ json . result ?? '' } ` ) ;
35
48
}
36
- return null ;
49
+ throw new Error ( 'Empty response' ) ;
37
50
}
38
51
39
52
// replace {api_key} with process.env[api_key]
@@ -71,84 +84,107 @@ export class ContractService {
71
84
72
85
async getABI ( ABICtor : typeof ABI , networkId : string , address : string ) {
73
86
const urls = this . getEtherscanUrls ( networkId ) ;
74
- const errors : string [ ] = [ ] ;
75
87
if ( ! urls . length ) {
76
88
throw new Error ( `No contract API available for ${ networkId } in the registry` ) ;
77
89
}
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 } ` ) ;
86
109
}
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' } ` ) ;
91
111
}
112
+ throw error ;
92
113
}
93
-
94
- throw new Error ( errors ?. [ 0 ] ) ;
95
114
}
96
115
97
116
async getStartBlock ( networkId : string , address : string ) : Promise < string > {
98
117
const urls = this . getEtherscanUrls ( networkId ) ;
99
118
if ( ! urls . length ) {
100
119
throw new Error ( `No contract API available for ${ networkId } in the registry` ) ;
101
120
}
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 } ` ) ;
118
148
}
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` ) ;
122
150
}
151
+ throw error ;
123
152
}
124
-
125
- throw new Error ( `Failed to fetch deploy contract transaction for ${ address } ` ) ;
126
153
}
127
154
128
155
async getContractName ( networkId : string , address : string ) : Promise < string > {
129
156
const urls = this . getEtherscanUrls ( networkId ) ;
130
157
if ( ! urls . length ) {
131
158
throw new Error ( `No contract API available for ${ networkId } in the registry` ) ;
132
159
}
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
+ }
142
174
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 } ` ) ;
144
183
}
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` ) ;
148
185
}
186
+ throw error ;
149
187
}
150
-
151
- throw new Error ( `Failed to fetch contract name for ${ address } ` ) ;
152
188
}
153
189
154
190
async getFromSourcify (
@@ -168,32 +204,35 @@ export class ContractService {
168
204
169
205
const chainId = network . caip2Id . split ( ':' ) [ 1 ] ;
170
206
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
+ ) ;
174
212
if ( resp . status === 404 ) throw new Error ( `Sourcify API says contract is not verified` ) ;
175
213
if ( ! resp . ok ) throw new Error ( `Sourcify API returned status ${ resp . status } ` ) ;
176
214
const json : {
177
215
abi : any [ ] ;
178
216
compilation : { name : string } ;
179
217
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
+ } ) ;
188
221
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` ) ;
194
224
}
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' ) ;
195
230
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
+ } ;
197
236
} catch ( error ) {
198
237
logger ( `Failed to fetch from Sourcify: ${ error } ` ) ;
199
238
}
@@ -206,29 +245,41 @@ export class ContractService {
206
245
if ( ! urls . length ) {
207
246
throw new Error ( `No JSON-RPC available for ${ networkId } in the registry` ) ;
208
247
}
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
- } ) ;
221
248
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 } ` ) ;
225
279
}
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 } ` ) ;
229
281
}
282
+ throw error ;
230
283
}
231
-
232
- throw new Error ( `JSON-RPC is unreachable` ) ;
233
284
}
234
285
}
0 commit comments