Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Typescript conversion of ethereum-chain-utils.js #26103

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { errorCodes, ethErrors } from 'eth-rpc-errors';
import { ApprovalType } from '@metamask/controller-utils';
import {
Hex,
Json,
JsonRpcParams,
JsonRpcRequest,
JsonRpcResponse,
isJsonRpcError,
} from '@metamask/utils';
import { JsonRpcEngineEndCallback } from 'json-rpc-engine';
import { OriginString } from '@metamask/permission-controller';

import {
BUILT_IN_INFURA_NETWORKS,
Expand All @@ -16,12 +26,25 @@ import { CaveatTypes } from '../../../../../shared/constants/permissions';
import { UNKNOWN_TICKER_SYMBOL } from '../../../../../shared/constants/app';
import { PermissionNames } from '../../../controllers/permissions';
import { getValidUrl } from '../../util';
import {
EndApprovalFlow,
FindNetworkConfigurationBy,
GetCaveat,
GetChainPermissionsFeatureFlag,
RequestPermittedChainsPermission,
RequestUserApproval,
SetActiveNetwork,
} from './types';

export function findExistingNetwork(chainId, findNetworkConfigurationBy) {
export function findExistingNetwork(
chainId: Hex,
findNetworkConfigurationBy: FindNetworkConfigurationBy,
) {
if (
Object.values(BUILT_IN_INFURA_NETWORKS)
.map(({ chainId: id }) => id)
.includes(chainId)
((inputId: Hex): inputId is keyof typeof CHAIN_ID_TO_TYPE_MAP =>
Object.values(BUILT_IN_INFURA_NETWORKS)
.map(({ chainId: id }) => id)
.find((id) => id === inputId) !== undefined)(chainId)
) {
return {
chainId,
Expand All @@ -34,9 +57,14 @@ export function findExistingNetwork(chainId, findNetworkConfigurationBy) {
return findNetworkConfigurationBy({ chainId });
}

export function validateChainId(chainId) {
const _chainId = typeof chainId === 'string' && chainId.toLowerCase();
if (!isPrefixedFormattedHexString(_chainId)) {
export function validateChainId(chainId: Hex): Hex {
const _chainId = chainId.toLowerCase();

if (
!((value: string): value is Hex => isPrefixedFormattedHexString(value))(
_chainId,
)
) {
throw ethErrors.rpc.invalidParams({
message: `Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`,
});
Expand All @@ -51,7 +79,9 @@ export function validateChainId(chainId) {
return _chainId;
}

export function validateSwitchEthereumChainParams(req, end) {
export function validateSwitchEthereumChainParams<
Params extends JsonRpcParams = { chainId: Hex } & JsonRpcParams,
>(req: JsonRpcRequest<Params>) {
if (!req.params?.[0] || typeof req.params[0] !== 'object') {
throw ethErrors.rpc.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
Expand All @@ -69,10 +99,14 @@ export function validateSwitchEthereumChainParams(req, end) {
});
}

return validateChainId(chainId, end);
return validateChainId(chainId);
}

export function validateAddEthereumChainParams(params, end) {
export function validateAddEthereumChainParams(
params: {
chainId: Hex;
} & Record<string, Json>,
) {
if (!params || typeof params !== 'object') {
throw ethErrors.rpc.invalidParams({
message: `Expected single, object parameter. Received:\n${JSON.stringify(
Expand Down Expand Up @@ -101,14 +135,14 @@ export function validateAddEthereumChainParams(params, end) {
});
}

const _chainId = validateChainId(chainId, end);
const _chainId = validateChainId(chainId);
if (!rpcUrls || !Array.isArray(rpcUrls) || rpcUrls.length === 0) {
throw ethErrors.rpc.invalidParams({
message: `Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`,
});
}

const isLocalhostOrHttps = (urlString) => {
const isLocalhostOrHttps = (urlString: string) => {
const url = getValidUrl(urlString);
return (
url !== null &&
Expand All @@ -118,11 +152,16 @@ export function validateAddEthereumChainParams(params, end) {
);
};

const firstValidRPCUrl = rpcUrls.find((rpcUrl) => isLocalhostOrHttps(rpcUrl));
const firstValidRPCUrl = rpcUrls.find(
(rpcUrl): rpcUrl is string =>
typeof rpcUrl === 'string' && isLocalhostOrHttps(rpcUrl),
);
const firstValidBlockExplorerUrl =
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls)
? blockExplorerUrls.find((blockExplorerUrl) =>
isLocalhostOrHttps(blockExplorerUrl),
? blockExplorerUrls.find(
(blockExplorerUrl): blockExplorerUrl is string =>
typeof blockExplorerUrl === 'string' &&
isLocalhostOrHttps(blockExplorerUrl),
)
: null;

Expand Down Expand Up @@ -185,22 +224,31 @@ export function validateAddEthereumChainParams(params, end) {
};
}

export async function switchChain(
res,
end,
origin,
chainId,
requestData,
networkClientId,
approvalFlowId,
type SwitchChainOptions = {
getChainPermissionsFeatureFlag: GetChainPermissionsFeatureFlag;
setActiveNetwork: SetActiveNetwork;
endApprovalFlow?: EndApprovalFlow;
requestUserApproval: RequestUserApproval;
getCaveat: GetCaveat;
requestPermittedChainsPermission: RequestPermittedChainsPermission;
};

export async function switchChain<Result extends Json = never>(
res: JsonRpcResponse<Result | null>,
end: JsonRpcEngineEndCallback,
origin: OriginString,
chainId: Hex,
requestData: Record<string, Json>,
networkClientId: string,
approvalFlowId: string | null,
{
getChainPermissionsFeatureFlag,
setActiveNetwork,
endApprovalFlow,
requestUserApproval,
getCaveat,
requestPermittedChainsPermission,
},
}: SwitchChainOptions,
) {
try {
if (getChainPermissionsFeatureFlag()) {
Expand All @@ -210,10 +258,7 @@ export async function switchChain(
caveatType: CaveatTypes.restrictNetworkSwitching,
}) ?? {};

if (
permissionedChainIds === undefined ||
!permissionedChainIds.includes(chainId)
) {
if (!permissionedChainIds?.includes(chainId)) {
await requestPermittedChainsPermission([chainId]);
}
} else {
Expand All @@ -226,22 +271,25 @@ export async function switchChain(

await setActiveNetwork(networkClientId);
res.result = null;
} catch (error) {
} catch (error: unknown) {
// We don't want to return an error if user rejects the request
// and this is a chained switch request after wallet_addEthereumChain.
// approvalFlowId is only defined when this call is of a
// wallet_addEthereumChain request so we can use it to determine
// if we should return an error
if (
isJsonRpcError(error) &&
error.code === errorCodes.provider.userRejectedRequest &&
approvalFlowId
) {
res.result = null;
return end();
}
// TODO: Remove at `@metamask/[email protected]`: `JsonRpcEngineEndCallback` (type of `end`), is redefined from `(error?: JsonRpcEngineCallbackError) => void` to `(error?: unknown) => void`.
// @ts-expect-error intentionally passing unhandled error of any type into `end`
return end(error);
} finally {
if (approvalFlowId) {
if (approvalFlowId && endApprovalFlow) {
endApprovalFlow({ id: approvalFlowId });
}
}
Expand Down
24 changes: 24 additions & 0 deletions app/scripts/lib/rpc-method-middleware/handlers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
AddApprovalOptions,
EndFlowOptions,
} from '@metamask/approval-controller';
import { ProviderConfig } from '@metamask/network-controller';

export type EndApprovalFlow = ({ id }: EndFlowOptions) => void;
export type FindNetworkConfigurationBy = (
rpcInfo: Record<string, string>,
) => ProviderConfig | null;
export type GetCaveat = (options: {
target: string;
caveatType: string;
}) => Record<string, string[]> | undefined;
export type GetChainPermissionsFeatureFlag = () => boolean;
export type RequestPermittedChainsPermission = (
chainIds: string[],
) => Promise<void>;
export type RequestUserApproval = (
options?: AddApprovalOptions,
) => Promise<unknown>;
export type SetActiveNetwork = (
networkConfigurationIdOrType: string,
) => Promise<void>;