Skip to content

Commit 4231880

Browse files
authored
refactor(receipt): move get-tx off web3.js Connection and lift the v1 cap (solana-foundation#1270)
Replaces the two `new Connection(...)` constructions in `app/features/receipt/api/get-tx.ts` with kit. T The transaction fetch delegates to `@entities/transaction-data`'s `fetchTransactionDetails`, which already normalizes kit's bigint upcasts and fetches at `MAX_SUPPORTED_TRANSACTION_VERSION`, lifting the previous `maxSupportedTransactionVersion: 0` cap so v1 transactions produce receipts instead of a 502. `ApiData.transaction` widens from web3.js `ParsedTransactionWithMeta` to the entity's `TransactionWithMeta`; the only consumer (`extractReceiptData`) was already typed against it. The spec test is rewritten against the kit seams, and the byte-identical duplicate `get-tx.test.ts` is deleted — both extensions ran under the same vitest project, so the suite was executing twice. Closes: HOO-1272
1 parent 30f0bdb commit 4231880

3 files changed

Lines changed: 75 additions & 250 deletions

File tree

Lines changed: 63 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
import { Connection } from '@solana/web3.js';
1+
import { fetchTransactionDetails } from '@entities/transaction-data/api/fetch-transaction-details';
2+
import { createSolanaRpc } from '@solana/kit';
23
import { beforeEach, describe, expect, it, vi } from 'vitest';
34

4-
import { Cluster } from '@/app/utils/cluster';
5+
import { Cluster, serverClusterUrl } from '@/app/utils/cluster';
56

67
import { mockSingleTransferTransaction } from '../../mocks/single-transfer';
78
import { getTx } from '../get-tx';
89

9-
vi.mock('@solana/web3.js', async () => {
10-
const actual = await vi.importActual('@solana/web3.js');
11-
return {
12-
...actual,
13-
Connection: vi.fn(),
14-
};
15-
});
10+
vi.mock('@entities/transaction-data/api/fetch-transaction-details', () => ({
11+
fetchTransactionDetails: vi.fn(),
12+
}));
1613

1714
vi.mock('../../env', () => ({
1815
isClusterProbeEnabled: true,
@@ -21,88 +18,90 @@ vi.mock('../../env', () => ({
2118
describe('getTx', () => {
2219
const mockSignature = '5yKzCuw1e9d58HcnzSL31cczfXUux2H4Ga5TAR2RcQLE5W8BiTAC9x9MvhLtc4h99sC9XxLEAjhrXyfKezdMkZFV';
2320

24-
let mockConnection: {
25-
getSignatureStatus: ReturnType<typeof vi.fn>;
26-
getParsedTransaction: ReturnType<typeof vi.fn>;
27-
};
21+
let mockGetSignatureStatuses: ReturnType<typeof vi.fn>;
22+
23+
function statusResponse(found: boolean) {
24+
return {
25+
send: vi.fn().mockResolvedValue({
26+
value: [found ? { confirmationStatus: 'confirmed', slot: 12345n } : null],
27+
}),
28+
};
29+
}
30+
31+
function statusFailure(error: Error) {
32+
return { send: vi.fn().mockRejectedValue(error) };
33+
}
2834

2935
beforeEach(() => {
3036
vi.clearAllMocks();
3137
vi.spyOn(console, 'error').mockImplementation(() => {});
3238

33-
mockConnection = {
34-
getParsedTransaction: vi.fn(),
35-
getSignatureStatus: vi.fn(),
36-
};
37-
38-
vi.mocked(Connection).mockImplementation(function () {
39-
return mockConnection as unknown as Connection;
40-
});
39+
mockGetSignatureStatuses = vi.fn();
40+
vi.mocked(createSolanaRpc).mockReturnValue({
41+
getSignatureStatuses: mockGetSignatureStatuses,
42+
} as unknown as ReturnType<typeof createSolanaRpc>);
4143
});
4244

4345
describe('successful cases', () => {
4446
it('should return transaction and cluster when found', async () => {
45-
mockConnection.getSignatureStatus.mockResolvedValueOnce({
46-
value: {
47-
confirmationStatus: 'confirmed',
48-
slot: 12345,
49-
},
50-
});
51-
52-
mockConnection.getParsedTransaction.mockResolvedValueOnce(mockSingleTransferTransaction);
47+
mockGetSignatureStatuses.mockReturnValueOnce(statusResponse(true));
48+
vi.mocked(fetchTransactionDetails).mockResolvedValueOnce(mockSingleTransferTransaction);
5349

5450
const result = await getTx(mockSignature);
5551

5652
expect(result).toEqual({
5753
cluster: Cluster.MainnetBeta,
5854
transaction: mockSingleTransferTransaction,
5955
});
60-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(1);
61-
expect(mockConnection.getParsedTransaction).toHaveBeenCalledTimes(1);
56+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(1);
57+
expect(mockGetSignatureStatuses).toHaveBeenCalledWith([mockSignature], {
58+
searchTransactionHistory: true,
59+
});
60+
expect(fetchTransactionDetails).toHaveBeenCalledTimes(1);
61+
expect(fetchTransactionDetails).toHaveBeenCalledWith(expect.any(String), mockSignature);
6262
});
6363

6464
it('should return transaction and cluster when found on devnet', async () => {
65-
mockConnection.getSignatureStatus.mockResolvedValueOnce({ value: null }).mockResolvedValueOnce({
66-
value: {
67-
confirmationStatus: 'confirmed',
68-
slot: 67890,
69-
},
70-
});
71-
72-
mockConnection.getParsedTransaction.mockResolvedValueOnce(mockSingleTransferTransaction);
65+
mockGetSignatureStatuses
66+
.mockReturnValueOnce(statusResponse(false))
67+
.mockReturnValueOnce(statusResponse(true));
68+
vi.mocked(fetchTransactionDetails).mockResolvedValueOnce(mockSingleTransferTransaction);
7369

7470
const result = await getTx(mockSignature);
7571

7672
expect(result).toEqual({
7773
cluster: Cluster.Devnet,
7874
transaction: mockSingleTransferTransaction,
7975
});
80-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(2);
81-
expect(mockConnection.getParsedTransaction).toHaveBeenCalledTimes(1);
76+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(2);
77+
expect(fetchTransactionDetails).toHaveBeenCalledTimes(1);
78+
expect(fetchTransactionDetails).toHaveBeenCalledWith(serverClusterUrl(Cluster.Devnet), mockSignature);
8279
});
8380
});
8481

8582
describe('error handling', () => {
8683
it('should throw error when cluster is not found', async () => {
87-
mockConnection.getSignatureStatus.mockResolvedValue({
88-
value: null,
89-
});
84+
mockGetSignatureStatuses.mockReturnValue(statusResponse(false));
9085

9186
await expect(getTx(mockSignature)).rejects.toThrow('Cluster not found');
9287

93-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(3);
88+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(3);
9489
});
9590

96-
it('should throw error when transaction is not found', async () => {
97-
mockConnection.getSignatureStatus.mockResolvedValue({
98-
value: {
99-
confirmationStatus: 'confirmed',
100-
slot: 12345,
101-
},
91+
it('should not report a transaction as found when the status response is empty', async () => {
92+
mockGetSignatureStatuses.mockReturnValue({
93+
send: vi.fn().mockResolvedValue({ value: [] }),
10294
});
10395

104-
mockConnection.getParsedTransaction.mockResolvedValueOnce(null);
105-
mockConnection.getParsedTransaction.mockResolvedValueOnce(null);
96+
await expect(getTx(mockSignature)).rejects.toThrow('Cluster not found');
97+
98+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(3);
99+
expect(fetchTransactionDetails).not.toHaveBeenCalled();
100+
});
101+
102+
it('should throw error when transaction is not found', async () => {
103+
mockGetSignatureStatuses.mockReturnValue(statusResponse(true));
104+
vi.mocked(fetchTransactionDetails).mockResolvedValue(null);
106105

107106
await expect(getTx(mockSignature)).rejects.toSatisfy((error: Error) => {
108107
return (
@@ -113,48 +112,41 @@ describe('getTx', () => {
113112
});
114113
});
115114

116-
it('should throw error when getParsedTransaction throws an error', async () => {
117-
mockConnection.getSignatureStatus.mockResolvedValue({
118-
value: {
119-
confirmationStatus: 'confirmed',
120-
slot: 12345,
121-
},
122-
});
115+
it('should throw error when the transaction fetch throws an error', async () => {
116+
mockGetSignatureStatuses.mockReturnValue(statusResponse(true));
123117

124118
const fetchError = new Error('Failed to fetch');
125-
mockConnection.getParsedTransaction.mockRejectedValueOnce(fetchError);
119+
vi.mocked(fetchTransactionDetails).mockRejectedValueOnce(fetchError);
126120

127121
await expect(getTx(mockSignature)).rejects.toSatisfy((error: Error) => {
128122
return error.message === 'Failed to fetch transaction' && error.cause === fetchError;
129123
});
130124
});
131125

132126
it('should throw immediately on mainnet network error', async () => {
133-
mockConnection.getSignatureStatus.mockRejectedValueOnce(new Error('Forbidden access'));
127+
mockGetSignatureStatuses.mockReturnValueOnce(statusFailure(new Error('Forbidden access')));
134128

135129
await expect(getTx(mockSignature)).rejects.toThrow('Failed to check the mainnet-beta');
136-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(1);
130+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(1);
137131
});
138132

139133
it('should throw on probe cluster network error', async () => {
140134
// Mainnet succeeds but tx not found
141-
mockConnection.getSignatureStatus.mockResolvedValueOnce({ value: null });
135+
mockGetSignatureStatuses.mockReturnValueOnce(statusResponse(false));
142136
// Devnet fails with network error
143-
mockConnection.getSignatureStatus.mockRejectedValueOnce(new Error('Network error'));
137+
mockGetSignatureStatuses.mockReturnValueOnce(statusFailure(new Error('Network error')));
144138

145139
await expect(getTx(mockSignature)).rejects.toThrow('Failed to check the devnet');
146-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(2);
140+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(2);
147141
});
148142
});
149143

150144
it('should check all clusters', async () => {
151-
mockConnection.getSignatureStatus.mockResolvedValue({
152-
value: null,
153-
});
145+
mockGetSignatureStatuses.mockReturnValue(statusResponse(false));
154146

155147
await expect(getTx(mockSignature)).rejects.toThrow('Cluster not found');
156148

157-
expect(Connection).toHaveBeenCalledTimes(3);
158-
expect(mockConnection.getSignatureStatus).toHaveBeenCalledTimes(3);
149+
expect(createSolanaRpc).toHaveBeenCalledTimes(3);
150+
expect(mockGetSignatureStatuses).toHaveBeenCalledTimes(3);
159151
});
160152
});

app/features/receipt/api/__tests__/get-tx.test.ts

Lines changed: 0 additions & 160 deletions
This file was deleted.

0 commit comments

Comments
 (0)