diff --git a/src/features/chains/ConfigureChains.tsx b/src/features/chains/ConfigureChains.tsx index 3fffc47..b654442 100644 --- a/src/features/chains/ConfigureChains.tsx +++ b/src/features/chains/ConfigureChains.tsx @@ -182,15 +182,15 @@ export function ConfigureChains() { } const customChainTextareaPlaceholder = `{ - "chainId": 5, - "name": "goerli", + "chainId": 11155111, + "name": "sepolia", "protocol": "ethereum", "rpcUrls": [{ "http": "https://foobar.com" }], "blockExplorers": [ { - "name": "GoerliScan", + "name": "Sepolia Etherscan", "family": "etherscan", - "url": "https://goerli.etherscan.io", - "apiUrl": "https://api-goerli.etherscan.io/api", + "url": "https://sepolia.etherscan.io", + "apiUrl": "https://api-sepolia.etherscan.io/api", "apiKey": "12345" } ], "blocks": { "confirmations": 1, "estimateBlockTime": 13 }, diff --git a/src/features/messages/pi-queries/fetchPiChainMessages.test.ts b/src/features/messages/pi-queries/fetchPiChainMessages.test.ts index c0b93ea..df039fb 100644 --- a/src/features/messages/pi-queries/fetchPiChainMessages.test.ts +++ b/src/features/messages/pi-queries/fetchPiChainMessages.test.ts @@ -6,21 +6,21 @@ import { SmartMultiProvider } from '../../providers/SmartMultiProvider'; import { fetchMessagesFromPiChain } from './fetchPiChainMessages'; -// NOTE: THE GOERLI MESSAGE MAY NEED TO BE UPDATED ON OCCASION AS IT GETS TOO OLD +// NOTE: THE SEPOLIA MESSAGE MAY NEED TO BE UPDATED ON OCCASION AS IT GETS TOO OLD // THIS IS DUE TO LIMITATIONS OF THE RPC PROVIDER // TODO: MOCK THE PROVIDER TO MAKE THESE NETWORK INDEPENDENT jest.setTimeout(30000); -const goerliMailbox = hyperlaneEnvironments.testnet.goerli.mailbox; -const goerliIgp = hyperlaneEnvironments.testnet.goerli.interchainGasPaymaster; -const goerliConfigWithExplorer: ChainConfig = { - ...chainMetadata.goerli, - mailbox: goerliMailbox, - interchainGasPaymaster: goerliIgp, +const sepoliaMailbox = hyperlaneEnvironments.testnet.sepolia.mailbox; +const sepoliaIgp = hyperlaneEnvironments.testnet.sepolia.interchainGasPaymaster; +const sepoliaConfigWithExplorer: ChainConfig = { + ...chainMetadata.sepolia, + mailbox: sepoliaMailbox, + interchainGasPaymaster: sepoliaIgp, }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const { blockExplorers, ...goerliConfigNoExplorer } = goerliConfigWithExplorer; +const { blockExplorers, ...sepoliaConfigNoExplorer } = sepoliaConfigWithExplorer; // https://explorer.hyperlane.xyz/message/0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459e const txHash = '0xea0ba6b69ca70147d7cfdc2a806fe6b6ca5bce143408ebcf348fdec30cdd7daf'; @@ -28,7 +28,7 @@ const msgId = '0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459 const senderAddress = '0x405bfdecb33230b4ad93c29ba4499b776cfba189'; const recipientAddress = '0x5da3b8d6f73df6003a490072106730218c475aad'; -const goerliMessage: Message = { +const sepoliaMessage: Message = { id: '', msgId: '0xfec74152c40d8dfe117bf1a83ba443c85d0de8962272445019c526686a70459e', originChainId: 5, @@ -66,94 +66,94 @@ const goerliMessage: Message = { describe('fetchMessagesFromPiChain', () => { it('Fetches messages using explorer for tx hash', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigWithExplorer, + sepoliaConfigWithExplorer, { input: txHash }, - createMP(goerliConfigWithExplorer), + createMP(sepoliaConfigWithExplorer), ); - expect(messages).toEqual([goerliMessage]); + expect(messages).toEqual([sepoliaMessage]); }); it('Fetches messages using explorer for msg id', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigWithExplorer, + sepoliaConfigWithExplorer, { input: msgId }, - createMP(goerliConfigWithExplorer), + createMP(sepoliaConfigWithExplorer), ); - expect(messages).toEqual([goerliMessage]); + expect(messages).toEqual([sepoliaMessage]); }); it('Fetches messages using explorer for sender address', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigWithExplorer, + sepoliaConfigWithExplorer, { input: senderAddress, - fromBlock: goerliMessage.origin.blockNumber - 100, + fromBlock: sepoliaMessage.origin.blockNumber - 100, }, - createMP(goerliConfigWithExplorer), + createMP(sepoliaConfigWithExplorer), ); const testMsg = messages.find((m) => m.msgId === msgId); expect(testMsg).toBeTruthy(); }); it('Fetches messages using explorer for recipient address', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigWithExplorer, + sepoliaConfigWithExplorer, { input: recipientAddress, - fromBlock: goerliMessage.origin.blockNumber - 100, + fromBlock: sepoliaMessage.origin.blockNumber - 100, }, - createMP(goerliConfigWithExplorer), + createMP(sepoliaConfigWithExplorer), ); const testMsg = messages.find((m) => m.msgId === msgId); expect(testMsg).toBeTruthy(); }); it('Fetches messages using provider for tx hash', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigNoExplorer, + sepoliaConfigNoExplorer, { input: txHash }, - createMP(goerliConfigNoExplorer), + createMP(sepoliaConfigNoExplorer), ); - expect(messages).toEqual([goerliMessage]); + expect(messages).toEqual([sepoliaMessage]); }); it('Fetches messages using provider for msg id', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigNoExplorer, + sepoliaConfigNoExplorer, { input: msgId }, - createMP(goerliConfigNoExplorer), + createMP(sepoliaConfigNoExplorer), ); - expect(messages).toEqual([goerliMessage]); + expect(messages).toEqual([sepoliaMessage]); }); it('Fetches messages using provider for sender address', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigNoExplorer, + sepoliaConfigNoExplorer, { input: senderAddress, }, - createMP(goerliConfigNoExplorer), + createMP(sepoliaConfigNoExplorer), ); const testMsg = messages.find((m) => m.msgId === msgId); expect(testMsg).toBeTruthy(); }); it('Fetches messages using provider for recipient address', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigNoExplorer, + sepoliaConfigNoExplorer, { input: recipientAddress, }, - createMP(goerliConfigNoExplorer), + createMP(sepoliaConfigNoExplorer), ); const testMsg = messages.find((m) => m.msgId === msgId); expect(testMsg).toBeTruthy(); }); it('Returns empty for invalid input', async () => { const messages = await fetchMessagesFromPiChain( - goerliConfigWithExplorer, + sepoliaConfigWithExplorer, { input: 'invalidInput', }, - createMP(goerliConfigNoExplorer), + createMP(sepoliaConfigNoExplorer), ); expect(messages).toEqual([]); }); }); function createMP(config: ChainConfig) { - return new SmartMultiProvider({ ...chainMetadata, goerli: config }); + return new SmartMultiProvider({ ...chainMetadata, sepolia: config }); } diff --git a/src/features/providers/HyperlaneEtherscanProvider.ts b/src/features/providers/HyperlaneEtherscanProvider.ts deleted file mode 100644 index 49ed70f..0000000 --- a/src/features/providers/HyperlaneEtherscanProvider.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { providers } from 'ethers'; - -import { ChainMetadata } from '@hyperlane-xyz/sdk'; -import { objFilter, sleep } from '@hyperlane-xyz/utils'; - -import { logger } from '../../utils/logger'; - -import { IProviderMethods, ProviderMethod, excludeMethods } from './ProviderMethods'; - -type ExplorerConfig = Exclude[number]; - -// Used for crude rate-limiting of explorer queries without API keys -const hostToLastQueried: Record = {}; -const ETHERSCAN_THROTTLE_TIME = 6000; // 6.0 seconds - -export class HyperlaneEtherscanProvider - extends providers.EtherscanProvider - implements IProviderMethods -{ - // Seeing problems with these two methods even though etherscan api claims to support them - public readonly supportedMethods = excludeMethods([ - ProviderMethod.Call, - ProviderMethod.EstimateGas, - ProviderMethod.SendTransaction, - ]); - - constructor(public readonly explorerConfig: ExplorerConfig, network: providers.Network) { - if (!explorerConfig.apiKey) { - logger.warn( - 'HyperlaneEtherscanProviders created without an API key will be severely rate limited. Consider using an API key for better reliability.', - ); - } - super(network, explorerConfig.apiKey); - } - - getBaseUrl(): string { - if (!this.explorerConfig) return ''; // Constructor net yet finished - const apiUrl = this.explorerConfig?.apiUrl; - if (!apiUrl) throw new Error('Explorer config missing apiUrl'); - if (apiUrl.endsWith('/api')) return apiUrl.slice(0, -4); - return apiUrl; - } - - getUrl(module: string, params: Record): string { - const combinedParams = objFilter(params, (k, v): v is string => !!k && !!v); - combinedParams['module'] = module; - if (this.apiKey) combinedParams['apikey'] = this.apiKey; - const parsedParams = new URLSearchParams(combinedParams); - return `${this.getBaseUrl()}/api?${parsedParams.toString()}`; - } - - getPostUrl(): string { - return `${this.getBaseUrl()}/api`; - } - - getHostname(): string { - return new URL(this.getBaseUrl()).hostname; - } - - getQueryWaitTime(): number { - if (!this.isCommunityResource()) return 0; - const hostname = this.getHostname(); - const lastExplorerQuery = hostToLastQueried[hostname] || 0; - return ETHERSCAN_THROTTLE_TIME - (Date.now() - lastExplorerQuery); - } - - async fetch(module: string, params: Record, post?: boolean): Promise { - if (!this.isCommunityResource()) return super.fetch(module, params, post); - - const hostname = this.getHostname(); - let waitTime = this.getQueryWaitTime(); - while (waitTime > 0) { - logger.debug(`HyperlaneEtherscanProvider waiting ${waitTime}ms to avoid rate limit`); - await sleep(waitTime); - waitTime = this.getQueryWaitTime(); - } - - hostToLastQueried[hostname] = Date.now(); - return super.fetch(module, params, post); - } - - async perform(method: string, params: any, reqId?: number): Promise { - logger.debug(`HyperlaneEtherscanProvider performing method ${method} for reqId ${reqId}`); - if (!this.supportedMethods.includes(method as ProviderMethod)) - throw new Error(`Unsupported method ${method}`); - - if (method === ProviderMethod.GetLogs) { - return this.performGetLogs(params); - } else { - return super.perform(method, params); - } - } - - // Overriding to allow more than one topic value - async performGetLogs(params: { filter: providers.Filter }) { - const args: Record = { action: 'getLogs' }; - if (params.filter.fromBlock) args.fromBlock = checkLogTag(params.filter.fromBlock); - if (params.filter.toBlock) args.toBlock = checkLogTag(params.filter.toBlock); - if (params.filter.address) args.address = params.filter.address; - const topics = params.filter.topics; - if (topics?.length) { - if (topics.length > 2) throw new Error(`Unsupported topic count ${topics.length} (max 2)`); - for (let i = 0; i < topics.length; i++) { - const topic = topics[i]; - if (!topic || typeof topic !== 'string' || topic.length !== 66) - throw new Error(`Unsupported topic format: ${topic}`); - args[`topic${i}`] = topic; - if (i < topics.length - 1) args[`topic${i}_${i + 1}_opr`] = 'and'; - } - } - - return this.fetch('logs', args); - } -} - -// From ethers/providers/src.ts/providers/etherscan-provider.ts -function checkLogTag(blockTag: providers.BlockTag): number | 'latest' { - if (typeof blockTag === 'number') return blockTag; - if (blockTag === 'pending') throw new Error('pending not supported'); - if (blockTag === 'latest') return blockTag; - return parseInt(blockTag.substring(2), 16); -} diff --git a/src/features/providers/HyperlaneJsonRpcProvider.ts b/src/features/providers/HyperlaneJsonRpcProvider.ts deleted file mode 100644 index 21b8bad..0000000 --- a/src/features/providers/HyperlaneJsonRpcProvider.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { BigNumber, providers } from 'ethers'; - -import { chunk, isBigNumberish, isNullish } from '@hyperlane-xyz/utils'; - -import { logger } from '../../utils/logger'; - -import { AllProviderMethods, IProviderMethods, ProviderMethod } from './ProviderMethods'; -import { RpcConfigWithConnectionInfo } from './types'; - -const NUM_LOG_BLOCK_RANGES_TO_QUERY = 10; -const NUM_PARALLEL_LOG_QUERIES = 5; - -export class HyperlaneJsonRpcProvider - extends providers.StaticJsonRpcProvider - implements IProviderMethods -{ - public readonly supportedMethods = AllProviderMethods; - - constructor(public readonly rpcConfig: RpcConfigWithConnectionInfo, network: providers.Network) { - super(rpcConfig.connection ?? rpcConfig.http, network); - } - - async perform(method: string, params: any, reqId?: number): Promise { - logger.debug(`HyperlaneJsonRpcProvider performing method ${method} for reqId ${reqId}`); - if (method === ProviderMethod.GetLogs) { - return this.performGetLogs(params); - } else { - return super.perform(method, params); - } - } - - async performGetLogs(params: { filter: providers.Filter }) { - const superPerform = () => super.perform(ProviderMethod.GetLogs, params); - - const paginationOptions = this.rpcConfig.pagination; - if (!paginationOptions || !params.filter) return superPerform(); - - const { fromBlock, toBlock, address, topics } = params.filter; - const { maxBlockRange, minBlockNumber, maxBlockAge } = paginationOptions; - - if (!maxBlockRange && !maxBlockAge && isNullish(minBlockNumber)) return superPerform(); - - const currentBlockNumber = await super.perform(ProviderMethod.GetBlockNumber, null); - - let endBlock: number; - if (isNullish(toBlock) || toBlock === 'latest') { - endBlock = currentBlockNumber; - } else if (isBigNumberish(toBlock)) { - endBlock = BigNumber.from(toBlock).toNumber(); - } else { - return superPerform(); - } - - let startBlock: number; - if (isNullish(fromBlock) || fromBlock === 'earliest') { - startBlock = 0; - } else if (isBigNumberish(fromBlock)) { - startBlock = BigNumber.from(fromBlock).toNumber(); - } else { - return superPerform(); - } - - if (startBlock > endBlock) { - logger.warn(`Start block ${startBlock} greater than end block. Using ${endBlock} instead`); - startBlock = endBlock; - } - const minForBlockRange = maxBlockRange - ? endBlock - maxBlockRange * NUM_LOG_BLOCK_RANGES_TO_QUERY + 1 - : 0; - if (startBlock < minForBlockRange) { - logger.warn( - `Start block ${startBlock} requires too many queries, using ${minForBlockRange}.`, - ); - startBlock = minForBlockRange; - } - const minForBlockAge = maxBlockAge ? currentBlockNumber - maxBlockAge : 0; - if (startBlock < minForBlockAge) { - logger.warn(`Start block ${startBlock} below max block age, increasing to ${minForBlockAge}`); - startBlock = minForBlockAge; - } - if (minBlockNumber && startBlock < minBlockNumber) { - logger.warn(`Start block ${startBlock} below config min, increasing to ${minBlockNumber}`); - startBlock = minBlockNumber; - } - - const blockChunkRange = maxBlockRange || endBlock - startBlock; - const blockChunks: [number, number][] = []; - for (let from = startBlock; from <= endBlock; from += blockChunkRange) { - const to = Math.min(from + blockChunkRange - 1, endBlock); - blockChunks.push([from, to]); - } - - let combinedResults: Array = []; - const requestChunks = chunk(blockChunks, NUM_PARALLEL_LOG_QUERIES); - for (const reqChunk of requestChunks) { - const resultPromises = reqChunk.map( - (blockChunk) => - super.perform(ProviderMethod.GetLogs, { - filter: { - address, - topics, - fromBlock: BigNumber.from(blockChunk[0]).toHexString(), - toBlock: BigNumber.from(blockChunk[1]).toHexString(), - }, - }) as Promise>, - ); - const results = await Promise.all(resultPromises); - combinedResults = [...combinedResults, ...results.flat()]; - } - - return combinedResults; - } - - getBaseUrl(): string { - return this.connection.url; - } -} diff --git a/src/features/providers/ProviderMethods.ts b/src/features/providers/ProviderMethods.ts deleted file mode 100644 index 03346c8..0000000 --- a/src/features/providers/ProviderMethods.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface IProviderMethods { - readonly supportedMethods: ProviderMethod[]; -} - -export enum ProviderMethod { - Call = 'call', - EstimateGas = 'estimateGas', - GetBalance = 'getBalance', - GetBlock = 'getBlock', - GetBlockNumber = 'getBlockNumber', - GetCode = 'getCode', - GetGasPrice = 'getGasPrice', - GetStorageAt = 'getStorageAt', - GetTransaction = 'getTransaction', - GetTransactionCount = 'getTransactionCount', - GetTransactionReceipt = 'getTransactionReceipt', - GetLogs = 'getLogs', - SendTransaction = 'sendTransaction', -} - -export const AllProviderMethods = Object.values(ProviderMethod); - -export function excludeMethods(exclude: ProviderMethod[]): ProviderMethod[] { - return AllProviderMethods.filter((m) => !exclude.includes(m)); -} diff --git a/src/features/providers/SmartMultiProvider.ts b/src/features/providers/SmartMultiProvider.ts index bfe179d..c586e15 100644 --- a/src/features/providers/SmartMultiProvider.ts +++ b/src/features/providers/SmartMultiProvider.ts @@ -2,6 +2,7 @@ import { ChainMap, ChainMetadata, ChainName, + HyperlaneSmartProvider, MultiProvider, chainMetadata, } from '@hyperlane-xyz/sdk'; @@ -9,8 +10,6 @@ import { import { logger } from '../../utils/logger'; import type { ChainConfig } from '../chains/chainConfig'; -import { HyperlaneSmartProvider } from './SmartProvider'; - export class SmartMultiProvider extends MultiProvider { constructor(chainMetadata?: ChainMap, options?: any) { super(chainMetadata, options); @@ -22,7 +21,7 @@ export class SmartMultiProvider extends MultiProvider { const { name, rpcUrls, blockExplorers } = metadata; if (!this.providers[name] && (rpcUrls?.length || blockExplorers?.length)) { - this.providers[name] = new HyperlaneSmartProvider(metadata); + this.providers[name] = new HyperlaneSmartProvider(name, rpcUrls, blockExplorers); } return (this.providers[name] as HyperlaneSmartProvider) || null; diff --git a/src/features/providers/SmartProvider.test.ts b/src/features/providers/SmartProvider.test.ts deleted file mode 100644 index 7102950..0000000 --- a/src/features/providers/SmartProvider.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { ethers } from 'ethers'; - -import { ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk'; -import { eqAddress } from '@hyperlane-xyz/utils'; - -import { logger } from '../../utils/logger'; - -import { ProviderMethod } from './ProviderMethods'; -import { HyperlaneSmartProvider } from './SmartProvider'; - -jest.setTimeout(60_000); - -const MIN_BLOCK_NUM = 8900000; -const DEFAULT_ACCOUNT = '0x9d525E28Fe5830eE92d7Aa799c4D21590567B595'; -const WETH_CONTRACT = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6'; -const WETH_TRANSFER_TOPIC0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; -const TRANSFER_TX_HASH = '0x45a586f90ffd5d0f8e618f0f3703b14c2c9e4611af6231d6fed32c62776b6c1b'; - -const goerliRpcConfig = { - ...chainMetadata.goerli.rpcUrls[0], - pagination: { - maxBlockRange: 1000, - minBlockNumber: MIN_BLOCK_NUM, - }, -}; -const justExplorersConfig: ChainMetadata = { ...chainMetadata.goerli, rpcUrls: [] as any }; -const justRpcsConfig: ChainMetadata = { - ...chainMetadata.goerli, - rpcUrls: [goerliRpcConfig], - blockExplorers: [], -}; -const combinedConfig: ChainMetadata = { ...chainMetadata.goerli, rpcUrls: [goerliRpcConfig] }; -const configs: [string, ChainMetadata][] = [ - ['Just Explorers', justExplorersConfig], - ['Just RPCs', justRpcsConfig], - ['Combined configs', combinedConfig], -]; - -describe('SmartProvider', () => { - let provider: HyperlaneSmartProvider; - - const itDoesIfSupported = (method: ProviderMethod, fn: () => any) => { - it(method, () => { - if (provider.supportedMethods.includes(method)) { - return fn(); - } - }); - }; - - for (const [description, config] of configs) { - describe(description, () => { - beforeAll(() => { - provider = new HyperlaneSmartProvider(config); - }); - - itDoesIfSupported(ProviderMethod.GetBlock, async () => { - const latestBlock = await provider.getBlock('latest'); - logger.debug('Latest block #', latestBlock.number); - expect(latestBlock.number).toBeGreaterThan(MIN_BLOCK_NUM); - expect(latestBlock.timestamp).toBeGreaterThan(Date.now() / 1000 - 60 * 60 * 24); - const firstBlock = await provider.getBlock(1); - expect(firstBlock.number).toEqual(1); - }); - - itDoesIfSupported(ProviderMethod.GetBlockNumber, async () => { - const result = await provider.getBlockNumber(); - logger.debug('Latest block #', result); - expect(result).toBeGreaterThan(MIN_BLOCK_NUM); - }); - - itDoesIfSupported(ProviderMethod.GetGasPrice, async () => { - const result = await provider.getGasPrice(); - logger.debug('Gas price', result.toString()); - expect(result.toNumber()).toBeGreaterThan(0); - }); - - itDoesIfSupported(ProviderMethod.GetBalance, async () => { - const result = await provider.getBalance(DEFAULT_ACCOUNT); - logger.debug('Balance', result.toString()); - expect(parseFloat(ethers.utils.formatEther(result))).toBeGreaterThan(1); - }); - - itDoesIfSupported(ProviderMethod.GetCode, async () => { - const result = await provider.getCode(WETH_CONTRACT); - logger.debug('Weth code snippet', result.substring(0, 12)); - expect(result.length).toBeGreaterThan(100); - }); - - itDoesIfSupported(ProviderMethod.GetStorageAt, async () => { - const result = await provider.getStorageAt(WETH_CONTRACT, 0); - logger.debug('Weth storage', result); - expect(result.length).toBeGreaterThan(20); - }); - - itDoesIfSupported(ProviderMethod.GetTransactionCount, async () => { - const result = await provider.getTransactionCount(DEFAULT_ACCOUNT, 'latest'); - logger.debug('Tx Count', result); - expect(result).toBeGreaterThan(40); - }); - - itDoesIfSupported(ProviderMethod.GetTransaction, async () => { - const result = await provider.getTransaction(TRANSFER_TX_HASH); - logger.debug('Transaction confirmations', result.confirmations); - expect(result.confirmations).toBeGreaterThan(1000); - }); - - itDoesIfSupported(ProviderMethod.GetTransactionReceipt, async () => { - const result = await provider.getTransactionReceipt(TRANSFER_TX_HASH); - logger.debug('Transaction receipt', result.confirmations); - expect(result.confirmations).toBeGreaterThan(1000); - }); - - itDoesIfSupported(ProviderMethod.GetLogs, async () => { - logger.debug('Testing logs with no from/to range'); - const result1 = await provider.getLogs({ - address: WETH_CONTRACT, - topics: [WETH_TRANSFER_TOPIC0], - }); - logger.debug('Logs found', result1.length); - expect(result1.length).toBeGreaterThan(100); - expect(eqAddress(result1[0].address, WETH_CONTRACT)).toBeTruthy(); - - logger.debug('Testing logs with small from/to range'); - const result2 = await provider.getLogs({ - address: WETH_CONTRACT, - topics: [WETH_TRANSFER_TOPIC0], - fromBlock: MIN_BLOCK_NUM, - toBlock: MIN_BLOCK_NUM + 100, - }); - expect(result2.length).toBeGreaterThan(10); - expect(eqAddress(result2[0].address, WETH_CONTRACT)).toBeTruthy(); - - logger.debug('Testing logs with large from/to range'); - const result3 = await provider.getLogs({ - address: WETH_CONTRACT, - topics: [WETH_TRANSFER_TOPIC0], - fromBlock: MIN_BLOCK_NUM, - toBlock: 'latest', - }); - expect(result3.length).toBeGreaterThan(10); - expect(eqAddress(result3[0].address, WETH_CONTRACT)).toBeTruthy(); - }); - - itDoesIfSupported(ProviderMethod.EstimateGas, async () => { - const result = await provider.estimateGas({ - to: DEFAULT_ACCOUNT, - from: DEFAULT_ACCOUNT, - value: 1, - }); - expect(result.toNumber()).toBeGreaterThan(10_000); - }); - - itDoesIfSupported(ProviderMethod.Call, async () => { - const result = await provider.call({ - to: WETH_CONTRACT, - from: DEFAULT_ACCOUNT, - data: '0x70a082310000000000000000000000004f7a67464b5976d7547c860109e4432d50afb38e', - }); - expect(result).toBe('0x0000000000000000000000000000000000000000000000000000000000000000'); - }); - - it('Handles parallel requests', async () => { - const result1Promise = provider.getLogs({ - address: WETH_CONTRACT, - topics: [WETH_TRANSFER_TOPIC0], - fromBlock: MIN_BLOCK_NUM, - toBlock: MIN_BLOCK_NUM + 100, - }); - const result2Promise = provider.getBlockNumber(); - const result3Promise = provider.getTransaction(TRANSFER_TX_HASH); - const [result1, result2, result3] = await Promise.all([ - result1Promise, - result2Promise, - result3Promise, - ]); - expect(result1).toBeTruthy(); - expect(result2).toBeTruthy(); - expect(result3).toBeTruthy(); - }); - - //TODO - // itDoesIfSupported(ProviderMethod.SendTransaction, async () => { - // const result = await provider.sendTransaction('0x1234'); - // expect(result.hash.length).toBeGreaterThan(10) - // }); - }); - } -}); diff --git a/src/features/providers/SmartProvider.ts b/src/features/providers/SmartProvider.ts deleted file mode 100644 index 61bea89..0000000 --- a/src/features/providers/SmartProvider.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { providers } from 'ethers'; - -import { ChainMetadata, ExplorerFamily } from '@hyperlane-xyz/sdk'; - -import { logger } from '../../utils/logger'; - -import { HyperlaneEtherscanProvider } from './HyperlaneEtherscanProvider'; -import { HyperlaneJsonRpcProvider } from './HyperlaneJsonRpcProvider'; -import { IProviderMethods, ProviderMethod } from './ProviderMethods'; -import { - ChainMetadataWithRpcConnectionInfo, - ProviderPerformResult, - ProviderStatus, - ProviderTimeoutResult, -} from './types'; - -const PROVIDER_STAGGER_DELAY_MS = 1000; // 1 seconds - -type HyperlaneProvider = HyperlaneEtherscanProvider | HyperlaneJsonRpcProvider; - -export class HyperlaneSmartProvider extends providers.BaseProvider implements IProviderMethods { - public readonly chainMetadata: ChainMetadataWithRpcConnectionInfo; - // TODO also support blockscout here - public readonly explorerProviders: HyperlaneEtherscanProvider[]; - public readonly rpcProviders: HyperlaneJsonRpcProvider[]; - public readonly supportedMethods: ProviderMethod[]; - public requestCount = 0; - - constructor(chainMetadata: ChainMetadataWithRpcConnectionInfo) { - const network = chainMetadataToProviderNetwork(chainMetadata); - super(network); - this.chainMetadata = chainMetadata; - const supportedMethods = new Set(); - - if (chainMetadata.blockExplorers?.length) { - this.explorerProviders = chainMetadata.blockExplorers - .map((explorerConfig) => { - if (!explorerConfig.family || explorerConfig.family === ExplorerFamily.Etherscan) { - const newProvider = new HyperlaneEtherscanProvider(explorerConfig, network); - newProvider.supportedMethods.forEach((m) => supportedMethods.add(m)); - return newProvider; - // TODO also support blockscout here - } else return null; - }) - .filter((e): e is HyperlaneEtherscanProvider => !!e); - } else { - this.explorerProviders = []; - } - - if (chainMetadata.rpcUrls?.length) { - this.rpcProviders = chainMetadata.rpcUrls.map((rpcConfig) => { - const newProvider = new HyperlaneJsonRpcProvider(rpcConfig, network); - newProvider.supportedMethods.forEach((m) => supportedMethods.add(m)); - return newProvider; - }); - } else { - this.rpcProviders = []; - } - - this.supportedMethods = [...supportedMethods.values()]; - } - - async detectNetwork(): Promise { - // For simplicity, efficiency, and better compat with new networks, this assumes - // the provided RPC urls are correct and returns static data here instead of - // querying each sub-provider for network info - return chainMetadataToProviderNetwork(this.chainMetadata); - } - - /** - * This perform method will trigger any providers that support the method - * one at a time in preferential order. If one is slow to respond, the next is triggered. - */ - async perform(method: string, params: { [name: string]: any }): Promise { - const allProviders = [...this.explorerProviders, ...this.rpcProviders]; - if (!allProviders.length) throw new Error('No providers available'); - - const supportedProviders = allProviders.filter((p) => - p.supportedMethods.includes(method as ProviderMethod), - ); - if (!supportedProviders.length) throw new Error(`No providers available for method ${method}`); - - this.requestCount += 1; - const reqId = this.requestCount; - - let pIndex = 0; - const maxPIndex = supportedProviders.length - 1; - const providerResultPromises: Promise[] = []; - const providerResultErrors: unknown[] = []; - // TODO consider implementing quorum and/or retry logic here similar to FallbackProvider/RetryProvider - while (true) { - if (pIndex <= maxPIndex) { - // Trigger the next provider in line - const provider = supportedProviders[pIndex]; - const providerUrl = provider.getBaseUrl(); - const isLastProvider = pIndex === maxPIndex; - - // Skip the explorer provider if it's currently in a cooldown period - if ( - this.isExplorerProvider(provider) && - provider.getQueryWaitTime() > 0 && - !isLastProvider && - method !== ProviderMethod.GetLogs // never skip GetLogs - ) { - pIndex += 1; - continue; - } - - const resultPromise = wrapProviderPerform(provider, providerUrl, method, params, reqId); - const timeoutPromise = timeoutResult(1); - const result = await Promise.race([resultPromise, timeoutPromise]); - - if (result.status === ProviderStatus.Success) { - return result.value; - } else if (result.status === ProviderStatus.Timeout) { - logger.warn( - `Slow response from provider using ${providerUrl}.${ - !isLastProvider ? ' Triggering next provider.' : '' - }`, - ); - providerResultPromises.push(resultPromise); - pIndex += 1; - } else if (result.status === ProviderStatus.Error) { - logger.warn( - `Error from provider using ${providerUrl}.${ - !isLastProvider ? ' Triggering next provider.' : '' - }`, - ); - providerResultErrors.push(result.error); - pIndex += 1; - } else { - throw new Error('Unexpected result from provider'); - } - } else if (providerResultPromises.length > 0) { - // All providers already triggered, wait for one to complete or all to fail/timeout - const timeoutPromise = timeoutResult(20); - const resultPromise = waitForProviderSuccess(providerResultPromises); - const result = await Promise.race([resultPromise, timeoutPromise]); - - if (result.status === ProviderStatus.Success) { - return result.value; - } else if (result.status === ProviderStatus.Timeout) { - throwCombinedProviderErrors( - providerResultErrors, - `All providers timed out for method ${method}`, - ); - } else if (result.status === ProviderStatus.Error) { - throwCombinedProviderErrors( - [result.error, ...providerResultErrors], - `All providers failed for method ${method}`, - ); - } else { - throw new Error('Unexpected result from provider'); - } - } else { - // All providers have already failed, all hope is lost - throwCombinedProviderErrors( - providerResultErrors, - `All providers failed for method ${method}`, - ); - } - } - } - - isExplorerProvider(p: HyperlaneProvider): p is HyperlaneEtherscanProvider { - return this.explorerProviders.includes(p as any); - } -} - -// Warp for additional logging and error handling -async function wrapProviderPerform( - provider: HyperlaneProvider, - providerUrl: string, - method: string, - params: any, - reqId: number, -): Promise { - try { - logger.debug(`Provider using ${providerUrl} performing method ${method} for reqId ${reqId}`); - const result = await provider.perform(method, params, reqId); - return { status: ProviderStatus.Success, value: result }; - } catch (error) { - logger.error(`Error performing ${method} on provider ${providerUrl} for reqId ${reqId}`, error); - return { status: ProviderStatus.Error, error }; - } -} - -async function waitForProviderSuccess( - _resultPromises: Promise[], -): Promise { - // A hack to remove the promise from the array when it resolves - const resolvedPromiseIndexes = new Set(); - const resultPromises = _resultPromises.map((p, i) => - p.then((r) => { - resolvedPromiseIndexes.add(i); - return r; - }), - ); - const combinedErrors: unknown[] = []; - for (let i = 0; i < resultPromises.length; i += 1) { - const promises = resultPromises.filter((_, i) => !resolvedPromiseIndexes.has(i)); - const result = await Promise.race(promises); - if (result.status === ProviderStatus.Success) { - return result; - } else if (result.status === ProviderStatus.Error) { - combinedErrors.push(result.error); - } else { - return { status: ProviderStatus.Error, error: new Error('Unexpected result from provider') }; - } - } - return { - status: ProviderStatus.Error, - // TODO combine errors - error: combinedErrors.length ? combinedErrors[0] : new Error('Unknown error from provider'), - }; -} - -function throwCombinedProviderErrors(errors: unknown[], fallbackMsg: string): void { - logger.error(fallbackMsg); - // TODO inspect the errors in some clever way to choose which to throw - if (errors.length > 0) throw errors[0]; - else throw new Error(fallbackMsg); -} - -function chainMetadataToProviderNetwork( - chainMetadata: ChainMetadata | ChainMetadataWithRpcConnectionInfo, -): providers.Network { - return { - name: chainMetadata.name, - chainId: chainMetadata.chainId as number, - // @ts-ignore add ensAddress to ChainMetadata - ensAddress: chainMetadata.ensAddress, - }; -} - -function timeoutResult(multiplier: number) { - return new Promise((resolve) => - setTimeout( - () => - resolve({ - status: ProviderStatus.Timeout, - }), - PROVIDER_STAGGER_DELAY_MS * multiplier, - ), - ); -} diff --git a/src/features/providers/types.ts b/src/features/providers/types.ts deleted file mode 100644 index 9218b7c..0000000 --- a/src/features/providers/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { utils } from 'ethers'; - -import type { ChainMetadata } from '@hyperlane-xyz/sdk'; - -export type RpcConfigWithConnectionInfo = ChainMetadata['rpcUrls'][number] & { - connection?: utils.ConnectionInfo; -}; - -export interface ChainMetadataWithRpcConnectionInfo extends Omit { - rpcUrls: Array; -} - -export enum ProviderStatus { - Success = 'success', - Error = 'error', - Timeout = 'timeout', -} - -export interface ProviderPerformResultBase { - status: ProviderStatus; -} - -export interface ProviderSuccessResult extends ProviderPerformResultBase { - status: ProviderStatus.Success; - value: any; -} - -export interface ProviderErrorResult extends ProviderPerformResultBase { - status: ProviderStatus.Error; - error: unknown; -} - -export interface ProviderTimeoutResult extends ProviderPerformResultBase { - status: ProviderStatus.Timeout; -} - -export type ProviderPerformResult = - | ProviderSuccessResult - | ProviderErrorResult - | ProviderTimeoutResult;