import { PortalMpcError } from './errors' import { sdkLogger } from '../logger' import Portal, { BackupMethods, GetTransactionsOrder, PortalCurve, } from '../index' import type { BackupArgs, BackupResponse, Balance, ClientResponse, EjectArgs as EjectArgs, GenerateArgs, IframeConfigurationOptions, MpcOptions, MpcStatus, NFT, PortalError, ProgressCallback, QuoteArgs, QuoteResponse, RecoverArgs, SignArgs, SimulateTransactionParam, SimulatedTransaction, Transaction, SharesOnDeviceResponse, WorkerResult, EjectResult, EvaluateTransactionParam, EvaluateTransactionOperationType, EvaluatedTransaction, NFTAsset, GetAssetsResponse, BuiltTransaction, EjectPrivateKeysArgs, EjectPrivateKeysResult, FundParams, FundResponse, } from '../../types' import type { GetTransactionHistoryParams, GetTransactionHistoryResponse, } from '../shared/types' import { YieldXyzEnterRequest, YieldXyzEnterYieldResponse, YieldXyzExitRequest, YieldXyzExitResponse, YieldXyzGetBalancesRequest, YieldXyzGetBalancesResponse, YieldXyzGetHistoricalActionsRequest, YieldXyzGetHistoricalActionsResponse, YieldXyzGetTransactionResponse, YieldXyzGetYieldsRequest, YieldXyzGetYieldsResponse, YieldXyzManageYieldRequest, YieldXyzManageYieldResponse, YieldXyzTrackTransactionRequest, YieldXyzTrackTransactionResponse, YieldXyzGetYieldDefaultsRequest, YieldXyzGetYieldDefaultsResponse, YieldXyzGetYieldValidatorsResponse, LifiQuoteRequest, LifiQuoteResponse, LifiRoutesRequest, LifiRoutesResponse, LifiStatusRequest, LifiStatusResponse, LifiStepTransactionRequest, LifiStepTransactionResponse, NoahGetPayoutChannelsRequest, NoahGetPayoutChannelsResponse, NoahGetPayoutChannelFormResponse, NoahGetPayoutCountriesResponse, NoahGetPaymentMethodsResponse, NoahGetPayoutQuoteRequest, NoahGetPayoutQuoteResponse, NoahInitiateKycRequest, NoahInitiateKycResponse, NoahInitiatePayinRequest, NoahInitiatePayinResponse, NoahInitiatePayoutRequest, NoahInitiatePayoutResponse, NoahSimulatePayinRequest, NoahSimulatePayinResponse, ZeroExOptions, ZeroExPriceRequest, ZeroExPriceResponse, ZeroExQuoteRequest, ZeroExQuoteResponse, ZeroExSourcesRequest, ZeroExSourcesResponse, ScanEVMRequest, ScanEVMResponse, ScanEip712Request, ScanEip712Response, ScanSolanaRequest, ScanSolanaResponse, ScanNftsRequest, ScanNftsResponse, ScanTokensRequest, ScanTokensResponse, ScanUrlRequest, ScanUrlResponse, BlockaidScanEvmTxRequest, BlockaidScanEvmTxResponse, BlockaidScanSolanaTxRequest, BlockaidScanSolanaTxResponse, BlockaidAddressScanRequest, BlockaidAddressScanResponse, BlockaidBulkTokenScanRequest, BlockaidBulkTokenScanResponse, BlockaidSiteScanRequest, BlockaidSiteScanResponse, ApproveDelegationRequest, ApproveDelegationResponse, RevokeDelegationRequest, RevokeDelegationResponse, GetDelegationStatusRequest, DelegationStatusResponse, TransferFromRequest, TransferFromResponse, RawSignOptions, RpcProxyRequest, RpcProxyResponse, BuildBatchedUserOpRequest, BuildBatchedUserOpResponse, BroadcastBatchedUserOpRequest, BroadcastBatchedUserOpResponse, } from '../shared/types' import { ScreenAddressApiResponse, ScreenAddressRequestOptions, } from '../../hypernative' import { generateTraceId } from '../shared/trace' const WEB_SDK_VERSION = '3.17.0' class Mpc { public iframe?: HTMLIFrameElement private portal: Portal private _ready = false private presignatureLogHandler: ((event: MessageEvent) => void) | null = null private firebaseGetToken?: ( options?: { forceRefresh?: boolean }, ) => Promise private boundFirebaseTokenBridge = (event: MessageEvent) => { const { origin } = event if (origin !== this.getOrigin()) { return } if (event.source !== this.iframe?.contentWindow) { return } const { type, data } = event.data || {} if (type !== 'portal:firebase:requestToken') { return } const requestId = data?.requestId as string | undefined if (!requestId) { return } const forceRefresh = data?.forceRefresh === true const getter = this.firebaseGetToken void (async () => { try { if (!getter) { throw new Error( 'Firebase storage is not configured (getToken missing)', ) } const token = await getter({ forceRefresh }) this.postMessage({ type: 'portal:firebase:requestTokenResult', data: { requestId, token }, }) } catch (e) { const message = (e instanceof Error ? e.message : String(e)) || 'Unknown firebase token error' this.postMessage({ type: 'portal:firebase:requestTokenError', data: { requestId, message }, }) } })() } public get ready() { return this._ready } private set ready(newReady: boolean) { this._ready = newReady } constructor({ portal }: MpcOptions) { this.portal = portal // Handle scoping of certain functions this.configureIframe = this.configureIframe.bind(this) // Create the iFrame for MPC operations this.appendIframe() window.addEventListener('message', this.boundFirebaseTokenBridge) } public configureFirebaseStorage(options: { getToken: (options?: { forceRefresh?: boolean }) => Promise tbsHost?: string }): void { this.firebaseGetToken = options.getToken this.postMessage({ type: 'portal:firebase:configure', data: { tbsHost: options.tbsHost }, }) } /******************************* * Wallet Methods *******************************/ public async backup( data: BackupArgs, progress: ProgressCallback = () => { // Noop }, traceId?: string, ): Promise { // validates password config for password backup this.validateBackupConfig(data) const resolvedTraceId = traceId ?? generateTraceId() sdkLogger.info( `[Portal MPC] backup started | backupMethod=${data.backupMethod} | traceId=${resolvedTraceId}`, ) return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:backup', errorMessage: 'portal:wasm:backupError', resultMessage: 'portal:wasm:backupResult', data, progressMessage: 'portal:wasm:backupProgress', progressCallback: progress, traceId: resolvedTraceId, mapReturnValue: (result: Record) => { const resultData = result as Record const backupIds = Array.isArray(resultData.backupIds) ? (resultData.backupIds as string[]) : [] const storageCallback = async () => { await this.setBackupStatus('STORED_CLIENT_BACKUP_SHARE', backupIds) } return { cipherText: result.cipherText as string, encryptionKey: typeof resultData.encryptionKey === 'string' ? (resultData.encryptionKey as string) : undefined, storageCallback, } }, }) } public clearLocalWallet(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:destroy', errorMessage: 'portal:destroyError', // This message is not used in the iframe resultMessage: 'portal:destroyResult', data: {}, mapReturnValue: () => true, }).then((result) => { this.teardownPresignatureLogForwarding() return result }) } public async generate( data: GenerateArgs, progress: ProgressCallback = () => { // Noop }, traceId?: string, ): Promise { const resolvedTraceId = traceId ?? generateTraceId() sdkLogger.info(`[Portal MPC] generate started | traceId=${resolvedTraceId}`) return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:generate', errorMessage: 'portal:wasm:generateError', resultMessage: 'portal:wasm:generateResult', data, progressMessage: 'portal:wasm:generateProgress', progressCallback: progress, traceId: resolvedTraceId, }) } public async getAddress(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:address', errorMessage: 'portal:addressError', // This message is not used in the iframe resultMessage: 'portal:addressResult', data: {}, }) } public async recover( data: RecoverArgs, progress: ProgressCallback = () => { // Noop }, traceId?: string, ): Promise { const resolvedTraceId = traceId ?? generateTraceId() sdkLogger.info( `[Portal MPC] recover started | backupMethod=${data.backupMethod} | traceId=${resolvedTraceId}`, ) return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:recover', errorMessage: 'portal:wasm:recoverError', resultMessage: 'portal:wasm:recoverResult', data, progressMessage: 'portal:wasm:recoverProgress', progressCallback: progress, traceId: resolvedTraceId, }) } public async eject(data: EjectArgs, traceId?: string): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:eject', errorMessage: 'portal:wasm:ejectError', resultMessage: 'portal:wasm:ejectResult', data, traceId, }) } public async ejectPrivateKeys( data: EjectPrivateKeysArgs, traceId?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:ejectPrivateKeys', errorMessage: 'portal:wasm:ejectPrivateKeysError', resultMessage: 'portal:wasm:ejectPrivateKeysResult', data, traceId, }) } public async rawSign( curve: PortalCurve, param: string, options?: RawSignOptions, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:mpc:rawSign', errorMessage: 'portal:mpc:rawSignError', resultMessage: 'portal:mpc:rawSignResult', data: { curve, param, ...(options?.signatureApprovalMemo !== undefined && { signatureApprovalMemo: options.signatureApprovalMemo, }), }, traceId: options?.traceId, }) } public async sign( data: SignArgs, progress: ProgressCallback = () => { // Noop }, ): Promise { const resolvedTraceId = data.traceId ?? generateTraceId() sdkLogger.info( `[Portal MPC] sign started | method=${data.method} | traceId=${resolvedTraceId} | chainId=${data.chainId}`, ) return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:sign', errorMessage: 'portal:wasm:signError', resultMessage: 'portal:wasm:signResult', data: { ...data, traceId: resolvedTraceId }, progressMessage: 'portal:wasm:signProgress', progressCallback: progress, traceId: resolvedTraceId, }) } public async checkSharesOnDevice(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:checkSharesOnDevice', errorMessage: 'portal:checkSharesOnDeviceError', resultMessage: 'portal:checkSharesOnDeviceResult', data: {}, }) } /******************************* * API Methods *******************************/ public async getBalances(chainId?: string): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getBalances', errorMessage: 'portal:getBalancesError', resultMessage: 'portal:getBalancesResult', data: { chainId }, }) } public async fund( chainId: string, params: FundParams, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:fund', errorMessage: 'portal:fundError', resultMessage: 'portal:fundResult', data: { chainId, params }, }) } public async getClient(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getClient', errorMessage: 'portal:getClientError', resultMessage: 'portal:getClientResult', data: {}, }) } public async getNFTs(chainId?: string): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getNFTs', errorMessage: 'portal:getNFTsError', resultMessage: 'portal:getNFTsResult', data: { chainId }, }) } public async getNFTAssets(chainId: string): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getNFTAssets', errorMessage: 'portal:getNFTAssetsError', resultMessage: 'portal:getNFTAssetsResult', data: { chainId }, }) } public async getAssets( chainId: string, includeNfts = false, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getAssets', errorMessage: 'portal:getAssetsError', resultMessage: 'portal:getAssetsResult', data: { chainId, includeNfts }, }) } public async buildTransaction( chainId: string, to: string, token: string, amount: string, traceId?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:buildTransaction', errorMessage: 'portal:buildTransactionError', resultMessage: 'portal:buildTransactionResult', data: { chainId, to, token, amount }, traceId, }) } public async getQuote( chainId: string, args: QuoteArgs, apiKey?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:swaps:getQuote', errorMessage: 'portal:swaps:getQuoteError', resultMessage: 'portal:swaps:getQuoteResult', data: { apiKey, args, chainId, }, }) } public async getSources( chainId: string, apiKey?: string, ): Promise> { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:swaps:getSources', errorMessage: 'portal:swaps:getSourcesError', resultMessage: 'portal:swaps:getSourcesResult', data: { apiKey, chainId, }, }) } /** * @deprecated This method is deprecated and will be removed in a future version. * Please use `getTransactionHistory()` instead, which uses the new Portal v3 API * endpoint and returns the unified transaction format across all chains. */ public async getTransactions( chainId: string, limit?: number, offset?: number, order?: GetTransactionsOrder, ): Promise { // Log deprecation warning sdkLogger.warn( '[DEPRECATED] getTransactions() is deprecated and will be removed in a future version. ' + 'Please use getTransactionHistory() instead, which provides improved type safety with ' + 'discriminated unions for regular transactions and UserOperations, and proper polymorphic ' + 'response types for Solana vs unified formats.' ) return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getTransactions', errorMessage: 'portal:getTransactionsError', resultMessage: 'portal:getTransactionsResult', data: { chainId, limit, offset, order, }, }) } /** * Retrieves transaction history for the client's wallet on the specified chain. * * This method uses the new Portal v3 API endpoint and returns the unified * transaction format. Supports EVM (EIP-155), Solana, Bitcoin, Tron, and Stellar chains. * * @param params - Request parameters * @param params.chainId - Chain ID in CAIP-2 format (e.g., 'eip155:1', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp') * @param params.limit - Maximum number of transactions to return (default: 50) * @param params.offset - Number of transactions to skip (default: 0) * @param params.order - Sort order ('asc' or 'desc') * @param params.address - Override wallet address (EVM only) * @param params.userOperations - Filter for ERC-4337 UserOperations (EVM only) * @returns Promise resolving to transaction history response */ public async getTransactionHistory( params: GetTransactionHistoryParams, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:getTransactionHistory', errorMessage: 'portal:getTransactionHistoryError', resultMessage: 'portal:getTransactionHistoryResult', data: params, }) } public async setBackupStatus( status: string, backupIds: string[], ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:api:setBackupStatus', errorMessage: 'portal:api:setBackupStatusError', resultMessage: 'portal:api:setBackupStatusResult', data: { backupIds, status, }, }) } public async simulateTransaction( transaction: SimulateTransactionParam, chainId?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:simulateTransaction', errorMessage: 'portal:simulateTransactionError', resultMessage: 'portal:simulateTransactionResult', data: { chainId, transaction, }, }) } public async evaluateTransaction( chainId: string, transaction: EvaluateTransactionParam, operationType: EvaluateTransactionOperationType = 'all', ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:evaluateTransaction', errorMessage: 'portal:evaluateTransactionError', resultMessage: 'portal:evaluateTransactionResult', data: { chainId, transaction, operationType, }, }) } public storedClientBackupShare( success: boolean, backupMethod: BackupMethods, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:storedClientBackupShare', errorMessage: 'portal:storedClientBackupShareError', resultMessage: 'portal:storedClientBackupShareResult', data: { success, backupMethod, }, mapReturnValue: () => undefined, }) } public async formatShares(shares: string): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:formatShares', errorMessage: 'portal:wasm:formatSharesError', resultMessage: 'portal:wasm:formatSharesResult', data: shares, }) } public async getCustodianIdClientIdHashes(data: any): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:wasm:getCustodianIdClientIdHashes', errorMessage: 'portal:wasm:getCustodianIdClientIdHashesError', resultMessage: 'portal:wasm:getCustodianIdClientIdHashesResult', data, }) } public async getYieldXyzYields( data: YieldXyzGetYieldsRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:discover', errorMessage: 'portal:yieldxyz:discoverError', resultMessage: 'portal:yieldxyz:discoverResult', data, }) } public async enterYieldXyzYield( data: YieldXyzEnterRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:enter', errorMessage: 'portal:yieldxyz:enterError', resultMessage: 'portal:yieldxyz:enterResult', data, }) } public async exitYieldXyzYield( data: YieldXyzExitRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:exit', errorMessage: 'portal:yieldxyz:exitError', resultMessage: 'portal:yieldxyz:exitResult', data, }) } public async getYieldXyzBalances( data: YieldXyzGetBalancesRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:getBalances', errorMessage: 'portal:yieldxyz:getBalancesError', resultMessage: 'portal:yieldxyz:getBalancesResult', data, }) } public async getYieldXyzHistoricalActions( data: YieldXyzGetHistoricalActionsRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:getHistoricalActions', errorMessage: 'portal:yieldxyz:getHistoricalActionsError', resultMessage: 'portal:yieldxyz:getHistoricalActionsResult', data, }) } public async manageYieldXyzYield( data: YieldXyzManageYieldRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:manage', errorMessage: 'portal:yieldxyz:manageYieldError', resultMessage: 'portal:yieldxyz:manageYieldResult', data, }) } public async trackYieldXyzTransaction( data: YieldXyzTrackTransactionRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:track', errorMessage: 'portal:yieldxyz:trackError', resultMessage: 'portal:yieldxyz:trackResult', data, }) } public async getYieldXyzTransaction( data: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:getTransaction', errorMessage: 'portal:yieldxyz:getTransactionError', resultMessage: 'portal:yieldxyz:getTransactionResult', data, }) } public async getYieldXyzDefaults( data?: YieldXyzGetYieldDefaultsRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:getDefaults', errorMessage: 'portal:yieldxyz:getDefaultsError', resultMessage: 'portal:yieldxyz:getDefaultsResult', data: data ?? {}, }) } public async getYieldXyzValidators( yieldId: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:yieldxyz:getValidators', errorMessage: 'portal:yieldxyz:getValidatorsError', resultMessage: 'portal:yieldxyz:getValidatorsResult', data: yieldId, }) } public async getLifiRoutes( data: LifiRoutesRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:lifi:getRoutes', errorMessage: 'portal:lifi:getRoutesError', resultMessage: 'portal:lifi:getRoutesResult', data, }) } public async getLifiQuote( data: LifiQuoteRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:lifi:getQuote', errorMessage: 'portal:lifi:getQuoteError', resultMessage: 'portal:lifi:getQuoteResult', data, }) } public async getLifiStatus( data: LifiStatusRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:lifi:getStatus', errorMessage: 'portal:lifi:getStatusError', resultMessage: 'portal:lifi:getStatusResult', data, }) } public async getLifiRouteStep( data: LifiStepTransactionRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:lifi:getRouteStep', errorMessage: 'portal:lifi:getRouteStepError', resultMessage: 'portal:lifi:getRouteStepResult', data, }) } /** * Noah ramp: postMessage bridge to the Portal iframe (connect-api). * Application code should call `portal.ramps.noah` instead of Mpc directly. */ public async initiateKyc( data: NoahInitiateKycRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:initiateKyc', errorMessage: 'portal:noah:initiateKycError', resultMessage: 'portal:noah:initiateKycResult', data, }) } public async initiatePayin( data: NoahInitiatePayinRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:initiatePayin', errorMessage: 'portal:noah:initiatePayinError', resultMessage: 'portal:noah:initiatePayinResult', data, }) } public async simulatePayin( data: NoahSimulatePayinRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:simulatePayin', errorMessage: 'portal:noah:simulatePayinError', resultMessage: 'portal:noah:simulatePayinResult', data, }) } public async getPayoutCountries(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:getPayoutCountries', errorMessage: 'portal:noah:getPayoutCountriesError', resultMessage: 'portal:noah:getPayoutCountriesResult', data: {}, }) } public async getPayoutChannels( data: NoahGetPayoutChannelsRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:getPayoutChannels', errorMessage: 'portal:noah:getPayoutChannelsError', resultMessage: 'portal:noah:getPayoutChannelsResult', data, }) } public async getPayoutChannelForm( channelId: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:getPayoutChannelForm', errorMessage: 'portal:noah:getPayoutChannelFormError', resultMessage: 'portal:noah:getPayoutChannelFormResult', data: channelId, }) } public async getPayoutQuote( data: NoahGetPayoutQuoteRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:getPayoutQuote', errorMessage: 'portal:noah:getPayoutQuoteError', resultMessage: 'portal:noah:getPayoutQuoteResult', data, }) } public async initiatePayout( data: NoahInitiatePayoutRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:initiatePayout', errorMessage: 'portal:noah:initiatePayoutError', resultMessage: 'portal:noah:initiatePayoutResult', data, }) } public async getPaymentMethods(): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:noah:getPaymentMethods', errorMessage: 'portal:noah:getPaymentMethodsError', resultMessage: 'portal:noah:getPaymentMethodsResult', data: {}, }) } public async getSwapsQuoteV2( data: ZeroExQuoteRequest, options?: ZeroExOptions, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:swaps:getQuoteV2', errorMessage: 'portal:swaps:getQuoteV2Error', resultMessage: 'portal:swaps:getQuoteV2Result', data: { ...data, options }, }) } public async getSwapsSourcesV2( data: ZeroExSourcesRequest, options?: ZeroExOptions, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:swaps:getSourcesV2', errorMessage: 'portal:swaps:getSourcesV2Error', resultMessage: 'portal:swaps:getSourcesV2Result', data: { ...data, options }, }) } public async getSwapsPrice( data: ZeroExPriceRequest, options?: ZeroExOptions, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:swaps:getPrice', errorMessage: 'portal:swaps:getPriceError', resultMessage: 'portal:swaps:getPriceResult', data: { ...data, options }, }) } /******************************* * Security Methods *******************************/ public async scanAddresses( data: { addresses: string[] } & ScreenAddressRequestOptions, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanAddresses', errorMessage: 'portal:security:scanAddressesError', resultMessage: 'portal:security:scanAddressesResult', data, }) } public async scanEVMTx( data: ScanEVMRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanEVMTx', errorMessage: 'portal:security:scanEVMTxError', resultMessage: 'portal:security:scanEVMTxResult', data, }) } public async scanEip712Tx( data: ScanEip712Request, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanEip712Tx', errorMessage: 'portal:security:scanEip712TxError', resultMessage: 'portal:security:scanEip712TxResult', data, }) } public async scanSolanaTx( data: ScanSolanaRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanSolanaTx', errorMessage: 'portal:security:scanSolanaTxError', resultMessage: 'portal:security:scanSolanaTxResult', data, }) } public async scanNFTs(nfts: ScanNftsRequest): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanNfts', errorMessage: 'portal:security:scanNftsError', resultMessage: 'portal:security:scanNftsResult', data: nfts, }) } public async scanTokens( tokens: ScanTokensRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanTokens', errorMessage: 'portal:security:scanTokensError', resultMessage: 'portal:security:scanTokensResult', data: tokens, }) } public async scanUrl(url: ScanUrlRequest): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:security:scanUrl', errorMessage: 'portal:security:scanUrlError', resultMessage: 'portal:security:scanUrlResult', data: { url }, }) } /******************************* * Blockaid Security Methods *******************************/ public async blockaidScanEvmTx( data: BlockaidScanEvmTxRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:blockaid:scanEvmTx', errorMessage: 'portal:blockaid:scanEvmTxError', resultMessage: 'portal:blockaid:scanEvmTxResult', data, }) } public async blockaidScanSolanaTx( data: BlockaidScanSolanaTxRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:blockaid:scanSolanaTx', errorMessage: 'portal:blockaid:scanSolanaTxError', resultMessage: 'portal:blockaid:scanSolanaTxResult', data, }) } public async blockaidScanAddress( data: BlockaidAddressScanRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:blockaid:scanAddress', errorMessage: 'portal:blockaid:scanAddressError', resultMessage: 'portal:blockaid:scanAddressResult', data, }) } public async blockaidScanTokens( data: BlockaidBulkTokenScanRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:blockaid:scanTokens', errorMessage: 'portal:blockaid:scanTokensError', resultMessage: 'portal:blockaid:scanTokensResult', data, }) } public async blockaidScanUrl( data: BlockaidSiteScanRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:blockaid:scanUrl', errorMessage: 'portal:blockaid:scanUrlError', resultMessage: 'portal:blockaid:scanUrlResult', data, }) } /******************************* * Delegations Methods *******************************/ public async delegationsApprove( data: ApproveDelegationRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:delegations:approve', errorMessage: 'portal:delegations:approveError', resultMessage: 'portal:delegations:approveResult', data, }) } public async delegationsRevoke( data: RevokeDelegationRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:delegations:revoke', errorMessage: 'portal:delegations:revokeError', resultMessage: 'portal:delegations:revokeResult', data, }) } public async delegationsGetStatus( data: GetDelegationStatusRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:delegations:getStatus', errorMessage: 'portal:delegations:getStatusError', resultMessage: 'portal:delegations:getStatusResult', data, }) } public async delegationsTransferFrom( data: TransferFromRequest, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:delegations:transferFrom', errorMessage: 'portal:delegations:transferFromError', resultMessage: 'portal:delegations:transferFromResult', data, }) } /******************************* * Account Abstraction Methods *******************************/ public async accountAbstractionBuildBatchedUserOp( data: BuildBatchedUserOpRequest, traceId?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:accountAbstraction:buildBatchedUserOp', errorMessage: 'portal:accountAbstraction:buildBatchedUserOpError', resultMessage: 'portal:accountAbstraction:buildBatchedUserOpResult', data, traceId, }) } public async accountAbstractionBroadcastBatchedUserOp( data: BroadcastBatchedUserOpRequest, traceId?: string, ): Promise { return this.handleRequestToIframeAndPost({ methodMessage: 'portal:accountAbstraction:broadcastBatchedUserOp', errorMessage: 'portal:accountAbstraction:broadcastBatchedUserOpError', resultMessage: 'portal:accountAbstraction:broadcastBatchedUserOpResult', data, traceId, }) } /*************************** * Private Methods ***************************/ /** * Util to handle requests to the iframe and post the result to the parent. * * @remarks * This is an advanced/internal API intended primarily for low-level integrations * (for example, framework or account-type abstractions such as EvmAccountType). * Typical consumers should prefer using the higher-level methods exposed on * the {@link Mpc} class (e.g. {@link delegationsApprove}, {@link delegationsRevoke}, * {@link delegationsGetStatus}, {@link delegationsTransferFrom}) instead of calling * this method directly. * * This method is not part of the stable public surface area and may change in * future releases without notice. */ public async handleRequestToIframeAndPost({ methodMessage, errorMessage, resultMessage, data, progressMessage, progressCallback, mapReturnValue, traceId: providedTraceId, }: { methodMessage: string errorMessage: string resultMessage: string data: RequestType progressMessage?: string progressCallback?: (status: MpcStatus) => void mapReturnValue?: (result: any) => ResponseType traceId?: string }): Promise { // Single traceId per operation: use provided or generate. Propagates to iframe and all internal REST/MPC calls. const traceId = providedTraceId ?? generateTraceId() sdkLogger.debug( '[Portal] request traceId:', traceId, 'method:', methodMessage, ) return new Promise((resolve, reject) => { const handleRequest = (event: MessageEvent) => { const { type, data: result } = event.data const { origin } = event // ignore any broadcast postMessages if (origin !== this.getOrigin()) { return } if (type === errorMessage) { // Remove the event listener window.removeEventListener('message', handleRequest) // Reject the promise with the error return reject(new PortalMpcError(result as PortalError)) } else if (type === resultMessage) { // Remove the event listener window.removeEventListener('message', handleRequest) // Resolve the promise with the result resolve(mapReturnValue ? mapReturnValue(result) : (result as ResponseType)) } if (type === progressMessage && progressCallback) { void progressCallback(result as MpcStatus) } } // Bind the function to the message event window.addEventListener('message', handleRequest) // Send the request to the iframe (traceId at top-level so data shape stays unchanged) this.postMessage({ type: methodMessage, data, traceId, }) }) } /** * Appends the iframe to the document body */ private appendIframe() { // Attempt authentication before appending the iframe const host = this.portal.host const source = host.startsWith('localhost:') ? `http://${host}/${WEB_SDK_VERSION}/iframe/index.html?parentOrigin=${window.location.origin}` : `https://${host}/${WEB_SDK_VERSION}/iframe/index.html?parentOrigin=${window.location.origin}` const iframe = document.createElement('iframe') iframe.height = '0' iframe.width = '0' iframe.src = source iframe.addEventListener('load', this.configureIframe) document.body.appendChild(iframe) this.iframe = iframe } private configureIframe = () => { const config: IframeConfigurationOptions = { apiKey: this.portal.apiKey, authToken: this.portal.authToken, authUrl: this.portal.authUrl, autoApprove: this.portal.autoApprove, gdrive: this.portal.gDriveConfig, passkey: this.portal.passkeyConfig, host: this.portal.host, mpcHost: this.portal.mpcHost, mpcVersion: this.portal.mpcVersion, featureFlags: this.portal.featureFlags, logLevel: this.portal.getLogLevel(), rpcConfig: this.portal.rpcConfig, iframeRpcConfig: this.portal.iframeRpcConfig, } const message = { type: 'portal:configure', data: config, } this.postMessage(message) this.setupPresignatureLogForwarding() this.waitForReadyMessage() } private setupPresignatureLogForwarding() { if (this.presignatureLogHandler) return const handler = (event: MessageEvent) => { if (event.origin !== this.getOrigin()) return const { type, data } = event.data || {} if (type === 'portal:presignature:log' && data?.message) { sdkLogger.warn(data.message) } } this.presignatureLogHandler = handler window.addEventListener('message', handler) } private teardownPresignatureLogForwarding() { if (!this.presignatureLogHandler) return window.removeEventListener('message', this.presignatureLogHandler) this.presignatureLogHandler = null } private getOrigin(): string { const host = this.portal.host const origin = host.startsWith('localhost:') ? `http://${host}` : `https://${host}` return origin } /** * Fetches a scoped JWT for passkey operations from the iframe. * The JWT has only "passkey" permission, reducing blast radius if compromised. */ public async getPasskeyJwt(): Promise { return new Promise((resolve, reject) => { const handleResult = (event: MessageEvent) => { const { type, data } = event.data const { origin } = event // ignore any broadcast postMessages if (origin !== this.getOrigin()) { return } if (type === 'portal:getPasskeyJwtResult') { window.removeEventListener('message', handleResult) window.removeEventListener('message', handleError) resolve(data.token) } } const handleError = (event: MessageEvent) => { const { type, data } = event.data const { origin } = event if (origin !== this.getOrigin()) { return } if (type === 'portal:getPasskeyJwtError') { window.removeEventListener('message', handleResult) window.removeEventListener('message', handleError) reject(new Error(data.message)) } } window.addEventListener('message', handleResult) window.addEventListener('message', handleError) this.postMessage({ type: 'portal:getPasskeyJwt', data: {}, }) }) } public async rpcRequest( data: Omit, options?: { timeoutMs?: number; traceId?: string }, ): Promise { const { timeoutMs = 30_000, traceId } = options ?? {} const requestId = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}` const resolvedTraceId = traceId ?? generateTraceId() sdkLogger.debug('[Portal] rpcRequest', { requestId, method: data.method, chainId: data.chainId, traceId: resolvedTraceId, timeoutMs, }) return new Promise((resolve, reject) => { let timeoutId: ReturnType | undefined const cleanup = () => { window.removeEventListener('message', handleResponse) if (timeoutId !== undefined) { clearTimeout(timeoutId) } } const handleResponse = (event: MessageEvent) => { const { origin } = event if (origin !== this.getOrigin()) return const { type, data: result } = event.data if ( type === 'portal:rpc:requestResult' && result?.requestId === requestId ) { cleanup() resolve(result as RpcProxyResponse) } else if ( type === 'portal:rpc:requestError' && result?.requestId === requestId ) { cleanup() reject(new Error(result.message ?? 'RPC proxy error')) } } timeoutId = setTimeout(() => { cleanup() const msg = `RPC request ${requestId} (${data.method}) timed out after ${timeoutMs}ms` sdkLogger.error('[Portal] rpcRequest timeout', { requestId, method: data.method, chainId: data.chainId, timeoutMs, }) reject(new Error(msg)) }, timeoutMs) window.addEventListener('message', handleResponse) this.postMessage({ type: 'portal:rpc:request', data: { ...data, requestId }, traceId: resolvedTraceId, }) }) } private postMessage(event: { type: string; data: any; traceId?: string }) { this.iframe?.contentWindow?.postMessage(event, this.getOrigin()) } private waitForReadyMessage() { const handleError = (message: MessageEvent) => { const { type, data } = message.data const { origin } = message // ignore any broadcast postMessages if (origin !== this.getOrigin()) { return } if ( type === 'portal:wasm:error' || type === 'portal:authenticationError' ) { window.removeEventListener('message', handleError) window.removeEventListener('message', handleReady) this.portal.triggerError(data as string) } } const handleReady = async (message: MessageEvent) => { const { type, data } = message.data const { origin } = message // ignore any broadcast postMessages if (origin !== this.getOrigin()) { return } if (type === 'portal:ready' && data === true) { // Unbind the event listener window.removeEventListener('message', handleReady) window.removeEventListener('message', handleError) // Update ready state this.ready = true // Update the address const address = await this.getAddress() this.portal.address = address // Trigger the ready callback this.portal.triggerReady() } } window.addEventListener('message', handleReady) window.addEventListener('message', handleError) } private validateBackupConfig(data: BackupArgs) { // Validate that backupMethod is one of the valid BackupMethods const validBackupMethods = Object.values(BackupMethods) if (!validBackupMethods.includes(data.backupMethod)) { throw new Error( `Invalid backup method: ${data.backupMethod }. Valid methods are: ${validBackupMethods.join(', ')}`, ) } if (data.backupMethod === BackupMethods.password) { if (!data.backupConfigs.passwordStorage) { throw new Error('Password storage config is required') } if (!data.backupConfigs.passwordStorage.password) { throw new Error('Password is required') } if (data.backupConfigs.passwordStorage.password.length < 4) { throw new Error('Password must be at least 4 characters') } } } } export { MpcError, MpcErrorCodes } from './errors' export default Mpc