import { CAIP2_CHAIN_ID_REGEX, CHAIN_NAMESPACES, FRIENDLY_CHAIN_NAME_TO_CAIP2, PortalRequestMethod, generateTraceId, sdkLogger, } from '@portal-hq/utils' import type { IPortalApi, IPortalProvider } from '@portal-hq/utils' import type { SendAssetParams } from '@portal-hq/utils' import type { AssetsResponse, BroadcastBitcoinP2wpkhTransactionResponse, BroadcastParam, BuildBitcoinP2wpkhTransactionResponse, FundParams, FundResponse, SendAssetResponse, } from '../../types' import type { ChainHandler } from './chains' import { BitcoinP2wpkhChainHandler, EvmChainHandler, SolanaChainHandler, } from './chains' import type { AssetManagerDependencies, IAssetManager } from './types' export type { IAssetManager, AssetManagerDependencies } export { EvmChainHandler, SolanaChainHandler, BitcoinP2wpkhChainHandler } export type { ChainHandler, SendOptions } from './chains' export class AssetManager implements IAssetManager { private readonly api: IPortalApi private readonly provider: IPortalProvider private readonly getAddress: () => Promise private readonly getChainId: () => number private readonly chainHandlers: Map public readonly bitcoin: BitcoinP2wpkhChainHandler constructor({ api, provider, getAddress, rawSign, getChainId, }: AssetManagerDependencies) { this.api = api this.provider = provider this.getAddress = getAddress this.getChainId = getChainId const evmHandler = new EvmChainHandler(api, provider) const solanaHandler = new SolanaChainHandler(api, provider) this.bitcoin = new BitcoinP2wpkhChainHandler(api, rawSign) this.chainHandlers = new Map([ [evmHandler.namespace, evmHandler], [solanaHandler.namespace, solanaHandler], [this.bitcoin.namespace, this.bitcoin], ]) } public async send( params: SendAssetParams, chain?: string, ): Promise { const { to, token, amount, sponsorGas, signatureApprovalMemo } = params if (!to || !token || !amount) { throw new Error( '[Portal] Missing required parameters: to, token, or amount', ) } const chainId = this.convertChainToChainId(chain) const chainNamespace = chainId.split(':')[0] ?? '' const handler = this.chainHandlers.get(chainNamespace) if (!handler) { throw new Error( `[Portal] Unsupported chain namespace: "${chainNamespace}". ` + `Please provide a supported "chain" to "sendAsset". ` + `Supported namespaces: eip155 (EVM), solana, bip122 (Bitcoin P2WPKH).`, ) } const options = { ...(sponsorGas !== undefined && { sponsorGas }), ...(signatureApprovalMemo !== undefined && { signatureApprovalMemo }), traceId: params.traceId ?? generateTraceId(), } sdkLogger.debug( '[Portal] sendAsset request (SDK/signing traceId; build/broadcast HTTP calls use their own X-Portal-Trace-Id)', { traceId: options.traceId }, ) return handler.send(params, chainId, options) } public async buildBitcoinP2wpkhTransaction( params: { to: string; token: string; amount: string }, chainId: string, ): Promise { return this.bitcoin.buildTransaction(params, chainId) } public async broadcastBitcoinP2wpkhTransaction( params: BroadcastParam, chainId: string, ): Promise { return this.bitcoin.broadcastTransaction(params, chainId) } public async getAssets(chainId: string): Promise { return await this.api.getAssets(chainId) } public async receiveTestnetAsset( chainId: string, params: FundParams, ): Promise { return await this.api.fund(chainId, params) } public async getBalanceAsNumber(chainId: string): Promise { const address = await this.getAddress() const hexBalance = await this.provider.request({ method: PortalRequestMethod.EthGetBalance, params: [address], chainId, }) return this.convertHexToBalance(hexBalance as string) } private convertHexToBalance(hexBalance: string): number { const balance: number = Math.round((Number(hexBalance) / 1e18 + Number.EPSILON) * 1000000) / 1000000 return balance } private convertChainToChainId(chain?: string): string { if (!chain) { const numericChainId = this.getChainId() if (!numericChainId) { throw new Error( 'Chain ID is not set and you did not provide "chain" in the function call. Please provide a chain ID or friendly chain name.', ) } return `${CHAIN_NAMESPACES.EIP155}:${numericChainId}` } if (CAIP2_CHAIN_ID_REGEX.test(chain)) { return chain } const lowerCaseChain = chain.toLowerCase() as keyof typeof FRIENDLY_CHAIN_NAME_TO_CAIP2 const chainId = FRIENDLY_CHAIN_NAME_TO_CAIP2[lowerCaseChain] if (!chainId) { throw new Error( `Unsupported chain: "${chain}". Please provide a supported chain, such as ${Object.keys( FRIENDLY_CHAIN_NAME_TO_CAIP2, ).join(', ')}`, ) } return chainId } }