diff --git a/modules/browser-node/src/index.ts b/modules/browser-node/src/index.ts index d9abad654..1c26ecbaa 100644 --- a/modules/browser-node/src/index.ts +++ b/modules/browser-node/src/index.ts @@ -2,7 +2,7 @@ import { deployments, VectorChainService } from "@connext/vector-contracts"; import { VectorEngine } from "@connext/vector-engine"; import { ChainAddresses, - ChainProviders, + ChainRpcProviders, ChannelRpcMethods, CreateUpdateDetails, EngineEvent, @@ -35,7 +35,7 @@ export type BrowserNodeSignerConfig = { messagingUrl?: string; logger?: BaseLogger; signer: IChannelSigner; - chainProviders: ChainProviders; + chainProviders: ChainRpcProviders; chainAddresses: ChainAddresses; }; @@ -49,7 +49,7 @@ export class BrowserNode implements INodeService { private supportedChains: number[] = []; private routerPublicIdentifier?: string; private iframeSrc?: string; - private chainProviders: ChainProviders = {}; + private chainProviders: ChainRpcProviders = {}; private chainAddresses?: ChainAddresses; private messagingUrl?: string; private natsUrl?: string; @@ -60,7 +60,7 @@ export class BrowserNode implements INodeService { routerPublicIdentifier?: string; supportedChains?: number[]; iframeSrc?: string; - chainProviders: ChainProviders; + chainProviders: ChainRpcProviders; messagingUrl?: string; natsUrl?: string; authUrl?: string; diff --git a/modules/contracts/src.ts/constants.ts b/modules/contracts/src.ts/constants.ts index e48d991cd..ce9b78171 100644 --- a/modules/contracts/src.ts/constants.ts +++ b/modules/contracts/src.ts/constants.ts @@ -1,13 +1,15 @@ 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 +17,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..f2071bf63 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, ChainRpcProvider, FullChannelState, Result } 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; @@ -70,9 +70,9 @@ describe("ethReader", () => { reset(); }); - describe.skip("getChainProviders", () => { - it("happy: getChainProvider", async () => { - const result = await ethReader.getChainProviders(); + describe.skip("getChainRpcProviders", () => { + it("happy: getChainRpcProvider", async () => { + const result = await ethReader.getChainRpcProviders(); console.log(result); }); }); diff --git a/modules/contracts/src.ts/services/ethReader.ts b/modules/contracts/src.ts/services/ethReader.ts index cf5f1531f..16fc8e3c0 100644 --- a/modules/contracts/src.ts/services/ethReader.ts +++ b/modules/contracts/src.ts/services/ethReader.ts @@ -8,7 +8,7 @@ import { IVectorChainReader, Result, ChainError, - ChainProviders, + ChainRpcProviders, RegisteredTransfer, TransferName, ChannelDispute, @@ -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,15 @@ export class EthereumChainReader implements IVectorChainReader { }; private contracts: Map = new Map(); constructor( - public readonly chainProviders: { [chainId: string]: JsonRpcProvider }, + // The chainProviders specified here are hydrated. + public readonly chainProviders: { [chainId: string]: ChainRpcProvider }, public readonly log: pino.BaseLogger, ) {} - getChainProviders(): Result { - const ret: ChainProviders = {}; + getChainRpcProviders(): Result { + const ret: ChainRpcProviders = {}; 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..bde17a727 100644 --- a/modules/contracts/src.ts/services/ethService.spec.ts +++ b/modules/contracts/src.ts/services/ethService.spec.ts @@ -1,5 +1,6 @@ import { ChainError, + ChainRpcProvider, FullChannelState, IChainServiceStore, IChannelSigner, @@ -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 32e1a5f3e..b123104f6 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, @@ -84,7 +85,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, @@ -528,7 +529,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/engine/src/index.ts b/modules/engine/src/index.ts index a8bb99f84..102de485c 100644 --- a/modules/engine/src/index.ts +++ b/modules/engine/src/index.ts @@ -624,7 +624,7 @@ export class VectorEngine implements IVectorEngine { ); } - const chainProviders = this.chainService.getChainProviders(); + const chainProviders = this.chainService.getChainRpcProviders(); if (chainProviders.isError) { return Result.fail(chainProviders.getError()!); } diff --git a/modules/engine/src/isAlive.ts b/modules/engine/src/isAlive.ts index dde351eec..518441dfb 100644 --- a/modules/engine/src/isAlive.ts +++ b/modules/engine/src/isAlive.ts @@ -11,7 +11,7 @@ export async function sendIsAlive( ): Promise { const method = "sendIsAlive"; const channels = await store.getChannelStates(); - const providers = chainService.getChainProviders(); + const providers = chainService.getChainRpcProviders(); if (providers.isError) { logger.error({ ...providers.getError(), method }, "Error getting chain providers"); return; diff --git a/modules/engine/src/testing/env.ts b/modules/engine/src/testing/env.ts index 37bcc8560..8c32d57a3 100644 --- a/modules/engine/src/testing/env.ts +++ b/modules/engine/src/testing/env.ts @@ -1,9 +1,9 @@ -import { ChainProviders } from "@connext/vector-types"; +import { ChainRpcProviders } from "@connext/vector-types"; import { Wallet } from "@ethersproject/wallet"; import pino from "pino"; type EngineTestEnv = { - chainProviders: ChainProviders; + chainProviders: ChainRpcProviders; chainAddresses: any; sugarDaddy: Wallet; logLevel?: pino.Level; diff --git a/modules/engine/src/testing/index.spec.ts b/modules/engine/src/testing/index.spec.ts index ddc741dcb..14a9b0a1e 100644 --- a/modules/engine/src/testing/index.spec.ts +++ b/modules/engine/src/testing/index.spec.ts @@ -43,7 +43,7 @@ describe("VectorEngine", () => { }); chainService = Sinon.createStubInstance(VectorChainService); - chainService.getChainProviders.returns(Result.ok(env.chainProviders)); + chainService.getChainRpcProviders.returns(Result.ok(env.chainProviders)); }); afterEach(() => Sinon.restore()); diff --git a/modules/engine/src/testing/isAlive.spec.ts b/modules/engine/src/testing/isAlive.spec.ts index 493e3b9e9..f2f3d1ee5 100644 --- a/modules/engine/src/testing/isAlive.spec.ts +++ b/modules/engine/src/testing/isAlive.spec.ts @@ -29,7 +29,7 @@ describe("checkIn", () => { messagingService = Sinon.createStubInstance(MemoryMessagingService); chainService = Sinon.createStubInstance(VectorChainService); - chainService.getChainProviders.returns(Result.ok(env.chainProviders)); + chainService.getChainRpcProviders.returns(Result.ok(env.chainProviders)); }); it("should send no checkIn messages if there are no channels", async () => { diff --git a/modules/iframe-app/src/ConnextManager.tsx b/modules/iframe-app/src/ConnextManager.tsx index 2a38d11c7..1c0c08f05 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, parseProviders, safeJsonParse } 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..4422fe059 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 tokenAddress = env.chainAddresses[chainId]?.testTokenAddress ?? ""; +export const provider = new ChainRpcProvider(chainId, env.chainProviders[chainId].split(",")); export const sugarDaddy = Wallet.fromMnemonic(env.sugarDaddyMnemonic).connect(provider); export const rando = Wallet.createRandom().connect(provider); diff --git a/modules/protocol/src/testing/vector.spec.ts b/modules/protocol/src/testing/vector.spec.ts index 214977ebc..9cfd169b9 100644 --- a/modules/protocol/src/testing/vector.spec.ts +++ b/modules/protocol/src/testing/vector.spec.ts @@ -41,7 +41,7 @@ describe("Vector", () => { chainReader = Sinon.createStubInstance(VectorChainReader); chainReader.getChannelFactoryBytecode.resolves(Result.ok(mkHash())); chainReader.getChannelMastercopyAddress.resolves(Result.ok(mkAddress())); - chainReader.getChainProviders.returns(Result.ok(env.chainProviders)); + chainReader.getChainRpcProviders.returns(Result.ok(env.chainProviders)); lockService = Sinon.createStubInstance(MemoryLockService); messagingService = Sinon.createStubInstance(MemoryMessagingService); storeService = Sinon.createStubInstance(MemoryStoreService); diff --git a/modules/router/src/config.ts b/modules/router/src/config.ts index 4470de1dd..e1efa14dd 100644 --- a/modules/router/src/config.ts +++ b/modules/router/src/config.ts @@ -20,7 +20,7 @@ export type RebalanceProfile = Static; const VectorRouterConfigSchema = Type.Object({ adminToken: Type.String(), allowedSwaps: Type.Array(AllowedSwapSchema), - chainProviders: Type.Dict(TUrl), + chainProviders: Type.Dict(Type.String()), dbUrl: Type.Optional(TUrl), nodeUrl: TUrl, routerUrl: TUrl, diff --git a/modules/router/src/index.ts b/modules/router/src/index.ts index 1b8de863d..c0fbf915a 100644 --- a/modules/router/src/index.ts +++ b/modules/router/src/index.ts @@ -4,7 +4,7 @@ import fastify from "fastify"; import pino from "pino"; import { Evt } from "evt"; import { VectorChainReader } from "@connext/vector-contracts"; -import { EventCallbackConfig, hydrateProviders, RestServerNodeService, ChannelSigner } from "@connext/vector-utils"; +import { EventCallbackConfig, hydrateProviders, parseProviders, RestServerNodeService, ChannelSigner } from "@connext/vector-utils"; import { IsAlivePayload, ConditionalTransferCreatedPayload, @@ -149,8 +149,8 @@ const server = fastify({ collectDefaultMetrics({ prefix: "router_" }); let router: IRouter; -const store = new PrismaStore(); -const hydratedProviders = hydrateProviders(config.chainProviders); +const store = new PrismaStore() +const hydratedProviders = hydrateProviders(parseProviders(config.chainProviders)); const chainService = new VectorChainReader(hydratedProviders, logger.child({ module: "RouterChainReader" })); const messagingService = new NatsRouterMessagingService({ signer, diff --git a/modules/router/src/metrics.ts b/modules/router/src/metrics.ts index ac91ba473..22759a6e2 100644 --- a/modules/router/src/metrics.ts +++ b/modules/router/src/metrics.ts @@ -13,6 +13,7 @@ import { getMainnetEquivalent, getExchangeRateInEth, calculateExchangeWad, + parseProviders, } from "@connext/vector-utils"; import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; @@ -35,7 +36,7 @@ const config = getConfig(); ///// Helpers/Utils export const wallet = Wallet.fromMnemonic(config.mnemonic); export const signerAddress = wallet.address; -export const hydrated: HydratedProviders = hydrateProviders(config.chainProviders); +export const hydrated: HydratedProviders = hydrateProviders(parseProviders(config.chainProviders)); export const rebalancedTokens: { [chainId: string]: { [assetId: string]: { diff --git a/modules/router/src/services/config.ts b/modules/router/src/services/config.ts index 79863ca70..9108d900b 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,7 @@ 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..6c536f030 100644 --- a/modules/router/src/test/autoRebalance.spec.ts +++ b/modules/router/src/test/autoRebalance.spec.ts @@ -1,20 +1,17 @@ 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"; -import PriorityQueue from "p-queue"; import { rebalanceIfNeeded } from "../services/autoRebalance"; import { getConfig } from "../config"; import * as metrics from "../metrics"; import { PrismaStore, RouterRebalanceStatus } from "../services/store"; import { _createQueueForSwap } from "../services/rebalanceQueue"; -import { AutoRebalanceServiceError } from "../errors"; const config = getConfig(); @@ -25,7 +22,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 +109,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 +129,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/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/server-node/src/index.ts b/modules/server-node/src/index.ts index 7460d032c..939f2b79f 100644 --- a/modules/server-node/src/index.ts +++ b/modules/server-node/src/index.ts @@ -19,7 +19,7 @@ import { VectorErrorJson, StoredTransaction, } from "@connext/vector-types"; -import { constructRpcRequest, getPublicIdentifierFromPublicKey, hydrateProviders } from "@connext/vector-utils"; +import { constructRpcRequest, getPublicIdentifierFromPublicKey, hydrateProviders, parseProviders } from "@connext/vector-utils"; import { WithdrawCommitment } from "@connext/vector-contracts"; import { Static, Type } from "@sinclair/typebox"; import { Wallet } from "@ethersproject/wallet"; @@ -51,7 +51,7 @@ server.register(fastifyCors, { export const store = new PrismaStore(); -export const _providers = hydrateProviders(config.chainProviders); +export const _providers = hydrateProviders(parseProviders(config.chainProviders)); server.addHook("onReady", async () => { const persistedNodes = await store.getNodeIndexes(); diff --git a/modules/types/src/chain.ts b/modules/types/src/chain.ts index 6048b28be..60c28e020 100644 --- a/modules/types/src/chain.ts +++ b/modules/types/src/chain.ts @@ -7,7 +7,7 @@ import { Balance, FullChannelState, FullTransferState } from "./channel"; import { ChannelDispute } from "./dispute"; import { Result, Values, VectorError } from "./error"; import { ChainServiceEvent, ChainServiceEventMap } from "./event"; -import { ChainProviders, HydratedProviders } from "./network"; +import { ChainRpcProviders, HydratedProviders } from "./network"; import { RegisteredTransfer, TransferName, TransferState, WithdrawCommitmentJson } from "./transferDefinitions"; export const GAS_ESTIMATES = { @@ -185,7 +185,7 @@ export interface IVectorChainReader { getDecimals(assetId: string, chainId: number): Promise>; - getChainProviders(): Result; + getChainRpcProviders(): Result; getHydratedProviders(): Result; diff --git a/modules/types/src/network.ts b/modules/types/src/network.ts index 64284a265..74176d4ce 100644 --- a/modules/types/src/network.ts +++ b/modules/types/src/network.ts @@ -1,9 +1,253 @@ -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; +export type ChainRpcProviders = { + [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. + var providerUrls: string[] = []; + var 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; + } 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 { + const providers = (this._provider as FallbackProvider).providerConfigs.map(p => p.provider as JsonRpcProvider); + var errors: Error[] = []; + 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); + } + +} \ No newline at end of file diff --git a/modules/types/src/vectorProvider.ts b/modules/types/src/vectorProvider.ts index bfd3b4e8f..950f410fc 100644 --- a/modules/types/src/vectorProvider.ts +++ b/modules/types/src/vectorProvider.ts @@ -2,7 +2,7 @@ import { MinimalTransaction } from "./chain"; import { FullTransferState, FullChannelState, ChainAddresses } from "./channel"; import { ChannelDispute, TransferDispute } from "./dispute"; import { VectorErrorJson } from "./error"; -import { ChainProviders } from "./network"; +import { ChainRpcProviders } from "./network"; import { EngineParams, NodeResponses } from "./schemas"; import { RegisteredTransfer } from "./transferDefinitions"; @@ -83,7 +83,7 @@ export type ChannelRpcMethodsPayloadMap = { [ChannelRpcMethods.connext_authenticate]: { signature?: string; signer?: string; - chainProviders: ChainProviders; + chainProviders: ChainRpcProviders; chainAddresses?: ChainAddresses; messagingUrl?: string; natsUrl?: string; diff --git a/modules/utils/src/eth.ts b/modules/utils/src/eth.ts index d3544c4d6..6c6282a20 100644 --- a/modules/utils/src/eth.ts +++ b/modules/utils/src/eth.ts @@ -1,16 +1,22 @@ -import { ChainProviders, HydratedProviders } from "@connext/vector-types"; +import { ChainRpcProviders, HydratedProviders } 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"; +import { ChainRpcProvider } from "@connext/vector-types"; 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 +26,21 @@ 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: { [chainId: string]: string }): ChainRpcProviders => { + var chainProviders: ChainRpcProviders = {} + Object.entries(prevChainRpcProviders).forEach( + ([chainId, urlString]) => { + chainProviders[chainId] = urlString.split(","); + } + ); + return chainProviders +} + +export const hydrateProviders = (chainProviders: ChainRpcProviders): HydratedProviders => { + 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; }; diff --git a/modules/utils/src/fees.spec.ts b/modules/utils/src/fees.spec.ts index 3d7233b1f..68ace379f 100644 --- a/modules/utils/src/fees.spec.ts +++ b/modules/utils/src/fees.spec.ts @@ -2,7 +2,7 @@ import { REDUCED_GAS_PRICE, Balance, ChainError, - ChainProviders, + ChainRpcProviders, ChannelDispute, FullTransferState, HydratedProviders, @@ -157,7 +157,7 @@ class MockChainReader implements IVectorChainReader { ): Promise> { throw new Error("Method not implemented."); } - getChainProviders(): Result { + getChainRpcProviders(): Result { throw new Error("Method not implemented."); } getHydratedProviders(): Result {