UNPKG

@metamask/network-controller

Version:

Provides an interface to the currently selected network via a MetaMask-compatible provider object

319 lines 13.2 kB
import { ChainId } from "@metamask/controller-utils"; import { PollingBlockTracker } from "@metamask/eth-block-tracker"; import { createInfuraMiddleware } from "@metamask/eth-json-rpc-infura"; import { createBlockCacheMiddleware, createBlockRefMiddleware, createBlockRefRewriteMiddleware, createBlockTrackerInspectorMiddleware, createInflightCacheMiddleware, createFetchMiddleware, createRetryOnEmptyMiddleware } from "@metamask/eth-json-rpc-middleware"; import { InternalProvider } from "@metamask/eth-json-rpc-provider"; import { providerFromMiddlewareV2 } from "@metamask/eth-json-rpc-provider"; import { asV2Middleware } from "@metamask/json-rpc-engine"; import { createScaffoldMiddleware, JsonRpcEngineV2 } from "@metamask/json-rpc-engine/v2"; import { RpcServiceChain } from "./rpc-service/rpc-service-chain.mjs"; import { NetworkClientType } from "./types.mjs"; const SECOND = 1000; /** * Create a JSON RPC network client for a specific network. * * @param args - The arguments. * @param args.id - The ID that will be assigned to the new network client in * the registry. * @param args.configuration - The network configuration. * @param args.getRpcServiceOptions - Factory for constructing RPC service * options. See {@link NetworkControllerOptions.getRpcServiceOptions}. * @param args.getBlockTrackerOptions - Factory for constructing block tracker * options. See {@link NetworkControllerOptions.getBlockTrackerOptions}. * @param args.messenger - The network controller messenger. * @param args.isRpcFailoverEnabled - Whether or not requests sent to the * primary RPC endpoint for this network should be automatically diverted to * provided failover endpoints if the primary is unavailable. This effectively * causes the `failoverRpcUrls` property of the network client configuration * to be honored or ignored. * @param args.logger - A `loglevel` logger. * @returns The network client. */ export function createNetworkClient({ id, configuration, getRpcServiceOptions, getBlockTrackerOptions, messenger, isRpcFailoverEnabled, logger, }) { const primaryEndpointUrl = configuration.type === NetworkClientType.Infura ? `https://${configuration.network}.infura.io/v3/${configuration.infuraProjectId}` : configuration.rpcUrl; const rpcServiceChain = createRpcServiceChain({ id, primaryEndpointUrl, configuration, getRpcServiceOptions, messenger, isRpcFailoverEnabled, logger, }); let rpcApiMiddleware; if (configuration.type === NetworkClientType.Infura) { rpcApiMiddleware = asV2Middleware(createInfuraMiddleware({ rpcService: rpcServiceChain, options: { source: 'metamask', }, })); } else { rpcApiMiddleware = createFetchMiddleware({ rpcService: rpcServiceChain }); } const rpcProvider = providerFromMiddlewareV2(rpcApiMiddleware); const blockTracker = createBlockTracker({ networkClientType: configuration.type, endpointUrl: primaryEndpointUrl, getOptions: getBlockTrackerOptions, provider: rpcProvider, }); const networkMiddleware = configuration.type === NetworkClientType.Infura ? createInfuraNetworkMiddleware({ blockTracker, network: configuration.network, rpcProvider, rpcApiMiddleware, }) : createCustomNetworkMiddleware({ blockTracker, chainId: configuration.chainId, rpcApiMiddleware, }); const provider = new InternalProvider({ engine: JsonRpcEngineV2.create({ middleware: [networkMiddleware], }), }); const destroy = () => { // TODO: Either fix this lint violation or explain why it's necessary to ignore. // eslint-disable-next-line @typescript-eslint/no-floating-promises blockTracker.destroy(); }; return { configuration, provider, blockTracker, destroy }; } /** * Creates an RPC service chain, which represents the primary endpoint URL for * the network as well as its failover URLs. * * @param args - The arguments. * @param args.id - The ID that will be assigned to the new network client in * the registry. * @param args.primaryEndpointUrl - The primary endpoint URL. * @param args.configuration - The network configuration. * @param args.getRpcServiceOptions - Factory for constructing RPC service * options. See {@link NetworkControllerOptions.getRpcServiceOptions}. * @param args.messenger - The network controller messenger. * @param args.isRpcFailoverEnabled - Whether or not requests sent to the * primary RPC endpoint for this network should be automatically diverted to * provided failover endpoints if the primary is unavailable. This effectively * causes the `failoverRpcUrls` property of the network client configuration * to be honored or ignored. * @param args.logger - A `loglevel` logger. * @returns The RPC service chain. */ function createRpcServiceChain({ id, primaryEndpointUrl, configuration, getRpcServiceOptions, messenger, isRpcFailoverEnabled, logger, }) { const availableEndpointUrls = isRpcFailoverEnabled ? [primaryEndpointUrl, ...(configuration.failoverRpcUrls ?? [])] : [primaryEndpointUrl]; const rpcServiceConfigurations = availableEndpointUrls.map((endpointUrl) => ({ ...getRpcServiceOptions(endpointUrl), endpointUrl, logger, })); /** * Extracts the error from Cockatiel's `FailureReason` type received in * circuit breaker event handlers. * * The `FailureReason` object can have two possible shapes: * - `{ error: Error }` - When the RPC service throws an error (the common * case for RPC failures). * - `{ value: T }` - When the RPC service returns a value that the retry * filter policy considers a failure. * * @param value - The event data object from the circuit breaker event * listener (after destructuring known properties like `endpointUrl`). This * represents Cockatiel's `FailureReason` type. * @returns The error or failure value, or `undefined` if neither property * exists (which shouldn't happen in practice unless the circuit breaker is * manually isolated). */ const getError = (value) => { if ('error' in value) { return value.error; } else if ('value' in value) { return value.value; } return undefined; }; const rpcServiceChain = new RpcServiceChain([ rpcServiceConfigurations[0], ...rpcServiceConfigurations.slice(1), ]); rpcServiceChain.onBreak((data) => { const error = getError(data); if (error === undefined) { // This error shouldn't happen in practice because we never call `.isolate` // on the circuit breaker policy, but we need to appease TypeScript. throw new Error('Could not make request to endpoint.'); } messenger.publish('NetworkController:rpcEndpointChainUnavailable', { chainId: configuration.chainId, networkClientId: id, error, }); }); rpcServiceChain.onServiceBreak(({ endpointUrl, primaryEndpointUrl: primaryEndpointUrlFromEvent, ...rest }) => { const error = getError(rest); if (error === undefined) { // This error shouldn't happen in practice because we never call `.isolate` // on the circuit breaker policy, but we need to appease TypeScript. throw new Error('Could not make request to endpoint.'); } messenger.publish('NetworkController:rpcEndpointUnavailable', { chainId: configuration.chainId, networkClientId: id, primaryEndpointUrl: primaryEndpointUrlFromEvent, endpointUrl, error, }); }); rpcServiceChain.onDegraded((data) => { const error = getError(data); messenger.publish('NetworkController:rpcEndpointChainDegraded', { chainId: configuration.chainId, networkClientId: id, error, }); }); rpcServiceChain.onServiceDegraded(({ endpointUrl, primaryEndpointUrl: primaryEndpointUrlFromEvent, ...rest }) => { const error = getError(rest); messenger.publish('NetworkController:rpcEndpointDegraded', { chainId: configuration.chainId, networkClientId: id, primaryEndpointUrl: primaryEndpointUrlFromEvent, endpointUrl, error, }); }); rpcServiceChain.onAvailable(() => { messenger.publish('NetworkController:rpcEndpointChainAvailable', { chainId: configuration.chainId, networkClientId: id, }); }); rpcServiceChain.onServiceRetry(({ attempt, endpointUrl, primaryEndpointUrl: primaryEndpointUrlFromEvent, }) => { messenger.publish('NetworkController:rpcEndpointRetried', { chainId: configuration.chainId, networkClientId: id, primaryEndpointUrl: primaryEndpointUrlFromEvent, endpointUrl, attempt, }); }); return rpcServiceChain; } /** * Create the block tracker for the network. * * @param args - The arguments. * @param args.networkClientType - The type of the network client ("infura" or * "custom"). * @param args.endpointUrl - The URL of the endpoint. * @param args.getOptions - Factory for the block tracker options. * @param args.provider - The EIP-1193 provider for the network's JSON-RPC * middleware stack. * @returns The created block tracker. */ function createBlockTracker({ networkClientType, endpointUrl, getOptions, provider, }) { const testOptions = // Needed for testing. // eslint-disable-next-line no-restricted-globals process.env.IN_TEST && networkClientType === NetworkClientType.Custom ? { pollingInterval: SECOND } : {}; return new PollingBlockTracker({ ...testOptions, ...getOptions(endpointUrl), provider, }); } /** * Create middleware for infura. * * @param args - The arguments. * @param args.blockTracker - The block tracker to use. * @param args.network - The Infura network to use. * @param args.rpcProvider - The RPC provider to use. * @param args.rpcApiMiddleware - Additional middleware. * @returns The collection of middleware that makes up the Infura client. */ function createInfuraNetworkMiddleware({ blockTracker, network, rpcProvider, rpcApiMiddleware, }) { return JsonRpcEngineV2.create({ middleware: [ createNetworkAndChainIdMiddleware({ network }), createBlockCacheMiddleware({ blockTracker }), createInflightCacheMiddleware(), createBlockRefMiddleware({ blockTracker, provider: rpcProvider }), createRetryOnEmptyMiddleware({ blockTracker, provider: rpcProvider }), createBlockTrackerInspectorMiddleware({ blockTracker }), rpcApiMiddleware, ], }).asMiddleware(); } /** * Creates static method middleware. * * @param args - The Arguments. * @param args.network - The Infura network to use. * @returns The middleware that implements the eth_chainId method. */ function createNetworkAndChainIdMiddleware({ network, }) { return createScaffoldMiddleware({ eth_chainId: ChainId[network], }); } const createChainIdMiddleware = (chainId) => { return ({ request, next }) => { if (request.method === 'eth_chainId') { return chainId; } return next(); }; }; /** * Creates custom middleware. * * @param args - The arguments. * @param args.blockTracker - The block tracker to use. * @param args.chainId - The chain id to use. * @param args.rpcApiMiddleware - Additional middleware. * @returns The collection of middleware that makes up the Infura client. */ function createCustomNetworkMiddleware({ blockTracker, chainId, rpcApiMiddleware, }) { // Needed for testing. // eslint-disable-next-line no-restricted-globals const testMiddlewares = process.env.IN_TEST ? [createEstimateGasDelayTestMiddleware()] : []; return JsonRpcEngineV2.create({ middleware: [ ...testMiddlewares, createChainIdMiddleware(chainId), createBlockRefRewriteMiddleware({ blockTracker }), createBlockCacheMiddleware({ blockTracker }), createInflightCacheMiddleware(), createBlockTrackerInspectorMiddleware({ blockTracker }), rpcApiMiddleware, ], }).asMiddleware(); } /** * For use in tests only. * Adds a delay to `eth_estimateGas` calls. * * @returns The middleware for delaying gas estimation calls by 2 seconds when in test. */ function createEstimateGasDelayTestMiddleware() { return async ({ request, next }) => { if (request.method === 'eth_estimateGas') { await new Promise((resolve) => setTimeout(resolve, SECOND * 2)); } return next(); }; } //# sourceMappingURL=create-network-client.mjs.map