Skip to content

Commit 4a5375d

Browse files
committed
Add Vitestified test from Kit
1 parent ada19c7 commit 4a5375d

File tree

1 file changed

+315
-0
lines changed

1 file changed

+315
-0
lines changed
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import {
2+
AccountRole,
3+
Address,
4+
Blockhash,
5+
ITransactionMessageWithFeePayer,
6+
Nonce,
7+
Rpc,
8+
SimulateTransactionApi,
9+
SOLANA_ERROR__INSTRUCTION_ERROR__INSUFFICIENT_FUNDS,
10+
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
11+
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT,
12+
SolanaError,
13+
TransactionError,
14+
TransactionMessage,
15+
compileTransaction,
16+
} from '@solana/kit';
17+
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
18+
import { estimateComputeUnitLimit } from '../src/estimateComputeLimitInternal';
19+
20+
// Spy on the `compileTransaction` function.
21+
vi.mock('@solana/kit', async (importOriginal) => {
22+
const actual = await importOriginal<typeof import('@solana/kit')>();
23+
return {
24+
...actual,
25+
compileTransaction: vi.fn(actual.compileTransaction),
26+
};
27+
});
28+
29+
const FOREVER_PROMISE = new Promise(() => {
30+
/* never resolve */
31+
});
32+
33+
const MOCK_BLOCKHASH_LIFETIME_CONSTRAINT = {
34+
blockhash: 'GNtuHnNyW68wviopST3ki37Afv7LPphxfSwiHAkX5Q9H' as Blockhash,
35+
lastValidBlockHeight: 0n,
36+
} as const;
37+
38+
describe('estimateComputeUnitLimit', () => {
39+
let sendSimulateTransactionRequest: Mock;
40+
let mockTransactionMessage: TransactionMessage &
41+
ITransactionMessageWithFeePayer;
42+
let rpc: Rpc<SimulateTransactionApi>;
43+
let simulateTransaction: Mock;
44+
45+
beforeEach(() => {
46+
mockTransactionMessage = {
47+
feePayer: {
48+
address: '7U8VWgTUucttJPt5Bbkt48WknWqRGBfstBt8qqLHnfPT' as Address,
49+
},
50+
instructions: [],
51+
version: 0,
52+
};
53+
sendSimulateTransactionRequest = vi.fn().mockReturnValue(FOREVER_PROMISE);
54+
simulateTransaction = vi.fn().mockReturnValue({
55+
send: sendSimulateTransactionRequest,
56+
});
57+
rpc = { simulateTransaction };
58+
});
59+
60+
it('aborts the `simulateTransaction` request when aborted', () => {
61+
const abortController = new AbortController();
62+
estimateComputeUnitLimit({
63+
abortSignal: abortController.signal,
64+
rpc,
65+
transactionMessage: {
66+
...mockTransactionMessage,
67+
lifetimeConstraint: MOCK_BLOCKHASH_LIFETIME_CONSTRAINT,
68+
},
69+
}).catch(() => {});
70+
71+
expect(sendSimulateTransactionRequest).toHaveBeenCalledWith({
72+
abortSignal: expect.objectContaining({ aborted: false }),
73+
});
74+
abortController.abort();
75+
expect(sendSimulateTransactionRequest).toHaveBeenCalledWith({
76+
abortSignal: expect.objectContaining({ aborted: true }),
77+
});
78+
});
79+
80+
it('passes the expected basic input to the simulation request', () => {
81+
estimateComputeUnitLimit({
82+
commitment: 'finalized',
83+
minContextSlot: 42n,
84+
rpc,
85+
transactionMessage: {
86+
...mockTransactionMessage,
87+
lifetimeConstraint: MOCK_BLOCKHASH_LIFETIME_CONSTRAINT,
88+
},
89+
}).catch(() => {});
90+
91+
expect(simulateTransaction).toHaveBeenCalledWith(
92+
expect.any(String),
93+
expect.objectContaining({
94+
commitment: 'finalized',
95+
encoding: 'base64',
96+
minContextSlot: 42n,
97+
sigVerify: false,
98+
})
99+
);
100+
});
101+
102+
it('appends a set compute unit limit instruction when one does not exist', () => {
103+
const transactionMessage = {
104+
...mockTransactionMessage, // No `SetComputeUnitLimit` instruction
105+
lifetimeConstraint: MOCK_BLOCKHASH_LIFETIME_CONSTRAINT,
106+
};
107+
108+
estimateComputeUnitLimit({
109+
rpc,
110+
transactionMessage,
111+
}).catch(() => {});
112+
113+
expect(compileTransaction).toHaveBeenCalledWith({
114+
...transactionMessage,
115+
instructions: [
116+
...transactionMessage.instructions,
117+
{
118+
data:
119+
// prettier-ignore
120+
new Uint8Array([
121+
0x02, // SetComputeUnitLimit instruction inde
122+
0xc0, 0x5c, 0x15, 0x00, // 1,400,000, MAX_COMPUTE_UNITS
123+
]),
124+
programAddress: 'ComputeBudget111111111111111111111111111111',
125+
},
126+
],
127+
});
128+
129+
vi.doUnmock('@solana/kit');
130+
});
131+
132+
it('replaces the existing set compute unit limit instruction when one exists', () => {
133+
const transactionMessage = {
134+
...mockTransactionMessage,
135+
instructions: [
136+
{
137+
programAddress:
138+
'4Kk4nA3F2nWHCcuyT8nR6oF7HQUQHmmzAVD5k8FQPKB2' as Address,
139+
},
140+
{
141+
data:
142+
// prettier-ignore
143+
new Uint8Array([
144+
0x02, // SetComputeUnitLimit instruction inde
145+
0x01, 0x02, 0x03, 0x04, // ComputeUnits(u32)
146+
]),
147+
programAddress:
148+
'ComputeBudget111111111111111111111111111111' as Address,
149+
},
150+
{
151+
programAddress:
152+
'4Kk4nA3F2nWHCcuyT8nR6oF7HQUQHmmzAVD5k8FQPKB2' as Address,
153+
},
154+
],
155+
lifetimeConstraint: MOCK_BLOCKHASH_LIFETIME_CONSTRAINT,
156+
};
157+
158+
estimateComputeUnitLimit({
159+
rpc,
160+
transactionMessage,
161+
}).catch(() => {});
162+
163+
expect(compileTransaction).toHaveBeenCalledWith(
164+
expect.objectContaining({
165+
instructions: [
166+
transactionMessage.instructions[0],
167+
{
168+
...transactionMessage.instructions[1],
169+
data: new Uint8Array([0x02, 0xc0, 0x5c, 0x15, 0x00]), // Replaced with MAX_COMPUTE_UNITS
170+
},
171+
transactionMessage.instructions[2],
172+
],
173+
})
174+
);
175+
});
176+
177+
it('does not ask for a replacement blockhash when the transaction message is a durable nonce transaction', () => {
178+
estimateComputeUnitLimit({
179+
rpc,
180+
transactionMessage: {
181+
...mockTransactionMessage,
182+
instructions: [
183+
{
184+
accounts: [
185+
{
186+
address:
187+
'7wJFRFuAE9x5Ptnz2VoBWsfecTCfuuM2sQCpECGypnTU' as Address,
188+
role: AccountRole.WRITABLE,
189+
},
190+
{
191+
address:
192+
'SysvarRecentB1ockHashes11111111111111111111' as Address,
193+
role: AccountRole.READONLY,
194+
},
195+
{
196+
address:
197+
'HzMoc78z1VNNf9nwD4Czt6CDYEb9LVD8KsVGP46FEmyJ' as Address,
198+
role: AccountRole.READONLY_SIGNER,
199+
},
200+
],
201+
data: new Uint8Array([4, 0, 0, 0]),
202+
programAddress: '11111111111111111111111111111111' as Address,
203+
},
204+
],
205+
lifetimeConstraint: {
206+
nonce: 'BzAqD6382v5r1pcELoi8HWrBDV4dSL9NGemMn2JYAhxc' as Nonce,
207+
},
208+
},
209+
}).catch(() => {});
210+
211+
expect(simulateTransaction).toHaveBeenCalledWith(
212+
expect.anything(),
213+
expect.objectContaining({ replaceRecentBlockhash: false })
214+
);
215+
});
216+
217+
it('asks for a replacement blockhash even when the transaction message has a blockhash lifetime', () => {
218+
estimateComputeUnitLimit({
219+
rpc,
220+
transactionMessage: {
221+
...mockTransactionMessage,
222+
lifetimeConstraint: MOCK_BLOCKHASH_LIFETIME_CONSTRAINT,
223+
},
224+
}).catch(() => {});
225+
226+
expect(simulateTransaction).toHaveBeenCalledWith(
227+
expect.anything(),
228+
expect.objectContaining({ replaceRecentBlockhash: true })
229+
);
230+
});
231+
232+
it('asks for a replacement blockhash when the transaction message has no lifetime', () => {
233+
estimateComputeUnitLimit({
234+
rpc,
235+
transactionMessage: mockTransactionMessage,
236+
}).catch(() => {});
237+
238+
expect(simulateTransaction).toHaveBeenCalledWith(
239+
expect.anything(),
240+
expect.objectContaining({ replaceRecentBlockhash: true })
241+
);
242+
});
243+
244+
it('returns the estimated compute units on success', async () => {
245+
expect.assertions(1);
246+
sendSimulateTransactionRequest.mockResolvedValue({
247+
value: { unitsConsumed: 42n },
248+
});
249+
250+
const estimatePromise = estimateComputeUnitLimit({
251+
rpc,
252+
transactionMessage: mockTransactionMessage,
253+
});
254+
255+
await expect(estimatePromise).resolves.toBe(42);
256+
});
257+
258+
it('caps the estimated compute units to MAX_COMPUTE_UNITS of 1.4M', async () => {
259+
expect.assertions(1);
260+
sendSimulateTransactionRequest.mockResolvedValue({
261+
value: { unitsConsumed: 1400000n /* MAX_COMPUTE_UNITS */ },
262+
});
263+
264+
const estimatePromise = estimateComputeUnitLimit({
265+
rpc,
266+
transactionMessage: mockTransactionMessage,
267+
});
268+
269+
await expect(estimatePromise).resolves.toBe(1400000);
270+
});
271+
272+
it('throws with the transaction error as cause when the transaction fails in simulation', async () => {
273+
expect.assertions(1);
274+
const transactionError: TransactionError = 'AccountNotFound';
275+
sendSimulateTransactionRequest.mockResolvedValue({
276+
value: { err: transactionError, unitsConsumed: 42n },
277+
});
278+
279+
const estimatePromise = estimateComputeUnitLimit({
280+
rpc,
281+
transactionMessage: mockTransactionMessage,
282+
});
283+
284+
await expect(estimatePromise).rejects.toThrow(
285+
new SolanaError(
286+
SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT,
287+
{
288+
cause: transactionError,
289+
unitsConsumed: 42,
290+
}
291+
)
292+
);
293+
});
294+
295+
it('throws with the cause when simulation fails', async () => {
296+
expect.assertions(1);
297+
const simulationError = new SolanaError(
298+
SOLANA_ERROR__INSTRUCTION_ERROR__INSUFFICIENT_FUNDS,
299+
{ index: 42 }
300+
);
301+
sendSimulateTransactionRequest.mockRejectedValue(simulationError);
302+
303+
const estimatePromise = estimateComputeUnitLimit({
304+
rpc,
305+
transactionMessage: mockTransactionMessage,
306+
});
307+
308+
await expect(estimatePromise).rejects.toThrow(
309+
new SolanaError(
310+
SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT,
311+
{ cause: simulationError }
312+
)
313+
);
314+
});
315+
});

0 commit comments

Comments
 (0)