const LOG_PREFIX = '[Portal.waitForConfirmation]' export type SolanaConfirmationCommitment = | 'processed' | 'confirmed' | 'finalized' export interface WaitForSolanaTxConfirmationOptions { pollIntervalMs: number timeoutMs: number commitment: SolanaConfirmationCommitment /** Optional headers (e.g. `Authorization` for Portal-hosted RPC). */ headers?: Record } const COMMITMENT_ORDER: Record = { processed: 0, confirmed: 1, finalized: 2, } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)) } function statusMeetsCommitment( confirmationStatus: string | undefined, required: SolanaConfirmationCommitment, ): boolean { if (!confirmationStatus) return false const level = COMMITMENT_ORDER[confirmationStatus as SolanaConfirmationCommitment] const need = COMMITMENT_ORDER[required] if (level === undefined || need === undefined) return false return level >= need } interface SignatureStatusRow { err?: unknown confirmationStatus?: string } function parseSignatureStatusesResult(raw: unknown): SignatureStatusRow | null { if (raw == null || typeof raw !== 'object') return null const row = raw as { value?: unknown } const arr = row.value if (!Array.isArray(arr) || arr.length === 0) return null const first = arr[0] if (first == null || typeof first !== 'object') return null return first as SignatureStatusRow } /** * Polls Solana JSON-RPC `getSignatureStatuses` until the signature reaches the * requested commitment, fails on-chain, or times out. */ export async function waitForSolanaTxConfirmation( signature: string, rpcUrl: string, options: WaitForSolanaTxConfirmationOptions, ): Promise { const { pollIntervalMs, timeoutMs, commitment, headers: extraHeaders = {}, } = options const deadline = Date.now() + timeoutMs const headers: Record = { 'Content-Type': 'application/json', ...extraHeaders, } while (Date.now() < deadline) { const res = await fetch(rpcUrl, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getSignatureStatuses', params: [[signature], { searchTransactionHistory: true }], }), }) if (!res.ok) { const text = await res.text() throw new Error( `${LOG_PREFIX} HTTP ${res.status} ${res.statusText} for getSignatureStatuses: ` + text.slice(0, 200), ) } const json = (await res.json()) as { result?: unknown error?: unknown } if (json.error) { throw new Error( `${LOG_PREFIX} RPC error for getSignatureStatuses: ${JSON.stringify(json.error)}`, ) } const statusRow = parseSignatureStatusesResult(json.result) if (statusRow != null) { if (statusRow.err != null) { return false } if (statusMeetsCommitment(statusRow.confirmationStatus, commitment)) { return true } } await sleep(pollIntervalMs) } return false }