import { ProviderRpcError, RpcErrorCodes } from './errors' import Portal from '../index' import { sdkLogger } from '../logger' import { generateTraceId, X_PORTAL_TRACE_ID_HEADER } from '../shared/trace' import type { EventHandler, ProviderOptions, RegisteredEventHandler, RequestArguments, RpcErrorOptions, } from '../../types' export enum RequestMethod { eth_accounts = 'eth_accounts', eth_blockNumber = 'eth_blockNumber', eth_call = 'eth_call', eth_chainId = 'eth_chainId', eth_coinbase = 'eth_coinbase', eth_compileSolidity = 'eth_compileSolidity', eth_compileLLL = 'eth_compileLLL', eth_compileSerpent = 'eth_compileSerpent', eth_estimateGas = 'eth_estimateGas', eth_gasPrice = 'eth_gasPrice', eth_getBalance = 'eth_getBalance', eth_getBlockByHash = 'eth_getBlockByHash', eth_getBlockByNumber = 'eth_getBlockByNumber', eth_getBlockTransactionCountByHash = 'eth_getBlockTransactionCountByHash', eth_getBlockTransactionCountByNumber = 'eth_getBlockTransactionCountByNumber', eth_getCode = 'eth_getCode', eth_getCompilers = 'eth_getCompilers', eth_getFilterChange = 'eth_getFilterChange', eth_getFilterLogs = 'eth_getFilterLogs', eth_getLogs = 'eth_getLogs', eth_getStorageAt = 'eth_getStorageAt', eth_getTransactionByHash = 'eth_getTransactionByHash', eth_getTransactionByBlockHashAndIndex = 'eth_getTransactionByBlockHashAndIndex', eth_getTransactionByBlockNumberAndIndex = 'eth_getTransactionByBlockNumberAndIndex', eth_getTransactionCount = 'eth_getTransactionCount', eth_getTransactionReceipt = 'eth_getTransactionReceipt', eth_getUncleByBlockHashAndIndex = 'eth_getUncleByBlockHashAndIndex', eth_getUncleByBlockNumberAndIndex = 'eth_getUncleByBlockNumberAndIndex', eth_getUncleCountByBlockHash = 'eth_getUncleCountByBlockHash', eth_getUncleCountByBlockNumber = 'eth_getUncleCountByBlockNumber', eth_getWork = 'eth_getWork', eth_hashrate = 'eth_hashrate', eth_mining = 'eth_mining', eth_newBlockFilter = 'eth_newBlockFilter', eth_newFilter = 'eth_newFilter', eth_newPendingTransactionFilter = 'eth_newPendingTransactionFilter', personal_sign = 'personal_sign', eth_protocolVersion = 'eth_protocolVersion', eth_requestAccounts = 'eth_requestAccounts', eth_sendRawTransaction = 'eth_sendRawTransaction', eth_sendTransaction = 'eth_sendTransaction', eth_sign = 'eth_sign', eth_signTransaction = 'eth_signTransaction', eth_signUserOperation = 'eth_signUserOperation', eth_signTypedData = 'eth_signTypedData', eth_signTypedData_v3 = 'eth_signTypedData_v3', eth_signTypedData_v4 = 'eth_signTypedData_v4', eth_submitHashrate = 'eth_submitHashrate', eth_submitWork = 'eth_submitWork', eth_syncing = 'eth_syncing', eth_uninstallFilter = 'eth_uninstallFilter', // Net Methods net_listening = 'net_listening', net_peerCount = 'net_peerCount', net_version = 'net_version', // Web3 Methods web3_clientVersion = 'web3_clientVersion', web3_sha3 = 'web3_sha3', // Solana RPC Methods sol_getAccountInfo = 'getAccountInfo', sol_getBalance = 'getBalance', sol_getBlock = 'getBlock', sol_getBlockCommitment = 'getBlockCommitment', sol_getBlockHeight = 'getBlockHeight', sol_getBlockProduction = 'getBlockProduction', sol_getBlockTime = 'getBlockTime', sol_getBlocks = 'getBlocks', sol_getBlocksWithLimit = 'getBlocksWithLimit', sol_getClusterNodes = 'getClusterNodes', sol_getEpochInfo = 'getEpochInfo', sol_getEpochSchedule = 'getEpochSchedule', sol_getFeeForMessage = 'getFeeForMessage', sol_getFirstAvailableBlock = 'getFirstAvailableBlock', sol_getGenesisHash = 'getGenesisHash', sol_getHealth = 'getHealth', sol_getHighestSnapshotSlot = 'getHighestSnapshotSlot', sol_getIdentity = 'getIdentity', sol_getInflationGovernor = 'getInflationGovernor', sol_getInflationRate = 'getInflationRate', sol_getInflationReward = 'getInflationReward', sol_getLargestAccounts = 'getLargestAccounts', sol_getLatestBlockhash = 'getLatestBlockhash', sol_getLeaderSchedule = 'getLeaderSchedule', sol_getMaxRetransmitSlot = 'getMaxRetransmitSlot', sol_getMaxShredInsertSlot = 'getMaxShredInsertSlot', sol_getMinimumBalanceForRentExemption = 'getMinimumBalanceForRentExemption', sol_getMultipleAccounts = 'getMultipleAccounts', sol_getProgramAccounts = 'getProgramAccounts', sol_getRecentPerformanceSamples = 'getRecentPerformanceSamples', sol_getRecentPrioritizationFees = 'getRecentPrioritizationFees', sol_getSignatureStatuses = 'getSignatureStatuses', sol_getSignaturesForAddress = 'getSignaturesForAddress', sol_getSlot = 'getSlot', sol_getSlotLeader = 'getSlotLeader', sol_getSlotLeaders = 'getSlotLeaders', sol_getStakeActivation = 'getStakeActivation', sol_getStakeMinimumDelegation = 'getStakeMinimumDelegation', sol_getSupply = 'getSupply', sol_getTokenAccountBalance = 'getTokenAccountBalance', sol_getTokenAccountsByDelegate = 'getTokenAccountsByDelegate', sol_getTokenAccountsByOwner = 'getTokenAccountsByOwner', sol_getTokenLargestAccounts = 'getTokenLargestAccounts', sol_getTokenSupply = 'getTokenSupply', sol_getTransaction = 'getTransaction', sol_getTransactionCount = 'getTransactionCount', sol_getVersion = 'getVersion', sol_getVoteAccounts = 'getVoteAccounts', sol_isBlockhashValid = 'isBlockhashValid', sol_minimumLedgerSlot = 'minimumLedgerSlot', sol_requestAirdrop = 'requestAirdrop', sol_sendTransaction = 'sendTransaction', sol_simulateTransaction = 'simulateTransaction', // Solana Wallet Methods sol_signAndConfirmTransaction = 'sol_signAndConfirmTransaction', sol_signAndSendTransaction = 'sol_signAndSendTransaction', sol_signMessage = 'sol_signMessage', sol_signTransaction = 'sol_signTransaction', // TRON Wallet Methods tron_sendTransaction = 'tron_sendTransaction', } const passiveSignerMethods = [ RequestMethod.eth_accounts, RequestMethod.eth_chainId, RequestMethod.eth_requestAccounts, ] const signerMethods = [ RequestMethod.eth_accounts, RequestMethod.eth_chainId, RequestMethod.eth_requestAccounts, RequestMethod.eth_sendTransaction, RequestMethod.eth_sign, RequestMethod.eth_signTransaction, RequestMethod.eth_signUserOperation, RequestMethod.eth_signTypedData_v3, RequestMethod.eth_signTypedData_v4, RequestMethod.personal_sign, RequestMethod.sol_signAndConfirmTransaction, RequestMethod.sol_signAndSendTransaction, RequestMethod.sol_signMessage, RequestMethod.sol_signTransaction, RequestMethod.tron_sendTransaction, ] const iframeProxiedMethodStrings: string[] = [ 'eth_getTransactionReceipt', 'eth_getUserOperationReceipt', 'getSignatureStatuses', ] class Provider { public events: Record private portal: Portal // eip155 chain reference ID private chainReferenceId?: number constructor({ portal, chainId }: ProviderOptions) { this.events = {} this.portal = portal this.chainReferenceId = chainId } /** * Invokes all registered event handlers with the data provided * - If any `once` handlers exist, they are removed after all handlers are invoked * * @param event The name of the event to be handled * @param data The data to be passed to registered event handlers * @returns BaseProvider */ public emit(event: string, data: any): Provider { // Grab the registered event handlers if any are available const handlers = this.events[event] || [] // Execute every event handler for (const registeredEventHandler of handlers) { void registeredEventHandler.handler(data) } // Remove any registered event handlers with the `once` flag this.events[event] = handlers.filter((handler) => !handler.once) return this } /** * Registers an event handler for the provided event * * @param event The event name to add a handler to * @param callback The callback to be invoked when the event is emitted * @returns BaseProvider */ public on(event: string, callback: EventHandler): Provider { // If no handlers are registered for this event, create an entry for the event if (!this.events[event]) { this.events[event] = [] } // Register event handler with the rudimentary event bus if (typeof callback !== 'undefined') { this.events[event].push({ handler: callback, once: false, }) } return this } public removeEventListener( event: string, listenerToRemove?: EventHandler, ): void { if (!this.events[event]) { return } if (!listenerToRemove) { this.events[event] = [] } else { const filterEventHandlers = ( registeredEventHandler: RegisteredEventHandler, ) => { return registeredEventHandler.handler !== listenerToRemove } this.events[event] = this.events[event].filter(filterEventHandlers) } } /** * Handles request routing in compliance with the EIP-1193 Ethereum Javascript Provider API * - See here for more info: https://eips.ethereum.org/EIPS/eip-1193 * * @param args The arguments of the request being made * @returns Promise */ public async request({ chainId: requestChainId, method, params, sponsorGas, signatureApprovalMemo, traceId: providedTraceId, }: RequestArguments): Promise { const traceId = providedTraceId ?? generateTraceId() sdkLogger.info( `[PortalProvider] request started | method=${method} | traceId=${traceId}`, ) const isSignerMethod = signerMethods.includes(method) const chainId = this.getCAIP2ChainId(requestChainId) if (!isSignerMethod && !method.startsWith('wallet_')) { // Send to Gateway for RPC calls const response = await this.handleGatewayRequest({ chainId, method, params, traceId, }) this.emit('portal_signatureReceived', { chainId, method, params, signature: response, }) if (response.error) { throw new ProviderRpcError(response.error as RpcErrorOptions) } return response.result } else if (isSignerMethod) { // Handle signing const transactionHash = await this.handleSigningRequest({ chainId, method, params, sponsorGas, signatureApprovalMemo, traceId, }) if (transactionHash) { this.emit('portal_signatureReceived', { chainId, method, params, signature: transactionHash, }) return transactionHash } } else { // Unsupported method throw new ProviderRpcError({ code: RpcErrorCodes.UnsupportedMethod, data: { method, params, }, }) } } public setChainId(chainId: number) { if (!Number.isInteger(chainId)) throw new Error( `[PortalProvider] Chain ID must be an integer, got ${chainId}`, ) this.chainReferenceId = chainId this.emit('chainChanged', { chainId }) } /************************ * Private Methods ************************/ /** * Kicks off the approval flow for a given request * * @param args The arguments of the request being made */ protected async getApproval({ method, params, signatureApprovalMemo, }: RequestArguments): Promise { // If autoApprove is enabled, just resolve to true if (this.portal.autoApprove) { return true } const signingHandlers = this.events['portal_signingRequested'] if (!signingHandlers || signingHandlers.length === 0) { throw new Error( `[PortalProvider] Auto-approve is disabled. Cannot perform signing requests without an event handler for the 'portal_signingRequested' event.`, ) } return new Promise((resolve) => { // Remove already used listeners this.removeEventListener('portal_signingApproved') this.removeEventListener('portal_signingRejected') const handleApproval = ({ method: approvedMethod, params: approvedParams, }: RequestArguments) => { // Remove already used listeners this.removeEventListener('portal_signingApproved') this.removeEventListener('portal_signingRejected') // First verify that this is the same signing request if ( method === approvedMethod && JSON.stringify(params) === JSON.stringify(approvedParams) ) { resolve(true) } } const handleRejection = ({ method: rejectedMethod, params: rejectedParams, }: RequestArguments) => { // Remove already used listeners this.removeEventListener('portal_signingApproved') this.removeEventListener('portal_signingRejected') // First verify that this is the same signing request if ( method === rejectedMethod && JSON.stringify(params) === JSON.stringify(rejectedParams) ) { resolve(false) } } // If the signing has been approved, resolve to true this.on('portal_signingApproved', handleApproval) // If the signing request has been rejected, resolve to false this.on('portal_signingRejected', handleRejection) // Tell any listening clients that signing has been requested const signingRequestedPayload = signatureApprovalMemo !== undefined ? { method, params, signatureApprovalMemo } : { method, params } this.emit('portal_signingRequested', signingRequestedPayload) }) } /** * Sends the provided request payload along to the RPC HttpRequester * * @param args The arguments of the request being made * @returns Promise */ private async handleGatewayRequest({ chainId, method, params, traceId, }: RequestArguments): Promise { if (iframeProxiedMethodStrings.includes(method)) { sdkLogger.info( `[PortalProvider] routing ${method} through iframe (chainId=${String(chainId)})`, { traceId }, ) return this.portal.mpc.rpcRequest( { method, params: Array.isArray(params) ? params : params != null ? [params] : [], chainId: chainId as string, }, { traceId }, ) } const requestBody = { body: JSON.stringify({ jsonrpc: '2.0', id: '0', method, params, }), method: 'POST', headers: { 'Content-Type': 'application/json', ...(traceId && { [X_PORTAL_TRACE_ID_HEADER]: traceId }), }, } const result = await fetch(this.portal.getRpcUrl(chainId), requestBody) return result.json() } /** * Sends the provided request payload along to the Signer * * @param args The arguments of the request being made * @returns Promise */ private async handleSigningRequest({ chainId, method, params, sponsorGas, signatureApprovalMemo, traceId, }: RequestArguments): Promise { const isApproved = passiveSignerMethods.includes(method) ? true : await this.getApproval({ method, params, signatureApprovalMemo }) if (!isApproved) { sdkLogger.warn( `[PortalProvider] Request for signing method '${method}' could not be completed because it was not approved by the user.`, ) return } switch (method) { case RequestMethod.eth_chainId: { this.enforceEip155ChainId(chainId) const chainReferenceId = parseInt((chainId as string).split(':')[1]) return `0x${chainReferenceId.toString(16)}` } case RequestMethod.eth_accounts: case RequestMethod.eth_requestAccounts: { this.enforceEip155ChainId(chainId) return [this.portal.address] } case RequestMethod.eth_sendTransaction: case RequestMethod.eth_sign: case RequestMethod.eth_signTransaction: case RequestMethod.eth_signUserOperation: case RequestMethod.eth_signTypedData_v3: case RequestMethod.eth_signTypedData_v4: case RequestMethod.personal_sign: { this.enforceEip155ChainId(chainId) const result = await this.portal.mpc.sign({ chainId: chainId as string, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas, ...(signatureApprovalMemo !== undefined && { signatureApprovalMemo, }), traceId, }) return result } case RequestMethod.sol_signAndConfirmTransaction: case RequestMethod.sol_signAndSendTransaction: case RequestMethod.sol_signMessage: case RequestMethod.sol_signTransaction: { this.enforceSolanaChainId(chainId) const result = await this.portal.mpc.sign({ chainId: chainId as string, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas, traceId, ...(signatureApprovalMemo !== undefined && { signatureApprovalMemo, }), }) return result } case RequestMethod.tron_sendTransaction: { this.enforceTronChainId(chainId) const result = await this.portal.mpc.sign({ chainId: chainId as string, method, params: this.buildParams(method, params), rpcUrl: this.portal.getRpcUrl(chainId), sponsorGas, traceId, ...(signatureApprovalMemo !== undefined && { signatureApprovalMemo, }), }) return result } default: throw new Error( '[PortalProvider] Method "' + method + '" not supported', ) } } private enforceEip155ChainId = (chainId?: string) => { if (!chainId) { throw new Error('[PortalProvider] Chain ID is required for the operation') } if (!chainId.startsWith('eip155:')) { throw new Error( `[PortalProvider] Chain ID must be prefixed with "eip155:" for the operation, got ${chainId}`, ) } } private enforceSolanaChainId = (chainId?: string) => { if (!chainId) { throw new Error('[PortalProvider] Chain ID is required for the operation') } if (!chainId.startsWith('solana:')) { throw new Error( `[PortalProvider] Chain ID must be prefixed with "solana:" for the operation, got ${chainId}`, ) } } private enforceTronChainId = (chainId?: string) => { if (!chainId) { throw new Error('[PortalProvider] Chain ID is required for the operation') } if (!chainId.startsWith('tron:')) { throw new Error( `[PortalProvider] Chain ID must be prefixed with "tron:" for the operation, got ${chainId}`, ) } } private buildParams = (method: string, txParams: any) => { let params = txParams switch (method) { case RequestMethod.eth_sign: case RequestMethod.personal_sign: case RequestMethod.eth_signTypedData_v3: case RequestMethod.eth_signTypedData_v4: case RequestMethod.sol_signAndConfirmTransaction: case RequestMethod.sol_signAndSendTransaction: case RequestMethod.sol_signMessage: case RequestMethod.sol_signTransaction: if (!Array.isArray(txParams)) { params = [txParams] } break default: if (Array.isArray(txParams)) { params = txParams[0] } } return params } private getCAIP2ChainId = (chainId?: string) => { if (!chainId && !this.chainReferenceId) throw new Error('[PortalProvider] Chain ID is required for the operation') return chainId || `eip155:${this.chainReferenceId}` } } export default Provider