@lukasrosario/account
Version:
Base Account SDK
194 lines (173 loc) • 6.48 kB
text/typescript
import type { Hex } from 'viem';
import { decodeEventLog, formatUnits } from 'viem';
import { logPaymentStatusCheckCompleted, logPaymentStatusCheckError, logPaymentStatusCheckStarted } from ':core/telemetry/events/payment.js';
import { ERC20_TRANSFER_ABI, TOKENS } from './constants.js';
import type { PaymentStatus, PaymentStatusOptions } from './types.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
*
* @example
* const status = await getPaymentStatus({
* id: "0x1234...5678",
* testnet: true
* })
*
* @note The id is the userOp hash returned from the pay function
*/
export async function getPaymentStatus(options: PaymentStatusOptions): Promise<PaymentStatus> {
const { id, testnet = false } = options;
// Generate correlation ID for this status check
const correlationId = crypto.randomUUID();
// Log status check started
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';
logPaymentStatusCheckError({ testnet, correlationId, errorMessage });
return {
status: 'failed',
id: id as Hex,
message: 'Unable to check payment status. Please try again later.',
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
logPaymentStatusCheckCompleted({ testnet, status: 'pending', correlationId });
const result = {
status: 'pending' as const,
id: id as Hex,
message: 'Your payment is being processed. This usually takes a few seconds.',
sender: userOpResponse.result.sender,
};
return result;
}
// Not found at all
logPaymentStatusCheckCompleted({ testnet, status: 'not_found', correlationId });
const result = {
status: 'not_found' as const,
id: id as Hex,
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: string | undefined;
let recipient: string | undefined;
if (txReceipt?.logs) {
const network = testnet ? 'baseSepolia' : 'base';
const usdcAddress = TOKENS.USDC.addresses[network].toLowerCase();
for (const log of txReceipt.logs) {
if (log.address?.toLowerCase() === usdcAddress) {
try {
const decoded = decodeEventLog({
abi: ERC20_TRANSFER_ABI,
data: log.data,
topics: log.topics,
});
if (decoded.eventName === 'Transfer' && decoded.args) {
// The Transfer event has indexed 'from' and 'to', and non-indexed 'value'
// viem's decodeEventLog returns indexed args in the args object
const args = decoded.args as { from: string; to: string; value: bigint };
if (args.value && args.to) {
amount = formatUnits(args.value, 6);
recipient = args.to;
break;
}
}
} catch (e) {
console.error('[getPaymentStatus] Error parsing log:', e);
}
}
}
}
logPaymentStatusCheckCompleted({ testnet, status: 'completed', correlationId });
const result = {
status: 'completed' as const,
id: id as Hex,
message: 'Payment completed successfully',
sender: receipt.result.sender,
amount,
recipient,
};
return result;
} else {
// Parse a user-friendly reason for failure
let userFriendlyError = 'Payment could not be completed';
if (reason) {
if (reason.toLowerCase().includes('insufficient')) {
userFriendlyError = 'Insufficient USDC balance';
} else if (reason.toLowerCase().includes('revert')) {
userFriendlyError = 'Payment was rejected';
} else {
userFriendlyError = reason;
}
}
logPaymentStatusCheckCompleted({ testnet, status: 'failed', correlationId });
const result = {
status: 'failed' as const,
id: id as Hex,
message: 'Payment failed',
sender: receipt.result.sender,
error: userFriendlyError,
};
return result;
}
} catch (error) {
console.error('[getPaymentStatus] Error checking status:', error);
const errorMessage = error instanceof Error ? error.message : 'Connection error';
logPaymentStatusCheckError({ testnet, correlationId, errorMessage });
const result = {
status: 'failed' as const,
id: id as Hex,
message: 'Unable to check payment status',
error: errorMessage,
};
return result;
}
}