-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor switchEthereumChain + addEthereumChain handlers
- Loading branch information
Showing
3 changed files
with
357 additions
and
303 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,244 @@ | ||
import { rpcErrors } from '@metamask/rpc-errors'; | ||
import validUrl from 'valid-url'; | ||
import { isSafeChainId } from '@metamask/controller-utils'; | ||
import { jsonRpcRequest } from '../../../util/jsonRpcRequest'; | ||
import { | ||
getDecimalChainId, | ||
isPrefixedFormattedHexString, | ||
getDefaultNetworkByChainId, | ||
} from '../../../util/networks'; | ||
|
||
const EVM_NATIVE_TOKEN_DECIMALS = 18; | ||
|
||
export function validateChainId(chainId) { | ||
const _chainId = typeof chainId === 'string' && chainId.toLowerCase(); | ||
|
||
if (!isPrefixedFormattedHexString(_chainId)) { | ||
throw rpcErrors.invalidParams( | ||
`Expected 0x-prefixed, unpadded, non-zero hexadecimal string 'chainId'. Received:\n${chainId}`, | ||
); | ||
} | ||
|
||
if (!isSafeChainId(_chainId)) { | ||
throw rpcErrors.invalidParams( | ||
`Invalid chain ID "${_chainId}": numerical value greater than max safe value. Received:\n${chainId}`, | ||
); | ||
} | ||
|
||
return _chainId; | ||
} | ||
|
||
export function validateAddEthereumChainParams(params) { | ||
if (!params || typeof params !== 'object') { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected single, object parameter. Received:\n${JSON.stringify( | ||
params, | ||
)}`, | ||
}); | ||
} | ||
|
||
const { | ||
chainId, | ||
chainName: rawChainName = null, | ||
blockExplorerUrls = null, | ||
nativeCurrency = null, | ||
rpcUrls, | ||
} = params; | ||
|
||
const allowedKeys = { | ||
chainId: true, | ||
chainName: true, | ||
blockExplorerUrls: true, | ||
nativeCurrency: true, | ||
rpcUrls: true, | ||
iconUrls: true, | ||
}; | ||
|
||
const extraKeys = Object.keys(params).filter((key) => !allowedKeys[key]); | ||
if (extraKeys.length) { | ||
throw rpcErrors.invalidParams( | ||
`Received unexpected keys on object parameter. Unsupported keys:\n${extraKeys}`, | ||
); | ||
} | ||
|
||
const _chainId = validateChainId(chainId); | ||
const firstValidRPCUrl = validateRpcUrls(rpcUrls); | ||
const firstValidBlockExplorerUrl = | ||
validateBlockExplorerUrls(blockExplorerUrls); | ||
const chainName = validateChainName(rawChainName); | ||
const ticker = validateNativeCurrency(nativeCurrency); | ||
|
||
return { | ||
chainId: _chainId, | ||
chainName, | ||
firstValidRPCUrl, | ||
firstValidBlockExplorerUrl, | ||
ticker, | ||
}; | ||
} | ||
|
||
function validateRpcUrls(rpcUrls) { | ||
const dirtyFirstValidRPCUrl = Array.isArray(rpcUrls) | ||
? rpcUrls.find((rpcUrl) => validUrl.isHttpsUri(rpcUrl)) | ||
: null; | ||
|
||
const firstValidRPCUrl = dirtyFirstValidRPCUrl | ||
? dirtyFirstValidRPCUrl.replace(/([^/])\/+$/g, '$1') | ||
: dirtyFirstValidRPCUrl; | ||
|
||
if (!firstValidRPCUrl) { | ||
throw rpcErrors.invalidParams( | ||
`Expected an array with at least one valid string HTTPS url 'rpcUrls', Received:\n${rpcUrls}`, | ||
); | ||
} | ||
|
||
return firstValidRPCUrl; | ||
} | ||
|
||
function validateBlockExplorerUrls(blockExplorerUrls) { | ||
const firstValidBlockExplorerUrl = | ||
blockExplorerUrls !== null && Array.isArray(blockExplorerUrls) | ||
? blockExplorerUrls.find((blockExplorerUrl) => | ||
validUrl.isHttpsUri(blockExplorerUrl), | ||
) | ||
: null; | ||
|
||
if (blockExplorerUrls !== null && !firstValidBlockExplorerUrl) { | ||
throw rpcErrors.invalidParams( | ||
`Expected null or array with at least one valid string HTTPS URL 'blockExplorerUrl'. Received: ${blockExplorerUrls}`, | ||
); | ||
} | ||
|
||
return firstValidBlockExplorerUrl; | ||
} | ||
|
||
function validateChainName(rawChainName) { | ||
if (typeof rawChainName !== 'string' || !rawChainName) { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected non-empty string 'chainName'. Received:\n${rawChainName}`, | ||
}); | ||
} | ||
return rawChainName.length > 100 | ||
? rawChainName.substring(0, 100) | ||
: rawChainName; | ||
} | ||
|
||
function validateNativeCurrency(nativeCurrency) { | ||
if (nativeCurrency !== null) { | ||
if (typeof nativeCurrency !== 'object' || Array.isArray(nativeCurrency)) { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected null or object 'nativeCurrency'. Received:\n${nativeCurrency}`, | ||
}); | ||
} | ||
if (nativeCurrency.decimals !== EVM_NATIVE_TOKEN_DECIMALS) { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected the number 18 for 'nativeCurrency.decimals' when 'nativeCurrency' is provided. Received: ${nativeCurrency.decimals}`, | ||
}); | ||
} | ||
|
||
if (!nativeCurrency.symbol || typeof nativeCurrency.symbol !== 'string') { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected a string 'nativeCurrency.symbol'. Received: ${nativeCurrency.symbol}`, | ||
}); | ||
} | ||
} | ||
const ticker = nativeCurrency?.symbol || 'ETH'; | ||
|
||
if (typeof ticker !== 'string' || ticker.length < 2 || ticker.length > 6) { | ||
throw rpcErrors.invalidParams({ | ||
message: `Expected 2-6 character string 'nativeCurrency.symbol'. Received:\n${ticker}`, | ||
}); | ||
} | ||
|
||
return ticker; | ||
} | ||
|
||
export async function validateRpcEndpoint(rpcUrl, chainId) { | ||
try { | ||
const endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId'); | ||
if (chainId !== endpointChainId) { | ||
throw rpcErrors.invalidParams({ | ||
message: `Chain ID returned by RPC URL ${rpcUrl} does not match ${chainId}`, | ||
data: { chainId: endpointChainId }, | ||
}); | ||
} | ||
} catch (err) { | ||
throw rpcErrors.internal({ | ||
message: `Request for method 'eth_chainId on ${rpcUrl} failed`, | ||
data: { networkErr: err }, | ||
}); | ||
} | ||
} | ||
|
||
export function findExistingNetwork(chainId, networkConfigurations) { | ||
const existingNetworkDefault = getDefaultNetworkByChainId(chainId); | ||
const existingEntry = Object.entries(networkConfigurations).find( | ||
([, networkConfiguration]) => networkConfiguration.chainId === chainId, | ||
); | ||
|
||
return existingEntry || existingNetworkDefault; | ||
} | ||
|
||
export async function switchToNetwork({ | ||
network, | ||
chainId, | ||
controllers, | ||
requestUserApproval, | ||
analytics, | ||
origin, | ||
isAddNetworkFlow = false, | ||
}) { | ||
const { | ||
CurrencyRateController, | ||
NetworkController, | ||
PermissionController, | ||
SelectedNetworkController, | ||
} = controllers; | ||
|
||
let networkConfigurationId, networkConfiguration; | ||
if (Array.isArray(network)) { | ||
[networkConfigurationId, networkConfiguration] = network; | ||
} else { | ||
networkConfiguration = network; | ||
} | ||
|
||
const requestData = { | ||
rpcUrl: networkConfiguration.rpcUrl, | ||
chainId, | ||
chainName: networkConfiguration.nickname || networkConfiguration.shortName, | ||
ticker: networkConfiguration.ticker || 'ETH', | ||
}; | ||
|
||
const analyticsParams = { | ||
chain_id: getDecimalChainId(chainId), | ||
source: 'Custom Network API', | ||
symbol: networkConfiguration?.ticker || 'ETH', | ||
...analytics, | ||
}; | ||
|
||
const requestModalType = isAddNetworkFlow ? 'new' : 'switch'; | ||
|
||
await requestUserApproval({ | ||
type: 'SWITCH_ETHEREUM_CHAIN', | ||
requestData: { ...requestData, type: requestModalType }, | ||
}); | ||
|
||
const originHasAccountsPermission = PermissionController.hasPermission( | ||
origin, | ||
'eth_accounts', | ||
); | ||
|
||
if (process.env.MULTICHAIN_V1 && originHasAccountsPermission) { | ||
SelectedNetworkController.setNetworkClientIdForDomain( | ||
origin, | ||
networkConfigurationId || networkConfiguration.networkType, | ||
); | ||
} else { | ||
CurrencyRateController.updateExchangeRate(requestData.ticker); | ||
NetworkController.setActiveNetwork( | ||
networkConfigurationId || networkConfiguration.networkType, | ||
); | ||
} | ||
|
||
return analyticsParams; | ||
} |
Oops, something went wrong.