From b62f9986b8432eb867ff5410d15f34d1cd739813 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 5 Nov 2024 13:37:49 -0500 Subject: [PATCH 1/2] Restrict chain ID lookup to supported configurations --- src/lib/wallet/evm/provider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/wallet/evm/provider.ts b/src/lib/wallet/evm/provider.ts index d7b76df..c397581 100644 --- a/src/lib/wallet/evm/provider.ts +++ b/src/lib/wallet/evm/provider.ts @@ -4,10 +4,14 @@ import config from "@unstoppabledomains/config"; import {getProviderUrl} from "@unstoppabledomains/ui-components/lib/wallet/evm/provider"; export const getWeb3Provider = (chainId: number) => { + // inspect configurations of supported chains const chainSymbol = Object.keys(config.BLOCKCHAINS).find(k => { return ( + config.WALLETS.CHAINS.SEND.map(c => + c.split("/")[0].toUpperCase(), + ).includes(k.toUpperCase()) && config.BLOCKCHAINS[k as keyof typeof config.BLOCKCHAINS].CHAIN_ID === - chainId + chainId ); }); if (!chainSymbol) { From eaa0294290fc478cc58dae2b8d283a258ac24879 Mon Sep 17 00:00:00 2001 From: Aaron Quirk Date: Tue, 5 Nov 2024 15:10:22 -0500 Subject: [PATCH 2/2] move RPC calls to background worker --- src/scripts/liteWalletProvider/background.ts | 85 +++++++++++++- src/scripts/liteWalletProvider/main.ts | 112 +++++++++---------- src/types/wallet/provider.ts | 11 +- 3 files changed, 145 insertions(+), 63 deletions(-) diff --git a/src/scripts/liteWalletProvider/background.ts b/src/scripts/liteWalletProvider/background.ts index 08f7d84..45ede13 100644 --- a/src/scripts/liteWalletProvider/background.ts +++ b/src/scripts/liteWalletProvider/background.ts @@ -1,4 +1,6 @@ /* eslint-disable promise/prefer-await-to-then */ +import {utils as web3utils} from "web3"; + import config from "../../config"; import {StorageSyncKey, chromeStorageSet} from "../../lib/chromeStorage"; import {Logger} from "../../lib/logger"; @@ -8,6 +10,7 @@ import { getConnectedSite, setConnectedSite, } from "../../lib/wallet/evm/connection"; +import {getWeb3Provider} from "../../lib/wallet/evm/provider"; import {getWalletPreferences} from "../../lib/wallet/preferences"; import {sleep} from "../../lib/wallet/sleep"; import {waitForXmtpMessages} from "../../lib/xmtp/listener"; @@ -17,6 +20,7 @@ import { NotConnectedError, ProviderEventResponse, ProviderRequest, + RpcRequest, getResponseType, isExternalRequestType, isInternalRequestType, @@ -82,6 +86,9 @@ export const backgroundEventListener = ( void waitForXmtpMessages(request.params[0]); } break; + case "rpcRequest": + void handleRpcRequest(request, popupResponseHandler); + break; } return true; } @@ -311,9 +318,18 @@ const handleResponse = ( popupResponseHandler: (response: ProviderEventResponse) => void, response: ProviderEventResponse, ) => { - // log the incoming event - Logger.log("Handling event complete", JSON.stringify(response)); - popupResponseHandler(response); + // make the response bigint safe + const normalizedResponse = JSON.parse( + JSON.stringify(response, (key, value) => + typeof value === "bigint" ? web3utils.numberToHex(value) : value, + ), + ); + + // log the event response + Logger.log("Handling event complete", JSON.stringify(normalizedResponse)); + + // call the response callback with result + popupResponseHandler(normalizedResponse); }; const handleTabStatus = async (tab: chrome.tabs.Tab) => { @@ -377,6 +393,69 @@ const handlePrepareXmtp = async ( } }; +const handleRpcRequest = async ( + request: ProviderRequest, + popupResponseHandler: (response: ProviderEventResponse) => void, +) => { + try { + // requires at least two parameters + if (!request?.params || request.params.length < 2) { + throw new Error("invalid parameters"); + } + + // parse the RPC parameters + const chainId = request.params[0] as number; + const rpcMethod = request.params[1] as RpcRequest; + const rpcParams = request.params.length > 2 ? request.params.slice(2) : []; + + // get web3 object + const web3 = getWeb3Provider(chainId); + + // call the web3 provider method + let result: any | undefined; + switch (rpcMethod) { + case "getBlockNumber": + result = web3utils.numberToHex(await web3.eth.getBlockNumber()); + break; + case "estimateGas": + result = web3utils.numberToHex( + await web3.eth.estimateGas(rpcParams[0]), + ); + break; + case "getTransaction": + result = await web3.eth.getTransaction(rpcParams[0]); + break; + case "getTransactionReceipt": + result = await web3.eth.getTransactionReceipt(rpcParams[0]); + break; + case "call": + result = await web3.eth.call( + rpcParams[0], + rpcParams.length > 1 ? rpcParams[1] : undefined, + ); + break; + default: + throw new Error(`RPC method not supported: ${rpcMethod}`); + } + + // parse the parameters and prepare the account + handleResponse(popupResponseHandler, { + type: getResponseType("rpcRequest"), + response: result, + }); + } catch (e) { + // provide an error response + Logger.warn( + "error handling RPC request", + JSON.stringify({request, error: String(e)}), + ); + handleResponse(popupResponseHandler, { + type: getResponseType("rpcRequest"), + error: String(e), + }); + } +}; + const handleFetchResolution = async ( request: ProviderRequest, popupResponseHandler: (response: ProviderEventResponse) => void, diff --git a/src/scripts/liteWalletProvider/main.ts b/src/scripts/liteWalletProvider/main.ts index de0479f..06d1a8b 100644 --- a/src/scripts/liteWalletProvider/main.ts +++ b/src/scripts/liteWalletProvider/main.ts @@ -16,7 +16,6 @@ import {Logger} from "../../lib/logger"; import {isEthAddress} from "../../lib/sherlock/matcher"; import {ResolutionData} from "../../lib/sherlock/types"; import {announceProvider} from "../../lib/wallet/evm/eip6963"; -import {getWeb3Provider} from "../../lib/wallet/evm/provider"; import {EIP_712_KEY, TypedMessage} from "../../types/wallet/eip712"; import {WalletPreferences} from "../../types/wallet/preferences"; import { @@ -35,6 +34,7 @@ import { ProviderMethodsWithPrompt, ProviderResponse, ResponseType, + RpcRequest, UnexpectedResponseError, UnsupportedMethodError, isClientSideRequestType, @@ -174,19 +174,19 @@ class LiteWalletProvider extends EventEmitter { break; // Transaction methods case "eth_blockNumber": - result = await this.handleGetBlockNumber(); + result = await this.handleRpcGetBlockNumber(); break; case "eth_call": - result = await this.handleReadOnlyCall(clone(request.params)); + result = await this.handleRpcCall(clone(request.params)); break; case "eth_estimateGas": - result = await this.handleEstimateGas(clone(request.params)); + result = await this.handleRpcEstimateGas(clone(request.params)); break; case "eth_getTransactionByHash": - result = await this.handleGetTransactionByHash(clone(request.params)); + result = await this.handleRpcGetTransaction(clone(request.params)); break; case "eth_getTransactionReceipt": - result = await this.handleGetTransactionReceipt( + result = await this.handleRpcGetTransactionReceipt( clone(request.params), ); break; @@ -516,75 +516,69 @@ class LiteWalletProvider extends EventEmitter { return [address]; } - private async handleGetBlockNumber() { - // retrieve the web3 provider for connected chain + private async handleRpcGetBlockNumber() { const chainIdHex = await this.handleGetConnectedChainIds(); - const web3 = getWeb3Provider(web3utils.hexToNumber(chainIdHex) as number); - - // query the block number - const block = await web3.eth.getBlockNumber(); - return web3utils.numberToHex(block); + const chainId = web3utils.hexToNumber(chainIdHex) as number; + return await this.handleRpcMethod(chainId, "getBlockNumber", []); } - private async handleEstimateGas(params: any[]) { + private async handleRpcEstimateGas(params: any[]) { // retrieve the web3 provider for connected chain const chainIdHex = await this.handleGetConnectedChainIds(); - const web3 = getWeb3Provider(web3utils.hexToNumber(chainIdHex) as number); - - // validate any Tx parameters have been passed - if (!params || params.length === 0) { - throw new EthereumProviderError(PROVIDER_CODE_USER_ERROR, InvalidTxError); - } - - // query the estimated gas - return web3utils.numberToHex(await web3.eth.estimateGas(params[0])); + const chainId = web3utils.hexToNumber(chainIdHex) as number; + return await this.handleRpcMethod(chainId, "estimateGas", params); } - private async handleGetTransactionByHash(params: any[]) { - // retrieve the web3 provider for connected chain + private async handleRpcGetTransaction(params: any[]) { const chainIdHex = await this.handleGetConnectedChainIds(); - const web3 = getWeb3Provider(web3utils.hexToNumber(chainIdHex) as number); - - // validate any Tx parameters have been passed - if (!params || params.length === 0) { - throw new EthereumProviderError(PROVIDER_CODE_USER_ERROR, InvalidTxError); - } - - // query the requested transaction - return await web3.eth.getTransaction(params[0]); + const chainId = web3utils.hexToNumber(chainIdHex) as number; + return await this.handleRpcMethod(chainId, "getTransaction", params); } - private async handleGetTransactionReceipt(params: any[]) { - // retrieve the web3 provider for connected chain + private async handleRpcGetTransactionReceipt(params: any[]) { const chainIdHex = await this.handleGetConnectedChainIds(); - const web3 = getWeb3Provider(web3utils.hexToNumber(chainIdHex) as number); - - // validate any Tx parameters have been passed - if (!params || params.length === 0) { - throw new EthereumProviderError(PROVIDER_CODE_USER_ERROR, InvalidTxError); - } - - // query the requested transaction - return await web3.eth.getTransactionReceipt(params[0]); + const chainId = web3utils.hexToNumber(chainIdHex) as number; + return await this.handleRpcMethod(chainId, "getTransactionReceipt", params); } - private async handleReadOnlyCall(params: any[]) { - // retrieve the web3 provider for connected chain + private async handleRpcCall(params: any[]) { const chainIdHex = await this.handleGetConnectedChainIds(); - const web3 = getWeb3Provider(web3utils.hexToNumber(chainIdHex) as number); + const chainId = web3utils.hexToNumber(chainIdHex) as number; + return await this.handleRpcMethod(chainId, "call", params); + } - // validate any Tx parameters have been passed - if (!params || params.length === 0) { - throw new EthereumProviderError(PROVIDER_CODE_USER_ERROR, InvalidTxError); - } + private async handleRpcMethod( + chainId: number, + method: RpcRequest, + params: any[], + ) { + const rpcResponse = new Promise((resolve, reject) => { + document.dispatchEvent( + new ProviderEvent("rpcRequest", {detail: [chainId, method, ...params]}), + ); + this.addEventListener("rpcResponse", (event: ProviderResponse) => { + if (event.detail.error) { + reject( + new EthereumProviderError( + PROVIDER_CODE_USER_ERROR, + event.detail.error, + ), + ); + } else if ("response" in event.detail) { + resolve(event.detail.response); + } else { + reject( + new EthereumProviderError( + PROVIDER_CODE_USER_ERROR, + UnexpectedResponseError, + ), + ); + } + }); + }); - // query the requested transaction - return await web3.eth.call( - // call parameters - params[0], - // optional block number - params.length > 1 ? params[1] : undefined, - ); + // return list of accounts + return await rpcResponse; } private async handleGetConnectedChainIds() { diff --git a/src/types/wallet/provider.ts b/src/types/wallet/provider.ts index 5aad0f6..87cdf5f 100644 --- a/src/types/wallet/provider.ts +++ b/src/types/wallet/provider.ts @@ -25,6 +25,13 @@ export const ProviderMethodsWithPrompt: ProviderMethod[] = [ "eth_sendTransaction", ]; +export type RpcRequest = + | "call" + | "estimateGas" + | "getBlockNumber" + | "getTransaction" + | "getTransactionReceipt"; + // define required EIP-1193 events export type Eip1193Event = | "accountsChanged" @@ -74,6 +81,7 @@ export const InternalMessageTypes = [ "signInRequest", "xmtpReadyRequest", "prepareXmtpRequest", + "rpcRequest", ] as const; export type InternalRequestType = (typeof InternalMessageTypes)[number]; export const isInternalRequestType = (v: string): v is InternalRequestType => { @@ -113,7 +121,8 @@ export type ResponseType = | "getPreferencesResponse" | "getDomainProfileResponse" | "getResolutionResponse" - | "prepareXmtpResponse"; + | "prepareXmtpResponse" + | "rpcResponse"; export const isResponseType = (v: string): v is ResponseType => { return isExternalRequestType(v.replaceAll("Response", "Request")); };