-
Notifications
You must be signed in to change notification settings - Fork 388
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace Fallback/Retry providers with SmartProvider (#3032)
### Description - Port `HyperlaneSmartProvider` from explorer - Use as default ethers v5 provider - Includes `HyperlaneJsonRpcProvider` and `HyperlaneEtherscanProvider` for more granular control ### Related issues Fixes #2024 ### Backward compatibility No, the RetryProvider was removed ### Testing Unit tests with Foundry/anvil E2E tests with Goerli via mocha E2E tests via metadata-health CI
- Loading branch information
Showing
17 changed files
with
1,139 additions
and
144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@hyperlane-xyz/sdk': minor | ||
--- | ||
|
||
Replace Fallback and Retry Providers with new SmartProvider with more effective fallback/retry logic |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/usr/bin/env bash | ||
|
||
function cleanup() { | ||
set +e | ||
pkill -f anvil | ||
rm -rf /tmp/anvil* | ||
set -e | ||
} | ||
|
||
cleanup | ||
|
||
echo "Starting anvil chain" | ||
anvil --chain-id 31337 -p 8545 --state /tmp/anvil1/state --gas-price 1 > /dev/null & | ||
|
||
echo "Running mocha tests" | ||
yarn mocha --config .mocharc.json './src/**/*.foundry-test.ts' | ||
|
||
cleanup | ||
|
||
echo "Done foundry tests" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
139 changes: 139 additions & 0 deletions
139
typescript/sdk/src/providers/SmartProvider/HyperlaneEtherscanProvider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import debug from 'debug'; | ||
import { providers } from 'ethers'; | ||
|
||
import { objFilter, sleep } from '@hyperlane-xyz/utils'; | ||
|
||
import { BlockExplorer } from '../../metadata/chainMetadataTypes'; | ||
|
||
import { | ||
IProviderMethods, | ||
ProviderMethod, | ||
excludeProviderMethods, | ||
} from './ProviderMethods'; | ||
|
||
// Used for crude rate-limiting of explorer queries without API keys | ||
const hostToLastQueried: Record<string, number> = {}; | ||
const ETHERSCAN_THROTTLE_TIME = 6000; // 6.0 seconds | ||
|
||
export class HyperlaneEtherscanProvider | ||
extends providers.EtherscanProvider | ||
implements IProviderMethods | ||
{ | ||
protected readonly logger = debug('hyperlane:EtherscanProvider'); | ||
// Seeing problems with these two methods even though etherscan api claims to support them | ||
public readonly supportedMethods = excludeProviderMethods([ | ||
ProviderMethod.Call, | ||
ProviderMethod.EstimateGas, | ||
ProviderMethod.SendTransaction, | ||
]); | ||
|
||
constructor( | ||
public readonly explorerConfig: BlockExplorer, | ||
network: providers.Networkish, | ||
) { | ||
super(network, explorerConfig.apiKey); | ||
if (!explorerConfig.apiKey) { | ||
this.logger( | ||
'HyperlaneEtherscanProviders created without an API key will be severely rate limited. Consider using an API key for better reliability.', | ||
); | ||
} | ||
} | ||
|
||
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, string>): 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<string, any>, | ||
post?: boolean, | ||
): Promise<any> { | ||
if (!this.isCommunityResource()) return super.fetch(module, params, post); | ||
|
||
const hostname = this.getHostname(); | ||
let waitTime = this.getQueryWaitTime(); | ||
while (waitTime > 0) { | ||
this.logger( | ||
`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<any> { | ||
this.logger( | ||
`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 }): Promise<any> { | ||
const args: Record<string, any> = { 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); | ||
} |
Oops, something went wrong.