@metamask/network-controller
Version:
Provides an interface to the currently selected network via a MetaMask-compatible provider object
323 lines • 13.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createNetworkClient = void 0;
const controller_utils_1 = require("@metamask/controller-utils");
const eth_block_tracker_1 = require("@metamask/eth-block-tracker");
const eth_json_rpc_infura_1 = require("@metamask/eth-json-rpc-infura");
const eth_json_rpc_middleware_1 = require("@metamask/eth-json-rpc-middleware");
const eth_json_rpc_provider_1 = require("@metamask/eth-json-rpc-provider");
const eth_json_rpc_provider_2 = require("@metamask/eth-json-rpc-provider");
const json_rpc_engine_1 = require("@metamask/json-rpc-engine");
const v2_1 = require("@metamask/json-rpc-engine/v2");
const rpc_service_chain_1 = require("./rpc-service/rpc-service-chain.cjs");
const types_1 = require("./types.cjs");
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.
*/
function createNetworkClient({ id, configuration, getRpcServiceOptions, getBlockTrackerOptions, messenger, isRpcFailoverEnabled, logger, }) {
const primaryEndpointUrl = configuration.type === types_1.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 === types_1.NetworkClientType.Infura) {
rpcApiMiddleware = (0, json_rpc_engine_1.asV2Middleware)((0, eth_json_rpc_infura_1.createInfuraMiddleware)({
rpcService: rpcServiceChain,
options: {
source: 'metamask',
},
}));
}
else {
rpcApiMiddleware = (0, eth_json_rpc_middleware_1.createFetchMiddleware)({ rpcService: rpcServiceChain });
}
const rpcProvider = (0, eth_json_rpc_provider_2.providerFromMiddlewareV2)(rpcApiMiddleware);
const blockTracker = createBlockTracker({
networkClientType: configuration.type,
endpointUrl: primaryEndpointUrl,
getOptions: getBlockTrackerOptions,
provider: rpcProvider,
});
const networkMiddleware = configuration.type === types_1.NetworkClientType.Infura
? createInfuraNetworkMiddleware({
blockTracker,
network: configuration.network,
rpcProvider,
rpcApiMiddleware,
})
: createCustomNetworkMiddleware({
blockTracker,
chainId: configuration.chainId,
rpcApiMiddleware,
});
const provider = new eth_json_rpc_provider_1.InternalProvider({
engine: v2_1.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 };
}
exports.createNetworkClient = createNetworkClient;
/**
* 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 rpc_service_chain_1.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 === types_1.NetworkClientType.Custom
? { pollingInterval: SECOND }
: {};
return new eth_block_tracker_1.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 v2_1.JsonRpcEngineV2.create({
middleware: [
createNetworkAndChainIdMiddleware({ network }),
(0, eth_json_rpc_middleware_1.createBlockCacheMiddleware)({ blockTracker }),
(0, eth_json_rpc_middleware_1.createInflightCacheMiddleware)(),
(0, eth_json_rpc_middleware_1.createBlockRefMiddleware)({ blockTracker, provider: rpcProvider }),
(0, eth_json_rpc_middleware_1.createRetryOnEmptyMiddleware)({ blockTracker, provider: rpcProvider }),
(0, eth_json_rpc_middleware_1.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 (0, v2_1.createScaffoldMiddleware)({
eth_chainId: controller_utils_1.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 v2_1.JsonRpcEngineV2.create({
middleware: [
...testMiddlewares,
createChainIdMiddleware(chainId),
(0, eth_json_rpc_middleware_1.createBlockRefRewriteMiddleware)({ blockTracker }),
(0, eth_json_rpc_middleware_1.createBlockCacheMiddleware)({ blockTracker }),
(0, eth_json_rpc_middleware_1.createInflightCacheMiddleware)(),
(0, eth_json_rpc_middleware_1.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.cjs.map