Finish HyperlaneEtherscanProvider impl and build test suite

pull/35/head
J M Rossy 2 years ago
parent 662f35f396
commit cf52a1244d
  1. 189
      src/features/providers/SmartProvider.test.ts
  2. 46
      src/features/providers/SmartProvider.ts

@ -5,119 +5,134 @@ import { ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import { areAddressesEqual } from '../../utils/addresses';
import { logger } from '../../utils/logger';
import { HyperlaneSmartProvider } from './SmartProvider';
import { HyperlaneSmartProvider, ProviderMethod } from './SmartProvider';
jest.setTimeout(30000);
const MIN_BLOCK_NUM = 8000000;
const DEFAULT_ACCOUNT = '0x4f7A67464B5976d7547c860109e4432d50AfB38e';
const DEFAULT_ACCOUNT = '0x9d525E28Fe5830eE92d7Aa799c4D21590567B595';
const WETH_CONTRACT = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6';
const WETH_TRANSFER_TOPIC0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const TRANSFER_TX_HASH = '0x45a586f90ffd5d0f8e618f0f3703b14c2c9e4611af6231d6fed32c62776b6c1b';
const justExplorersConfig: ChainMetadata = { ...chainMetadata.goerli, publicRpcUrls: [] };
const justRpcsConfig: ChainMetadata = { ...chainMetadata.goerli, blockExplorers: [] };
const combinedConfig: ChainMetadata = { ...chainMetadata.goerli };
const configs: [string, ChainMetadata][] = [
['Just Explorers', justExplorersConfig],
['Just RPCs', justRpcsConfig],
['Combined configs', combinedConfig],
];
describe('SmartProvider', () => {
let config: ChainMetadata;
let provider: HyperlaneSmartProvider;
describe('Just Etherscan', () => {
beforeAll(() => {
config = { ...chainMetadata.goerli };
config.publicRpcUrls = [];
provider = new HyperlaneSmartProvider(config);
const itDoesIfSupported = (method: ProviderMethod, fn: () => any) => {
it(method, () => {
if (provider.supportedMethods.includes(method)) {
return fn();
}
});
};
it('Fetches blocks', 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);
});
for (const [description, config] of configs) {
describe(description, () => {
beforeAll(() => {
provider = new HyperlaneSmartProvider(config);
});
it('Fetches block number', async () => {
const result = await provider.getBlockNumber();
logger.debug('Latest block #', result);
expect(result).toBeGreaterThan(MIN_BLOCK_NUM);
});
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);
});
it('Fetches gas price', async () => {
const result = await provider.getGasPrice();
logger.debug('Gas price', result.toString());
expect(result.toNumber()).toBeGreaterThan(0);
});
itDoesIfSupported(ProviderMethod.GetBlockNumber, async () => {
const result = await provider.getBlockNumber();
logger.debug('Latest block #', result);
expect(result).toBeGreaterThan(MIN_BLOCK_NUM);
});
it('Fetches account balance', async () => {
const result = await provider.getBalance(DEFAULT_ACCOUNT);
logger.debug('Balance', result.toString());
expect(parseFloat(ethers.utils.formatEther(result))).toBeGreaterThan(1);
});
itDoesIfSupported(ProviderMethod.GetGasPrice, async () => {
const result = await provider.getGasPrice();
logger.debug('Gas price', result.toString());
expect(result.toNumber()).toBeGreaterThan(0);
});
it('Fetches code', async () => {
const result = await provider.getCode(WETH_CONTRACT);
logger.debug('Weth code snippet', result.substring(0, 12));
expect(result.length).toBeGreaterThan(100);
});
itDoesIfSupported(ProviderMethod.GetBalance, async () => {
const result = await provider.getBalance(DEFAULT_ACCOUNT);
logger.debug('Balance', result.toString());
expect(parseFloat(ethers.utils.formatEther(result))).toBeGreaterThan(1);
});
it('Fetches storage at location', async () => {
const result = await provider.getStorageAt(WETH_CONTRACT, 0);
logger.debug('Weth storage', result);
expect(result.length).toBeGreaterThan(20);
});
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);
});
it('Fetches transaction count', async () => {
const result = await provider.getTransactionCount(DEFAULT_ACCOUNT, 'latest');
logger.debug('Tx Count', result);
expect(result).toBeGreaterThan(40);
});
itDoesIfSupported(ProviderMethod.GetStorageAt, async () => {
const result = await provider.getStorageAt(WETH_CONTRACT, 0);
logger.debug('Weth storage', result);
expect(result.length).toBeGreaterThan(20);
});
it('Fetches transaction by hash', async () => {
const result = await provider.getTransaction(TRANSFER_TX_HASH);
logger.debug('Transaction confirmations', result.confirmations);
expect(result.confirmations).toBeGreaterThan(1000);
});
itDoesIfSupported(ProviderMethod.GetTransactionCount, async () => {
const result = await provider.getTransactionCount(DEFAULT_ACCOUNT, 'latest');
logger.debug('Tx Count', result);
expect(result).toBeGreaterThan(40);
});
it('Fetches transaction receipt', async () => {
const result = await provider.getTransactionReceipt(TRANSFER_TX_HASH);
logger.debug('Transaction receipt', result.confirmations);
expect(result.confirmations).toBeGreaterThan(1000);
});
itDoesIfSupported(ProviderMethod.GetTransaction, async () => {
const result = await provider.getTransaction(TRANSFER_TX_HASH);
logger.debug('Transaction confirmations', result.confirmations);
expect(result.confirmations).toBeGreaterThan(1000);
});
it('Fetches logs', async () => {
const result = await provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
itDoesIfSupported(ProviderMethod.GetTransactionReceipt, async () => {
const result = await provider.getTransactionReceipt(TRANSFER_TX_HASH);
logger.debug('Transaction receipt', result.confirmations);
expect(result.confirmations).toBeGreaterThan(1000);
});
logger.debug('Logs found', result.length);
expect(result.length).toBeGreaterThan(100);
expect(areAddressesEqual(result[0].address, WETH_CONTRACT)).toBeTruthy();
});
//TODO
it.skip('Estimates gas', async () => {
try {
const result = await provider.estimateGas({ to: DEFAULT_ACCOUNT, value: 1 });
expect('Provider should throw').toStrictEqual(true);
} catch (error) {
expect(error).toBe('TODO');
}
});
itDoesIfSupported(ProviderMethod.GetLogs, async () => {
const result = await provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
});
// console.log(result);
console.log(JSON.stringify(result.slice(0, 20)));
logger.debug('Logs found', result.length);
expect(result.length).toBeGreaterThan(100);
expect(areAddressesEqual(result[0].address, WETH_CONTRACT)).toBeTruthy();
});
//TODO
it.skip('Sends transaction', async () => {
try {
const result = await provider.sendTransaction('0x1234');
expect('Provider should throw').toStrictEqual(true);
} catch (error) {
expect(error).toBe('TODO');
}
});
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.skip('Performs eth_call', async () => {
//TODO
// itDoesIfSupported(ProviderMethod.SendTransaction, async () => {
// const result = await provider.sendTransaction('0x1234');
// expect(result.hash.length).toBeGreaterThan(10)
// });
});
// TODO find way to have ethers Request-Rate Exceeded always show
// so we can test how often it hits throttle
});
}
});

