import Mpc from '../../mpc' import type { ApproveDelegationRequest, ApproveDelegationResponse, RevokeDelegationRequest, RevokeDelegationResponse, GetDelegationStatusRequest, DelegationStatusResponse, TransferFromRequest, TransferFromResponse, DelegationSubmitProgress, } from '../../shared/types' export interface IPortalDelegationsApi { approve(params: ApproveDelegationRequest): Promise revoke(params: RevokeDelegationRequest): Promise getStatus( params: GetDelegationStatusRequest, ): Promise transferFrom(params: TransferFromRequest): Promise } const DELEGATIONS_SUBMIT_CONFIG_ERROR = '[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.' /** Optional callback for sign-and-submit flows. Invoked once per transaction. */ export interface DelegationSubmitOptions { signAndSendTransaction?: ( transaction: unknown, chainId: string, ) => Promise onProgress?: (event: DelegationSubmitProgress) => void } /** * Full delegations surface exposed on {@link Portal.delegations}, including sign-and-submit helpers. */ export interface IDelegations extends IPortalDelegationsApi { /** Sets the default signer used by approveAndSubmit, revokeAndSubmit, and transferAndSubmit when options omit signAndSendTransaction. */ setSignAndSendTransaction( fn: (transaction: unknown, chainId: string) => Promise, ): void approveAndSubmit( params: ApproveDelegationRequest, options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> revokeAndSubmit( params: RevokeDelegationRequest, options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> transferAndSubmit( params: TransferFromRequest, options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> } export interface DelegationsOptions { mpc: Mpc /** * When set, enables {@link Delegations.approveAndSubmit}, {@link Delegations.revokeAndSubmit}, * and {@link Delegations.transferAndSubmit}. `Portal` passes this by default using the iframe provider. */ signAndSendTransaction?: ( transaction: unknown, chainId: string, ) => Promise } export default class Delegations implements IDelegations { private readonly mpc: Mpc private signAndSendTransaction?: ( transaction: unknown, chainId: string, ) => Promise constructor(options: DelegationsOptions) { this.mpc = options.mpc this.signAndSendTransaction = options.signAndSendTransaction } public setSignAndSendTransaction( fn: (transaction: unknown, chainId: string) => Promise, ): void { this.signAndSendTransaction = fn } /** * Approves a delegation for a specified token on a given chain. * Returns transactions for EVM chains or encodedTransactions for Solana chains. * @param params - Delegation approval parameters * @returns Promise resolving to approval response with transaction data */ public async approve( params: ApproveDelegationRequest, ): Promise { return this.mpc?.delegationsApprove(params) } /** * Revokes a delegation for a specified token on a given chain. * Returns transactions for EVM chains or encodedTransactions for Solana chains. * @param params - Delegation revocation parameters * @returns Promise resolving to revocation response with transaction data */ public async revoke( params: RevokeDelegationRequest, ): Promise { return this.mpc?.delegationsRevoke(params) } /** * Retrieves the delegation status for a specified token and delegate address. * @param params - Delegation status query parameters * @returns Promise resolving to delegation status information */ public async getStatus( params: GetDelegationStatusRequest, ): Promise { return this.mpc?.delegationsGetStatus(params) } /** * Transfers tokens from one address to another using delegated authority. * The caller must have been previously approved as a delegate. * Returns transactions for EVM chains or encodedTransactions for Solana chains. * @param params - Transfer from delegation parameters * @returns Promise resolving to transfer response with transaction data */ public async transferFrom( params: TransferFromRequest, ): Promise { return this.mpc?.delegationsTransferFrom(params) } /** * Approves a delegation, then signs and broadcasts each returned transaction via * {@link DelegationsOptions.signAndSendTransaction}. * * Requires `signAndSendTransaction` (provided by default on `Portal.delegations`). * Confirming transactions on-chain remains the application's responsibility. * * @param params - Same as {@link Delegations.approve} * @param options - Optional progress callbacks * @returns Transaction hashes in submission order */ public async approveAndSubmit( params: Parameters[0], options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> { const signAndSend = options?.signAndSendTransaction ?? this.signAndSendTransaction if (!signAndSend) { throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR) } const response = await this.approve(params) return this.executeAndTrack( response, params.chain, signAndSend, options?.onProgress, ) } /** * Revokes a delegation, then signs and broadcasts each returned transaction. * * Requires `signAndSendTransaction` (provided by default on `Portal.delegations`). * * @param params - Same as {@link Delegations.revoke} * @param options - Optional progress callbacks * @returns Transaction hashes in submission order */ public async revokeAndSubmit( params: Parameters[0], options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> { const signAndSend = options?.signAndSendTransaction ?? this.signAndSendTransaction if (!signAndSend) { throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR) } const response = await this.revoke(params) return this.executeAndTrack( response, params.chain, signAndSend, options?.onProgress, ) } /** * Executes a delegated transfer, then signs and broadcasts each returned transaction. * * Requires `signAndSendTransaction` (provided by default on `Portal.delegations`). * * @param params - Same as {@link Delegations.transferFrom} * @param options - Optional progress callbacks * @returns Transaction hashes in submission order */ public async transferAndSubmit( params: Parameters[0], options?: DelegationSubmitOptions, ): Promise<{ hashes: string[] }> { const signAndSend = options?.signAndSendTransaction ?? this.signAndSendTransaction if (!signAndSend) { throw new Error(DELEGATIONS_SUBMIT_CONFIG_ERROR) } const response = await this.transferFrom(params) return this.executeAndTrack( response, params.chain, signAndSend, options?.onProgress, ) } private normalizeToTransactionList( response: | ApproveDelegationResponse | RevokeDelegationResponse | TransferFromResponse, ): unknown[] { const transactions = Array.isArray(response.transactions) ? response.transactions : [] const encodedTransactions = Array.isArray(response.encodedTransactions) ? response.encodedTransactions : [] if (transactions.length > 0) { return transactions } if (encodedTransactions.length > 0) { return encodedTransactions } return [] } private async executeAndTrack( response: | ApproveDelegationResponse | RevokeDelegationResponse | TransferFromResponse, chainId: string, signAndSend: (transaction: unknown, chainId: string) => Promise, onProgress?: (event: DelegationSubmitProgress) => void, ): Promise<{ hashes: string[] }> { const transactions = this.normalizeToTransactionList(response) if (transactions.length === 0) { throw new Error('No transactions in delegation response.') } const total = transactions.length const hashes: string[] = [] for (let index = 0; index < transactions.length; index++) { const tx = transactions[index] // Validate transaction exists (defensive check for sparse arrays) if (!tx) { throw new Error( `Transaction at index ${index} is undefined or null. This indicates a malformed API response.`, ) } onProgress?.({ step: 'signing', index, total }) const hash = await signAndSend(tx, chainId) if (typeof hash !== 'string' || hash.trim().length === 0) { throw new Error( `Invalid transaction hash returned from signAndSendTransaction at index ${index} for chain ${chainId}.`, ) } hashes.push(hash) onProgress?.({ step: 'submitted', index, total, hash }) } return { hashes } } }