diff --git a/modules/contracts/src.ts/constants.ts b/modules/contracts/src.ts/constants.ts index e48d991cd..0f40026fd 100644 --- a/modules/contracts/src.ts/constants.ts +++ b/modules/contracts/src.ts/constants.ts @@ -1,13 +1,14 @@ import { HDNode } from "@ethersproject/hdnode"; import { Wallet } from "@ethersproject/wallet"; import { JsonRpcProvider } from "@ethersproject/providers"; -import { network, ethers }from "hardhat"; +import { ChainRpcProvider } from "@connext/vector-types"; +import { network, ethers } from "hardhat"; import pino from "pino"; // Get defaults from env const chainProviders = JSON.parse(process.env.CHAIN_PROVIDERS ?? "{}"); const chainId = Object.keys(chainProviders)[0]; -const url = Object.values(chainProviders)[0]; +const urls = Object.values(chainProviders)[0]; const mnemonic = process.env.SUGAR_DADDY ?? "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"; export const defaultLogLevel = process.env.LOG_LEVEL || "info"; @@ -15,9 +16,9 @@ export const logger = pino({ level: defaultLogLevel }); export const networkName = network.name; -export const provider = url - ? new JsonRpcProvider(url as string, parseInt(chainId)) - : ethers.provider as JsonRpcProvider; +export const provider = urls + ? new ChainRpcProvider(parseInt(chainId), (urls as string).split(",")) + : new ChainRpcProvider(parseInt(chainId), [ethers.provider as JsonRpcProvider]); const hdNode = HDNode.fromMnemonic(mnemonic).derivePath("m/44'/60'/0'/0"); diff --git a/modules/contracts/src.ts/services/ethReader.spec.ts b/modules/contracts/src.ts/services/ethReader.spec.ts index 0abf25a39..a58133263 100644 --- a/modules/contracts/src.ts/services/ethReader.spec.ts +++ b/modules/contracts/src.ts/services/ethReader.spec.ts @@ -1,6 +1,6 @@ -import { ChainError, FullChannelState, Result } from "@connext/vector-types"; +import { ChainError, FullChannelState, Result, ChainRpcProvider } from "@connext/vector-types"; import { createTestChannelState, expect, getTestLoggers, mkHash } from "@connext/vector-utils"; -import { JsonRpcProvider, TransactionReceipt } from "@ethersproject/providers"; +import { TransactionReceipt } from "@ethersproject/providers"; import { AddressZero, One, Zero } from "@ethersproject/constants"; import { parseUnits } from "@ethersproject/units"; import { restore, reset, createStubInstance, SinonStubbedInstance } from "sinon"; @@ -9,8 +9,8 @@ import { EthereumChainReader, MIN_GAS_PRICE, BUMP_GAS_PRICE } from "./ethReader" let ethReader: EthereumChainReader; let channelState: FullChannelState; -let provider1337: SinonStubbedInstance; -let provider1338: SinonStubbedInstance; +let provider1337: SinonStubbedInstance; +let provider1338: SinonStubbedInstance; const assertResult = (result: Result, isError: boolean, unwrappedVal?: any) => { if (isError) { @@ -46,7 +46,7 @@ describe("ethReader", () => { beforeEach(() => { // eth service deps - const _provider = createStubInstance(JsonRpcProvider); + const _provider = createStubInstance(ChainRpcProvider); _provider.getTransaction.resolves(_txResponse); provider1337 = _provider; provider1338 = _provider; diff --git a/modules/contracts/src.ts/services/ethReader.ts b/modules/contracts/src.ts/services/ethReader.ts index cf5f1531f..5af797d03 100644 --- a/modules/contracts/src.ts/services/ethReader.ts +++ b/modules/contracts/src.ts/services/ethReader.ts @@ -26,6 +26,7 @@ import { CoreChannelState, CoreTransferState, TransferDispute, + ChainRpcProvider, } from "@connext/vector-types"; import axios from "axios"; import { encodeBalance, encodeTransferResolver, encodeTransferState } from "@connext/vector-utils"; @@ -33,7 +34,7 @@ import { BigNumber } from "@ethersproject/bignumber"; import { parseUnits } from "@ethersproject/units"; import { AddressZero, HashZero } from "@ethersproject/constants"; import { Contract } from "@ethersproject/contracts"; -import { JsonRpcProvider, TransactionRequest } from "@ethersproject/providers"; +import { TransactionRequest } from "@ethersproject/providers"; import pino from "pino"; import { ChannelFactory, ChannelMastercopy, TransferDefinition, TransferRegistry, VectorChannel } from "../artifacts"; @@ -59,14 +60,14 @@ export class EthereumChainReader implements IVectorChainReader { }; private contracts: Map = new Map(); constructor( - public readonly chainProviders: { [chainId: string]: JsonRpcProvider }, + public readonly chainProviders: { [chainId: string]: ChainRpcProvider }, public readonly log: pino.BaseLogger, ) {} getChainProviders(): Result { const ret: ChainProviders = {}; Object.entries(this.chainProviders).forEach(([name, value]) => { - ret[parseInt(name)] = value.connection.url; + ret[parseInt(name)] = value.providerUrls; }); return Result.ok(ret); } diff --git a/modules/contracts/src.ts/services/ethService.spec.ts b/modules/contracts/src.ts/services/ethService.spec.ts index 7bcc3ca44..89906d730 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -7,6 +7,7 @@ import { Result, StoredTransaction, TransactionReason, + ChainRpcProvider, } from "@connext/vector-types"; import { ChannelSigner, @@ -19,7 +20,7 @@ import { mkHash, } from "@connext/vector-utils"; import { AddressZero, One, Zero } from "@ethersproject/constants"; -import { JsonRpcProvider, TransactionReceipt, TransactionResponse } from "@ethersproject/providers"; +import { TransactionReceipt, TransactionResponse } from "@ethersproject/providers"; import { BigNumber } from "ethers"; import { parseUnits } from "ethers/lib/utils"; import { restore, reset, createStubInstance, SinonStubbedInstance, stub, SinonStub } from "sinon"; @@ -30,8 +31,8 @@ import { BIG_GAS_PRICE, EthereumChainService } from "./ethService"; let storeMock: SinonStubbedInstance; let signer: SinonStubbedInstance; let ethService: EthereumChainService; -let provider1337: SinonStubbedInstance; -let provider1338: SinonStubbedInstance; +let provider1337: SinonStubbedInstance; +let provider1338: SinonStubbedInstance; let sendTxWithRetriesMock: SinonStub; let approveMock: SinonStub; @@ -96,7 +97,7 @@ describe("ethService unit test", () => { signer.connect.returns(signer as any); (signer as any)._isSigner = true; - const _provider = createStubInstance(JsonRpcProvider); + const _provider = createStubInstance(ChainRpcProvider); _provider.getTransaction.resolves(txResponse); provider1337 = _provider; provider1338 = _provider; diff --git a/modules/contracts/src.ts/services/ethService.ts b/modules/contracts/src.ts/services/ethService.ts index dcd66d3a2..da0bbe390 100644 --- a/modules/contracts/src.ts/services/ethService.ts +++ b/modules/contracts/src.ts/services/ethService.ts @@ -17,6 +17,7 @@ import { StringifiedTransactionResponse, getConfirmationsForChain, StoredTransaction, + ChainRpcProvider, } from "@connext/vector-types"; import { delay, @@ -29,7 +30,7 @@ import { import { Signer } from "@ethersproject/abstract-signer"; import { BigNumber } from "@ethersproject/bignumber"; import { Contract } from "@ethersproject/contracts"; -import { JsonRpcProvider, TransactionReceipt, TransactionResponse } from "@ethersproject/providers"; +import { TransactionReceipt, TransactionResponse } from "@ethersproject/providers"; import { Wallet } from "@ethersproject/wallet"; import { BaseLogger } from "pino"; import PriorityQueue from "p-queue"; @@ -54,7 +55,7 @@ export const BIG_GAS_PRICE = parseUnits("1500", "gwei"); // TODO: Deprecate. Note that this is used in autoRebalance.ts. export const waitForTransaction = async ( - provider: JsonRpcProvider, + provider: ChainRpcProvider, transactionHash: string, confirmations?: number, timeout?: number, @@ -74,8 +75,9 @@ export const waitForTransaction = async ( }; export class EthereumChainService extends EthereumChainReader implements IVectorChainService { + private nonces: Map = new Map(); private signers: Map = new Map(); - private queue: PriorityQueue = new PriorityQueue({ concurrency: 1 }); + private queues: Map = new Map(); private evts: { [eventName in ChainServiceEvent]: Evt } = { ...this.disputeEvts, [ChainServiceEvents.TRANSACTION_SUBMITTED]: new Evt(), @@ -84,7 +86,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector }; constructor( private readonly store: IChainServiceStore, - chainProviders: { [chainId: string]: JsonRpcProvider }, + chainProviders: { [chainId: string]: ChainRpcProvider }, signer: string | Signer, log: BaseLogger, private readonly defaultRetries = 3, @@ -95,6 +97,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector parseInt(chainId), typeof signer === "string" ? new Wallet(signer, provider) : (signer.connect(provider) as Signer), ); + this.queues.set(parseInt(chainId), new PriorityQueue({ concurrency: 1 })); }); // TODO: Check to see which tx's are still active / unresolved, and resolve them. @@ -195,21 +198,36 @@ export class EthereumChainService extends EthereumChainReader implements IVector txFn: (gasPrice: BigNumber, nonce: number) => Promise, gasPrice: BigNumber, signer: Signer, + chainId: number, nonce?: number, ): Promise> { // Queue up the execution of the transaction. - return await this.queue.add( - async (): Promise> => { - try { - // Send transaction using the passed in callback. - const actualNonce: number = nonce ?? (await signer.getTransactionCount("pending")); - const response: TransactionResponse | undefined = await txFn(gasPrice, actualNonce); - return Result.ok(response); - } catch (e) { - return Result.fail(e); + if (!this.queues.has(chainId)) { + return Result.fail(new ChainError(ChainError.reasons.SignerNotFound)); + } + // Define task to send tx with proper nonce + const task = async (): Promise> => { + try { + // Send transaction using the passed in callback. + const stored = this.nonces.get(chainId); + const nonceToUse: number = nonce ?? stored ?? (await signer.getTransactionCount("pending")); + const response: TransactionResponse | undefined = await txFn(gasPrice, nonceToUse); + // After calling tx fn, set nonce to the greatest of + // stored, pending, or incremented + const pending = await signer.getTransactionCount("pending"); + const incremented = (response?.nonce ?? nonceToUse) + 1; + // Ensure the nonce you store is *always* the greatest of the values + const toCompare = stored ?? 0; + if (toCompare < pending || toCompare < incremented) { + this.nonces.set(chainId, incremented > pending ? incremented : pending); } - }, - ); + return Result.ok(response); + } catch (e) { + return Result.fail(e); + } + }; + const result = await this.queues.get(chainId)!.add(task); + return result; } /// Check to see if any txs were left in an unfinished state. This should only execute on @@ -389,7 +407,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector { method, methodId, nonce, tryNumber, channelAddress, gasPrice: gasPrice.toString() }, "Attempting to send transaction", ); - const result = await this.sendTx(txFn, gasPrice, signer, nonce); + const result = await this.sendTx(txFn, gasPrice, signer, chainId, nonce); if (!result.isError) { const response = result.getValue(); if (response) { @@ -561,7 +579,7 @@ export class EthereumChainService extends EthereumChainReader implements IVector * the tx will be resubmitted at the same nonce. */ public async waitForConfirmation(chainId: number, responses: TransactionResponse[]): Promise { - const provider: JsonRpcProvider = this.chainProviders[chainId]; + const provider = this.chainProviders[chainId]; if (!provider) { throw new ChainError(ChainError.reasons.ProviderNotFound); } diff --git a/modules/documentation/docs/changelog.md b/modules/documentation/docs/changelog.md index 1b000eadb..7177cc0bf 100644 --- a/modules/documentation/docs/changelog.md +++ b/modules/documentation/docs/changelog.md @@ -2,6 +2,9 @@ ## Next Release +- \[contracts\] Add internal nonce tracking `ethService` +- \[contracts\] Add per-chain queues + ## 0.2.5-beta.18 - \[node\] Save transaction hash to commitment properly diff --git a/modules/iframe-app/src/ConnextManager.tsx b/modules/iframe-app/src/ConnextManager.tsx index 2a38d11c7..61f8e0c3a 100644 --- a/modules/iframe-app/src/ConnextManager.tsx +++ b/modules/iframe-app/src/ConnextManager.tsx @@ -6,7 +6,7 @@ import { EngineParams, jsonifyError, } from "@connext/vector-types"; -import { ChannelSigner, constructRpcRequest, safeJsonParse } from "@connext/vector-utils"; +import { ChannelSigner, constructRpcRequest, safeJsonParse, parseProviders } from "@connext/vector-utils"; import { entropyToMnemonic } from "@ethersproject/hdnode"; import { keccak256 } from "@ethersproject/keccak256"; import { toUtf8Bytes } from "@ethersproject/strings"; @@ -89,7 +89,7 @@ export default class ConnextManager { this.browserNode = await BrowserNode.connect({ signer, chainAddresses: chainAddresses ?? config.chainAddresses, - chainProviders, + chainProviders: parseProviders(chainProviders), logger: pino(), messagingUrl: _messagingUrl, authUrl: _authUrl, diff --git a/modules/protocol/src/testing/constants.ts b/modules/protocol/src/testing/constants.ts index 26bddb49c..af91a906a 100644 --- a/modules/protocol/src/testing/constants.ts +++ b/modules/protocol/src/testing/constants.ts @@ -1,11 +1,11 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; +import { ChainRpcProvider } from "@connext/vector-types"; import { Wallet } from "@ethersproject/wallet"; import { env } from "./env"; export const chainId = parseInt(Object.keys(env.chainProviders)[0]); export const tokenAddress = env.chainAddresses[chainId]?.testTokenAddress ?? ""; -export const provider = new JsonRpcProvider(env.chainProviders[chainId], chainId); +export const provider = new ChainRpcProvider(chainId, [env.chainProviders[chainId]]); export const sugarDaddy = Wallet.fromMnemonic(env.sugarDaddyMnemonic).connect(provider); export const rando = Wallet.createRandom().connect(provider); diff --git a/modules/router/src/services/config.ts b/modules/router/src/services/config.ts index 79863ca70..4d7315577 100644 --- a/modules/router/src/services/config.ts +++ b/modules/router/src/services/config.ts @@ -4,8 +4,8 @@ import { jsonifyError, IVectorChainReader, DEFAULT_ROUTER_MAX_SAFE_PRICE_IMPACT, + ChainRpcProvider, } from "@connext/vector-types"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { StableSwap } from "@connext/vector-contracts"; import { getAddress } from "@ethersproject/address"; import { BigNumber } from "@ethersproject/bignumber"; @@ -132,7 +132,9 @@ export const onSwapGivenIn = async ( logger: BaseLogger, ): Promise> => { const { stableAmmChainId, stableAmmAddress } = getConfig(); - const stableAmmProvider: JsonRpcProvider = new JsonRpcProvider(getConfig().chainProviders[stableAmmChainId!]); + const stableAmmProvider: ChainRpcProvider = new ChainRpcProvider( + stableAmmChainId!, [getConfig().chainProviders[stableAmmChainId!]] + ); // if there's no swap, rate is 1:1 if (fromAssetId === toAssetId && fromChainId === toChainId) { diff --git a/modules/router/src/test/autoRebalance.spec.ts b/modules/router/src/test/autoRebalance.spec.ts index 6cfc17d6a..7464fef46 100644 --- a/modules/router/src/test/autoRebalance.spec.ts +++ b/modules/router/src/test/autoRebalance.spec.ts @@ -1,9 +1,8 @@ import { VectorChainReader } from "@connext/vector-contracts"; import { expect, getRandomBytes32, getTestLoggers, mkAddress, mkBytes32 } from "@connext/vector-utils"; import Sinon from "sinon"; -import { AllowedSwap, Result } from "@connext/vector-types"; +import { AllowedSwap, ChainRpcProvider, Result } from "@connext/vector-types"; import { Wallet } from "@ethersproject/wallet"; -import { JsonRpcProvider } from "@ethersproject/providers"; import { BigNumber } from "@ethersproject/bignumber"; import { parseEther } from "@ethersproject/units"; import axios from "axios"; @@ -25,7 +24,7 @@ const { log } = getTestLoggers(testName, config.logLevel as any); const setupForRebalance = ( mockAxios: Sinon.SinonStubbedInstance, wallet: Sinon.SinonStubbedInstance, - hydratedProviders: { [chainId: number]: Sinon.SinonStubbedInstance }, + hydratedProviders: { [chainId: number]: Sinon.SinonStubbedInstance }, chainService: Sinon.SinonStubbedInstance, ): { transaction: { @@ -112,7 +111,7 @@ describe(testName, () => { describe("rebalanceIfNeeded", () => { let wallet: Sinon.SinonStubbedInstance; let chainService: Sinon.SinonStubbedInstance; - let hydratedProviders: { [chainId: number]: Sinon.SinonStubbedInstance }; + let hydratedProviders: { [chainId: number]: Sinon.SinonStubbedInstance }; let mockAxios: Sinon.SinonStubbedInstance; let mockConfirmation: Sinon.SinonStubbedInstance; let store: Sinon.SinonStubbedInstance; @@ -132,8 +131,8 @@ describe(testName, () => { chainService = Sinon.createStubInstance(VectorChainReader); hydratedProviders = { - 1337: Sinon.createStubInstance(JsonRpcProvider), - 1338: Sinon.createStubInstance(JsonRpcProvider), + 1337: Sinon.createStubInstance(ChainRpcProvider), + 1338: Sinon.createStubInstance(ChainRpcProvider), }; const parseBalanceStub = Sinon.stub(metrics, "getDecimals").resolves(18); hydratedProviders[1337].getGasPrice.resolves(BigNumber.from(138)); diff --git a/modules/router/src/test/services/config.spec.ts b/modules/router/src/test/services/config.spec.ts index 7ae0cc1e3..fd2e21ab1 100644 --- a/modules/router/src/test/services/config.spec.ts +++ b/modules/router/src/test/services/config.spec.ts @@ -83,7 +83,15 @@ describe("config.ts", () => { }); describe("onSwapGivenIn", () => { - it("error if getOnchainBalance errors", async () => { + let testName = "error if getOnchainBalance errors"; + it(testName, async () => { + const { stableAmmChainId, stableAmmAddress } = config.getConfig(); + if (!stableAmmChainId || !stableAmmAddress) { + log.warn( + `AMM configuration (stableAmmChainId, stableAmmAddress) not provided. Skipping unit test: ${testName}` + ); + return; + } ethReader.getOnchainBalance.onFirstCall().resolves(Result.fail(new ChainError("getOnchainBalance error"))); const res = await onSwapGivenIn( transferAmount, @@ -100,7 +108,15 @@ describe("config.ts", () => { expect(res.getError()!.message).to.be.eq(ConfigServiceError.reasons.CouldNotGetAssetBalance); }); - it("error if provider isn't provided", async () => { + testName = "error if provider isn't provided"; + it(testName, async () => { + const { stableAmmChainId, stableAmmAddress } = config.getConfig(); + if (!stableAmmChainId || !stableAmmAddress) { + log.warn( + `AMM configuration (stableAmmChainId, stableAmmAddress) not provided. Skipping unit test: ${testName}` + ); + return; + } ethReader.getOnchainBalance.onFirstCall().resolves(Result.ok(parseEther("100"))); ethReader.getOnchainBalance.onSecondCall().resolves(Result.ok(parseEther("100"))); const res = await onSwapGivenIn( diff --git a/modules/router/src/test/utils/mocks.ts b/modules/router/src/test/utils/mocks.ts index d6963ac25..2d987ca59 100644 --- a/modules/router/src/test/utils/mocks.ts +++ b/modules/router/src/test/utils/mocks.ts @@ -1,7 +1,7 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; +import { ChainRpcProvider } from "@connext/vector-types"; import { createStubInstance } from "sinon"; -export const mockProvider = createStubInstance(JsonRpcProvider, { +export const mockProvider = createStubInstance(ChainRpcProvider, { waitForTransaction: Promise.resolve({ logs: [] } as any), getNetwork: Promise.resolve({ chainId: 1337, name: "" }), }); diff --git a/modules/types/src/network.ts b/modules/types/src/network.ts index 64284a265..445ed7c29 100644 --- a/modules/types/src/network.ts +++ b/modules/types/src/network.ts @@ -1,9 +1,259 @@ -import { JsonRpcProvider } from "@ethersproject/providers"; +import { FilterByBlockHash, BlockWithTransactions, TransactionRequest } from "@ethersproject/abstract-provider"; +import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; +import { + JsonRpcProvider, + FallbackProvider, + Block, + BlockTag, + EventType, + Filter, + Listener, + Log, + Network, + Provider, + Resolver, + TransactionReceipt, + TransactionResponse, +} from "@ethersproject/providers"; +import { Deferrable } from "@ethersproject/properties"; export type ChainProviders = { - [chainId: number]: string; + [chainId: number]: string[]; }; export type HydratedProviders = { - [chainId: number]: JsonRpcProvider; + [chainId: number]: ChainRpcProvider; }; + +/* Represents an aggregate of providers for a particular chain. Leverages functionality from +* @ethersproject/providers/FallbackProvider in order to fallback to other providers in the +* event of failed requests. +*/ +export class ChainRpcProvider implements Provider { + readonly chainId: number; + readonly providerUrls: string[]; + readonly _provider: JsonRpcProvider | FallbackProvider; + + RPC_TIMEOUT: number = 10_000; + _isProvider: boolean = true; + _networkPromise: Promise; + _network: Network; + anyNetwork: boolean = false; + + constructor(chainId: number, providers: string[] | JsonRpcProvider[], stallTimeout?: number) { + // We'll collect all the provider URLs as we hydrate each provider. + let providerUrls: string[] = []; + let provider: JsonRpcProvider | FallbackProvider; + if (providers.length > 1) { + provider = new FallbackProvider( + // Map the provider URLs into JsonRpcProviders + providers.map((provider: string | JsonRpcProvider, priority: number) => { + const hydratedProvider = (typeof(provider) === "string") ? new JsonRpcProvider(provider, chainId) : provider; + providerUrls.push(hydratedProvider.connection.url); + return { + provider: hydratedProvider, + priority: priority, + // Timeout before also triggering the next provider; this does not stop + // this provider and if its result comes back before a quorum is reached + // it will be incorporated into the vote + // - lower values will cause more network traffic but may result in a + // faster retult. + // TODO: Should we have our own default timeout defined, as well as a config option for this? + // Default timeout is written as either 2sec or .75sec (in @ethers-project/fallback-provider.ts): + // config.stallTimeout = isCommunityResource(configOrProvider) ? 2000: 750; + stallTimeout, + weight: 1 + } + }), + // Quorum stays at 1, since we only ever want to send reqs to 1 node at a time. + 1 + ); + } else if (providers.length === 1) { + const singleProvider = providers[0]; + provider = (typeof(singleProvider) === "string") ? new JsonRpcProvider(singleProvider, chainId) : singleProvider; + providerUrls = [provider.connection.url]; + } else { + throw new Error("At least one provider must be defined.") + } + + this._networkPromise = provider.getNetwork(); + this._network = provider.network; + + this._provider = provider; + this.chainId = chainId; + this.providerUrls = providerUrls; + } + + send(method: string, params: any[]): Promise { + if (this._provider instanceof JsonRpcProvider) { + return (this._provider as JsonRpcProvider).send(method, params); + } else { + // NOTE: The Promise.race below is a substitute for the target functionality of FallbackProvider. In any other method + // call in this class, we literally just call the underlying provider. FallbackProvider does this sort of 'race-like' + // operation we see here internally, under the hood. + const providers = (this._provider as FallbackProvider).providerConfigs.map(p => p.provider as JsonRpcProvider); + let errors: Error[] = []; + // We execute the RPC send call on every child provider (AS JsonRpcProvider) belonging to our FallbackProvider. + // Race below should return fastest RPC response. + return Promise.race( + providers.map(provider => { + return new Promise(async (resolve, reject) => { + try { + const result = await provider.send(method, params); + resolve(result); + } catch (e) { + errors.push(e); + // If this was the last request, and we've gotten all errors, let's reject. + if (errors.length === providers.length) { + reject(errors); + } + } + }); + }) + .concat( + // Ten second timeout to reject with errors. + new Promise((_, reject) => { + setTimeout(() => reject(errors), this.RPC_TIMEOUT) + }) + ) + ); + } + } + + async call(transaction: Deferrable, blockTag?: BlockTag | Promise): Promise { + return this._provider.call(transaction, blockTag); + } + + async estimateGas(transaction: Deferrable): Promise { + return this._provider.estimateGas(transaction); + } + + poll(): Promise { + return this._provider.poll(); + } + + resetEventsBlock(blockNumber: number): void { + return this._provider.resetEventsBlock(blockNumber); + } + + detectNetwork(): Promise { + return this._provider.detectNetwork(); + } + + getNetwork(): Promise { + return this._provider.getNetwork(); + } + + waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise { + return this._provider.waitForTransaction(transactionHash, confirmations, timeout); + } + + getBlockNumber(): Promise { + return this._provider.getBlockNumber(); + } + + getGasPrice(): Promise { + return this._provider.getGasPrice(); + } + + getBalance(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + return this._provider.getBalance(addressOrName, blockTag); + } + + getTransactionCount(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + return this._provider.getTransactionCount(addressOrName, blockTag); + } + + getCode(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + return this._provider.getCode(addressOrName, blockTag); + } + + getStorageAt(addressOrName: string | Promise, position: BigNumberish | Promise, blockTag?: BlockTag | Promise): Promise { + return this._provider.getStorageAt(addressOrName, position); + } + + sendTransaction(signedTransaction: string | Promise): Promise { + return this._provider.sendTransaction(signedTransaction); + } + + getBlock(blockHashOrBlockTag: BlockTag | Promise): Promise { + return this._provider.getBlock(blockHashOrBlockTag); + } + + getBlockWithTransactions(blockHashOrBlockTag: BlockTag | Promise): Promise { + return this._provider.getBlockWithTransactions(blockHashOrBlockTag); + } + + getTransaction(transactionHash: string | Promise): Promise { + return this._provider.getTransaction(transactionHash); + } + + getTransactionReceipt(transactionHash: string | Promise): Promise { + return this._provider.getTransactionReceipt(transactionHash); + } + + getLogs(filter: Filter | FilterByBlockHash | Promise): Promise { + return this._provider.getLogs(filter); + } + + getEtherPrice(): Promise { + return this._provider.getEtherPrice(); + } + + getResolver(name: string): Promise { + return this._provider.getResolver(name); + } + + resolveName(name: string | Promise): Promise { + return this._provider.resolveName(name); + } + + lookupAddress(address: string | Promise): Promise { + return this._provider.lookupAddress(address); + } + + perform(method: string, params: any): Promise { + return this._provider.perform(method, params); + } + + on(eventName: EventType, listener: Listener): this { + this._provider.on(eventName, listener); + return this; + } + + off(eventName: EventType, listener?: Listener): this { + this._provider.off(eventName, listener); + return this; + } + + once(eventName: EventType, listener: Listener): this { + this._provider.once(eventName, listener); + return this; + } + + emit(eventName: EventType, ...args: any[]): boolean { + return this._provider.emit(eventName, ...args); + } + + listenerCount(eventName?: EventType): number { + return this._provider.listenerCount(eventName); + } + + listeners(eventName?: EventType): Listener[] { + return this._provider.listeners(eventName); + } + + removeAllListeners(eventName?: EventType): this { + this._provider.removeAllListeners(eventName); + return this; + } + + addListener(eventName: EventType, listener: Listener): Provider { + return this._provider.addListener(eventName, listener); + } + + removeListener(eventName: EventType, listener: Listener): Provider { + return this._provider.removeListener(eventName, listener); + } + +} diff --git a/modules/utils/src/eth.ts b/modules/utils/src/eth.ts index d3544c4d6..282f940be 100644 --- a/modules/utils/src/eth.ts +++ b/modules/utils/src/eth.ts @@ -1,16 +1,21 @@ -import { ChainProviders, HydratedProviders } from "@connext/vector-types"; +import { ChainProviders, HydratedProviders, ChainRpcProvider } from "@connext/vector-types"; import { Provider } from "@ethersproject/abstract-provider"; import { BigNumber } from "@ethersproject/bignumber"; -import { JsonRpcProvider, StaticJsonRpcProvider } from "@ethersproject/providers"; +import { JsonRpcProvider } from "@ethersproject/providers"; const classicProviders = ["https://www.ethercluster.com/etc"]; const classicChainIds = [61]; const minGasPrice = BigNumber.from(1_000); -export const getEthProvider = (providerUrl: string, chainId?: number): JsonRpcProvider => - new JsonRpcProvider( - providerUrl, - classicProviders.includes(providerUrl) || classicChainIds.includes(chainId) ? "classic" : undefined, +export const getEthProvider = (providerUrl: string, chainId?: number): ChainRpcProvider => + new ChainRpcProvider( + chainId, + [ + new JsonRpcProvider( + providerUrl, + classicProviders.includes(providerUrl) || classicChainIds.includes(chainId) ? "classic" : undefined, + ) + ] ); // xDai hardcoded their gas price to 0 but it's not actually zero.. @@ -20,10 +25,40 @@ export const getGasPrice = async (provider: Provider, providedChainId?: number): return chainId === 100 && price.lt(minGasPrice) ? minGasPrice : price; }; -export const hydrateProviders = (chainProviders: ChainProviders): HydratedProviders => { - const hydratedProviders: { [url: string]: JsonRpcProvider } = {}; +/// Parse CSV formatted provider dict into ChainRpcProviders, which uses a list of Urls per chainId. +export const parseProviders = ( + prevChainRpcProviders: ChainProviders | + { [chainId: string]: string; } | + { [chainId: string]: string[]; } +): ChainProviders => { + let chainProviders: ChainProviders = {} + Object.entries(prevChainRpcProviders).forEach( + ([chainId, urls]) => { + // TODO: Wrap parseInt operation with descriptive error. + let key: number; + try { + key = parseInt(chainId); + } catch (e) { + throw new Error( + `Failed to parse integer chain ID. Please ensure config chain IDs are numeric. Error: ${e}` + ); + } + // Check if providers are still in string format and need to be parsed out. + chainProviders[key] = typeof(urls) === "string" ? urls.split(",") : urls; + } + ); + return chainProviders +} + +export const hydrateProviders = ( + chainProviders: ChainProviders | + { [chainId: string]: string; } | + { [chainId: string]: string[]; } +): HydratedProviders => { + chainProviders = parseProviders(chainProviders); + const hydratedProviders: { [url: string]: ChainRpcProvider } = {}; Object.entries(chainProviders).map(([chainId, url]) => { - hydratedProviders[chainId] = new StaticJsonRpcProvider(url as string, parseInt(chainId)); + hydratedProviders[chainId] = new ChainRpcProvider(parseInt(chainId), url); }); return hydratedProviders; };