UNPKG

@base-org/account

Version:
226 lines 9.88 kB
import { decodeEventLog, formatUnits, getAddress, isAddressEqual } from 'viem'; import { logPaymentStatusCheckCompleted, logPaymentStatusCheckError, logPaymentStatusCheckStarted, } from '../../core/telemetry/events/payment.js'; import { ERC20_TRANSFER_ABI, TOKENS } from './constants.js'; /** * Check the status of a payment transaction using its transaction ID (userOp hash) * * @param options - Payment status check options * @returns Promise<PaymentStatus> - Status information about the payment * @throws Error if unable to connect to the RPC endpoint or if the RPC request fails * * @example * ```typescript * try { * const status = await getPaymentStatus({ * id: "0x1234...5678", * testnet: true * }) * * if (status.status === 'failed') { * console.log(`Payment failed: ${status.reason}`) * } * } catch (error) { * console.error('Unable to check payment status:', error.message) * } * ``` * * @note The id is the userOp hash returned from the pay function */ export async function getPaymentStatus(options) { const { id, testnet = false, telemetry = true } = options; // Generate correlation ID for this status check const correlationId = crypto.randomUUID(); // Log status check started if (telemetry) { logPaymentStatusCheckStarted({ testnet, correlationId }); } try { // Get the bundler URL based on network const bundlerUrl = testnet ? 'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O' : 'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O'; // Call eth_getUserOperationReceipt via the bundler const receipt = await fetch(bundlerUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getUserOperationReceipt', params: [id], }), }).then((res) => res.json()); // Handle RPC errors if (receipt.error) { console.error('[getPaymentStatus] RPC error:', receipt.error); const errorMessage = receipt.error.message || 'Network error'; if (telemetry) { logPaymentStatusCheckError({ testnet, correlationId, errorMessage }); } // Re-throw error for RPC failures throw new Error(`RPC error: ${errorMessage}`); } // If no result, payment is still pending or not found if (!receipt.result) { // Try eth_getUserOperationByHash to see if it's in mempool const userOpResponse = await fetch(bundlerUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_getUserOperationByHash', params: [id], }), }).then((res) => res.json()); if (userOpResponse.result) { // UserOp exists but no receipt yet - it's pending if (telemetry) { logPaymentStatusCheckCompleted({ testnet, status: 'pending', correlationId }); } const result = { status: 'pending', id: id, message: 'Your payment is being processed. This usually takes a few seconds.', sender: userOpResponse.result.sender, }; return result; } // Not found at all if (telemetry) { logPaymentStatusCheckCompleted({ testnet, status: 'not_found', correlationId }); } const result = { status: 'not_found', id: id, message: 'Payment not found. Please check your transaction ID.', }; return result; } // Parse the receipt const { success, receipt: txReceipt, reason } = receipt.result; // Determine status based on success flag if (success) { // Parse USDC amount from logs let amount; let recipient; if (txReceipt?.logs) { const network = testnet ? 'baseSepolia' : 'base'; const usdcAddress = TOKENS.USDC.addresses[network].toLowerCase(); // Normalize sender address for comparison const senderAddress = receipt.result.sender ? getAddress(receipt.result.sender) : undefined; // Collect all USDC transfers const usdcTransfers = []; for (let i = 0; i < txReceipt.logs.length; i++) { const log = txReceipt.logs[i]; // Check if this is a USDC log const logAddressLower = log.address?.toLowerCase(); const isUsdcLog = logAddressLower === usdcAddress; if (isUsdcLog) { try { const decoded = decodeEventLog({ abi: ERC20_TRANSFER_ABI, data: log.data, topics: log.topics, }); if (decoded.eventName === 'Transfer' && decoded.args) { const args = decoded.args; if (args.value && args.to && args.from) { const formattedAmount = formatUnits(args.value, 6); usdcTransfers.push({ from: args.from, to: args.to, value: args.value, formattedAmount, logIndex: i, }); } } } catch (_e) { // Do not fail here - fail when we can't find a single valid transfer } } } // Now select the correct transfer // Strategy: Find the transfer from the sender (smart wallet) address if (usdcTransfers.length > 0 && senderAddress) { // Look for transfers from the sender address (smart wallet) // Compare checksummed addresses for consistency const senderTransfers = usdcTransfers.filter((t) => { try { return isAddressEqual(t.from, senderAddress); } catch { return false; } }); if (senderTransfers.length === 0) { // No transfer from the sender wallet was found throw new Error(`Unable to find USDC transfer from sender wallet ${receipt.result.sender}. ` + `Found ${usdcTransfers.length} USDC transfer(s) but none originated from the sender wallet.`); } if (senderTransfers.length > 1) { // Multiple transfers from the sender wallet found const transferDetails = senderTransfers .map((t) => `${t.formattedAmount} USDC to ${t.to}`) .join(', '); throw new Error(`Found multiple USDC transfers from sender wallet ${receipt.result.sender}: ${transferDetails}. Expected exactly one transfer.`); } // Exactly one transfer from sender found amount = senderTransfers[0].formattedAmount; recipient = senderTransfers[0].to; } } if (telemetry) { logPaymentStatusCheckCompleted({ testnet, status: 'completed', correlationId }); } const result = { status: 'completed', id: id, message: 'Payment completed successfully', sender: receipt.result.sender, amount, recipient, }; return result; } // else block - Parse a user-friendly reason for failure let userFriendlyReason = 'Payment could not be completed'; if (reason) { if (reason.toLowerCase().includes('insufficient')) { userFriendlyReason = 'Insufficient USDC balance'; } else { userFriendlyReason = reason; } } if (telemetry) { logPaymentStatusCheckCompleted({ testnet, status: 'failed', correlationId }); } const result = { status: 'failed', id: id, message: 'Payment failed', sender: receipt.result.sender, reason: userFriendlyReason, }; return result; } catch (error) { console.error('[getPaymentStatus] Error checking status:', error); const errorMessage = error instanceof Error ? error.message : 'Connection error'; if (telemetry) { logPaymentStatusCheckError({ testnet, correlationId, errorMessage }); } // Re-throw the error throw error; } } //# sourceMappingURL=getPaymentStatus.js.map