diff --git a/packages/sdk/explorer/src/sourcify.test.ts b/packages/sdk/explorer/src/sourcify.test.ts index 08d0cf481..1b0680b4b 100644 --- a/packages/sdk/explorer/src/sourcify.test.ts +++ b/packages/sdk/explorer/src/sourcify.test.ts @@ -56,6 +56,22 @@ describe('sourcify helpers', () => { }) describe('when a full match does not exist', () => { + describe('when contract has been verified on celoscan', () => { + it('returns the metadata from celoscan', async () => { + fetchMock + .get( + `https://repo.sourcify.dev/contracts/full_match/42220/${address}/metadata.json`, + 400 + ) + .get( + `https://api.celoscan.io/api?module=contract&action=getabi&address=${address}`, + CONTRACT_VIA_CELLOSCAN + ) + const result = await fetchMetadata(connection, address) + expect(result).toBeInstanceOf(Metadata) + expect(result?.abi).toBeDefined() + }) + }) describe('but a partial match exists', () => { it('returns the metadata from the partial match', async () => { fetchMock @@ -67,6 +83,11 @@ describe('sourcify helpers', () => { `https://repo.sourcify.dev/contracts/partial_match/42220/${address}/metadata.json`, {} ) + .get(`https://api.celoscan.io/api?module=contract&action=getabi&address=${address}`, { + status: '0', + message: 'NOTOK', + }) + const metadata = await fetchMetadata(connection, address) expect(metadata).toBeInstanceOf(Metadata) }) @@ -83,6 +104,10 @@ describe('sourcify helpers', () => { `https://repo.sourcify.dev/contracts/partial_match/42220/${address}/metadata.json`, 400 ) + .get(`https://api.celoscan.io/api?module=contract&action=getabi&address=${address}`, { + status: '0', + message: 'NOTOK', + }) const metadata = await fetchMetadata(connection, address) expect(metadata).toEqual(null) }) @@ -93,6 +118,7 @@ describe('sourcify helpers', () => { describe('Metadata', () => { describe('get abi', () => { it('returns the abi when it finds it', () => { + // @ts-expect-error -- test is purposefully incorrect const metadata = new Metadata(connection, address, { output: { abi: [{}] } }) const abi = metadata.abi expect(abi).not.toBeNull() @@ -100,6 +126,7 @@ describe('sourcify helpers', () => { }) it('returns null when there is no abi', () => { + // @ts-expect-error -- test is purposefully incorrect const metadata = new Metadata(connection, address, { output: { other: [{}] } }) const abi = metadata.abi expect(abi).toBeNull() @@ -109,6 +136,7 @@ describe('sourcify helpers', () => { describe('get contractName', () => { describe('when the structure does not contain it', () => { it('returns null', () => { + // @ts-expect-error -- test is purposefully incorrect const metadata = new Metadata(connection, address, { output: { abi: [{}] } }) const name = metadata.contractName expect(name).toBeNull() @@ -251,3 +279,10 @@ describe('sourcify helpers', () => { }) }) }) + +const CONTRACT_VIA_CELLOSCAN = { + status: '1', + message: 'OK', + result: + '[{"name":"Transfer","inputs":[{"name":"sender","type":"address","indexed":true},{"name":"receiver","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"Approval","inputs":[{"name":"owner","type":"address","indexed":true},{"name":"spender","type":"address","indexed":true},{"name":"value","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"TokenExchange","inputs":[{"name":"buyer","type":"address","indexed":true},{"name":"sold_id","type":"int128","indexed":false},{"name":"tokens_sold","type":"uint256","indexed":false},{"name":"bought_id","type":"int128","indexed":false},{"name":"tokens_bought","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"AddLiquidity","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"invariant","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidity","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidityOne","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amount","type":"uint256","indexed":false},{"name":"coin_amount","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RemoveLiquidityImbalance","inputs":[{"name":"provider","type":"address","indexed":true},{"name":"token_amounts","type":"uint256[2]","indexed":false},{"name":"fees","type":"uint256[2]","indexed":false},{"name":"invariant","type":"uint256","indexed":false},{"name":"token_supply","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"RampA","inputs":[{"name":"old_A","type":"uint256","indexed":false},{"name":"new_A","type":"uint256","indexed":false},{"name":"initial_time","type":"uint256","indexed":false},{"name":"future_time","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"name":"StopRampA","inputs":[{"name":"A","type":"uint256","indexed":false},{"name":"t","type":"uint256","indexed":false}],"anonymous":false,"type":"event"},{"stateMutability":"nonpayable","type":"constructor","inputs":[],"outputs":[]},{"stateMutability":"nonpayable","type":"function","name":"initialize","inputs":[{"name":"_name","type":"string"},{"name":"_symbol","type":"string"},{"name":"_coins","type":"address[4]"},{"name":"_rate_multipliers","type":"uint256[4]"},{"name":"_A","type":"uint256"},{"name":"_fee","type":"uint256"}],"outputs":[],"gas":516829},{"stateMutability":"view","type":"function","name":"decimals","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":390},{"stateMutability":"nonpayable","type":"function","name":"transfer","inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"gas":79005},{"stateMutability":"nonpayable","type":"function","name":"transferFrom","inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"gas":116985},{"stateMutability":"nonpayable","type":"function","name":"approve","inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"gas":39211},{"stateMutability":"nonpayable","type":"function","name":"permit","inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_deadline","type":"uint256"},{"name":"_v","type":"uint8"},{"name":"_r","type":"bytes32"},{"name":"_s","type":"bytes32"}],"outputs":[{"name":"","type":"bool"}],"gas":102281},{"stateMutability":"view","type":"function","name":"get_balances","inputs":[],"outputs":[{"name":"","type":"uint256[2]"}],"gas":4782},{"stateMutability":"view","type":"function","name":"admin_fee","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":570},{"stateMutability":"view","type":"function","name":"A","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":10508},{"stateMutability":"view","type":"function","name":"A_precise","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":10508},{"stateMutability":"view","type":"function","name":"get_virtual_price","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":649135},{"stateMutability":"view","type":"function","name":"calc_token_amount","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_is_deposit","type":"bool"}],"outputs":[{"name":"","type":"uint256"}],"gas":1284256},{"stateMutability":"nonpayable","type":"function","name":"add_liquidity","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_min_mint_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":2144257},{"stateMutability":"nonpayable","type":"function","name":"add_liquidity","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_min_mint_amount","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":2144257},{"stateMutability":"view","type":"function","name":"get_dy","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"dx","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":995830},{"stateMutability":"nonpayable","type":"function","name":"exchange","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"_dx","type":"uint256"},{"name":"_min_dy","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":1150187},{"stateMutability":"nonpayable","type":"function","name":"exchange","inputs":[{"name":"i","type":"int128"},{"name":"j","type":"int128"},{"name":"_dx","type":"uint256"},{"name":"_min_dy","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":1150187},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"_min_amounts","type":"uint256[2]"}],"outputs":[{"name":"","type":"uint256[2]"}],"gas":241198},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"_min_amounts","type":"uint256[2]"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256[2]"}],"gas":241198},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_imbalance","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_max_burn_amount","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":2144337},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_imbalance","inputs":[{"name":"_amounts","type":"uint256[2]"},{"name":"_max_burn_amount","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":2144337},{"stateMutability":"view","type":"function","name":"calc_withdraw_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"}],"outputs":[{"name":"","type":"uint256"}],"gas":1229},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"},{"name":"_min_received","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":1535032},{"stateMutability":"nonpayable","type":"function","name":"remove_liquidity_one_coin","inputs":[{"name":"_burn_amount","type":"uint256"},{"name":"i","type":"int128"},{"name":"_min_received","type":"uint256"},{"name":"_receiver","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":1535032},{"stateMutability":"nonpayable","type":"function","name":"ramp_A","inputs":[{"name":"_future_A","type":"uint256"},{"name":"_future_time","type":"uint256"}],"outputs":[],"gas":161164},{"stateMutability":"nonpayable","type":"function","name":"stop_ramp_A","inputs":[],"outputs":[],"gas":157387},{"stateMutability":"view","type":"function","name":"admin_balances","inputs":[{"name":"i","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":7829},{"stateMutability":"nonpayable","type":"function","name":"withdraw_admin_fees","inputs":[],"outputs":[],"gas":28911},{"stateMutability":"pure","type":"function","name":"version","inputs":[],"outputs":[{"name":"","type":"string"}],"gas":6677},{"stateMutability":"view","type":"function","name":"coins","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"address"}],"gas":3225},{"stateMutability":"view","type":"function","name":"balances","inputs":[{"name":"arg0","type":"uint256"}],"outputs":[{"name":"","type":"uint256"}],"gas":3255},{"stateMutability":"view","type":"function","name":"fee","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3240},{"stateMutability":"view","type":"function","name":"initial_A","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3270},{"stateMutability":"view","type":"function","name":"future_A","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3300},{"stateMutability":"view","type":"function","name":"initial_A_time","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3330},{"stateMutability":"view","type":"function","name":"future_A_time","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3360},{"stateMutability":"view","type":"function","name":"name","inputs":[],"outputs":[{"name":"","type":"string"}],"gas":13679},{"stateMutability":"view","type":"function","name":"symbol","inputs":[],"outputs":[{"name":"","type":"string"}],"gas":11438},{"stateMutability":"view","type":"function","name":"balanceOf","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":3716},{"stateMutability":"view","type":"function","name":"allowance","inputs":[{"name":"arg0","type":"address"},{"name":"arg1","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":4012},{"stateMutability":"view","type":"function","name":"totalSupply","inputs":[],"outputs":[{"name":"","type":"uint256"}],"gas":3510},{"stateMutability":"view","type":"function","name":"DOMAIN_SEPARATOR","inputs":[],"outputs":[{"name":"","type":"bytes32"}],"gas":3540},{"stateMutability":"view","type":"function","name":"nonces","inputs":[{"name":"arg0","type":"address"}],"outputs":[{"name":"","type":"uint256"}],"gas":3836}]', +} diff --git a/packages/sdk/explorer/src/sourcify.ts b/packages/sdk/explorer/src/sourcify.ts index ad572934f..adf514c3d 100644 --- a/packages/sdk/explorer/src/sourcify.ts +++ b/packages/sdk/explorer/src/sourcify.ts @@ -70,7 +70,7 @@ export class Metadata { private jsonInterfaceMethodToString: (item: AbiItem) => string private address: Address - constructor(connection: Connection, address: Address, response: any) { + constructor(connection: Connection, address: Address, response: MetadataResponse) { this.abiCoder = connection.getAbiCoder() this.response = response as MetadataResponse @@ -169,8 +169,8 @@ export class Metadata { } /** - * Fetch the sourcify response and instantiate a Metadata wrapper class around it. - * Try a full_match but fallback to partial_match when not strict. + * Fetch the sourcify or celoscan response and instantiate a Metadata wrapper class around it. + * Try a full_match but fallback to partial_match when not strict. (only valid for sourcify) * @param connection @celo/connect instance * @param contract the address of the contract to query * @param strict only allow full matches https://docs.sourcify.dev/docs/full-vs-partial-match/ @@ -184,7 +184,14 @@ export async function fetchMetadata( const fullMatchMetadata = await querySourcify(connection, 'full_match', contract) if (fullMatchMetadata !== null) { return fullMatchMetadata - } else if (strict) { + } + + const fullMatchFromCeloScan = await queryCeloScan(connection, contract) + if (fullMatchFromCeloScan !== null) { + return fullMatchFromCeloScan + } + + if (strict) { return null } else { return querySourcify(connection, 'partial_match', contract) @@ -208,7 +215,42 @@ async function querySourcify( `https://repo.sourcify.dev/contracts/${matchType}/${chainID}/${contract}/metadata.json` ) if (resp.ok) { - return new Metadata(connection, contract, await resp.json()) + return new Metadata(connection, contract, (await resp.json()) as MetadataResponse) + } + return null +} + +type CeloScanResponse = + | { + status: '1' + message: 'OK' + result: string // JSON stringified ABI + } + | { + status: '0' + message: 'NOTOK' + result: string // Error message + } + +/** + * Fetch the celoScan response and instantiate a Metadata wrapper class around it. + * @param connection @celo/connect instance + * @param contract the address of the contract to query + * @returns Metadata + */ +export async function queryCeloScan( + connection: Connection, + contract: Address +): Promise { + const resp = await fetch( + `https://api.celoscan.io/api?module=contract&action=getabi&address=${contract}` + ) + if (resp.ok) { + const json = (await resp.json()) as CeloScanResponse + if (json.message === 'OK') { + const data = JSON.parse(json.result) as AbiItem[] + return new Metadata(connection, contract, { output: { abi: data } }) + } } return null }