diff --git a/.changeset/violet-students-shop.md b/.changeset/violet-students-shop.md new file mode 100644 index 000000000..a6dcc96ab --- /dev/null +++ b/.changeset/violet-students-shop.md @@ -0,0 +1,10 @@ +--- +'@xchainjs/xchain-evm-providers': patch +'@xchainjs/xchain-arbitrum': patch +'@xchainjs/xchain-ethereum': patch +'@xchainjs/xchain-avax': patch +'@xchainjs/xchain-base': patch +'@xchainjs/xchain-bsc': patch +--- + +Support etherscan v2 diff --git a/packages/xchain-arbitrum/__e2e__/arbitrum.e2e.ts b/packages/xchain-arbitrum/__e2e__/arbitrum.e2e.ts index 899731b87..abcd53815 100644 --- a/packages/xchain-arbitrum/__e2e__/arbitrum.e2e.ts +++ b/packages/xchain-arbitrum/__e2e__/arbitrum.e2e.ts @@ -32,9 +32,9 @@ const TestnetUSDCAsset: TokenAsset = { describe('Arbitrum', () => { it('should fetch Arbitrum balances', async () => { - const address = testnetClient.getAddress(0) + const address = '0x46545017fa98CA2efeF277c90B4c0044ca913596' console.log(address) - const balances = await testnetClient.getBalance(address) + const balances = await mainnetClient.getBalance(address, []) balances.forEach((bal: Balance) => { console.log(`${assetToString(bal.asset)} = ${bal.amount.amount()}`) }) @@ -42,20 +42,20 @@ describe('Arbitrum', () => { }) it('should fetch arbitrum txs', async () => { const address = '0x007ab5199b6c57f7aa51bc3d0604a43505501a0c' - const txPage = await testnetClient.getTransactions({ address }) + const txPage = await mainnetClient.getTransactions({ address }) console.log(JSON.stringify(txPage, null, 2)) expect(txPage.total).toBeGreaterThan(0) expect(txPage.txs.length).toBeGreaterThan(0) }) it('should fetch arbitrum erc20 txs', async () => { - const address = '0xe77872fb49750e6ae361fc13aa67397637ddcf5d' - const txPage = await testnetClient.getTransactions({ address, asset: '0x179522635726710dd7d2035a81d856de4aa7836c' }) + const address = '0x9f0b60CD0FCfE9828a92c2F6f6E6B4Cf8DAb003a' + const txPage = await mainnetClient.getTransactions({ address, asset: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' }) console.log(JSON.stringify(txPage, null, 2)) expect(txPage.total).toBeGreaterThan(0) expect(txPage.txs.length).toBeGreaterThan(0) }) it('should fetch arbitrum transfer transaction', async () => { - const txId = '0x1c3b135fe8f108b57973aee6f567aca4219ae5ec903461ac427d6ea71d25f3fb' + const txId = '0x11bbb59714a8056e2af018692f6153b0514514fe9b9edd23d94d4220821159da' const tx = await testnetClient.getTransactionData(txId) console.log(JSON.stringify(tx, null, 2)) const amount = assetToBase(assetAmount('0.001', 18)) diff --git a/packages/xchain-arbitrum/src/const.ts b/packages/xchain-arbitrum/src/const.ts index 6ee0f973f..df90fd707 100644 --- a/packages/xchain-arbitrum/src/const.ts +++ b/packages/xchain-arbitrum/src/const.ts @@ -1,7 +1,7 @@ // Import necessary modules and classes from external packages and files import { ExplorerProvider, Network } from '@xchainjs/xchain-client' import { EVMClientParams } from '@xchainjs/xchain-evm' -import { EtherscanProvider } from '@xchainjs/xchain-evm-providers' +import { EtherscanProviderV2 } from '@xchainjs/xchain-evm-providers' import { Asset, AssetType, TokenAsset } from '@xchainjs/xchain-util' import { BigNumber, ethers } from 'ethers' @@ -23,6 +23,7 @@ export const AssetARB: TokenAsset = { } // Define JSON-RPC providers for mainnet and testnet +// Ankr api key const ARBITRUM_MAINNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider('https://arb1.arbitrum.io/rpc') const ARBITRUM_TESTNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider('https://goerli-rollup.arbitrum.io/rpc') @@ -34,22 +35,24 @@ const ethersJSProviders = { } // Define online providers (Etherscan) for mainnet and testnet -const ARB_ONLINE_PROVIDER_MAINNET = new EtherscanProvider( +const ARB_ONLINE_PROVIDER_MAINNET = new EtherscanProviderV2( ARBITRUM_MAINNET_ETHERS_PROVIDER, - 'https://api.arbiscan.io', - process.env.ARBISCAN_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', ARBChain, AssetAETH, 18, + 42161, ) -const ARB_ONLINE_PROVIDER_TESTNET = new EtherscanProvider( +const ARB_ONLINE_PROVIDER_TESTNET = new EtherscanProviderV2( ARBITRUM_TESTNET_ETHERS_PROVIDER, - 'https://api-goerli.arbiscan.io', - process.env.ARBISCAN_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', ARBChain, AssetAETH, 18, + 421614, ) // Define providers for different networks diff --git a/packages/xchain-avax/__e2e__/avax-client.e2e.ts b/packages/xchain-avax/__e2e__/avax-client.e2e.ts index f2c556c8d..23a95fafd 100644 --- a/packages/xchain-avax/__e2e__/avax-client.e2e.ts +++ b/packages/xchain-avax/__e2e__/avax-client.e2e.ts @@ -1,6 +1,5 @@ import { Balance, Network, TxType } from '@xchainjs/xchain-client' import { ApproveParams, EstimateApproveParams, IsApprovedParams } from '@xchainjs/xchain-evm' -import { CovalentProvider, EvmOnlineDataProvider } from '@xchainjs/xchain-evm-providers' import { AssetType, TokenAsset, assetAmount, assetToBase, assetToString, baseAmount } from '@xchainjs/xchain-util' import AvaxClient from '../src' @@ -17,46 +16,48 @@ const assetRIP: TokenAsset = { type: AssetType.TOKEN, } -const AVAX_ONLINE_PROVIDER_TESTNET = new CovalentProvider( - process.env.COVALENT_API_KEY as string, - AVAXChain, - 43113, - AssetAVAX, - 18, -) - -const AVAX_ONLINE_PROVIDER_MAINNET = new CovalentProvider( - process.env.COVALENT_API_KEY as string, - AVAXChain, - 43114, - AssetAVAX, - 18, -) - -const avaxProviders = { - [Network.Mainnet]: AVAX_ONLINE_PROVIDER_MAINNET, - [Network.Testnet]: AVAX_ONLINE_PROVIDER_TESTNET, - [Network.Stagenet]: AVAX_ONLINE_PROVIDER_MAINNET, -} - -const fakeProviders = { - [Network.Mainnet]: {} as EvmOnlineDataProvider, - [Network.Testnet]: {} as EvmOnlineDataProvider, - [Network.Stagenet]: {} as EvmOnlineDataProvider, -} - -defaultAvaxParams.network = Network.Testnet +// const AVAX_ONLINE_PROVIDER_TESTNET = new CovalentProvider( +// process.env.COVALENT_API_KEY as string, +// AVAXChain, +// 43113, +// AssetAVAX, +// 18, +// ) + +// const AVAX_ONLINE_PROVIDER_MAINNET = new CovalentProvider( +// process.env.COVALENT_API_KEY as string, +// AVAXChain, +// 43114, +// AssetAVAX, +// 18, +// ) + +// const avaxProviders = { +// [Network.Mainnet]: AVAX_ONLINE_PROVIDER_MAINNET, +// [Network.Testnet]: AVAX_ONLINE_PROVIDER_TESTNET, +// [Network.Stagenet]: AVAX_ONLINE_PROVIDER_MAINNET, +// } + +// const fakeProviders = { +// [Network.Mainnet]: {} as EvmOnlineDataProvider, +// [Network.Testnet]: {} as EvmOnlineDataProvider, +// [Network.Stagenet]: {} as EvmOnlineDataProvider, +// } + +defaultAvaxParams.network = Network.Mainnet defaultAvaxParams.phrase = process.env.TESTNET_PHRASE // eslint-disable-next-line @typescript-eslint/no-explicit-any -defaultAvaxParams.dataProviders = [fakeProviders as any, avaxProviders] -const client = new AvaxClient(defaultAvaxParams) +// defaultAvaxParams.dataProviders = [fakeProviders as any, avaxProviders] +const client = new AvaxClient({ + ...defaultAvaxParams, +}) function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } describe('xchain-evm (Avax) Integration Tests', () => { it('should fetch avax balances', async () => { - const address = await client.getAddressAsync(0) + const address = '0x09383137C1eEe3E1A8bc781228E4199f6b4A9bbf' console.log(address) const balances = await client.getBalance(address) balances.forEach((bal: Balance) => { @@ -72,7 +73,7 @@ describe('xchain-evm (Avax) Integration Tests', () => { expect(txPage.txs.length).toBeGreaterThan(0) }) it('should fetch single avax transfer tx', async () => { - const txId = '0x206d2300e57d0c23e48b8c4cc4af9c87abf33e2f406ac2265915b3d7b0e131e2' + const txId = '0x4dab51e68d03df97aaf3c8fd9afa3026f6ca7531f79b11a0c0ed39df6d0119e9' const tx = await client.getTransactionData(txId) console.log(JSON.stringify(tx, null, 2)) const amount = assetToBase(assetAmount('0.01', 18)) diff --git a/packages/xchain-avax/src/const.ts b/packages/xchain-avax/src/const.ts index eafad03d4..c4821a449 100644 --- a/packages/xchain-avax/src/const.ts +++ b/packages/xchain-avax/src/const.ts @@ -1,7 +1,7 @@ // Import necessary modules and classes from external packages and files import { ExplorerProvider, Network } from '@xchainjs/xchain-client' import { EVMClientParams } from '@xchainjs/xchain-evm' -import { EtherscanProvider, RoutescanProvider } from '@xchainjs/xchain-evm-providers' +import { EtherscanProviderV2, RoutescanProvider } from '@xchainjs/xchain-evm-providers' import { Asset, AssetType } from '@xchainjs/xchain-util' import { BigNumber, ethers } from 'ethers' @@ -19,8 +19,15 @@ const ankrApiKey = process.env.ANKR_API_KEY // Define JSON-RPC providers for mainnet and testnet const AVALANCHE_MAINNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider( `https://rpc.ankr.com/avalanche/${ankrApiKey}`, + { name: 'avalanche', chainId: 43114 }, +) +const AVALANCHE_TESTNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider( + `https://rpc.ankr.com/avalanche_fuji/${ankrApiKey}`, + { + name: 'fuji', + chainId: 43113, + }, ) -const AVALANCHE_TESTNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider('https://rpc.ankr.com/avalanche_fuji') // Define ethers providers for different networks const ethersJSProviders = { @@ -30,21 +37,23 @@ const ethersJSProviders = { } // Define online providers (Etherscan) for mainnet and testnet -const AVAX_ONLINE_PROVIDER_TESTNET = new EtherscanProvider( +const AVAX_ONLINE_PROVIDER_TESTNET = new EtherscanProviderV2( AVALANCHE_TESTNET_ETHERS_PROVIDER, - 'https://api-testnet.snowtrace.io', - process.env.SNOWTRACE_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', AVAXChain, AssetAVAX, 18, + 43113, ) -const AVAX_ONLINE_PROVIDER_MAINNET = new EtherscanProvider( +const AVAX_ONLINE_PROVIDER_MAINNET = new EtherscanProviderV2( AVALANCHE_MAINNET_ETHERS_PROVIDER, - 'https://api.snowtrace.io', - process.env.SNOWTRACE_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', AVAXChain, AssetAVAX, 18, + 43114, ) // Define providers for different networks diff --git a/packages/xchain-base/__e2e__/base.e2e.ts b/packages/xchain-base/__e2e__/base.e2e.ts index 41da3bbd3..dd058391e 100644 --- a/packages/xchain-base/__e2e__/base.e2e.ts +++ b/packages/xchain-base/__e2e__/base.e2e.ts @@ -20,7 +20,7 @@ const mainnetClient = new BaseClient({ const testnetClient = new BaseClient({ ...defaultBaseParams, phrase: process.env.TESTNET_PHRASE, - network: Network.Mainnet, + network: Network.Testnet, }) const MainnetUSDTAsset: TokenAsset = { @@ -40,7 +40,7 @@ describe('Base', () => { it('should fetch Base balances', async () => { const address = testnetClient.getAddress(0) console.log(address) - const balances = await testnetClient.getBalance('0x585142ebBA458B681caea61Bb178E529EdAd23f4') + const balances = await testnetClient.getBalance('0x537b2331DdaA849e62756ab7d32A6749e83443aE', []) balances.forEach((bal: Balance) => { console.log(`${assetToString(bal.asset)} = ${bal.amount.amount()}`) }) diff --git a/packages/xchain-base/src/const.ts b/packages/xchain-base/src/const.ts index 866917dbb..1d6881fb1 100644 --- a/packages/xchain-base/src/const.ts +++ b/packages/xchain-base/src/const.ts @@ -1,7 +1,7 @@ // Import necessary modules and classes from external packages and files import { ExplorerProvider, Network } from '@xchainjs/xchain-client' import { EVMClientParams } from '@xchainjs/xchain-evm' -import { EtherscanProvider } from '@xchainjs/xchain-evm-providers' +import { EtherscanProviderV2 } from '@xchainjs/xchain-evm-providers' import { Asset, AssetType } from '@xchainjs/xchain-util' import { BigNumber, ethers } from 'ethers' @@ -26,22 +26,24 @@ const ethersJSProviders = { } // Define online providers (Etherscan) for mainnet and testnet -const BASE_ONLINE_PROVIDER_MAINNET = new EtherscanProvider( +const BASE_ONLINE_PROVIDER_MAINNET = new EtherscanProviderV2( BASE_MAINNET_ETHERS_PROVIDER, - 'https://api.basescan.org', - process.env.BASE_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', BASEChain, AssetBETH, 18, + 8453, ) -const BASE_ONLINE_PROVIDER_TESTNET = new EtherscanProvider( +const BASE_ONLINE_PROVIDER_TESTNET = new EtherscanProviderV2( BASE_TESTNET_ETHERS_PROVIDER, - 'https://api-sepolia.basescan.org', - process.env.BASE_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', BASEChain, AssetBETH, 18, + 84532, ) // Define providers for different networks diff --git a/packages/xchain-bsc/__e2e__/bsc-client.e2e.ts b/packages/xchain-bsc/__e2e__/bsc-client.e2e.ts index d8f310404..fb0a16890 100644 --- a/packages/xchain-bsc/__e2e__/bsc-client.e2e.ts +++ b/packages/xchain-bsc/__e2e__/bsc-client.e2e.ts @@ -41,9 +41,9 @@ describe('xchain-evm (Bsc) Integration Tests', () => { expect(assetInfo).toEqual(correctAssetInfo) }) it('should fetch bsc balances', async () => { - const address = await clientTestnet.getAddressAsync(0) + const address = "0x1a3d9D7A717D64e6088aC937d5aAcDD3E20ca963" console.log(address) - const balances = await clientTestnet.getBalance(address) + const balances = await clientTestnet.getBalance(address, []) balances.forEach((bal: Balance) => { console.log(`${assetToString(bal.asset)} = ${bal.amount.amount()}`) }) diff --git a/packages/xchain-bsc/src/const.ts b/packages/xchain-bsc/src/const.ts index 1726423a2..2bb38f8f9 100644 --- a/packages/xchain-bsc/src/const.ts +++ b/packages/xchain-bsc/src/const.ts @@ -3,7 +3,7 @@ */ import { ExplorerProvider, Network } from '@xchainjs/xchain-client' // Importing ExplorerProvider and Network from xchain-client import { EVMClientParams } from '@xchainjs/xchain-evm' // Importing EVMClientParams from xchain-evm -import { EtherscanProvider } from '@xchainjs/xchain-evm-providers' // Importing EtherscanProvider from xchain-evm-providers +import { EtherscanProviderV2 } from '@xchainjs/xchain-evm-providers' // Importing EtherscanProvider from xchain-evm-providers import { Asset, AssetType } from '@xchainjs/xchain-util' // Importing Asset from xchain-util import { BigNumber, ethers } from 'ethers' // Importing BigNumber and ethers from ethers library @@ -49,21 +49,23 @@ const ethersJSProviders = { } // ONLINE providers -const BSC_ONLINE_PROVIDER_TESTNET = new EtherscanProvider( +const BSC_ONLINE_PROVIDER_TESTNET = new EtherscanProviderV2( BSC_TESTNET_ETHERS_PROVIDER, - 'https://api-testnet.bscscan.com', - process.env.BSCSCAN_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', BSCChain, AssetBSC, BSC_GAS_ASSET_DECIMAL, + 97, ) -const BSC_ONLINE_PROVIDER_MAINNET = new EtherscanProvider( +const BSC_ONLINE_PROVIDER_MAINNET = new EtherscanProviderV2( BSC_MAINNET_ETHERS_PROVIDER, - 'https://api.bscscan.com', - process.env.BSCSCAN_API_KEY || '', + 'https://api.etherscan.io/v2', + process.env.ETHERSCAN_API_KEY || '', BSCChain, AssetBSC, BSC_GAS_ASSET_DECIMAL, + 56, ) const bscProviders = { [Network.Mainnet]: BSC_ONLINE_PROVIDER_MAINNET, diff --git a/packages/xchain-ethereum/__e2e__/eth-client.e2e.ts b/packages/xchain-ethereum/__e2e__/eth-client.e2e.ts index 1fc62a0a7..3a4c31bc5 100644 --- a/packages/xchain-ethereum/__e2e__/eth-client.e2e.ts +++ b/packages/xchain-ethereum/__e2e__/eth-client.e2e.ts @@ -32,9 +32,9 @@ function delay(ms: number) { } describe('xchain-evm (Eth) Integration Tests', () => { it('should fetch eth balances', async () => { - const address = '0x26000cc95ab0886FE8439E53c73b1219Eba9DBCF' + const address = '0xBCB883aB7f1cAFC8C6C5fa7F5aF9d064cCE73AC1' console.log(address) - const balances = await clientTestnet.getBalance(address) + const balances = await clientTestnet.getBalance(address, []) balances.forEach((bal: Balance) => { console.log(`${assetToString(bal.asset)} = ${bal.amount.amount()}`) }) diff --git a/packages/xchain-ethereum/src/const.ts b/packages/xchain-ethereum/src/const.ts index a7bc7f200..471e503df 100644 --- a/packages/xchain-ethereum/src/const.ts +++ b/packages/xchain-ethereum/src/const.ts @@ -1,6 +1,6 @@ import { ExplorerProvider, Network } from '@xchainjs/xchain-client' import { EVMClientParams } from '@xchainjs/xchain-evm' -import { EtherscanProvider } from '@xchainjs/xchain-evm-providers' +import { EtherscanProviderV2 } from '@xchainjs/xchain-evm-providers' import { Asset, AssetType } from '@xchainjs/xchain-util' import { BigNumber, ethers } from 'ethers' @@ -31,11 +31,12 @@ export const AssetETH: Asset = { } // ===== Ethers providers ===== -// Mainnet ethers provider -const ETH_MAINNET_ETHERS_PROVIDER = new ethers.providers.EtherscanProvider('homestead', process.env.ETHERSCAN_API_KEY) -// Testnet ethers provider as per https://docs.ethers.org/v5/api/providers/api-providers/#EtherscanProvider +const ETH_MAINNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider('https://eth.llamarpc.com', 'homestead') const network = ethers.providers.getNetwork('sepolia') -const ETH_TESTNET_ETHERS_PROVIDER = new ethers.providers.EtherscanProvider(network, process.env.ETHERSCAN_API_KEY) +const ETH_TESTNET_ETHERS_PROVIDER = new ethers.providers.JsonRpcProvider( + 'https://ethereum-sepolia-rpc.publicnode.com', + network, +) // Object to map network to ethers providers const ethersJSProviders = { @@ -47,22 +48,24 @@ const ethersJSProviders = { // ===== ONLINE providers ===== // Testnet online provider -const ETH_ONLINE_PROVIDER_TESTNET = new EtherscanProvider( +const ETH_ONLINE_PROVIDER_TESTNET = new EtherscanProviderV2( ETH_TESTNET_ETHERS_PROVIDER, - 'https://api-sepolia.etherscan.io/', + 'https://api.etherscan.io/v2', process.env.ETHERSCAN_API_KEY || '', ETHChain, AssetETH, ETH_GAS_ASSET_DECIMAL, + 11155111, ) // Mainnet online provider -const ETH_ONLINE_PROVIDER_MAINNET = new EtherscanProvider( +const ETH_ONLINE_PROVIDER_MAINNET = new EtherscanProviderV2( ETH_MAINNET_ETHERS_PROVIDER, - 'https://api.etherscan.io/', + 'https://api.etherscan.io/v2', process.env.ETHERSCAN_API_KEY || '', ETHChain, AssetETH, ETH_GAS_ASSET_DECIMAL, + 1, ) // Object to map network to online providers const ethProviders = { diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/erc20.json b/packages/xchain-evm-providers/src/providers/etherscan-v2/erc20.json new file mode 100644 index 000000000..ca65e65e5 --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/erc20.json @@ -0,0 +1,229 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-types-v2.ts b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-types-v2.ts new file mode 100644 index 000000000..7fa6cf1d4 --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-types-v2.ts @@ -0,0 +1,61 @@ +export type GasOracleResponseV2 = { + LastBlock: string + SafeGasPrice: string + ProposeGasPrice: string + FastGasPrice: string +} + +export type TokenBalanceParamV2 = { + address: string + assetAddress: string + chainId: number +} + +export type TransactionHistoryParamV2 = { + address?: string + assetAddress?: string + page?: number + offset?: number + startblock?: number + endblock?: number + chainId: number +} + +export type TokenTransactionInfoV2 = { + blockNumber: string + timeStamp: string + hash: string + nonce: string + blockHash: string + from: string + contractAddress: string + to: string + value: string + tokenName: string + tokenSymbol: string + tokenDecimal: string + transactionIndex: string + gas: string + gasPrice: string + gasUsed: string + cumulativeGasUsed: string + input: string + confirmations: string +} + +export type ETHTransactionInfoV2 = { + blockNumber: string + timeStamp: string + hash: string + from: string + to: string + value: string + contractAddress: string + input: string + type: string + gas: string + gasUsed: string + traceId: string + isError: string + errCode: string +} diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-v2.ts b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-v2.ts new file mode 100644 index 000000000..cd12a9c58 --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-api-v2.ts @@ -0,0 +1,240 @@ +import { TxType } from '@xchainjs/xchain-client' +import { Asset, Chain, TokenAsset, assetFromString, baseAmount, bnOrZero } from '@xchainjs/xchain-util' +import axios from 'axios' +import { BigNumberish } from 'ethers' + +import { Tx } from '../../types' + +import { + ETHTransactionInfoV2, + GasOracleResponseV2, + TokenBalanceParamV2, + TokenTransactionInfoV2, + TransactionHistoryParamV2, +} from './etherscan-api-types-v2' +import { validateAddress } from './utils' + +const getApiKeyQueryParameter = (apiKey?: string): string => (!!apiKey ? `&apiKey=${apiKey}` : '') + +/** + * Filter self txs + * + * @returns {T[]} + * + **/ +export const filterSelfTxs = (txs: T[]): T[] => { + const filterTxs = txs.filter((tx) => tx.from !== tx.to) + let selfTxs = txs.filter((tx) => tx.from === tx.to) + while (selfTxs.length) { + const selfTx = selfTxs[0] + filterTxs.push(selfTx) + selfTxs = selfTxs.filter((tx) => tx.hash !== selfTx.hash) + } + + return filterTxs +} + +/** + * Check if the symbol is valid. + * + * @param {string|null|undefined} symbol + * @returns {boolean} `true` or `false`. + */ +export const validateSymbol = (symbol?: string | null): boolean => (symbol ? symbol.length >= 3 : false) + +/** + * Get transactions from ETH transaction + * + * @param {ETHTransactionInfo} tx + * @returns {Tx} The parsed transaction. + */ +export const getTxFromEthTransaction = (tx: ETHTransactionInfoV2, gasAsset: Asset, decimals: number): Tx => { + return { + asset: gasAsset, + from: [ + { + from: tx.from, + amount: baseAmount(tx.value, decimals), + }, + ], + to: [ + { + to: tx.to, + amount: baseAmount(tx.value, decimals), + }, + ], + date: new Date(parseInt(tx.timeStamp) * 1000), + type: TxType.Transfer, + hash: tx.hash, + } +} + +/** + * Get transactions from token tx + * + * @param {TokenTransactionInfo} tx + * @returns {Tx|null} The parsed transaction. + */ +export const getTxFromTokenTransaction = (tx: TokenTransactionInfoV2, chain: Chain, decimals: number): Tx | null => { + const decimal = parseInt(tx.tokenDecimal) || decimals + const symbol = tx.tokenSymbol + const address = tx.contractAddress + + if (validateSymbol(symbol) && validateAddress(address)) { + const tokenAsset = assetFromString(`${chain}.${symbol}-${address}`) as Asset | TokenAsset + if (tokenAsset) { + return { + asset: tokenAsset, + from: [ + { + from: tx.from, + amount: baseAmount(tx.value, decimal), + }, + ], + to: [ + { + to: tx.to, + amount: baseAmount(tx.value, decimal), + }, + ], + date: new Date(parseInt(tx.timeStamp) * 1000), + type: TxType.Transfer, + hash: tx.hash, + } + } + } + + return null +} + +/** + * SafeGasPrice, ProposeGasPrice And FastGasPrice returned in string-Gwei + * + * @see https://etherscan.io/apis#gastracker + * + * @param {string} baseUrl The etherscan node url. + * @param {number} chainId The chain id identifier https://docs.etherscan.io/etherscan-v2/getting-started/supported-chains. + * @param {string} apiKey The etherscan API key. (optional) + * @returns {GasOracleResponse} LastBlock, SafeGasPrice, ProposeGasPrice, FastGasPrice + */ +export const getGasOracle = async (baseUrl: string, chainId: number, apiKey?: string): Promise => { + const url = baseUrl + `/api?chainid=${chainId}&module=gastracker&action=gasoracle` + const result = (await axios.get(url + getApiKeyQueryParameter(apiKey))).data.result + + if (typeof result === 'string') throw Error(`Can not retrieve gasOracle: ${result}`) + + return result +} + +/** + * Get token balance + * + * @see https://etherscan.io/apis#tokens + * + * @param {string} baseUrl The etherscan node url. + * @param {string} address The address. + * @param {string} assetAddress The token contract address. + * @param {number} chainId The chain id identifier https://docs.etherscan.io/etherscan-v2/getting-started/supported-chains. + * @param {string} apiKey The etherscan API key. (optional) + * @returns {BigNumberish} The token balance + */ +export const getTokenBalance = async ({ + baseUrl, + address, + assetAddress, + chainId, + apiKey, +}: TokenBalanceParamV2 & { baseUrl: string; apiKey?: string }): Promise => { + const url = + baseUrl + + `/api?chainid=${chainId}&module=account&action=tokenbalance&contractaddress=${assetAddress}&address=${address}` + + return (await axios.get(url + getApiKeyQueryParameter(apiKey))).data.result +} + +/** + * Get ETH transaction history + * + * @see https://etherscan.io/apis#accounts + * + * @param {string} baseUrl The etherscan node url. + * @param {string} address The address. + * @param {TransactionHistoryParam} params The search options. + * @param {number} chainId The chain id identifier https://docs.etherscan.io/etherscan-v2/getting-started/supported-chains. + * @param {string} apiKey The etherscan API key. (optional) + * @returns {ETHTransactionInfo[]} The ETH transaction history + */ +export const getGasAssetTransactionHistory = async ({ + gasAsset, + gasDecimals, + baseUrl, + address, + page, + offset, + startblock, + endblock, + chainId, + apiKey, +}: TransactionHistoryParamV2 & { gasAsset: Asset; gasDecimals: number; baseUrl: string; apiKey?: string }): Promise< + Tx[] +> => { + let url = baseUrl + `/api?chainid=${chainId}&module=account&action=txlist&sort=desc` + getApiKeyQueryParameter(apiKey) + if (address) url += `&address=${address}` + if (offset) url += `&offset=${offset}` + if (page) url += `&page=${page}` + if (startblock) url += `&startblock=${startblock}` + if (endblock) url += `&endblock=${endblock}` + + const result = (await axios.get(url)).data.result + if (JSON.stringify(result).includes('Invalid API Key')) throw new Error('Invalid API Key') + if (typeof result !== 'object') throw new Error(result) + + return filterSelfTxs(result) + .filter((tx) => !bnOrZero(tx.value).isZero()) + .map((tx) => getTxFromEthTransaction(tx, gasAsset, gasDecimals)) +} + +/** + * Get token transaction history + * + * @see https://etherscan.io/apis#accounts + * + * @param {string} baseUrl The etherscan node url. + * @param {string} address The address. + * @param {TransactionHistoryParam} params The search options. + * @param {string} apiKey The etherscan API key. (optional) + * @returns {Tx[]} The token transaction history + */ +export const getTokenTransactionHistory = async ({ + gasDecimals, + baseUrl, + address, + assetAddress, + page, + offset, + startblock, + endblock, + chainId, + apiKey, + chain, +}: TransactionHistoryParamV2 & { gasDecimals: number; chain: Chain; baseUrl: string; apiKey?: string }): Promise< + Tx[] +> => { + let url = + baseUrl + `/api?chainid=${chainId}&module=account&action=tokentx&sort=desc` + getApiKeyQueryParameter(apiKey) + if (address) url += `&address=${address}` + if (assetAddress) url += `&contractaddress=${assetAddress}` + if (offset) url += `&offset=${offset}` + if (page) url += `&page=${page}` + if (startblock) url += `&startblock=${startblock}` + if (endblock) url += `&endblock=${endblock}` + const result = (await axios.get(url)).data.result + if (JSON.stringify(result).includes('Invalid API Key')) throw new Error('Invalid API Key') + + return filterSelfTxs(result) + .filter((tx) => !bnOrZero(tx.value).isZero()) + .reduce((acc, cur) => { + const tx = getTxFromTokenTransaction(cur, chain, gasDecimals) + return tx ? [...acc, tx] : acc + }, [] as Tx[]) +} diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-data-provider-v2.ts b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-data-provider-v2.ts new file mode 100644 index 000000000..816e5c3e0 --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/etherscan-data-provider-v2.ts @@ -0,0 +1,215 @@ +import { Provider } from '@ethersproject/abstract-provider' +import { FeeOption, FeeRates, TxHistoryParams } from '@xchainjs/xchain-client' +import { + Address, + Asset, + AssetType, + BaseAmount, + Chain, + TokenAsset, + assetToString, + baseAmount, +} from '@xchainjs/xchain-util' +import axios from 'axios' +import { BigNumber, ethers } from 'ethers' + +import { Balance, CompatibleAsset, EvmOnlineDataProvider, Tx, TxsPage } from '../../types' + +import erc20ABI from './erc20.json' +import * as etherscanAPI from './etherscan-api-v2' +import { ERC20TxV2, GetERC20TxsResponseV2 } from './types-v2' + +export class EtherscanProviderV2 implements EvmOnlineDataProvider { + private provider: Provider + private apiKey: string + protected baseUrl: string + protected chain: Chain + protected nativeAsset: Asset + protected nativeAssetDecimals: number + protected chainId: number + + constructor( + provider: Provider, + baseUrl: string, + apiKey: string, + chain: Chain, + nativeAsset: Asset, + nativeAssetDecimals: number, + chainId: number, + ) { + this.provider = provider + this.baseUrl = baseUrl + this.apiKey = apiKey + this.chain = chain + this.nativeAsset = nativeAsset + this.nativeAssetDecimals = nativeAssetDecimals + this.chainId = chainId + this.nativeAsset + this.chain + } + async getBalance(address: Address, assets?: CompatibleAsset[]): Promise { + //validate assets are for the correct chain + assets?.forEach((i) => { + if (i.chain !== this.chain) throw Error(`${assetToString(i)} is not an asset of ${this.chain}`) + }) + const balances: Balance[] = [] + balances.push(await this.getNativeAssetBalance(address)) + + if (assets) { + for (const asset of assets) { + const splitSymbol = asset.symbol.split('-') + const tokenSymbol = splitSymbol[0] + const contractAddress = splitSymbol[1] + balances.push(await this.getTokenBalance(address, contractAddress, tokenSymbol)) + } + } else { + // Get All Erc-20 txs + const response = ( + await axios.get( + `${this.baseUrl}/api?chainId=${this.chainId}&module=account&action=tokentx&address=${address}&sort=asc&apikey=${this.apiKey}`, + ) + ).data + + const erc20TokenTxs = this.getUniqueContractAddresses(response.result) + for (const erc20Token of erc20TokenTxs) { + balances.push(await this.getTokenBalance(address, erc20Token.contractAddress, erc20Token.tokenSymbol)) + } + } + + return balances + } + private async getNativeAssetBalance(address: Address): Promise<{ asset: Asset; amount: BaseAmount }> { + const gasAssetBalance: BigNumber = await this.provider.getBalance(address.toLowerCase()) + const amount = baseAmount(gasAssetBalance.toString(), this.nativeAssetDecimals) + return { + asset: this.nativeAsset, + amount, + } + } + private async getTokenBalance( + address: Address, + contractAddress: string, + tokenTicker: string, + ): Promise<{ asset: TokenAsset; amount: BaseAmount }> { + const asset: TokenAsset = { + chain: this.chain, + symbol: `${tokenTicker}-${contractAddress}`, + ticker: tokenTicker, + type: AssetType.TOKEN, + } + + const contract: ethers.Contract = new ethers.Contract(contractAddress.toLowerCase(), erc20ABI, this.provider) + const balance = (await contract.balanceOf(address.toLowerCase())).toString() + + const decimals = (await contract.decimals()).toString() + const amount = baseAmount(balance, Number.parseInt(decimals)) + + return { + asset, + amount, + } + } + + private getUniqueContractAddresses(array: ERC20TxV2[]): ERC20TxV2[] { + const mySet = new Set() + return array.filter((x) => { + const key = x.contractAddress, + isNew = !mySet.has(key) + if (isNew) mySet.add(key) + return isNew + }) + } + + async getTransactions(params: TxHistoryParams): Promise { + const offset = params?.offset || 0 + const limit = params?.limit || 10 + const assetAddress = params?.asset + + const maxCount = 10000 + + let transactions + + if (assetAddress) { + transactions = await etherscanAPI.getTokenTransactionHistory({ + gasDecimals: this.nativeAssetDecimals, + baseUrl: this.baseUrl, + address: params?.address, + assetAddress, + page: 0, + offset: maxCount, + chainId: this.chainId, + apiKey: this.apiKey, + chain: this.chain, + }) + } else { + transactions = await etherscanAPI.getGasAssetTransactionHistory({ + gasAsset: this.nativeAsset, + gasDecimals: this.nativeAssetDecimals, + baseUrl: this.baseUrl, + address: params?.address, + page: 0, + offset: maxCount, + chainId: this.chainId, + apiKey: this.apiKey, + }) + } + + return { + total: transactions.length, + txs: transactions.filter((_, index) => index >= offset && index < offset + limit), + } + } + + async getTransactionData(txHash: string, assetAddress?: Address): Promise { + let tx + + const txInfo = await this.provider.getTransaction(txHash) + + if (txInfo) { + if (assetAddress) { + tx = + ( + await etherscanAPI.getTokenTransactionHistory({ + gasDecimals: this.nativeAssetDecimals, + baseUrl: this.baseUrl, + assetAddress, + address: txInfo.from, + startblock: txInfo.blockNumber, + endblock: txInfo.blockNumber ? txInfo.blockNumber + 1 : undefined, // To be compatible with Routescan + chainId: this.chainId, + apiKey: this.apiKey, + chain: this.chain, + }) + ).filter((info) => info.hash === txHash)[0] ?? null + } else { + tx = + ( + await etherscanAPI.getGasAssetTransactionHistory({ + gasAsset: this.nativeAsset, + gasDecimals: this.nativeAssetDecimals, + baseUrl: this.baseUrl, + startblock: txInfo.blockNumber, + endblock: txInfo.blockNumber ? txInfo.blockNumber + 1 : undefined, // To be compatible with Routescan + chainId: this.chainId, + apiKey: this.apiKey, + address: txInfo.from, + }) + ).filter((info) => info.hash === txHash)[0] ?? null + } + } + + if (!tx) throw new Error('Could not get transaction history') + + return tx + } + + async getFeeRates(): Promise { + const gasOracleResponse = await etherscanAPI.getGasOracle(this.baseUrl, this.chainId, this.apiKey) + + return { + [FeeOption.Average]: Number(gasOracleResponse.SafeGasPrice) * 10 ** 9, + [FeeOption.Fast]: Number(gasOracleResponse.ProposeGasPrice) * 10 ** 9, + [FeeOption.Fastest]: Number(gasOracleResponse.FastGasPrice) * 10 ** 9, + } + } +} diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/types-v2.ts b/packages/xchain-evm-providers/src/providers/etherscan-v2/types-v2.ts new file mode 100644 index 000000000..05e20710d --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/types-v2.ts @@ -0,0 +1,79 @@ +import { Address } from '@xchainjs/xchain-util' + +export type ERC20TxV2 = { + timeStamp: string + hash: string + from: string + contractAddress: string + to: string + value: string + tokenName: string + tokenSymbol: string + tokenDecimal: string +} +export type GasAssetTxV2 = { + timeStamp: string + hash: string + from: string + contractAddress: string + to: string + value: string +} +export type GetERC20TxsResponseV2 = { + status: string + message: string + result: ERC20TxV2[] +} +export type GetGasAssetTxsResponseV2 = { + status: string + message: string + result: GasAssetTxV2[] +} + +export type LogEventParamV2 = { + name: string //to or from or value + type: string //address or contract + value: string +} + +export type LogEventV2 = { + sender_contract_decimals: number + sender_contract_ticker_symbol: string | null | undefined + sender_address: string | null | undefined //ERC-20 contract address + decoded: DecodedEventV2 | null | undefined +} +export type DecodedEventV2 = { + name: string //Transfer or Approval etc + params: LogEventParamV2[] | null | undefined +} +export type GetTransactionsItemV2 = { + tx_hash: string + block_signed_at: string + from_address: string + to_address: string + value: string + log_events: LogEventV2[] +} +export type GetTransactionsResponseV2 = { + data: { + address: string + items: GetTransactionsItemV2[] + pagination: { + has_more: boolean + page_number: number + page_size: number + total_count: number | null + } + } +} +export type GetTransactionResponseV2 = { + data: { + items: GetTransactionsItemV2[] + } +} +export type getTxsParamsV2 = { + address: Address + offset: number + limit: number + assetAddress: string | undefined +} diff --git a/packages/xchain-evm-providers/src/providers/etherscan-v2/utils.ts b/packages/xchain-evm-providers/src/providers/etherscan-v2/utils.ts new file mode 100644 index 000000000..c63fa1f3b --- /dev/null +++ b/packages/xchain-evm-providers/src/providers/etherscan-v2/utils.ts @@ -0,0 +1,228 @@ +import { Address, Asset, BaseAmount, baseAmount } from '@xchainjs/xchain-util' +import { Signer, ethers, providers } from 'ethers' + +import erc20ABI from './erc20.json' + +export const MAX_APPROVAL: ethers.BigNumber = ethers.BigNumber.from(2).pow(256).sub(1) + +/** + * Validate the given address. + * + * @param {Address} address + * @returns {boolean} `true` or `false` + */ +export const validateAddress = (address: Address): boolean => { + try { + ethers.utils.getAddress(address) + return true + } catch (error) { + return false + } +} + +/** + * Get token address from asset. + * + * @param {Asset} asset + * @returns {Address|null} The token address. + */ +export const getTokenAddress = (asset: Asset): Address | null => { + try { + // strip 0X only - 0x is still valid + return ethers.utils.getAddress(asset.symbol.slice(asset.ticker.length + 1).replace(/^0X/, '')) + } catch (err) { + return null + } +} + +/** + * Check if the symbol is valid. + * + * @param {string|null|undefined} symbol + * @returns {boolean} `true` or `false`. + */ +export const validateSymbol = (symbol?: string | null): boolean => (symbol ? symbol.length >= 3 : false) + +/** + * Calculate fees by multiplying . + * + * @returns {Fees} The default gas price. + */ +export const getFee = ({ + gasPrice, + gasLimit, + decimals, +}: { + gasPrice: BaseAmount + gasLimit: ethers.BigNumber + decimals: number +}) => baseAmount(gasPrice.amount().multipliedBy(gasLimit.toString()), decimals) + +/** + * Get address prefix based on the network. + * + * @returns {string} The address prefix based on the network. + * + **/ +export const getPrefix = () => '0x' + +/** + * Filter self txs + * + * @returns {T[]} + * + **/ +export const filterSelfTxs = (txs: T[]): T[] => { + const filterTxs = txs.filter((tx) => tx.from !== tx.to) + let selfTxs = txs.filter((tx) => tx.from === tx.to) + while (selfTxs.length) { + const selfTx = selfTxs[0] + filterTxs.push(selfTx) + selfTxs = selfTxs.filter((tx) => tx.hash !== selfTx.hash) + } + + return filterTxs +} + +/** + * Returns approval amount + * + * If given amount is not set or zero, `MAX_APPROVAL` amount is used + */ +export const getApprovalAmount = (amount?: BaseAmount): ethers.BigNumber => + amount && amount.gt(baseAmount(0, amount.decimal)) ? ethers.BigNumber.from(amount.amount().toFixed()) : MAX_APPROVAL + +/** + * Call a contract function. + * + * @param {Provider} provider Provider to interact with the contract. + * @param {Address} contractAddress The contract address. + * @param {ContractInterface} abi The contract ABI json. + * @param {string} funcName The function to be called. + * @param {unknown[]} funcParams The parameters of the function. + * @returns {BigNumber} The result of the contract function call. + */ +export const estimateCall = async ({ + provider, + contractAddress, + abi, + funcName, + funcParams = [], +}: { + provider: providers.Provider + contractAddress: Address + abi: ethers.ContractInterface + funcName: string + funcParams?: unknown[] +}): Promise => { + const contract: ethers.Contract = new ethers.Contract(contractAddress, abi, provider) + return await contract.estimateGas[funcName](...funcParams) +} + +/** + * Calls a contract function. + * + * @param {Provider} provider Provider to interact with the contract. + * @param {signer} Signer of the transaction (optional - needed for sending transactions only) + * @param {Address} contractAddress The contract address. + * @param {ContractInterface} abi The contract ABI json. + * @param {string} funcName The function to be called. + * @param {unknow[]} funcParams (optional) The parameters of the function. + * + * @returns {T} The result of the contract function call. + + */ +export const call = async ({ + provider, + signer, + contractAddress, + abi, + funcName, + funcParams = [], +}: { + provider: providers.Provider + signer?: Signer + contractAddress: Address + abi: ethers.ContractInterface + funcName: string + funcParams?: unknown[] +}): Promise => { + let contract = new ethers.Contract(contractAddress, abi, provider) + if (signer) { + // For sending transactions a signer is needed + contract = contract.connect(signer) + } + return contract[funcName](...funcParams) +} + +/** + * Estimate gas for calling `approve`. + * + * @param {Provider} provider Provider to interact with the contract. + * @param {Address} contractAddress The contract address. + * @param {Address} spenderAddress The spender address. + * @param {Address} fromAddress The address a transaction is sent from. + * @param {BaseAmount} amount (optional) The amount of token. By default, it will be unlimited token allowance. + * + * @returns {BigNumber} Estimated gas + */ +export const estimateApprove = async ({ + provider, + contractAddress, + spenderAddress, + fromAddress, + abi, + amount, +}: { + provider: providers.Provider + contractAddress: Address + spenderAddress: Address + fromAddress: Address + abi: ethers.ContractInterface + amount?: BaseAmount +}): Promise => { + const txAmount = getApprovalAmount(amount) + return await estimateCall({ + provider, + contractAddress, + abi, + funcName: 'approve', + funcParams: [spenderAddress, txAmount, { from: fromAddress }], + }) +} + +/** + * Check allowance. + * + * @param {Provider} provider Provider to interact with the contract. + * @param {Address} contractAddress The contract (ERC20 token) address. + * @param {Address} spenderAddress The spender address (router). + * @param {Address} fromAddress The address a transaction is sent from. + * @param {BaseAmount} amount The amount to check if it's allowed to spend or not (optional). + * @param {number} walletIndex (optional) HD wallet index + * @returns {boolean} `true` or `false`. + */ +export const isApproved = async ({ + provider, + contractAddress, + spenderAddress, + fromAddress, + amount, +}: { + provider: providers.Provider + contractAddress: Address + spenderAddress: Address + fromAddress: Address + amount?: BaseAmount +}): Promise => { + const txAmount = ethers.BigNumber.from(amount?.amount().toFixed() ?? 1) + const contract: ethers.Contract = new ethers.Contract(contractAddress, erc20ABI, provider) + const allowance: ethers.BigNumberish = await contract.allowance(fromAddress, spenderAddress) + + return txAmount.lte(allowance) +} + +/** + * Removes `0x` or `0X` from address + */ +export const strip0x = (addr: Address) => addr.replace(/^0(x|X)/, '') diff --git a/packages/xchain-evm-providers/src/providers/index.ts b/packages/xchain-evm-providers/src/providers/index.ts index 0baf89dea..a79d9f209 100644 --- a/packages/xchain-evm-providers/src/providers/index.ts +++ b/packages/xchain-evm-providers/src/providers/index.ts @@ -1,6 +1,9 @@ export * from './etherscan/etherscan-data-provider' export * from './etherscan/types' +export * from './etherscan-v2/etherscan-data-provider-v2' +export * from './etherscan-v2/types-v2' + import { DecodedEvent, GetBalanceResponse,