import { sdkLogger } from '@portal-hq/utils' import { pollLoop, PollLoopTimeoutError, type PollTickResult, } from './pollLoop' import type { EvmRequestFn } from './waitForEvmTxConfirmation' const LOG_PREFIX = '[Portal.waitForConfirmation]' type ConfirmationMode = 'auto' | 'tx' | 'userOp' type ReceiptState = 'not_found' | 'pending' | 'success' | 'failed' interface EvmTxReceiptLike { blockNumber?: string | null status?: string | number | null } interface UserOpReceiptLike { success?: boolean | null receipt?: { blockNumber?: string | null status?: string | number | null } | null } function parseStatusToOutcome(status: unknown): 'success' | 'failed' | 'unknown' { if (typeof status === 'boolean') return status ? 'success' : 'failed' if (typeof status === 'number') return status === 1 ? 'success' : 'failed' if (typeof status === 'string') { const normalized = status.trim().toLowerCase() if (normalized === '0x1' || normalized === '1') return 'success' if (normalized === '0x0' || normalized === '0') return 'failed' } return 'unknown' } function parseTxReceiptState(raw: unknown): ReceiptState { if (raw == null || typeof raw !== 'object') return 'not_found' const receipt = raw as EvmTxReceiptLike if (receipt.blockNumber == null) return 'pending' const outcome = parseStatusToOutcome(receipt.status) if (outcome === 'success') return 'success' if (outcome === 'failed') return 'failed' return 'pending' } function parseUserOpReceiptState(raw: unknown): ReceiptState { if (raw == null || typeof raw !== 'object') return 'not_found' const receipt = raw as UserOpReceiptLike if (typeof receipt.success === 'boolean') { return receipt.success ? 'success' : 'failed' } const nested = receipt.receipt if (nested == null) return 'pending' if (nested.blockNumber == null) return 'pending' const outcome = parseStatusToOutcome(nested.status) if (outcome === 'success') return 'success' if (outcome === 'failed') return 'failed' return 'pending' } function isMethodNotSupportedError(error: unknown): boolean { const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase() return ( message.includes('method not found') || message.includes('does not exist/is not available') || message.includes('-32601') || message.includes('eth_getuseroperationreceipt') ) } export interface WaitForEvmOrUserOpConfirmationOptions { pollIntervalMs?: number timeoutMs?: number onTimeout?: 'resolve_false' | 'throw' lockModeAfterDetection?: boolean } export async function waitForEvmOrUserOpConfirmation( hash: string, network: string, request: EvmRequestFn, options: WaitForEvmOrUserOpConfirmationOptions = {}, ): Promise { const { pollIntervalMs = 4_000, timeoutMs = 300_000, onTimeout = 'resolve_false', lockModeAfterDetection = true, } = options let mode: ConfirmationMode = 'auto' let userOpMethodSupported = true sdkLogger.debug(`${LOG_PREFIX} waiting for tx/userOp confirmation`, { hash, network, pollIntervalMs, timeoutMs, mode, }) const tick = async (): Promise> => { if (mode !== 'userOp') { try { const txRaw = await request('eth_getTransactionReceipt', [hash], network) const txState = parseTxReceiptState(txRaw) if (txState === 'success') return { kind: 'stop', value: true } if (txState === 'failed') return { kind: 'stop', value: false } if (lockModeAfterDetection && mode === 'auto' && txState === 'pending') { mode = 'tx' sdkLogger.debug(`${LOG_PREFIX} mode locked`, { hash, network, mode }) } } catch (error) { sdkLogger.warn(`${LOG_PREFIX} tx receipt poll transient error`, { hash, network, error: error instanceof Error ? error.message : String(error), }) } } if (mode !== 'tx' && userOpMethodSupported) { try { const userOpRaw = await request( 'eth_getUserOperationReceipt', [hash], network, ) const userOpState = parseUserOpReceiptState(userOpRaw) if (userOpState === 'success') return { kind: 'stop', value: true } if (userOpState === 'failed') return { kind: 'stop', value: false } if ( lockModeAfterDetection && mode === 'auto' && userOpState === 'pending' ) { mode = 'userOp' sdkLogger.debug(`${LOG_PREFIX} mode locked`, { hash, network, mode }) } } catch (error) { if (isMethodNotSupportedError(error)) { userOpMethodSupported = false sdkLogger.debug( `${LOG_PREFIX} eth_getUserOperationReceipt unsupported; disabling userOp polling`, { hash, network }, ) } else { sdkLogger.warn(`${LOG_PREFIX} userOp receipt poll transient error`, { hash, network, error: error instanceof Error ? error.message : String(error), }) } } } return { kind: 'continue' } } try { return await pollLoop({ tick, intervalMs: pollIntervalMs, initialDelayMs: 0, timeoutMs, }) } catch (error) { if (error instanceof PollLoopTimeoutError) { const msg = `${LOG_PREFIX} timeout after ${timeoutMs}ms waiting for confirmation on ${hash} (${network}).` if (onTimeout === 'throw') { throw new Error(`${msg} Confirmation not reached.`) } sdkLogger.warn(`${msg} Returning false (resolve_false).`) return false } throw error } }