11import { Effect } from 'effect'
22import * as RequestModel from './request-model.js'
33
4+ type FetchResult =
5+ | { type : 'success' ; data : RequestModel . ContractABI [ ] }
6+ | { type : 'missing' ; reason : string }
7+ | { type : 'error' ; cause : unknown }
8+
49const endpoints : { [ k : number ] : string } = {
510 // all mainnet
611 1 : 'https://api.etherscan.io/api' ,
@@ -50,35 +55,59 @@ const endpoints: { [k: number]: string } = {
5055async function fetchContractABI (
5156 { address, chainId } : RequestModel . GetContractABIStrategyParams ,
5257 config ?: { apikey ?: string ; endpoint ?: string } ,
53- ) : Promise < RequestModel . ContractABI [ ] > {
54- const endpoint = config ?. endpoint ?? endpoints [ chainId ]
55- const params : Record < string , string > = {
56- module : 'contract' ,
57- action : 'getabi' ,
58- address,
59- }
58+ ) : Promise < FetchResult > {
59+ try {
60+ const endpoint = config ?. endpoint ?? endpoints [ chainId ]
61+ const params : Record < string , string > = {
62+ module : 'contract' ,
63+ action : 'getabi' ,
64+ address,
65+ }
6066
61- if ( config ?. apikey ) {
62- params [ 'apikey' ] = config . apikey
63- }
67+ if ( config ?. apikey ) {
68+ params [ 'apikey' ] = config . apikey
69+ }
6470
65- const searchParams = new URLSearchParams ( params )
71+ const searchParams = new URLSearchParams ( params )
6672
67- const response = await fetch ( `${ endpoint } ?${ searchParams . toString ( ) } ` )
68- const json = ( await response . json ( ) ) as { status : string ; result : string }
73+ const response = await fetch ( `${ endpoint } ?${ searchParams . toString ( ) } ` )
74+ const json = ( await response . json ( ) ) as { status : string ; result : string }
6975
70- if ( json . status === '1' ) {
71- return [
72- {
73- type : 'address' ,
74- address,
75- chainID : chainId ,
76- abi : json . result ,
77- } ,
78- ]
79- }
76+ if ( json . status === '1' ) {
77+ return {
78+ type : 'success' ,
79+ data : [
80+ {
81+ type : 'address' ,
82+ address,
83+ chainID : chainId ,
84+ abi : json . result ,
85+ } ,
86+ ] ,
87+ }
88+ }
8089
81- throw new Error ( `Failed to fetch ABI for ${ address } on chain ${ chainId } ` )
90+ // If the API request was successful but no ABI was found
91+ if (
92+ json . status === '0' &&
93+ ( json . result === 'Contract source code not verified' || json . result . includes ( 'not verified' ) )
94+ ) {
95+ return {
96+ type : 'missing' ,
97+ reason : `No verified ABI found: ${ json . result } ` ,
98+ }
99+ }
100+
101+ return {
102+ type : 'error' ,
103+ cause : json ,
104+ }
105+ } catch ( error ) {
106+ return {
107+ type : 'error' ,
108+ cause : error ,
109+ }
110+ }
82111}
83112
84113export const EtherscanStrategyResolver = ( config ?: {
@@ -90,9 +119,25 @@ export const EtherscanStrategyResolver = (config?: {
90119 type : 'address' ,
91120 resolver : ( req : RequestModel . GetContractABIStrategyParams ) =>
92121 Effect . withSpan (
93- Effect . tryPromise ( {
94- try : ( ) => fetchContractABI ( req , config ) ,
95- catch : ( ) => new RequestModel . ResolveStrategyABIError ( 'etherscan' , req . address , req . chainId ) ,
122+ Effect . gen ( function * ( ) {
123+ const result = yield * Effect . promise ( ( ) => fetchContractABI ( req , config ) )
124+
125+ if ( result . type === 'success' ) {
126+ return result . data
127+ } else if ( result . type === 'missing' ) {
128+ return yield * Effect . fail (
129+ new RequestModel . MissingABIStrategyError (
130+ req . address ,
131+ req . chainId ,
132+ 'etherscan-strategy' ,
133+ undefined ,
134+ undefined ,
135+ result . reason ,
136+ ) ,
137+ )
138+ } else {
139+ return yield * Effect . fail ( new RequestModel . ResolveStrategyABIError ( 'etherscan' , req . address , req . chainId ) )
140+ }
96141 } ) ,
97142 'AbiStrategy.EtherscanStrategyResolver' ,
98143 { attributes : { chainId : req . chainId , address : req . address } } ,
0 commit comments