import { type QuoteArgs, type QuoteResponse } from '@portal-hq/swaps' import { Balance, CHAIN_NAMESPACES, DEFAULT_HOSTS, Dapp, EvaluateTransactionOperationType, Events, GetTransactionsOrder, HttpError, HttpRequester, IPortalApi, IPortalProvider, NFT, Network, PortalRequestMethod, RawSignOptions, Transaction, generateTraceId, getClientPlatformVersion, sdkLogger, } from '@portal-hq/utils' import type { BackupSharePairMetadata, BlockaidValidateTrxRes, ClientResponse, EvaluateTransactionParam, GetTransactionHistoryParams, GetTransactionHistoryResponse, SigningSharePairMetadata, SimulateTransactionParam, SimulatedTransaction, } from '@portal-hq/utils/types' import { AssetsResponse, BroadcastBitcoinP2wpkhTransactionResponse, BroadcastParam, BuildBitcoinP2wpkhTransactionResponse, BuildEip155TransactionResponse, BuildSolanaTransactionResponse, FeatureFlags, FundParams, FundResponse, PortalApiOptions, YieldXyzEnterRequest, YieldXyzEnterYieldResponse, YieldXyzExitRequest, YieldXyzExitResponse, YieldXyzGetBalancesRequest, YieldXyzGetBalancesResponse, YieldXyzGetHistoricalActionsRequest, YieldXyzGetHistoricalActionsResponse, YieldXyzGetTransactionResponse, YieldXyzGetYieldDefaultsRequest, YieldXyzGetYieldDefaultsResponse, YieldXyzGetYieldValidatorsResponse, YieldXyzGetYieldsRequest, YieldXyzGetYieldsResponse, YieldXyzManageYieldRequest, YieldXyzManageYieldResponse, YieldXyzTrackTransactionRequest, YieldXyzTrackTransactionResponse, } from '../../types' import { BackupMethodsUpperCase, PortalCurve } from '../mpc' class PortalApi implements IPortalApi { public apiKey: string public apiUrl: string public requests: HttpRequester private provider: IPortalProvider private featureFlags?: FeatureFlags private get chainId(): string | undefined { return this.provider.chainId } constructor({ apiKey, provider, host = DEFAULT_HOSTS.API, featureFlags, }: PortalApiOptions) { this.apiKey = apiKey this.apiUrl = host.startsWith('localhost:') ? `http://${host}` : `https://${host}` this.provider = provider this.requests = new HttpRequester({ baseUrl: this.apiUrl, }) this.featureFlags = featureFlags } public async getClient(traceId?: string): Promise { const requestTraceId = traceId ?? generateTraceId() const path = '/api/v3/clients/me' const client = await this.requests.get(path, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: requestTraceId, }) if (!client) { throw new Error('[PortalApi] Client not found') } try { void this.track(Events.GetClient, { path }) } catch (error) { // Noop } return client } public async getClientCipherText(backupSharePairId: string): Promise { const path = `/api/v3/clients/me/backup-share-pairs/${backupSharePairId}/cipher-text` const response = await this.requests.get<{ cipherText: string }>(path, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }) try { void this.track(Events.GetClientCipherText, { path }) } catch (error) { // Noop } return response.cipherText } public async getEnabledDapps(): Promise { const dapps = await this.requests.get(`/api/v1/config/dapps`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }) try { void this.track(Events.GetEnabledDapps, { path: '/api/v1/config/dapps' }) } catch (error) { // Noop } return dapps } public async getNetworks(): Promise { const path = '/api/v1/config/networks' const networks = await this.requests.get(path, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }) try { void this.track(Events.GetNetworks, { path }) } catch (error) { // Noop } return networks } public async getNFTs(chainId?: string, traceId?: string): Promise { const requestTraceId = traceId ?? generateTraceId() const path = '/api/v3/clients/me/nfts' chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const nfts = await this.requests.get(`${path}?chainId=${chainId}`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: requestTraceId, }) try { void this.track(Events.GetNFTs, { chainId, path, }) } catch (error) { // Noop } return nfts } /** * @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, traceId?: string, ): Promise { sdkLogger.warn( '[DEPRECATED] getTransactions() is deprecated. Please use getTransactionHistory() instead.', ) const requestTraceId = traceId ?? generateTraceId() const path = '/api/v3/clients/me/transactions' chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } // Start building the URI with query parameters let uri = `${path}?chainId=${chainId}` if (limit !== undefined) { uri += `&limit=${limit}` } if (offset !== undefined) { uri += `&offset=${offset}` } if (order !== undefined) { uri += `&order=${order}` } const transactions = await this.requests.get(uri, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: requestTraceId, }) try { void this.track(Events.GetTransactions, { chainId, limit, offset, order, path: uri, }) } catch (error) { // Noop } return transactions } /** * 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 */ public async getTransactionHistory( params: GetTransactionHistoryParams, traceId?: string, ): Promise { const requestTraceId = traceId ?? generateTraceId() const { chainId, limit, offset, order, address, userOperations: userOperationsParam, } = params if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const isEvmChain = chainId.startsWith('eip155:') let userOperations = userOperationsParam if (userOperations === undefined && isEvmChain) { try { const client = await this.getClient(requestTraceId) if (client?.isAccountAbstracted) { userOperations = 'only' } } catch { // Fallback: proceed with existing logic if client check fails } } const path = `/api/v3/clients/me/chains/${encodeURIComponent(chainId)}/transactions` const queryParams = new URLSearchParams() this.appendQueryParam(queryParams, 'limit', limit) this.appendQueryParam(queryParams, 'offset', offset) this.appendQueryParam(queryParams, 'order', order) this.appendQueryParam(queryParams, 'address', address) this.appendQueryParam(queryParams, 'userOperations', userOperations) const queryString = queryParams.toString() const uri = queryString ? `${path}?${queryString}` : path try { const response = await this.requests.get( uri, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: requestTraceId, }, ) try { void this.track(Events.GetTransactionHistory, { chainId, limit, offset, order, address, userOperations, path: '/api/v3/clients/me/chains/:chainId/transactions', }) } catch { // Noop } return response } catch (error) { if (error instanceof HttpError) { throw new Error(`Failed to fetch transaction history: ${error.message}`) } throw error } } public async getBalances( chainId?: string, traceId?: string, ): Promise { const requestTraceId = traceId ?? generateTraceId() const path = '/api/v3/clients/me/balances' chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const balances = await this.requests.get( `${path}?chainId=${chainId}`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: requestTraceId, }, ) try { void this.track(Events.GetBalances, { chainId, path, }) } catch (error) { // Noop } return balances } public async prepareClientEject( walletId: string, backupMethod: BackupMethodsUpperCase, ): Promise { const path = `/api/v3/clients/me/wallets/${walletId}/prepare-eject` const response = await this.requests.post<{ share: string }>(path, { body: { backupMethod, }, headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }) try { void this.track(Events.PrepareClientEject, { path, }) } catch (error) { // Noop } return response.share } public async simulateTransaction( transaction: SimulateTransactionParam, chainId?: string, ): Promise { const path = '/api/v3/clients/me/simulate-transaction' chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const simulatedTransaction = await this.requests.post( `${path}?chainId=${chainId}`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, body: { ...transaction, }, traceId: generateTraceId(), }, ) try { void this.track(Events.SimulateTransaction, { chainId, path, }) } catch (error) { // Noop } return simulatedTransaction } public async evaluateTransaction( transaction: EvaluateTransactionParam, chainId: string, operationType?: EvaluateTransactionOperationType, ): Promise { const path = '/api/v3/clients/me/evaluate-transaction' const encodedChainId = encodeURIComponent(chainId) // Build the request body const body: Record = { to: transaction.to, } // Add optional transaction fields if they exist if (transaction.value !== undefined) body.value = transaction.value if (transaction.data !== undefined) body.data = transaction.data if (transaction.maxFeePerGas !== undefined) body.maxFeePerGas = transaction.maxFeePerGas if (transaction.maxPriorityFeePerGas !== undefined) body.maxPriorityFeePerGas = transaction.maxPriorityFeePerGas if (transaction.gas !== undefined) body.gas = transaction.gas if (transaction.gasPrice !== undefined) body.gasPrice = transaction.gasPrice // Add operationType if provided if (operationType !== undefined) { body.operationType = operationType } const evaluatedTransaction = await this.requests.post( `${path}?chainId=${encodedChainId}`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, body, traceId: generateTraceId(), }, ) try { void this.track(Events.EvaluateTransaction, { chainId, path, }) } catch (error) { // Noop } return evaluatedTransaction } public async storeClientCipherText( backupSharePairId: string, cipherText: string, ): Promise { const path = `/api/v3/clients/me/backup-share-pairs/${backupSharePairId}` await this.requests.patch(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { clientCipherText: cipherText, }, traceId: generateTraceId(), }) try { void this.track(Events.StoreClientCipherText, { path, }) } catch (error) { // Noop } return true } /** * @deprecated Use portal.trading.zeroX.getQuote instead */ public async getQuote( apiKey: string, args: QuoteArgs, chainId?: string, ): Promise { let address try { const addresses = await this.provider.addresses if (!addresses) { throw new Error('[PortalApi] No addresses found') } address = addresses.eip155 } catch (error) { address = await this.provider.address if (!address) { throw new Error('[PortalApi] No address found') } } chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const quote = await this.requests.post( `/api/v1/swaps/quote`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, body: { ...args, address, apiKey, chainId, }, traceId: generateTraceId(), }, ) try { void this.track(Events.GetQuote, { address, chainId, path: `/api/v1/swaps/quote`, }) } catch (error) { // Noop } return quote } /** * @deprecated Use portal.trading.zeroX.getSources instead */ public async getSources( apiKey: string, chainId?: string, ): Promise> { const addresses = await this.provider.addresses const legacyAddress = await this.provider.address if (!addresses || !legacyAddress) { throw new Error('[PortalApi] No addresses found') } const address = addresses.eip155 ?? legacyAddress chainId = chainId ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const sources = await this.requests.post>( '/api/v1/swaps/sources', { headers: { Authorization: `Bearer ${this.apiKey}`, }, body: { address, apiKey, chainId, }, traceId: generateTraceId(), }, ) try { void this.track(Events.GetSources, { address, chainId, path: `/api/v1/swaps/sources`, }) } catch (error) { // Noop } return sources } public async storedClientSigningShares( signingSharePairIds: string[], ): Promise { const path = '/api/v3/clients/me/signing-share-pairs' await this.requests.patch(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { signingSharePairIds: signingSharePairIds, status: 'STORED_CLIENT', }, traceId: generateTraceId(), }) try { void this.track(Events.StoredClientSigningShare, { path, }) } catch (error) { // Noop } } public async storedClientBackupShares( backupSharePairIds: string[], ): Promise { const path = '/api/v3/clients/me/backup-share-pairs' await this.requests.patch(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { backupSharePairIds, status: 'STORED_CLIENT_BACKUP_SHARE', }, traceId: generateTraceId(), }) try { void this.track(Events.StoredClientBackupShare, { path, }) } catch (error) { // Noop } } public async storedClientBackupSharesKey( backupSharePairIds: string[], ): Promise { const path = '/api/v3/clients/me/backup-share-pairs' await this.requests.patch(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { backupSharePairIds, status: 'STORED_CLIENT_BACKUP_SHARE_KEY', }, traceId: generateTraceId(), }) try { void this.track(Events.StoredClientBackupShareKey, { path, }) } catch (error) { // Noop } } public async storedClientBackupShare( success: boolean, backupMethod: | 'CUSTOM' | 'GDRIVE' | 'ICLOUD' | 'PASSKEY' | 'PASSWORD' | 'FIREBASE' | 'UNKNOWN' = 'UNKNOWN', ): Promise { // Build the request body. const body: { backupMethod: | 'CUSTOM' | 'GDRIVE' | 'ICLOUD' | 'PASSKEY' | 'PASSWORD' | 'FIREBASE' | 'UNKNOWN' success: boolean isMultiBackupEnabled?: boolean } = { backupMethod, success, } // Conditionally add feature flag isMultiBackupEnabled to the request body. if (typeof this.featureFlags?.isMultiBackupEnabled === 'boolean') { body.isMultiBackupEnabled = this.featureFlags.isMultiBackupEnabled } // Make the request. await this.requests.put( '/api/v2/clients/me/wallet/stored-client-backup-share', { headers: { Authorization: `Bearer ${this.apiKey}`, }, body, traceId: generateTraceId(), }, ) // Track the event. try { void this.track(Events.StoredClientBackupShare, { backupMethod, isMultiBackupEnabled: body.isMultiBackupEnabled, path: '/api/v2/clients/me/wallet/stored-client-backup-share', success, }) } catch (error) { // Noop } } public async storedClientBackupShareKey( success: boolean, backupMethod: | 'CUSTOM' | 'GDRIVE' | 'ICLOUD' | 'FIREBASE' | 'PASSKEY' | 'PASSWORD' | 'UNKNOWN' = 'UNKNOWN', ): Promise { // Build the request body. const body: { backupMethod: | 'CUSTOM' | 'GDRIVE' | 'ICLOUD' | 'FIREBASE' | 'PASSKEY' | 'PASSWORD' | 'UNKNOWN' success: boolean isMultiBackupEnabled?: boolean } = { backupMethod, success, } // Conditionally add feature flag isMultiBackupEnabled to the request body. if (typeof this.featureFlags?.isMultiBackupEnabled === 'boolean') { body.isMultiBackupEnabled = this.featureFlags.isMultiBackupEnabled } // Make the request. await this.requests.put( '/api/v2/clients/me/wallet/stored-client-backup-share-key', { headers: { Authorization: `Bearer ${this.apiKey}`, }, body, traceId: generateTraceId(), }, ) // Track the event. try { void this.track(Events.StoredClientBackupShareKey, { backupMethod, isMultiBackupEnabled: body.isMultiBackupEnabled, path: '/api/v2/clients/me/wallet/stored-client-backup-share-key', success, }) } catch (error) { // Noop } } public async getSigningSharesMetadata( chainId?: string, ): Promise { const client = await this.getClient() if (!client) { throw new Error('[PortalApi] Client Not Available') } if (chainId) { const namespace = chainId.split(':')[0] const curve = namespace === 'eip155' ? PortalCurve.SECP256K1 : PortalCurve.ED25519 const wallet = client.wallets.find((wallet) => wallet.curve === curve) if (!wallet) { throw new Error(`[PortalApi] No Wallet Found For ChainId: ${chainId}`) } return await this.requests.get( `/api/v3/clients/me/wallets/${wallet.id}/signing-share-pairs`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }, ) } const walletIds = client.wallets.map((wallet) => wallet.id) const sharePairGroups: SigningSharePairMetadata[][] = [] await Promise.all( walletIds.map(async (id) => { const sharePairs = await this.requests.get( `/api/v3/clients/me/wallets/${id}/signing-share-pairs`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }, ) sharePairGroups.push(sharePairs) }), ) return sharePairGroups.reduce((acc, val) => acc.concat(val), []) } public async getBackupSharesMetadata( chainId?: string, ): Promise { const client = await this.getClient() if (!client) { throw new Error('[PortalApi] Client Not Available') } if (chainId) { const namespace = chainId.split(':')[0] const curve = namespace === 'eip155' ? PortalCurve.SECP256K1 : PortalCurve.ED25519 const wallet = client.wallets.find((wallet) => wallet.curve === curve) if (!wallet) { throw new Error(`[PortalApi] No Wallet Found For ChainId: ${chainId}`) } return await this.requests.get( `/api/v3/clients/me/wallets/${wallet.id}/backup-share-pairs`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }, ) } const walletIds = client.wallets.map((wallet) => wallet.id) const sharePairGroups: BackupSharePairMetadata[][] = [] await Promise.all( walletIds.map(async (id) => { const sharePairs = await this.requests.get( `/api/v3/clients/me/wallets/${id}/backup-share-pairs`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, traceId: generateTraceId(), }, ) sharePairGroups.push(sharePairs) }), ) return sharePairGroups.reduce((acc, val) => acc.concat(val), []) } public async ejectClient(): Promise { const clientPlatform = 'REACT_NATIVE' const clientPlatformVersion = getClientPlatformVersion() const path = '/api/v3/clients/me/eject' await this.requests.post(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { clientPlatform, clientPlatformVersion, }, traceId: generateTraceId(), }) try { void this.track(Events.EjectClient, { path, }) } catch (error) { // Noop } } public async buildTransaction( to: string, token: string, amount: string, chain?: string, traceId?: string, ): Promise { const requestTraceId = traceId ?? generateTraceId() const chainId = chain ?? this.chainId if (!chainId) { throw new Error('[PortalApi] ChainId not found. ChainId is required.') } const path = `/api/v3/clients/me/chains/${chainId}/assets/send/build-transaction` const transaction = await this.requests.post< BuildEip155TransactionResponse | BuildSolanaTransactionResponse >(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { to, token, amount, }, traceId: requestTraceId, }) return transaction } public async identify( traits: Record = {}, ): Promise<{ success: boolean }> { const { success } = await this.requests.post<{ success: boolean }>( `/api/v1/analytics/identify`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, body: { traits, }, traceId: generateTraceId(), }, ) return { success } } public async track( event: string, properties: Record = {}, ): Promise<{ success: boolean }> { const { success } = await this.requests.post<{ success: boolean }>( `/api/v1/analytics/track`, { headers: { Authorization: `Bearer ${this.apiKey}`, }, body: { event, properties, }, traceId: generateTraceId(), }, ) return { success } } public async fund( chainId: string, params: FundParams, ): Promise { const path = '/api/v3/clients/me/fund' const response = await this.requests.post(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { amount: params.amount, chainId, token: params.token, }, traceId: generateTraceId(), }) try { void this.track(Events.FundClient, { chainId, path, }) } catch (error) { // Noop } return response } public async getYields( request?: YieldXyzGetYieldsRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/yields' const queryParams = new URLSearchParams() this.appendQueryParam(queryParams, 'offset', request?.offset) this.appendQueryParam(queryParams, 'limit', request?.limit) this.appendQueryParam(queryParams, 'network', request?.network) this.appendQueryParam(queryParams, 'yieldId', request?.yieldId) this.appendQueryParam(queryParams, 'type', request?.type) this.appendQueryParam( queryParams, 'hasCooldownPeriod', request?.hasCooldownPeriod, ) this.appendQueryParam( queryParams, 'hasWarmupPeriod', request?.hasWarmupPeriod, ) this.appendQueryParam(queryParams, 'token', request?.token) this.appendQueryParam(queryParams, 'inputToken', request?.inputToken) this.appendQueryParam(queryParams, 'provider', request?.provider) this.appendQueryParam(queryParams, 'search', request?.search) this.appendQueryParam(queryParams, 'sort', request?.sort) const queryString = queryParams.toString() const fullPath = queryString ? `${path}?${queryString}` : path const response = await this.requests.get( fullPath, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzDiscover, { path: fullPath, network: request?.network, limit: request?.limit, offset: request?.offset, }) } catch (error) { // Noop } return response } public async enterYield( request: YieldXyzEnterRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/actions/enter' let address = request.address if (!address) { const addresses = await this.provider.addresses address = addresses?.eip155 if (!address) { address = await this.provider.address } if (!address) { throw new Error('[PortalApi] No address found for yield enter action') } } const response = await this.requests.post( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { address, yieldId: request.yieldId, arguments: { amount: request.amount, }, }, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzEnter, { path, yieldId: request.yieldId, amount: request.amount, }) } catch (error) { // Noop } return response } public async exitYield( request: YieldXyzExitRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/actions/exit' let address = request.address if (!address) { const addresses = await this.provider.addresses address = addresses?.eip155 if (!address) { address = await this.provider.address } if (!address) { throw new Error('[PortalApi] No address found for yield exit action') } } const response = await this.requests.post(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { address, yieldId: request.yieldId, arguments: { amount: request.amount, }, }, traceId: generateTraceId(), }) try { void this.track(Events.YieldXyzExit, { path, yieldId: request.yieldId, amount: request.amount, }) } catch (error) { // Noop } return response } public async manageYield( request: YieldXyzManageYieldRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/actions/manage' let address = request.address if (!address) { const addresses = await this.provider.addresses address = addresses?.eip155 if (!address) { address = await this.provider.address } if (!address) { throw new Error('[PortalApi] No address found for yield manage action') } } let passthrough: string if (request.passthrough && typeof request.passthrough === 'string') { passthrough = request.passthrough.trim() } else { const passthroughData = { addresses: { address }, args: request.arguments || {}, } const passthroughJson = JSON.stringify(passthroughData) passthrough = btoa(passthroughJson) } const body = { action: request.action, yieldId: request.yieldId, address, passthrough, } const response = await this.requests.post( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzManage, { path, yieldId: request.yieldId, action: request.action, }) } catch { // Noop } return response } public async getYieldBalances( request: YieldXyzGetBalancesRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/yields/balances' const response = await this.requests.post( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: request, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzGetBalances, { path, queries: request.queries, }) } catch (error) { // Noop } return response } public async getHistoricalYieldActions( request: YieldXyzGetHistoricalActionsRequest, ): Promise { const path = '/api/v3/clients/me/integrations/yield-xyz/actions' const queryParams = new URLSearchParams() this.appendQueryParam(queryParams, 'address', request.address) this.appendQueryParam(queryParams, 'offset', request.offset) this.appendQueryParam(queryParams, 'limit', request.limit) this.appendQueryParam(queryParams, 'status', request.status) this.appendQueryParam(queryParams, 'intent', request.intent) this.appendQueryParam(queryParams, 'type', request.type) this.appendQueryParam(queryParams, 'yieldId', request.yieldId) const queryString = queryParams.toString() const fullPath = queryString ? `${path}?${queryString}` : path const response = await this.requests.get(fullPath, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }) try { void this.track(Events.YieldXyzGetHistoricalActions, { path: fullPath, address: request.address, yieldId: request.yieldId, limit: request.limit, offset: request.offset, }) } catch (error) { // Noop } return response } public async getYieldTransaction( transactionId: string, ): Promise { const path = `/api/v3/clients/me/integrations/yield-xyz/transactions/${transactionId}` const response = await this.requests.get( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzGetTransaction, { path, transactionId, }) } catch (error) { // Noop } return response } public async submitTransactionHash( request: YieldXyzTrackTransactionRequest, ): Promise { const path = `/api/v3/clients/me/integrations/yield-xyz/transactions/${request.transactionId}/submit-hash` const response = await this.requests.put( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: request, traceId: generateTraceId(), }, ) try { void this.track(Events.YieldXyzTrack, { path, transactionId: request.transactionId, }) } catch (error) { // Noop } return response } public async getYieldDefaults( request?: YieldXyzGetYieldDefaultsRequest, ): Promise { const pathBase = '/api/v3/clients/me/integrations/yield-xyz/yields/defaults' const queryParams = new URLSearchParams() this.appendQueryParam( queryParams, 'includeOpportunities', request?.includeOpportunities, ) const queryString = queryParams.toString() const fullPath = queryString ? `${pathBase}?${queryString}` : pathBase return this.requests.get(fullPath, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }) } public async getYieldValidators( yieldId: string, ): Promise { const path = `/api/v3/clients/me/integrations/yield-xyz/yields/${encodeURIComponent(yieldId)}/validators` return this.requests.get(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }) } private appendQueryParam( params: URLSearchParams, key: string, value: unknown, ): void { if (value !== undefined && value !== null) { params.append(key, String(value)) } } public async getAssets(chainId: string): Promise { const path = `/api/v3/clients/me/chains/${chainId}/assets` const response = await this.requests.get(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', }, traceId: generateTraceId(), }) try { void this.track(Events.GetAssets, { chainId, path, }) } catch { // Noop } return response } public async buildBitcoinP2wpkhTransaction( chainId: string, params: { to: string; token: string; amount: string }, ): Promise { if (!chainId.startsWith(`${CHAIN_NAMESPACES.BIP122}:`)) { throw new Error( `[PortalApi] Invalid chainId: "${chainId}". ChainId must start with "${CHAIN_NAMESPACES.BIP122}:" for Bitcoin transactions.`, ) } const path = `/api/v3/clients/me/chains/${chainId}/assets/send/build-transaction` const response = await this.requests.post(path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { to: params.to, token: params.token, amount: params.amount, }, traceId: generateTraceId(), }) return response } public async broadcastBitcoinP2wpkhTransaction( chainId: string, params: BroadcastParam, ): Promise { if (!chainId.startsWith(`${CHAIN_NAMESPACES.BIP122}:`)) { throw new Error( `[PortalApi] Invalid chainId: "${chainId}". ChainId must start with "${CHAIN_NAMESPACES.BIP122}:" for Bitcoin transactions.`, ) } const path = `/api/v3/clients/me/chains/${chainId}/assets/send/broadcast-transaction` const response = await this.requests.post( path, { headers: { Authorization: `Bearer ${this.apiKey}`, Accept: 'application/json', 'Content-Type': 'application/json', }, body: { signatures: params.signatures, rawTxHex: params.rawTxHex, }, traceId: generateTraceId(), }, ) return response } public async rawSign( message: string, chainId?: string, options?: RawSignOptions, ): Promise { return this.provider.request({ method: PortalRequestMethod.RawSign, params: [message], chainId, options, }) } } export default PortalApi export { BackupMethods } from '../mpc'