import { Connection, PublicKey, Transaction as SolanaTransaction, SystemProgram, } from '@solana/web3.js' import { GetAssetsResponse, BuiltTransaction, BuiltEip155Transaction, BuiltTronTransaction, EvaluateTransactionOperationType, EvaluateTransactionParam, EvaluatedTransaction, FundParams, FundResponse, NFTAsset, OrgBackupShares, SendAssetParams, GetTransactionHistoryParams, GetTransactionHistoryResponse, type BackupConfigs, type BackupResponse, type Balance, type EthereumTransaction, type FeatureFlags, type GDriveConfig, type NFT, type PasskeyConfig, type QuoteArgs, type QuoteResponse, type RawSignOptions, type RpcConfig, type SimulateTransactionParam, type SimulatedTransaction, type Transaction, type TypedData, type BuildBatchedUserOpRequest, type BuildBatchedUserOpResponse, type BroadcastBatchedUserOpRequest, type BroadcastBatchedUserOpResponse, type UserOperationCall, type SendBatchUserOpRequest, type SendBatchUserOpTransaction, type SendBatchedAssetsRequest, } from './shared/types' import { EjectPrivateKeysResult, type ClientResponse, type ClientResponseWallet, type EjectResult, type RequestArguments, type SharesOnDeviceResponse, type ProgressCallback, type BackupOptions, type BackupShareResult, type PasskeyOptions, type PortalOptions, type FirebaseStorageConfigOptions, } from '../types' import Mpc from './mpc' import Provider, { RequestMethod } from './provider' import Yield from './integrations/yield' import Trading from './integrations/trading' import Ramps from './integrations/ramps' import Security from './integrations/security' import Delegations from './integrations/delegations' import PasskeyService from './passkeys' import { EvmAccountType } from './namespaces/evmAccountType' import { sdkLogger } from './logger' import { generateTraceId } from './shared/trace' import { waitForEvmOrUserOpConfirmation } from './internal/waitForEvmOrUserOpConfirmation' import { waitForSolanaTxConfirmation } from './internal/waitForSolanaTxConfirmation' import type { LogLevel, ILogger } from '../types' class Portal { public address?: string public apiKey?: string public authToken?: string public authUrl?: string public autoApprove: boolean public gDriveConfig?: GDriveConfig public passkeyConfig?: PasskeyConfig public host: string public mpc: Mpc public yield: Yield public ramps: Ramps public trading: Trading public security: Security public evmAccountType: EvmAccountType public delegations: Delegations public mpcHost: string public mpcVersion: string public provider: Provider public featureFlags: FeatureFlags private errorCallbacks: ((reason: string) => any | Promise)[] = [] private _rpcConfig: RpcConfig private _iframeRpcConfig?: RpcConfig private readyCallbacks: (() => any | Promise)[] = [] private passkeyService?: PasskeyService private passkeyServiceDefaultDomain?: string private logger: ILogger = console public get ready() { return this.mpc.ready } public get rpcConfig(): RpcConfig { return this._rpcConfig } public get iframeRpcConfig(): RpcConfig | undefined { return this._iframeRpcConfig } constructor({ // Required rpcConfig, // Optional iframeRpcConfig, apiKey, authToken, authUrl, autoApprove = false, gdrive, passkey, host = 'web.portalhq.io', mpcVersion = 'v6', mpcHost = 'mpc-client.portalhq.io', featureFlags = {}, chainId, logLevel = 'none', logger = console, }: PortalOptions) { this.apiKey = apiKey this.authToken = authToken this.authUrl = authUrl this.autoApprove = autoApprove this._rpcConfig = rpcConfig this._iframeRpcConfig = iframeRpcConfig this.host = host this.mpcHost = mpcHost this.mpcVersion = mpcVersion this.featureFlags = featureFlags this.logger = logger // SDK logging is global: one shared logger for the entire runtime. The last Portal // created or the last setLogLevel() call wins for all instances. sdkLogger.configure(logLevel, logger) if (gdrive) { this.gDriveConfig = gdrive } if (passkey) { this.passkeyConfig = passkey } this.mpc = new Mpc({ portal: this, }) this.ramps = new Ramps({ mpc: this.mpc }) this.security = new Security({ mpc: this.mpc }) this.provider = new Provider({ portal: this, chainId: chainId ? Number(chainId) : undefined, }) const signAndSendTransaction = async ( transaction: unknown, network: string, ): Promise => { const method = network.startsWith('solana') ? RequestMethod.sol_signAndSendTransaction : RequestMethod.eth_sendTransaction const hash = await this.provider.request({ chainId: network, method, params: [transaction], }) if (typeof hash !== 'string' || hash.length === 0) { throw new Error( '[Portal] Signing request did not return a transaction hash. The user may have rejected the request, or the provider did not complete signing.', ) } return hash } const evmRequestFn = (method: string, params: unknown[], network: string) => this.provider.request({ chainId: network, method, params }) this.yield = new Yield({ mpc: this.mpc, waitForConfirmation: this.waitForConfirmation.bind(this), evmRequestFn, }) this.trading = new Trading({ mpc: this.mpc, signAndSendTransaction, waitForConfirmation: this.waitForConfirmation.bind(this), evmRequestFn, }) this.yield.yieldXyz.setSignAndSendTransaction(signAndSendTransaction) this.delegations = new Delegations({ mpc: this.mpc, signAndSendTransaction, }) this.evmAccountType = new EvmAccountType({ mpc: this.mpc }) } /*********************************** * Logging ***********************************/ /** * Set the SDK log level. Use this to enable or disable SDK logging at runtime. * Note: Logging configuration is global for the entire SDK runtime. Changing the * level on one Portal instance affects all Portal instances and all SDK log output. * @example * portal.setLogLevel('debug') // Enable verbose logging * portal.setLogLevel('none') // Disable all logs */ public setLogLevel(level: LogLevel): void { sdkLogger.configure(level, this.logger) } /** * Get the current SDK log level. * Note: This returns the global SDK log level (shared across all Portal instances). */ public getLogLevel(): LogLevel { return sdkLogger.getLogLevel() } /***************************** * Initialization Methods *****************************/ public onInitializationError( callback: (reason: string) => any | Promise, ): () => void { if (!this.ready) { this.errorCallbacks.push(callback) } return () => { this.errorCallbacks = this.errorCallbacks.filter((cb) => cb !== callback) } } public onReady(callback: () => any | Promise): () => void { if (this.ready) { callback() } else { this.readyCallbacks.push(callback) } return () => { this.readyCallbacks = this.readyCallbacks.filter((cb) => cb !== callback) } } public triggerError(data: string) { if (!this.ready && this.errorCallbacks.length > 0) { this.errorCallbacks.forEach((callback) => { callback(data) }) } } public triggerReady() { if (this.ready && this.readyCallbacks.length > 0) { this.readyCallbacks.forEach((callback) => { callback() }) this.readyCallbacks = [] } } /***************************** * Wallet Methods *****************************/ public async clearLocalWallet(): Promise { return await this.mpc.clearLocalWallet() } public async createWallet( progress: ProgressCallback = () => { // Noop }, ): Promise { const address = await this.mpc.generate( { host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }, progress, ) this.address = address return address } public async generateBackupShare( progress: ProgressCallback = () => { // Noop }, ): Promise { const response = await this.mpc.backup( { backupMethod: BackupMethods.passkey, backupConfigs: { skipStorageWrite: true, }, host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }, progress, ) if (!response.encryptionKey) { throw new Error( '[Portal] Passkey backup did not return an encryption key. Please ensure you are using the latest iframe bundle.', ) } return { cipherText: response.cipherText, encryptionKey: response.encryptionKey, } } public async registerPasskeyAndStoreEncryptionKey( cipherText: string, encryptionKey: string, options: PasskeyOptions = {}, ): Promise { const { service, customDomain, relyingPartyId, relyingPartyName } = this.resolvePasskeyOptions(options) await service.registerPasskeyAndStoreKey({ customDomain, relyingPartyName, encryptionKey, relyingPartyId, cipherText, }) } public async authenticatePasskeyAndRetrieveKey( options: PasskeyOptions = {}, ): Promise { const { service, customDomain, relyingPartyId, relyingPartyName } = this.resolvePasskeyOptions(options) return await service.authenticatePasskeyAndRetrieveKey({ customDomain, relyingPartyName, relyingPartyId, }) } /** * Register a passkey without tying it to an encryption key. * The encryption key can be stored later using authenticatePasskeyAndWriteKey. */ public async registerPasskey(options: PasskeyOptions = {}): Promise { const { service, customDomain, relyingPartyId, relyingPartyName } = this.resolvePasskeyOptions(options) await service.registerPasskey({ customDomain, relyingPartyName, relyingPartyId, }) } /** * Authenticate with passkey and store an encryption key. * Used after registerPasskey to associate an encryption key with the passkey. */ public async authenticatePasskeyAndWriteKey( encryptionKey: string, options: PasskeyOptions = {}, ): Promise { const { service, customDomain, relyingPartyId, relyingPartyName } = this.resolvePasskeyOptions(options) await service.authenticatePasskeyAndWriteKey({ customDomain, relyingPartyName, relyingPartyId, encryptionKey, }) } public async backupWithPasskey( options: BackupOptions = {}, progress: ProgressCallback = () => { // Noop }, ): Promise { const { usePopup = true, customDomain, relyingPartyName, backupMethod = BackupMethods.passkey, } = options if (usePopup) { await this.backupWallet(backupMethod, progress, {}) return } if (backupMethod !== BackupMethods.passkey) { throw new Error( `[Portal] Direct passkey backup currently supports only BackupMethods.passkey (received ${backupMethod}).`, ) } const { cipherText, encryptionKey } = await this.generateBackupShare( progress, ) await this.registerPasskeyAndStoreEncryptionKey(cipherText, encryptionKey, { customDomain, relyingPartyName, usePopup, }) await this.storedClientBackupShare(true, BackupMethods.passkey) } public configureFirebaseStorage( options: FirebaseStorageConfigOptions, ): void { this.mpc.configureFirebaseStorage(options) } public async backupWallet( backupMethod: BackupMethods, progress: ProgressCallback = () => { // Noop }, backupConfigs: BackupConfigs = {}, ): Promise { const response = await this.mpc.backup( { backupMethod, backupConfigs, host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }, progress, ) return response } public async recoverWallet( cipherText: string, backupMethod: BackupMethods, backupConfigs: BackupConfigs = {}, progress: ProgressCallback = () => { // Noop }, ): Promise { const address = await this.mpc.recover( { cipherText, backupMethod, backupConfigs, host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }, progress, ) this.address = address return address } public async provisionWallet( cipherText: string, backupMethod: BackupMethods, backupConfigs: BackupConfigs, progress: ProgressCallback = () => { // Noop }, ): Promise { return this.recoverWallet(cipherText, backupMethod, backupConfigs, progress) } public async eject( backupMethod: BackupMethods, backupConfigs: BackupConfigs, orgBackupShare = '', clientBackupCipherText = '', ): Promise { const client = await this.mpc.getClient() if (!client) { throw new Error('Client not found.') } if (!client.environment.backupWithPortalEnabled) { if (clientBackupCipherText === '') { throw new Error('clientBackupCipherText cannot be empty string.') } if (orgBackupShare === '') { throw new Error('orgBackupShare cannot be empty string.') } } const { SECP256K1 } = await this.mpc.eject({ cipherText: clientBackupCipherText, backupMethod, backupConfigs, organizationBackupShare: orgBackupShare, host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }) return { SECP256K1, } } public async ejectPrivateKeys( backupMethod: BackupMethods, backupConfigs: BackupConfigs = {}, orgBackupShares: OrgBackupShares, clientBackupCipherText = '', ): Promise { const client = await this.mpc.getClient() if (!client) { throw new Error('Client not found.') } if (!client.environment.backupWithPortalEnabled) { if (clientBackupCipherText === '') { throw new Error('clientBackupCipherText cannot be empty string.') } if (orgBackupShares.SECP256K1 === '') { throw new Error('SECP256K1 orgBackupShare cannot be empty string.') } } const { SECP256K1, ED25519 } = await this.mpc.ejectPrivateKeys({ cipherText: clientBackupCipherText, backupMethod, backupConfigs, organizationBackupShares: orgBackupShares, host: this.host, mpcVersion: this.mpcVersion, featureFlags: this.featureFlags, }) return { SECP256K1, ED25519, } } public async getEip155Address(): Promise { const client = await this.mpc?.getClient() const eip155Address = client?.metadata?.namespaces?.eip155?.address || '' return eip155Address } public async getSolanaAddress(): Promise { const client = await this.mpc?.getClient() const solAddress = client?.metadata?.namespaces?.solana?.address || '' return solAddress } public async getTronAddress(): Promise { const client = await this.mpc?.getClient() const tronAddress = client?.metadata?.namespaces?.tron?.address || '' return tronAddress } public async doesWalletExist(chainId?: string): Promise { const client = await this.mpc?.getClient() if (chainId) { const namespace = chainId?.split(':')?.[0] || '' const namespaceInfo = client?.metadata?.namespaces?.[namespace as ChainNamespace] return !!namespaceInfo } return client?.wallets?.length > 0 } public async isWalletOnDevice(chainId?: string): Promise { const client = await this.mpc?.getClient() const sharesOnDevice = await this.mpc?.checkSharesOnDevice() if (chainId) { const namespace = chainId?.split(':')?.[0] || '' const curve = client?.metadata?.namespaces?.[namespace as ChainNamespace]?.curve || '' return sharesOnDevice[curve as keyof SharesOnDeviceResponse] || false } return sharesOnDevice.ED25519 && sharesOnDevice.SECP256K1 } public async isWalletBackedUp(chainId?: string): Promise { const client = await this.mpc?.getClient() if (chainId) { const namespace = chainId?.split(':')?.[0] || '' const curve = client?.metadata?.namespaces?.[namespace as ChainNamespace]?.curve if (!curve) { return false } const wallet = client?.wallets.find((w) => w.curve === curve) if (!wallet) { return false } return wallet.backupSharePairs.some( (share) => share.status === 'completed', ) } else { return client?.wallets.some((wallet) => wallet.backupSharePairs.some((share) => share.status === 'completed'), ) } } public async isWalletRecoverable(chainId?: string): Promise { const availableRecoveryMethods = await this.availableRecoveryMethods( chainId, ) return availableRecoveryMethods.length > 0 } public async availableRecoveryMethods( chainId?: string, ): Promise { const client = await this.mpc?.getClient() // If the client is not found, return an empty array. if (!client) { return [] } // If a chainId is provided, return the recovery methods for that chain. if (chainId) { const namespace = chainId.split(':')[0] || '' const curve = client.metadata?.namespaces?.[namespace as ChainNamespace]?.curve if (!curve) { return [] } const wallet = client.wallets.find((w) => w.curve === curve) if (!wallet) { return [] } const methods = this.extractBackupMethod(wallet) return this.getUniqueBackupMethod(methods) } // Otherwise, return the recovery methods for all wallets. const allMethods = client.wallets.flatMap((wallet) => this.extractBackupMethod(wallet), ) return this.getUniqueBackupMethod(allMethods) } public async receiveTestnetAsset( chainId: string, params: FundParams, ): Promise { return await this.mpc?.fund(chainId, params) } public async sendAsset( chain: string, params: SendAssetParams, ): Promise { const traceId = params.traceId ?? generateTraceId() sdkLogger.debug( '[Portal] sendAsset started (single traceId for build + sign)', { traceId }, ) try { // Convert the chain to a chain ID const chainId = this.convertChainToChainId(chain) // Build the transaction (same traceId for correlation with backend) const buildTxResponse = await this.buildTransaction( chainId, params.to, params.token, params.amount, traceId, ) // Get the chain namespace const chainNamespace = chainId.split(':')[0] ?? '' // Send the transaction based on chain namespace (same traceId on request) let response switch (chainNamespace) { case 'eip155': { response = await this.request({ chainId, method: 'eth_sendTransaction', params: [buildTxResponse.transaction], sponsorGas: params.sponsorGas, signatureApprovalMemo: params.signatureApprovalMemo, traceId, }) break } case 'solana': { response = await this.request({ chainId, method: 'sol_signAndSendTransaction', params: [buildTxResponse.transaction], sponsorGas: params.sponsorGas, signatureApprovalMemo: params.signatureApprovalMemo, traceId, }) break } case 'tron': { const tronTx = buildTxResponse as BuiltTronTransaction response = await this.request({ chainId, method: 'tron_sendTransaction', params: [tronTx.transaction.id], sponsorGas: params.sponsorGas, signatureApprovalMemo: params.signatureApprovalMemo, traceId, }) break } default: { throw new Error(`Unsupported chain namespace: ${chainNamespace}`) } } return response } catch (error: any) { throw new Error(`Failed to send asset: ${error.message}`) } } /** * Wait until a transaction is confirmed on-chain, or until timeout. * * - **EVM (`eip155:*`):** Polls both `eth_getTransactionReceipt` and * `eth_getUserOperationReceipt` (auto-detect, locks after first hit). * - **Solana (`solana:*`):** Polls `getSignatureStatuses` until the * commitment level from `options.commitment` * is met (default `confirmed`). * - **TRON (`tron:*`):** Confirmation polling is not supported. * Always returns `false`. Transaction status must be verified * directly via TRON RPC using the transaction ID returned by `sendAsset`. * - **Other networks:** Returns `false` (unsupported for polling). * * Optional `options` tune poll/timeout for all polled chains; EVM also accepts * `onTimeout` (default `resolve_false`) and `lockModeAfterDetection` (default `true`) for dual-receipt polling and timeout handling. * * All RPC calls are routed through the iframe proxy to avoid CORS issues. */ public async waitForConfirmation( txHash: string, network: string, options?: { pollIntervalMs?: number timeoutMs?: number /** EVM / dual-mode only. @default 'resolve_false' */ onTimeout?: 'resolve_false' | 'throw' /** EVM / dual-mode only. @default true */ lockModeAfterDetection?: boolean /** Solana only. @default 'confirmed' */ commitment?: 'processed' | 'confirmed' | 'finalized' }, ): Promise { const requestFn = (method: string, params: unknown[], chainId: string) => this.provider.request({ chainId, method, params }) if (network.startsWith('eip155:')) { return waitForEvmOrUserOpConfirmation(txHash, network, requestFn, { pollIntervalMs: options?.pollIntervalMs ?? 4_000, timeoutMs: options?.timeoutMs ?? 900_000, onTimeout: options?.onTimeout ?? 'resolve_false', lockModeAfterDetection: options?.lockModeAfterDetection ?? true, }) } if (network.toLowerCase().startsWith('solana:')) { return waitForSolanaTxConfirmation(txHash, network, requestFn, { pollIntervalMs: options?.pollIntervalMs ?? 4_000, timeoutMs: options?.timeoutMs ?? 900_000, commitment: options?.commitment ?? 'confirmed', }) } sdkLogger.warn( `[Portal.waitForConfirmation] Unsupported network: "${network}". Returning false (cannot verify confirmation).`, { txHash, network }, ) return false } /**************************** * Provider Methods ****************************/ public async request(request: RequestArguments): Promise { return this.provider.request(request) } public updateChain(newChainId: string) { this.provider.setChainId(Number(newChainId)) } /** * Estimates the amount of gas that will be required to execute an Ethereum transaction. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_estimateGas' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details. * @returns {Promise} A Promise that resolves to the gas estimate. */ public async ethEstimateGas( chainId: string, transaction: EthereumTransaction, ): Promise { sdkLogger.warn( '"portal.ethEstimateGas" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_estimateGas, params: transaction, }) } /** * Gets the current gas price for the Ethereum network. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_gasPrice' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @returns {Promise} A Promise that resolves to the current gas price. */ public async ethGasPrice(chainId: string): Promise { sdkLogger.warn( '"portal.ethGasPrice" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_gasPrice, params: [], }) as Promise } /** * Gets the balance of the current Ethereum address. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_getBalance' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @returns {Promise} A Promise that resolves to the current balance. */ public async ethGetBalance(chainId: string): Promise { sdkLogger.warn( '"portal.ethGetBalance" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_getBalance, params: [this.address, 'latest'], }) as Promise } /** * Sends an Ethereum transaction. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_getTransactionCount' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details. * @returns {Promise} A Promise that resolves to the transaction hash. */ public async ethSendTransaction( chainId: string, transaction: EthereumTransaction, ): Promise { sdkLogger.warn( '"portal.ethSendTransaction" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_sendTransaction, params: transaction, }) as Promise } /** * Signs an Ethereum transaction. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTransaction' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {EthereumTransaction} transaction - The transaction object containing the necessary transaction details. * @returns {Promise} A Promise that resolves to the signed transaction. */ public async ethSignTransaction( chainId: string, transaction: EthereumTransaction, ): Promise { sdkLogger.warn( '"portal.ethSignTransaction" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_signTransaction, params: transaction, }) as Promise } /** * Signs an Ethereum message using EIP-712. * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {TypedData} data - The typed data object to sign. * @returns {Promise} A Promise that resolves to the signed message. */ public async ethSignTypedData( chainId: string, data: TypedData, ): Promise { sdkLogger.warn( '"portal.ethSignTypedData" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_signTypedData, params: [this.address, data], }) as Promise } /** * Signs an Ethereum message using EIP-712 (legacy). * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData_v3' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {TypedData} data - The typed data object to sign. * @returns {Promise} A Promise that resolves to the signed message. */ public async ethSignTypedDataV3( chainId: string, data: TypedData, ): Promise { sdkLogger.warn( '"portal.ethSignTypedDataV3" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_signTypedData_v3, params: [this.address, data], }) as Promise } /** * Signs an Ethereum message using EIP-712 (v4). * * @deprecated This method is deprecated. Use `portal.request` with method 'eth_signTypedData_v4' instead. * * @param {string} chainId - The chain ID of the Ethereum network. * @param {TypedData} data - The typed data object to sign. * @returns {Promise} A Promise that resolves to the signed message. */ public async ethSignTypedDataV4( chainId: string, data: TypedData, ): Promise { sdkLogger.warn( '"portal.ethSignTypedDataV4" is deprecated. Use "portal.request" instead.', ) return this.provider.request({ chainId, method: RequestMethod.eth_signTypedData_v4, params: [this.address, data], }) as Promise } public async personalSign(chainId: string, message: string): Promise { sdkLogger.warn( '"portal.personalSign" is deprecated. Use "portal.request" instead.', ) return (await this.provider.request({ chainId, method: RequestMethod.personal_sign, params: [this.stringToHex(message), this.address], })) as Promise } public async rawSign( curve: PortalCurve, param: string, options?: RawSignOptions, ): Promise { const response = await this.mpc.rawSign(curve, param, options) return response } /** * Build, sign, and broadcast a native SOL transfer on the given Solana CAIP-2 `chainId`. * `to` must be a valid Solana public key (base58); `lamports` must be a positive integer. */ public async sendSol({ chainId, to, lamports, }: { chainId: string to: string lamports: number }): Promise { // Ensure the chainId is solana. if (!chainId.startsWith('solana:')) { throw new Error( '[Portal] Invalid chainId. Please provide a chainId that starts with "solana:"', ) } if (!to || typeof to !== 'string') { throw new Error('[Portal] Invalid "to" Solana address provided') } const toTrimmed = to.trim() let toPublicKey: PublicKey try { toPublicKey = new PublicKey(toTrimmed) } catch { throw new Error( '[Portal] Invalid "to" Solana address provided (not a valid public key)', ) } // Validate the lamports. if (typeof lamports !== 'number' || lamports <= 0) { throw new Error( '[Portal] Invalid lamports amount, must be a positive number greater than 0', ) } // Get the most recent blockhash. const blockhashResponse = await this.provider.request({ chainId: chainId, method: RequestMethod.sol_getLatestBlockhash, params: [], }) const blockhash = blockhashResponse?.value?.blockhash || '' // If we didn't get a blockhash, throw an error. if (!blockhash) { throw new Error('[Portal] Failed to get most recent blockhash') } // Get the Solana address from the client, validate the addresses. const solanaAddress = await this.getSolanaAddress() if (!solanaAddress) { throw new Error('[Portal] Failed to get Solana address') } // Get the Solana gateway URL. const gatewayUrl = this.getRpcUrl(chainId) if (!gatewayUrl) { throw new Error('[Portal] No RPC endpoint configured for chainId') } // Create a new connection to the Solana network. new Connection(gatewayUrl, 'confirmed') // The sender's public key. const fromPublicKey = new PublicKey(solanaAddress) // Create a new transaction. const transaction = new SolanaTransaction().add( SystemProgram.transfer({ fromPubkey: fromPublicKey, toPubkey: toPublicKey, lamports, }), ) transaction.recentBlockhash = blockhash transaction.feePayer = fromPublicKey const compiledMessage = transaction.compileMessage() // Build the transaction and its message. const message = { accountKeys: compiledMessage.accountKeys.map((key) => key.toBase58()), header: compiledMessage.header, instructions: compiledMessage.instructions, recentBlockhash: blockhash, } const formattedTransaction = { signatures: null, message, } // Attempt to sign and send the transaction const transactionResult = await this.provider.request({ chainId: chainId, method: RequestMethod.sol_signAndSendTransaction, params: [formattedTransaction], }) // If we didn't get a transactionResult, throw an error. if (!transactionResult) { throw new Error('[Portal] Failed to send Solana transaction') } // Return the transactionResult. return transactionResult } public sendEth = async ({ chainId, to, value, }: { chainId: string to: string value: string }) => { return this.provider.request({ chainId, method: RequestMethod.eth_sendTransaction, params: [ { from: this.address, to, value, }, ], }) } /******************************* * API Methods *******************************/ /** * @deprecated This method is deprecated. Use `portal.getAssets` instead. */ public async getBalances(chainId: string): Promise { return await this.mpc?.getBalances(chainId) } public async getClient(): Promise { return this.mpc?.getClient() } /** * @deprecated This method is deprecated. Use `portal.getNFTAssets` instead. */ public async getNFTs(chainId: string): Promise { return this.mpc?.getNFTs(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. * * Legacy endpoint: /api/v3/clients/me/transactions?chainId={chainId} * New endpoint: /api/v3/clients/me/chains/{chain}/transactions */ public async getTransactions( chainId: string, limit?: number, offset?: number, order?: GetTransactionsOrder, ): Promise { return this.mpc?.getTransactions(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. * * Response format varies by chain: * - Solana returns the legacy format (will be migrated in a future release) * - All other chains return the unified TransactionHistoryItem format * * @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, max: 1000 for EVM, 15 for Solana) * @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, must match client's known addresses) * @param params.userOperations - Filter for ERC-4337 UserOperations (EVM only): 'include', 'only', or 'exclude' * @returns Promise resolving to transaction history response * * @example * ```typescript * // Fetch latest 100 transactions for Ethereum mainnet * const response = await portal.getTransactionHistory({ * chainId: 'eip155:1', * limit: 100, * order: 'desc' * }); * * console.log(response.data.transactions); // Array of TransactionHistoryItem * ``` */ public async getTransactionHistory( params: GetTransactionHistoryParams, ): Promise { return this.mpc?.getTransactionHistory(params) } /** * @deprecated This method is deprecated. Use `portal.evaluateTransaction` instead. */ public async simulateTransaction( chainId: string, transaction: SimulateTransactionParam, ): Promise { return this.mpc?.simulateTransaction(transaction, chainId) } public async evaluateTransaction( chainId: string, transaction: EvaluateTransactionParam, operationType: EvaluateTransactionOperationType = 'all', ): Promise { return this.mpc?.evaluateTransaction(chainId, transaction, operationType) } public async getAssets( chainId: string, includeNfts = false, ): Promise { return this.mpc?.getAssets(chainId, includeNfts) } public async getNFTAssets(chainId: string): Promise { return this.mpc?.getNFTAssets(chainId) } public async buildTransaction( chainId: string, to: string, token: string, amount: string, traceId?: string, ): Promise { return this.mpc?.buildTransaction(chainId, to, token, amount, traceId) } /******************************* * Account Abstraction Methods *******************************/ public async buildBatchedUserOp( data: BuildBatchedUserOpRequest, ): Promise { return this.mpc?.accountAbstractionBuildBatchedUserOp(data) } public async broadcastBatchedUserOp( data: BroadcastBatchedUserOpRequest, ): Promise { return this.mpc?.accountAbstractionBroadcastBatchedUserOp(data) } /** * Build, sign, and broadcast a batch of EIP-155 UserOperations (ERC-4337). * * Returns the broadcast response containing `userOpHash`. Broadcasting does not * guarantee on-chain inclusion — UserOps are processed by a bundler that may * delay or drop them. To wait for confirmation call: * `await portal.waitForConfirmation(result.data.userOpHash, data.chain)` * * @param data.chain - CAIP-2 chain ID; must start with 'eip155:' * @param data.transactions - Ordered list of transfers to batch * @param data.signatureApprovalMemo - Optional memo for the signing approval prompt * @param data.traceId - Optional trace ID for request correlation */ public async sendBatchUserOp( data: SendBatchUserOpRequest, ): Promise { if (!data.chain.startsWith('eip155:')) { throw new Error( '[Portal.sendBatchUserOp] UserOperations are only supported on EIP-155 (EVM) chains', ) } if (!data.transactions || data.transactions.length === 0) { throw new Error( '[Portal.sendBatchUserOp] transactions must contain at least one transaction', ) } // Validate every destination address before starting any async work. for (const tx of data.transactions) { if (!tx.to || !/^0x[0-9a-fA-F]{40}$/i.test(tx.to)) { throw new Error( `[Portal.sendBatchUserOp] Invalid 'to' address "${String(tx.to)}": must be a 42-character EVM address (0x + 40 hex chars)`, ) } } const traceId = data.traceId ?? generateTraceId() // Build each transaction sequentially. The iframe serializes all postMessages // via _portalMessageQueue, so Promise.all would only create the illusion of // parallelism while hiding failures from all-but-the-first transaction. const calls: UserOperationCall[] = [] for (let index = 0; index < data.transactions.length; index++) { calls.push( await this.buildUserOpCall( data.chain, data.transactions[index], traceId, index, 'Portal.sendBatchUserOp', ), ) } // Build the batched UserOperation. let buildResponse: BuildBatchedUserOpResponse try { buildResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp( { chain: data.chain, calls }, traceId, ) } catch (error) { throw new Error( `[Portal.sendBatchUserOp] Failed to build UserOperation: ${error instanceof Error ? error.message : String(error)}`, ) } // Validate, sign, and broadcast the built UserOperation, reusing the same // traceId for end-to-end correlation. return this.signAndBroadcastBuiltUserOp( buildResponse, data.chain, data.signatureApprovalMemo, traceId, 'Portal.sendBatchUserOp', ) } /** * Build, sign, and broadcast a gas-subsidized batch where the end user * reimburses the platform — in a fee token (e.g. USDC) — for the gas Portal's * paymaster sponsored. * * The flow is two-pass because the reimbursement amount depends on the gas of * the batch it's part of (a chicken-and-egg the helper resolves for you): * 1. build `[...userCalls, feeCallPlaceholder]` to read the estimated gas cost * (`metadata.estimatedGasCostWei`; the placeholder ensures the estimate * reflects the FINAL batch shape) * 2. convert that native gas cost → fee-token amount via your `convertGasToFeeAmount` * 3. build `[...userCalls, feeCall]` with the real amount, sign, broadcast * * Important characteristics: * - You are charging the build-time gas estimate (an upper bound), not the * post-execution actual — actual gas is unknowable before the op runs, and an * atomic batch must fix the reimbursement amount at sign time. Use `bufferBps` * to absorb gas-price drift between the two builds. * - Conversion (native → fee token) is entirely yours; Portal does no FX. * - Throws if the estimated gas cost is 0. Some chains/providers carry no * on-chain fee on the user operation (e.g. Ultra Relay bundler-level * sponsorship on Monad mainnet), so there's nothing to reimburse from; those * chains need a gas price sourced another way. * * @param data.chain - CAIP-2 chain ID; must start with 'eip155:' * @param data.transactions - The end user's actual transfers to execute * @param data.gasReimbursement - Fee token, recipient, and conversion callback * @param data.signatureApprovalMemo - Optional memo for the signing approval prompt * @param data.traceId - Optional trace ID for request correlation */ public async sendBatchedAssets( data: SendBatchedAssetsRequest, ): Promise { const ctx = 'Portal.sendBatchedAssets' if (!data.chain.startsWith('eip155:')) { throw new Error( `[${ctx}] UserOperations are only supported on EIP-155 (EVM) chains`, ) } if (!data.transactions || data.transactions.length === 0) { throw new Error( `[${ctx}] transactions must contain at least one transaction`, ) } const gr = data.gasReimbursement if (!gr || typeof gr.convertGasToFeeAmount !== 'function') { throw new Error( `[${ctx}] gasReimbursement.convertGasToFeeAmount (a function) is required`, ) } if (!gr.feeToken) { throw new Error(`[${ctx}] gasReimbursement.feeToken is required`) } // Validate every destination address before starting any async work. const evmAddress = /^0x[0-9a-fA-F]{40}$/i for (const tx of data.transactions) { if (!tx.to || !evmAddress.test(tx.to)) { throw new Error( `[${ctx}] Invalid 'to' address "${String(tx.to)}": must be a 42-character EVM address (0x + 40 hex chars)`, ) } } if (!gr.feeRecipient || !evmAddress.test(gr.feeRecipient)) { throw new Error( `[${ctx}] Invalid gasReimbursement.feeRecipient "${String(gr.feeRecipient)}": must be a 42-character EVM address (0x + 40 hex chars)`, ) } const traceId = data.traceId ?? generateTraceId() // 1. Build the user's actual calls once — they don't change between passes. const userCalls: UserOperationCall[] = [] for (let index = 0; index < data.transactions.length; index++) { userCalls.push( await this.buildUserOpCall( data.chain, data.transactions[index], traceId, index, ctx, ), ) } // 2. Build a placeholder fee call so the estimation pass reflects the final // batch shape (an N+1-call executeBatch). The amount does not affect the // gas estimate; only the call's presence and shape do. const feeIndex = data.transactions.length const placeholderAmount = gr.placeholderAmount ?? '0.01' let placeholderFeeCall: UserOperationCall try { placeholderFeeCall = await this.buildUserOpCall( data.chain, { token: gr.feeToken, value: placeholderAmount, to: gr.feeRecipient }, traceId, feeIndex, ctx, ) } catch (error) { throw new Error( `[${ctx}] Failed to build placeholder fee call: ${error instanceof Error ? error.message : String(error)}`, ) } // 3. Estimation pass — build the full batch to read the gas it's bounded by. let estimateResponse: BuildBatchedUserOpResponse try { estimateResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp( { chain: data.chain, calls: [...userCalls, placeholderFeeCall] }, traceId, ) } catch (error) { throw new Error( `[${ctx}] Failed to estimate UserOperation gas: ${error instanceof Error ? error.message : String(error)}`, ) } let gasCostWei = this.resolveUserOpGasCostWei(estimateResponse, ctx) // Guard against a zero gas cost. Some chains/providers return no on-chain fee // on the user operation (e.g. Ultra Relay bundler-level sponsorship on Monad // mainnet), so the build-time cost is 0 — there's nothing to derive a // reimbursement from. Fail loudly rather than silently charging the user 0. if (gasCostWei === BigInt(0)) { throw new Error( `[${ctx}] Estimated gas cost is 0 — this chain/provider carries no on-chain fee on the user operation (e.g. Ultra Relay bundler-level sponsorship). Cannot derive a reimbursement amount; supply the gas price another way for this chain.`, ) } // Apply optional buffer (basis points) to absorb gas-price drift before FX. if (gr.bufferBps && gr.bufferBps > 0) { gasCostWei = (gasCostWei * BigInt(10000 + Math.floor(gr.bufferBps))) / BigInt(10000) } // 4. Platform-owned conversion: native gas cost (wei) → fee-token amount. let feeAmount: string try { feeAmount = await gr.convertGasToFeeAmount(gasCostWei) } catch (error) { throw new Error( `[${ctx}] gasReimbursement.convertGasToFeeAmount threw: ${error instanceof Error ? error.message : String(error)}`, ) } if (typeof feeAmount !== 'string' || feeAmount.length === 0) { throw new Error( `[${ctx}] convertGasToFeeAmount must return a non-empty amount string, got "${String(feeAmount)}"`, ) } // 5. Build the real fee call with the converted amount. let feeCall: UserOperationCall try { feeCall = await this.buildUserOpCall( data.chain, { token: gr.feeToken, value: feeAmount, to: gr.feeRecipient }, traceId, feeIndex, ctx, ) } catch (error) { throw new Error( `[${ctx}] Failed to build fee reimbursement call: ${error instanceof Error ? error.message : String(error)}`, ) } // 6. Final pass — build the batch we'll actually sign and broadcast. let buildResponse: BuildBatchedUserOpResponse try { buildResponse = await this.mpc?.accountAbstractionBuildBatchedUserOp( { chain: data.chain, calls: [...userCalls, feeCall] }, traceId, ) } catch (error) { throw new Error( `[${ctx}] Failed to build UserOperation: ${error instanceof Error ? error.message : String(error)}`, ) } return this.signAndBroadcastBuiltUserOp( buildResponse, data.chain, data.signatureApprovalMemo, traceId, ctx, ) } /** * Build a single ERC-4337 call from a high-level transfer descriptor by routing * it through `buildTransaction` (which resolves token contract + calldata and * normalizes the amount). Native transfers carry `value` (the base-unit amount); * ERC-20 transfers carry `data` and omit `value`. */ private async buildUserOpCall( chain: string, tx: SendBatchUserOpTransaction, traceId: string, index: number, ctx: string, ): Promise { let builtTx: BuiltTransaction try { builtTx = await this.buildTransaction( chain, tx.to, tx.token, tx.value, traceId, ) } catch (error) { throw new Error( `[${ctx}] Failed to build call for transaction at index ${index}: ${error instanceof Error ? error.message : String(error)}`, ) } // Validate the built transaction shape before casting. buildTransaction // returns BuiltTransaction (a union type), so the cast is TypeScript-only and // provides no runtime safety against a changed or unexpected backend response. const rawTx = builtTx as { transaction?: Record metadata?: Record } if (typeof rawTx?.transaction?.to !== 'string' || !rawTx.transaction.to) { throw new Error( `[${ctx}] buildTransaction returned unexpected shape for transaction at index ${index}`, ) } const eip155Tx = builtTx as BuiltEip155Transaction // Normalize the data field — guard against undefined/null/empty string from // the backend. Empty data means no calldata (native transfer). rawAmount is // passed as-is (decimal string e.g. '1000000000000000000'); the AA backend's // build-user-operation endpoint accepts decimal or hex. const txData = eip155Tx.transaction.data || '0x' const isNativeTransfer = txData === '0x' return { to: eip155Tx.transaction.to, data: txData, ...(isNativeTransfer ? { value: eip155Tx.metadata.rawAmount } : {}), } } /** * Resolve the native gas cost (in wei) a built UserOperation is bounded by, * from the backend-computed `metadata.estimatedGasCostWei` (`totalGas * * maxFeePerGas`, an upper bound). Throws if the field is absent — the * connect-api build-user-operation gas-cost change must be deployed. */ private resolveUserOpGasCostWei( buildResponse: BuildBatchedUserOpResponse, ctx: string, ): bigint { const wei = buildResponse?.metadata?.estimatedGasCostWei if (wei == null || `${wei}`.length === 0) { throw new Error( `[${ctx}] build response is missing metadata.estimatedGasCostWei; the connect-api build-user-operation gas-cost change must be deployed for the target environment`, ) } return this.toBigIntOrThrow(wei, 'metadata.estimatedGasCostWei', ctx) } private toBigIntOrThrow(value: unknown, field: string, ctx: string): bigint { try { // BigInt() accepts both decimal ('1000000000') and hex ('0x3b9aca00') strings. return BigInt(value as string | number) } catch { throw new Error( `[${ctx}] Could not parse ${field} as an integer: "${String(value)}"`, ) } } /** * Validate a built UserOperation, sign its hash with the SECP256K1 raw signer, * and broadcast it. Shared by the batched-UserOp send paths. */ private async signAndBroadcastBuiltUserOp( buildResponse: BuildBatchedUserOpResponse, chain: string, signatureApprovalMemo: string | undefined, traceId: string, ctx: string, ): Promise { const { userOperation, userOpHash } = buildResponse.data // Validate userOperation is parseable JSON before signing or broadcasting. // A malformed string from the backend would produce an opaque bundler rejection. try { const parsed: unknown = JSON.parse(userOperation) if (!parsed || typeof parsed !== 'object') { throw new Error('parsed value is not a JSON object') } } catch (e) { throw new Error( `[${ctx}] buildBatchedUserOp returned an invalid userOperation: ${e instanceof Error ? e.message : String(e)}`, ) } // Validate userOpHash is a valid 32-byte hex string before signing. // Signing an empty or malformed hash wastes the signing operation and produces // a signature that no bundler will accept. if (!userOpHash || !/^(0x)?[0-9a-fA-F]{64}$/.test(userOpHash)) { throw new Error( `[${ctx}] Invalid userOpHash received from buildBatchedUserOp: "${String(userOpHash)}"`, ) } // Sign the userOpHash — strip the 0x prefix before passing to rawSign. let signature: string try { const hashToSign = userOpHash.startsWith('0x') ? userOpHash.slice(2) : userOpHash signature = await this.rawSign(PortalCurve.SECP256K1, hashToSign, { signatureApprovalMemo, traceId, }) } catch (error) { throw new Error( `[${ctx}] Failed to sign userOpHash: ${error instanceof Error ? error.message : String(error)}`, ) } // Broadcast the signed UserOperation, reusing the same traceId for end-to-end correlation. try { return await this.mpc?.accountAbstractionBroadcastBatchedUserOp( { chain, userOperation, signature }, traceId, ) } catch (error) { throw new Error( `[${ctx}] Failed to broadcast UserOperation: ${error instanceof Error ? error.message : String(error)}`, ) } } /******************************* * Swaps Methods *******************************/ /** * @deprecated This method is deprecated. Use `portal.trading.zeroX.getQuote` instead. */ public async getQuote( apiKey: string, args: QuoteArgs, chainId: string, ): Promise { return this.mpc?.getQuote(chainId, args, apiKey) } /** * @deprecated This method is deprecated. Use `portal.trading.zeroX.getSources` instead. */ public async getSources( apiKey: string, chainId: string, ): Promise> { return this.mpc?.getSources(chainId, apiKey) } /******************************* * Wallet Safeguarding Methods *******************************/ public async storedClientBackupShare( success: boolean, backupMethod: BackupMethods, ): Promise { return await this.mpc?.storedClientBackupShare(success, backupMethod) } /**************************** * RPC Methods ****************************/ public getRpcUrl(chainId?: string) { // Ensure a chainId is provided. if (!chainId) { throw new Error( '[Portal] No chainId provided. Please provide a chainId to get the RPC endpoint', ) } // eslint-disable-next-line no-prototype-builtins if (!this._rpcConfig.hasOwnProperty(chainId)) { throw new Error( `[Portal] No RPC endpoint configured for chainId: ${chainId}`, ) } const gatewayUrl = this._rpcConfig[chainId] // If the RPC endpoint is a string, return it as-is. if (typeof gatewayUrl === 'string') { return gatewayUrl } // Otherwise, something is wrong with the configuration. throw new Error( `[Portal] Could not find a valid rpcConfig entry for chainId: ${chainId}`, ) } private stringToHex(str: string): string { if (str.startsWith('0x')) { return str } let hex = '0x' for (let i = 0; i < str.length; i++) { const charCode = str.charCodeAt(i) const hexValue = charCode.toString(16) hex += hexValue.padStart(2, '0') // Ensure two-digit hex value } return hex } private extractBackupMethod(wallet: ClientResponseWallet): BackupMethods[] { return wallet.backupSharePairs .filter((share) => share.status === 'completed') .map((share) => share.backupMethod) } private getUniqueBackupMethod(methods: BackupMethods[]): BackupMethods[] { return Array.from(new Set(methods)) } private convertChainToChainId(chain: string): string { // If the chain is not provided, throw an error. if (!chain) { throw new Error( 'You did not provide "chain" in the function call. Please provide a chain ID or friendly chain name.', ) } // If the chain is already in CAIP-2 format (e.g., "eip155:1"), return it as-is. if (chain.includes(':')) { return chain } // A mapping of friendly chain names to CAIP-2 chain IDs. const friendlyChainToChainId = { ethereum: 'eip155:1', sepolia: 'eip155:11155111', base: 'eip155:8453', 'base-sepolia': 'eip155:84531', polygon: 'eip155:137', 'polygon-mumbai': 'eip155:80001', 'polygon-amoy': 'eip155:80002', solana: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', 'solana-devnet': 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', optimism: 'eip155:10', arbitrum: 'eip155:42161', avalanche: 'eip155:43114', } // Convert the chain to lowercase to match the friendly chain names. const lowerCaseChain = chain.toLowerCase() as keyof typeof friendlyChainToChainId // Get the CAIP-2 chain ID from the mapping, if it exists. const chainId = friendlyChainToChainId[lowerCaseChain] // If the chain is not supported, throw an error. if (!chainId) { throw new Error( `Unsupported chain: "${chain}". Please provide a supported chain, such as ${Object.keys( friendlyChainToChainId, ).join(', ')}`, ) } return chainId } private ensurePasskeyService(): PasskeyService { const defaultDomain = this.getDefaultPasskeyDomain() if ( !this.passkeyService || this.passkeyServiceDefaultDomain !== defaultDomain ) { this.passkeyService = new PasskeyService({ defaultDomain, getJwt: () => this.mpc.getPasskeyJwt(), }) this.passkeyServiceDefaultDomain = defaultDomain } return this.passkeyService } private getDefaultPasskeyDomain(): string { return this.passkeyConfig?.webAuthnHost ?? 'backup.web.portalhq.io' } private extractRpId(domain?: string): string { const targetDomain = domain ?? this.getDefaultPasskeyDomain() try { const normalized = targetDomain.startsWith('http') ? targetDomain : `https://${targetDomain}` return new URL(normalized).hostname } catch { return targetDomain } } private resolvePasskeyOptions(options: PasskeyOptions): { service: PasskeyService customDomain: string | undefined relyingPartyId: string relyingPartyName: string } { if (options.usePopup) { throw new Error( '[Portal] This method does not support the popup flow. Use usePopup: false.', ) } const service = this.ensurePasskeyService() const domain = options.customDomain ?? this.getDefaultPasskeyDomain() const relyingPartyId = options.relyingPartyId ?? this.extractRpId(domain) const relyingPartyName = options.relyingPartyName ?? this.passkeyConfig?.relyingParty ?? 'Portal' return { service, customDomain: options.customDomain, relyingPartyId, relyingPartyName, } } } export type { IDelegations, IPortalDelegationsApi, DelegationsOptions, DelegationSubmitOptions, } from './integrations/delegations' export { type YieldXyzEnterRequest, type YieldXyzEnterYieldResponse, type YieldXyzExitRequest, type YieldXyzExitResponse, type YieldXyzGetBalancesRequest, type YieldXyzGetBalancesResponse, type YieldXyzGetHistoricalActionsRequest, type YieldXyzGetHistoricalActionsResponse, type YieldXyzGetTransactionRequest, type YieldXyzGetTransactionResponse, type YieldXyzGetYieldsRequest, type YieldXyzGetYieldsResponse, type YieldXyzManageYieldRequest, type YieldXyzManageYieldResponse, type YieldXyzTrackTransactionRequest, type YieldXyzTrackTransactionResponse, type YieldXyzGetYieldDefaultsRequest, type YieldXyzGetYieldDefaultsResponse, type YieldXyzGetYieldValidatorsResponse, type YieldDepositParams, type YieldDepositResult, type YieldSubmitOptions, type YieldSubmitProgress, type YieldWithdrawParams, type YieldWithdrawResult, type LifiTradeAssetParams, type LifiTradeAssetOptions, type LifiTradeAssetResult, type LifiPollStatusOptions, type ZeroXTradeAssetParams, type ZeroXTradeAssetOptions, type ZeroXTradeAssetResult, type ApproveDelegationRequest, type ApproveDelegationResponse, type RevokeDelegationRequest, type RevokeDelegationResponse, type GetDelegationStatusRequest, type DelegationStatusResponse, type TransferFromRequest, type TransferFromResponse, type DelegationSubmitProgress, type EvmAccountTypeGetStatusRequest, type EvmAccountTypeGetStatusResponse, type BuildAuthorizationListRequest, type BuildAuthorizationListResponse, type Build7702UpgradeTxRequest, type Build7702UpgradeTxResponse, type UpgradeTo7702Request, type UpgradeTo7702Response, type GetAddressesResponse, type GetTransactionHistoryParams, type GetTransactionHistoryResponse, type TransactionHistoryItem, type SolanaTransactionDetails, type Transaction, type UserOperationCall, type BuildBatchedUserOpRequest, type BuildBatchedUserOpResponse, type BroadcastBatchedUserOpRequest, type BroadcastBatchedUserOpResponse, type SendBatchUserOpTransaction, type SendBatchUserOpRequest, type GasReimbursement, type SendBatchedAssetsRequest, } from './shared/types' export type { NoahGetPaymentMethodsResponse, NoahGetPaymentMethodsResponseData, NoahGetPayoutChannelFormResponse, NoahGetPayoutChannelFormResponseData, NoahGetPayoutChannelsRequest, NoahGetPayoutChannelsResponse, NoahGetPayoutChannelsResponseData, NoahGetPayoutCountriesResponse, NoahGetPayoutCountriesResponseData, NoahGetPayoutQuoteRequest, NoahGetPayoutQuoteResponse, NoahGetPayoutQuoteResponseData, NoahInitiateKycRequest, NoahInitiateKycResponse, NoahInitiateKycResponseData, NoahInitiatePayinRequest, NoahInitiatePayinResponse, NoahInitiatePayinResponseData, NoahInitiatePayoutRequest, NoahInitiatePayoutResponse, NoahInitiatePayoutResponseData, NoahSingleOnchainDepositSourceTriggerInput, NoahSimulatePayinRequest, NoahSimulatePayinResponse, NoahSimulatePayinResponseData, NoahSolanaCaipId, PortalApiSuccessEnvelope, } from './shared/types' export type { TradingOptions, LifiOptions, ZeroXOptions, ILiFi, IZeroX, } from './integrations/trading' export { MpcError, MpcErrorCodes } from './mpc' export { PortalMpcError } from './mpc/errors' export { type Address, type Dapp, type FirebaseStorageConfigOptions, type ILogger, type LogLevel, type MpcStatus, type PortalOptions, } from '../types' export { RequestMethod } from './provider' export enum MpcStatuses { DecryptingShare = 'Decrypting share', Done = 'Done', EncryptingShare = 'Encrypting share', GeneratingShare = 'Generating share', ParsingShare = 'Parsing share', ReadingShare = 'Reading share', RecoveringBackupShare = 'Recovering backup share', RecoveringSigningShare = 'Recovering signing share', StoringShare = 'Storing share', } export enum BackupMethods { gdrive = 'GDRIVE', password = 'PASSWORD', passkey = 'PASSKEY', custom = 'CUSTOM', firebase = 'FIREBASE', unknown = 'UNKNOWN', } export enum GetTransactionsOrder { ASC = 'asc', DESC = 'desc', } export enum PortalCurve { ED25519 = 'ED25519', SECP256K1 = 'SECP256K1', } export enum ChainNamespace { EIP155 = 'eip155', SOLANA = 'solana', TRON = 'tron', } export default Portal