@ -3,6 +3,7 @@ import { providers, utils } from 'ethers';
import { ChainMetadata, ExplorerFamily, objFilter } from '@hyperlane-xyz/sdk';
import { logger } from '../../utils/logger';
import { sleep } from '../../utils/timeout';
type RpcConfigWithConnectionInfo = ChainMetadata['publicRpcUrls'][number] & {
connection?: utils.ConnectionInfo;
@ -95,13 +96,15 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
);
// TODO consider implementing quorum and/or retry logic here similar to FallbackProvider/RetryProvider
// TODO trigger next provider if current one takes too long
// This will help spread load across providers and ease rate limiting
for (const provider of supportedProviders) {
const providerUrl =
provider instanceof providers.JsonRpcProvider ? provider.connection.url : provider.baseUrl;
try {
const result = await provider.perform(method, params);
if (result === null || result === undefined) {
logger.error('Nullish result from provider using url:', providerUrl);
throw new Error(`Nullish result from provider using url: ${providerUrl}`);
}
return result;
} catch (error) {
@ -113,16 +116,23 @@ export class HyperlaneSmartProvider extends providers.BaseProvider implements IP
}
}
// Used for crude rate-limiting of explorer queries without API keys
const hostToLastQueried: Record<string, number> = {};
const ETHERSCAN_THROTTLE_TIME = 5100; // 5.1 seconds
export class HyperlaneEtherscanProvider
extends providers.EtherscanProvider
implements IProviderMethods
{
public readonly supportedMethods: ProviderMethod[];
// 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) {
super(network, explorerConfig.apiKey);
const unsupportedMethods: ProviderMethod[] = [ProviderMethod.SendTransaction];
this.supportedMethods = AllProviderMethods.filter((m) => !unsupportedMethods.includes(m));
}
getBaseUrl(): string {
@ -145,11 +155,29 @@ export class HyperlaneEtherscanProvider
return `${this.getBaseUrl()}/api`;
}
getHostname(): string {
return new URL(this.getBaseUrl()).hostname;
}
async fetch(module: string, params: Record<string, any>, post?: boolean): Promise<any> {
// TODO wrap this in intelligent rate limiting based on this.isCommunityResource
return super.fetch(module, params, post);
if (!this.isCommunityResource()) return super.fetch(module, params, post);
const hostname = this.getHostname();
try {
const lastExplorerQuery = hostToLastQueried[hostname] || 0;
const waitTime = ETHERSCAN_THROTTLE_TIME - (Date.now() - lastExplorerQuery);
if (waitTime > 0) await sleep(waitTime);
const result = await super.fetch(module, params, post);
return result;
} finally {
hostToLastQueried[hostname] = Date.now();
}
}
async perform(method: string, params: any): Promise<any> {
if (!this.supportedMethods.includes(method as ProviderMethod))
throw new Error(`Unsupported method ${method}`);
return super.perform(method, params);
}
//TODO fix bug with getTxCount method
}
export class HyperlaneJsonRpcProvider
@ -176,3 +204,7 @@ function chainMetadataToProviderNetwork(chainMetadata: ChainMetadata): providers
ensAddress: chainMetadata.ensAddress,
};
}
function excludeMethods(exclude: ProviderMethod[]): ProviderMethod[] {
return AllProviderMethods.filter((m) => !exclude.includes(m));
}

Loading…
Cancel
Save