@lukasrosario/account
Version:
Base Account SDK
297 lines (260 loc) • 8.64 kB
text/typescript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getPaymentStatus } from './getPaymentStatus.js';
import type { PaymentStatus } from './types.js';
// Mock fetch globally
global.fetch = vi.fn();
// Mock telemetry events
vi.mock(':core/telemetry/events/payment.js', () => ({
logPaymentStatusCheckStarted: vi.fn(),
logPaymentStatusCheckCompleted: vi.fn(),
logPaymentStatusCheckError: vi.fn(),
}));
describe('getPaymentStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock console methods to avoid noise in tests
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
// Mock crypto.randomUUID
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('mock-correlation-id'),
});
});
it('should return completed status for successful payment', async () => {
const mockReceipt = {
jsonrpc: '2.0',
id: 1,
result: {
success: true,
receipt: {
transactionHash: '0xabc123',
logs: [
{
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
data: '0x0000000000000000000000000000000000000000000000000000000000989680', // 10 USDC (10 * 10^6)
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', // Transfer event topic
'0x0000000000000000000000004a7c6899cdcb379e284fbfd045462e751da4c7ce', // from address (padded)
'0x000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb336', // to address (padded)
],
},
],
},
sender: '0xsender',
},
};
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => mockReceipt,
} as Response);
const status = await getPaymentStatus({
id: '0x123456',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'completed',
id: '0x123456',
message: 'Payment completed successfully',
sender: '0xsender',
amount: '10',
recipient: '0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336',
});
expect(fetch).toHaveBeenCalledWith(
'https://api.developer.coinbase.com/rpc/v1/base/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'eth_getUserOperationReceipt',
params: ['0x123456'],
}),
})
);
});
it('should return failed status for failed payment', async () => {
const mockReceipt = {
jsonrpc: '2.0',
id: 1,
result: {
success: false,
receipt: {
transactionHash: '0xdef456',
},
sender: '0xsender',
reason: 'Insufficient USDC balance',
},
};
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => mockReceipt,
} as Response);
const status = await getPaymentStatus({
id: '0x789abc',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'failed',
id: '0x789abc',
message: 'Payment failed',
sender: '0xsender',
error: 'Insufficient USDC balance',
});
});
it('should return pending status when userOp exists but no receipt', async () => {
// First call returns no receipt
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({ jsonrpc: '2.0', id: 1, result: null }),
} as Response);
// Second call returns userOp
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({
jsonrpc: '2.0',
id: 2,
result: {
sender: '0xpendingSender',
// other userOp fields...
},
}),
} as Response);
const status = await getPaymentStatus({
id: '0xpending123',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'pending',
id: '0xpending123',
message: 'Your payment is being processed. This usually takes a few seconds.',
sender: '0xpendingSender',
});
expect(fetch).toHaveBeenCalledTimes(2);
});
it('should return not_found status when payment does not exist', async () => {
// Both calls return null
vi.mocked(fetch)
.mockResolvedValueOnce({
json: async () => ({ jsonrpc: '2.0', id: 1, result: null }),
} as Response)
.mockResolvedValueOnce({
json: async () => ({ jsonrpc: '2.0', id: 2, result: null }),
} as Response);
const status = await getPaymentStatus({
id: '0xnotfound',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'not_found',
id: '0xnotfound',
message: 'Payment not found. Please check your transaction ID.',
});
});
it('should handle RPC errors gracefully', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({
jsonrpc: '2.0',
id: 1,
error: {
code: -32602,
message: 'Invalid params',
},
}),
} as Response);
const status = await getPaymentStatus({
id: '0xinvalid',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'failed',
id: '0xinvalid',
message: 'Unable to check payment status. Please try again later.',
error: 'Invalid params',
});
});
it('should handle network errors gracefully', async () => {
vi.mocked(fetch).mockRejectedValueOnce(new Error('Network error'));
const status = await getPaymentStatus({
id: '0xnetworkerror',
testnet: false,
});
expect(status).toEqual<PaymentStatus>({
status: 'failed',
id: '0xnetworkerror',
message: 'Unable to check payment status',
error: 'Network error',
});
});
it('should use testnet bundler URL when testnet is true', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({ jsonrpc: '2.0', id: 1, result: null }),
} as Response);
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({ jsonrpc: '2.0', id: 2, result: null }),
} as Response);
await getPaymentStatus({
id: '0xtestnet',
testnet: true,
});
expect(fetch).toHaveBeenCalledWith(
'https://api.developer.coinbase.com/rpc/v1/base-sepolia/S-fOd2n2Oi4fl4e1Crm83XeDXZ7tkg8O',
expect.any(Object)
);
});
it('should parse user-friendly failure reasons', async () => {
const testCases = [
{ reason: 'execution reverted: insufficient balance', expected: 'Insufficient USDC balance' },
{ reason: 'transaction reverted', expected: 'Payment was rejected' },
{ reason: 'custom error message', expected: 'custom error message' },
];
for (const { reason, expected } of testCases) {
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => ({
jsonrpc: '2.0',
id: 1,
result: {
success: false,
receipt: { transactionHash: '0xfailed' },
sender: '0xsender',
reason,
},
}),
} as Response);
const status = await getPaymentStatus({
id: '0xfailedreason',
testnet: false,
});
expect(status.error).toBe(expected);
}
});
it('should handle logs with different USDC addresses on testnet', async () => {
const mockReceipt = {
jsonrpc: '2.0',
id: 1,
result: {
success: true,
receipt: {
transactionHash: '0xabc123',
logs: [
{
address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', // testnet USDC
data: '0x0000000000000000000000000000000000000000000000000000000000989680', // 10 USDC
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x0000000000000000000000004a7c6899cdcb379e284fbfd045462e751da4c7ce', // from address (padded)
'0x000000000000000000000000f1ddf1fc0310cb11f0ca87508207012f4a9cb336', // to address (padded)
],
},
],
},
sender: '0xsender',
},
};
vi.mocked(fetch).mockResolvedValueOnce({
json: async () => mockReceipt,
} as Response);
const status = await getPaymentStatus({
id: '0x123456',
testnet: true,
});
expect(status.amount).toBe('10');
expect(status.recipient).toBe('0xf1DdF1fc0310Cb11F0Ca87508207012F4a9CB336');
});
});