Implement more of SmartProvider features and tests

pull/35/head
J M Rossy 2 years ago
parent af899ae4fc
commit 662f35f396
  1. 90
      src/features/providers/SmartProvider.test.ts
  2. 78
      src/features/providers/SmartProvider.ts

@ -1,5 +1,8 @@
import { ethers } from 'ethers';
import { ChainMetadata, chainMetadata } from '@hyperlane-xyz/sdk';
import { areAddressesEqual } from '../../utils/addresses';
import { logger } from '../../utils/logger';
import { HyperlaneSmartProvider } from './SmartProvider';
@ -7,6 +10,10 @@ import { HyperlaneSmartProvider } from './SmartProvider';
jest.setTimeout(30000);
const MIN_BLOCK_NUM = 8000000;
const DEFAULT_ACCOUNT = '0x4f7A67464B5976d7547c860109e4432d50AfB38e';
const WETH_CONTRACT = '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6';
const WETH_TRANSFER_TOPIC0 = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const TRANSFER_TX_HASH = '0x45a586f90ffd5d0f8e618f0f3703b14c2c9e4611af6231d6fed32c62776b6c1b';
describe('SmartProvider', () => {
let config: ChainMetadata;
@ -23,11 +30,92 @@ describe('SmartProvider', () => {
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);
});
// TODO impl tests for all major methods
it('Fetches block number', async () => {
const result = await provider.getBlockNumber();
logger.debug('Latest block #', result);
expect(result).toBeGreaterThan(MIN_BLOCK_NUM);
});
it('Fetches gas price', async () => {
const result = await provider.getGasPrice();
logger.debug('Gas price', result.toString());
expect(result.toNumber()).toBeGreaterThan(0);
});
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);
});
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);
});
it('Fetches storage at location', async () => {
const result = await provider.getStorageAt(WETH_CONTRACT, 0);
logger.debug('Weth storage', result);
expect(result.length).toBeGreaterThan(20);
});
it('Fetches transaction count', async () => {
const result = await provider.getTransactionCount(DEFAULT_ACCOUNT, 'latest');
logger.debug('Tx Count', result);
expect(result).toBeGreaterThan(40);
});
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);
});
it('Fetches transaction receipt', async () => {
const result = await provider.getTransactionReceipt(TRANSFER_TX_HASH);
logger.debug('Transaction receipt', result.confirmations);
expect(result.confirmations).toBeGreaterThan(1000);
});
it('Fetches logs', async () => {
const result = await provider.getLogs({
address: WETH_CONTRACT,
topics: [WETH_TRANSFER_TOPIC0],
});
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');
}
});
//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');
}
});
it.skip('Performs eth_call', async () => {
//TODO
});
// TODO find way to have ethers Request-Rate Exceeded always show
// so we can test how often it hits throttle

@ -14,24 +14,50 @@ interface ChainMetadataWithRpcConnectionInfo extends ChainMetadata {
publicRpcUrls: RpcConfigWithConnectionInfo[];
}
export class HyperlaneSmartProvider extends providers.BaseProvider {
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',
}
const AllProviderMethods = Object.values(ProviderMethod);
interface IProviderMethods {
readonly supportedMethods: ProviderMethod[];
}
export class HyperlaneSmartProvider extends providers.BaseProvider implements IProviderMethods {
public readonly chainMetadata: ChainMetadataWithRpcConnectionInfo;
// TODO also support blockscout here
public readonly explorerProviders: HyperlaneEtherscanProvider[];
public readonly rpcProviders: providers.StaticJsonRpcProvider[];
public readonly rpcProviders: HyperlaneJsonRpcProvider[];
public readonly supportedMethods: ProviderMethod[];
constructor(chainMetadata: ChainMetadataWithRpcConnectionInfo) {
const network = chainMetadataToProviderNetwork(chainMetadata);
super(network);
this.chainMetadata = chainMetadata;
const supportedMethods = new Set<ProviderMethod>();
if (chainMetadata.blockExplorers?.length) {
this.explorerProviders = chainMetadata.blockExplorers
.map((explorerConfig) => {
if (!explorerConfig.family || explorerConfig.family === ExplorerFamily.Etherscan)
return new HyperlaneEtherscanProvider(explorerConfig, network);
// TODO also support blockscout here
else return null;
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 {
@ -39,12 +65,16 @@ export class HyperlaneSmartProvider extends providers.BaseProvider {
}
if (chainMetadata.publicRpcUrls?.length) {
this.rpcProviders = chainMetadata.publicRpcUrls.map(
(rpcConfig) => new HyperlaneJsonRpcProvider(rpcConfig, network),
);
this.rpcProviders = chainMetadata.publicRpcUrls.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<providers.Network> {
@ -57,9 +87,15 @@ export class HyperlaneSmartProvider extends providers.BaseProvider {
async perform(method: string, params: { [name: string]: any }): Promise<any> {
const allProviders = [...this.explorerProviders, ...this.rpcProviders];
if (!allProviders.length) throw new Error('No providers available');
if (!this.supportedMethods.includes(method as ProviderMethod))
throw new Error(`No providers available for method ${method}`);
const supportedProviders = allProviders.filter((p) =>
p.supportedMethods.includes(method as ProviderMethod),
);
// TODO consider implementing quorum and/or retry logic here similar to FallbackProvider/RetryProvider
for (const provider of allProviders) {
for (const provider of supportedProviders) {
const providerUrl =
provider instanceof providers.JsonRpcProvider ? provider.connection.url : provider.baseUrl;
try {
@ -77,9 +113,16 @@ export class HyperlaneSmartProvider extends providers.BaseProvider {
}
}
export class HyperlaneEtherscanProvider extends providers.EtherscanProvider {
export class HyperlaneEtherscanProvider
extends providers.EtherscanProvider
implements IProviderMethods
{
public readonly supportedMethods: ProviderMethod[];
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 {
@ -101,9 +144,20 @@ export class HyperlaneEtherscanProvider extends providers.EtherscanProvider {
getPostUrl(): string {
return `${this.getBaseUrl()}/api`;
}
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);
}
//TODO fix bug with getTxCount method
}
export class HyperlaneJsonRpcProvider extends providers.StaticJsonRpcProvider {
export class HyperlaneJsonRpcProvider
extends providers.StaticJsonRpcProvider
implements IProviderMethods
{
public readonly supportedMethods = AllProviderMethods;
constructor(rpcConfig: RpcConfigWithConnectionInfo, network: providers.Network) {
super(rpcConfig.connection ?? rpcConfig.http, network);
}

Loading…
Cancel
Save