@solana/kora 0.2.0-beta.3 → 0.2.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
- import { appendTransactionMessageInstruction, compileTransaction, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
1
+ import { address, appendTransactionMessageInstruction, compileTransaction, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
2
+ import { getTransferSolInstruction } from '@solana-program/system';
2
3
  import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
4
+ import { createKitKoraClient } from '../src/index.js';
3
5
  import { runAuthenticationTests } from './auth-setup.js';
4
6
  import setupTestSuite from './setup.js';
5
7
  function transactionFromBase64(base64) {
@@ -54,6 +56,7 @@ async function buildTokenTransferTransaction(params) {
54
56
  return { blockhash: blockhash, transaction: base64Transaction };
55
57
  }
56
58
  const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
59
+ const FREE_PRICING = process.env.FREE_PRICING === 'true';
57
60
  const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
58
61
  describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without auth'} | signer type: ${KORA_SIGNER_TYPE})`, () => {
59
62
  let client;
@@ -172,9 +175,12 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
172
175
  const fee = await client.estimateTransactionFee({ fee_token: usdcMint, transaction });
173
176
  expect(fee).toBeDefined();
174
177
  expect(typeof fee.fee_in_lamports).toBe('number');
175
- expect(fee.fee_in_lamports).toBeGreaterThan(0);
178
+ expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
176
179
  expect(typeof fee.fee_in_token).toBe('number');
177
- expect(fee.fee_in_token).toBeGreaterThan(0);
180
+ if (!FREE_PRICING) {
181
+ expect(fee.fee_in_lamports).toBeGreaterThan(0);
182
+ expect(fee.fee_in_token).toBeGreaterThan(0);
183
+ }
178
184
  });
179
185
  it('should sign transaction', async () => {
180
186
  const { transaction } = await buildTokenTransferTransaction({
@@ -246,7 +252,8 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
246
252
  expect(original_transaction).toBe(transaction);
247
253
  });
248
254
  });
249
- describe('Bundle Operations', () => {
255
+ // Bundle tests require bundle.enabled = true in the Kora config
256
+ (FREE_PRICING ? describe.skip : describe)('Bundle Operations', () => {
250
257
  it('should sign bundle of transactions', async () => {
251
258
  // Create two transfer transactions for the bundle
252
259
  const { transaction: tx1String } = await buildTokenTransferTransaction({
@@ -324,17 +331,37 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
324
331
  await expect(client.estimateTransactionFee({ fee_token: usdcMint, transaction: 'invalid_transaction' })).rejects.toThrow();
325
332
  });
326
333
  });
327
- describe('End-to-End Flows', () => {
328
- it('should handle transfer and sign flow', async () => {
329
- const { transaction } = await buildTokenTransferTransaction({
330
- amount: 1000000n,
331
- client,
332
- destinationWallet: koraAddress,
333
- mint: usdcMint,
334
- sourceWallet: testWallet,
335
- });
336
- const signResult = await client.signTransaction({ transaction });
337
- expect(signResult.signed_transaction).toBeDefined();
334
+ if (FREE_PRICING) {
335
+ describe('Kit Client (free pricing)', () => {
336
+ let freeClient;
337
+ beforeAll(async () => {
338
+ const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
339
+ freeClient = await createKitKoraClient({
340
+ endpoint: koraRpcUrl,
341
+ rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
342
+ feeToken: usdcMint,
343
+ feePayerWallet: testWallet,
344
+ });
345
+ }, 30000);
346
+ it('should send transaction without payment instruction when fee is 0', async () => {
347
+ const ix = getTransferSolInstruction({
348
+ source: testWallet,
349
+ destination: address(koraAddress),
350
+ amount: 1000,
351
+ });
352
+ const result = await freeClient.sendTransaction([ix]);
353
+ expect(result.status).toBe('successful');
354
+ expect(result.context.signature).toBeDefined();
355
+ }, 30000);
356
+ it('should strip placeholder from planned message when fee is 0', async () => {
357
+ const ix = getTransferSolInstruction({
358
+ source: testWallet,
359
+ destination: address(koraAddress),
360
+ amount: 1000,
361
+ });
362
+ const result = await freeClient.sendTransaction([ix]);
363
+ expect(result.status).toBe('successful');
364
+ }, 30000);
338
365
  });
339
- });
366
+ }
340
367
  });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,491 @@
1
+ import { createKitKoraClient } from '../src/kit/index.js';
2
+ import { address, createNoopSigner } from '@solana/kit';
3
+ // Mock fetch globally
4
+ const mockFetch = jest.fn();
5
+ global.fetch = mockFetch;
6
+ const MOCK_ENDPOINT = 'http://localhost:8080';
7
+ const MOCK_RPC_URL = 'http://127.0.0.1:8899';
8
+ const MOCK_PAYER_ADDRESS = 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7';
9
+ const MOCK_PAYMENT_ADDRESS = 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7';
10
+ const MOCK_FEE_TOKEN = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
11
+ const MOCK_WALLET_ADDRESS = 'BrEe1Xjy2Ky72doGBAhyUPCxMm5b4bRTm3AD6MNMfKmq';
12
+ const MOCK_WALLET = createNoopSigner(MOCK_WALLET_ADDRESS);
13
+ const MOCK_SIGNATURE = '5wBzExmp8yR5M6m4KjV8WT9T6B1NMQkaMbsFWqBoDPBMYWxDx6EuSGxNqKfXnBhDhAkEqMiGRjEwKnGhSN3pi3n';
14
+ function mockRpcResponse(result) {
15
+ mockFetch.mockResolvedValueOnce({
16
+ json: jest.fn().mockResolvedValueOnce({
17
+ jsonrpc: '2.0',
18
+ id: 1,
19
+ result,
20
+ }),
21
+ });
22
+ }
23
+ function mockSimulateResponse(unitsConsumed = 50000) {
24
+ const body = JSON.stringify({
25
+ jsonrpc: '2.0',
26
+ id: 1,
27
+ result: {
28
+ context: { slot: 1 },
29
+ value: { err: null, logs: [], unitsConsumed },
30
+ },
31
+ });
32
+ mockFetch.mockResolvedValueOnce({
33
+ ok: true,
34
+ status: 200,
35
+ statusText: 'OK',
36
+ headers: new Headers({ 'content-type': 'application/json' }),
37
+ text: jest.fn().mockResolvedValueOnce(body),
38
+ json: jest.fn().mockResolvedValueOnce(JSON.parse(body)),
39
+ });
40
+ }
41
+ function mockRpcError(code, message) {
42
+ mockFetch.mockResolvedValueOnce({
43
+ json: jest.fn().mockResolvedValueOnce({
44
+ jsonrpc: '2.0',
45
+ id: 1,
46
+ error: { code, message },
47
+ }),
48
+ });
49
+ }
50
+ describe('createKitKoraClient', () => {
51
+ beforeEach(() => {
52
+ mockFetch.mockClear();
53
+ });
54
+ afterEach(() => {
55
+ jest.resetAllMocks();
56
+ });
57
+ describe('initialization', () => {
58
+ it('should fetch payer info on creation', async () => {
59
+ mockRpcResponse({
60
+ signer_address: MOCK_PAYER_ADDRESS,
61
+ payment_address: MOCK_PAYMENT_ADDRESS,
62
+ });
63
+ const client = await createKitKoraClient({
64
+ endpoint: MOCK_ENDPOINT,
65
+ rpcUrl: MOCK_RPC_URL,
66
+ feeToken: MOCK_FEE_TOKEN,
67
+ feePayerWallet: MOCK_WALLET,
68
+ });
69
+ expect(client.paymentAddress).toBe(MOCK_PAYMENT_ADDRESS);
70
+ // ClientWithPayer: payer is a NoopSigner for the Kora fee payer
71
+ expect(client.payer.address).toBe(MOCK_PAYER_ADDRESS);
72
+ expect(mockFetch).toHaveBeenCalledTimes(1);
73
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
74
+ expect(body.method).toBe('getPayerSigner');
75
+ });
76
+ it('should throw if getPayerSigner fails', async () => {
77
+ mockRpcError(-32000, 'Server error');
78
+ await expect(createKitKoraClient({
79
+ endpoint: MOCK_ENDPOINT,
80
+ rpcUrl: MOCK_RPC_URL,
81
+ feeToken: MOCK_FEE_TOKEN,
82
+ feePayerWallet: MOCK_WALLET,
83
+ })).rejects.toThrow('RPC Error -32000: Server error');
84
+ });
85
+ it('should expose kora namespace for raw RPC access', async () => {
86
+ mockRpcResponse({
87
+ signer_address: MOCK_PAYER_ADDRESS,
88
+ payment_address: MOCK_PAYMENT_ADDRESS,
89
+ });
90
+ const client = await createKitKoraClient({
91
+ endpoint: MOCK_ENDPOINT,
92
+ rpcUrl: MOCK_RPC_URL,
93
+ feeToken: MOCK_FEE_TOKEN,
94
+ feePayerWallet: MOCK_WALLET,
95
+ });
96
+ expect(client.kora).toBeDefined();
97
+ expect(typeof client.kora.getConfig).toBe('function');
98
+ expect(typeof client.kora.getBlockhash).toBe('function');
99
+ expect(typeof client.kora.estimateTransactionFee).toBe('function');
100
+ });
101
+ it('should implement Kit plugin interfaces', async () => {
102
+ mockRpcResponse({
103
+ signer_address: MOCK_PAYER_ADDRESS,
104
+ payment_address: MOCK_PAYMENT_ADDRESS,
105
+ });
106
+ const client = await createKitKoraClient({
107
+ endpoint: MOCK_ENDPOINT,
108
+ rpcUrl: MOCK_RPC_URL,
109
+ feeToken: MOCK_FEE_TOKEN,
110
+ feePayerWallet: MOCK_WALLET,
111
+ });
112
+ // ClientWithPayer
113
+ expect(client.payer).toBeDefined();
114
+ expect(client.payer.address).toBe(MOCK_PAYER_ADDRESS);
115
+ // ClientWithTransactionPlanning
116
+ expect(typeof client.planTransaction).toBe('function');
117
+ expect(typeof client.planTransactions).toBe('function');
118
+ // ClientWithTransactionSending
119
+ expect(typeof client.sendTransaction).toBe('function');
120
+ expect(typeof client.sendTransactions).toBe('function');
121
+ });
122
+ });
123
+ describe('sendTransaction', () => {
124
+ let client;
125
+ beforeEach(async () => {
126
+ // Mock getPayerSigner for init
127
+ mockRpcResponse({
128
+ signer_address: MOCK_PAYER_ADDRESS,
129
+ payment_address: MOCK_PAYMENT_ADDRESS,
130
+ });
131
+ client = await createKitKoraClient({
132
+ endpoint: MOCK_ENDPOINT,
133
+ rpcUrl: MOCK_RPC_URL,
134
+ feeToken: MOCK_FEE_TOKEN,
135
+ feePayerWallet: MOCK_WALLET,
136
+ });
137
+ mockFetch.mockClear();
138
+ });
139
+ it('should call getBlockhash, simulateTransaction, estimateTransactionFee, and signAndSendTransaction', async () => {
140
+ // Mock getBlockhash
141
+ mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
142
+ // Mock simulateTransaction (CU estimation)
143
+ mockSimulateResponse();
144
+ // Mock estimateTransactionFee
145
+ mockRpcResponse({
146
+ fee_in_lamports: 5000,
147
+ fee_in_token: 50000,
148
+ signer_pubkey: MOCK_PAYER_ADDRESS,
149
+ payment_address: MOCK_PAYMENT_ADDRESS,
150
+ });
151
+ // Mock signAndSendTransaction
152
+ mockRpcResponse({
153
+ signature: MOCK_SIGNATURE,
154
+ signed_transaction: 'base64signedtx',
155
+ signer_pubkey: MOCK_PAYER_ADDRESS,
156
+ });
157
+ const dummyIx = {
158
+ programAddress: address('11111111111111111111111111111111'),
159
+ accounts: [],
160
+ data: new Uint8Array(4),
161
+ };
162
+ const result = await client.sendTransaction([dummyIx]);
163
+ expect(result.status).toBe('successful');
164
+ expect(result.context.signature).toBe(MOCK_SIGNATURE);
165
+ expect(mockFetch).toHaveBeenCalledTimes(4);
166
+ const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
167
+ expect(calls).toEqual([
168
+ 'getBlockhash',
169
+ 'simulateTransaction',
170
+ 'estimateTransactionFee',
171
+ 'signAndSendTransaction',
172
+ ]);
173
+ });
174
+ it('should skip payment instruction when fee is 0', async () => {
175
+ mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
176
+ mockSimulateResponse();
177
+ mockRpcResponse({
178
+ fee_in_lamports: 0,
179
+ fee_in_token: 0,
180
+ signer_pubkey: MOCK_PAYER_ADDRESS,
181
+ payment_address: MOCK_PAYMENT_ADDRESS,
182
+ });
183
+ // Mock signAndSendTransaction
184
+ mockRpcResponse({
185
+ signature: MOCK_SIGNATURE,
186
+ signed_transaction: 'base64signedtx',
187
+ signer_pubkey: MOCK_PAYER_ADDRESS,
188
+ });
189
+ const dummyIx = {
190
+ programAddress: address('11111111111111111111111111111111'),
191
+ accounts: [],
192
+ data: new Uint8Array(4),
193
+ };
194
+ const result = await client.sendTransaction([dummyIx]);
195
+ expect(result.status).toBe('successful');
196
+ expect(result.context.signature).toBe(MOCK_SIGNATURE);
197
+ });
198
+ it('should propagate fee estimation errors', async () => {
199
+ mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
200
+ mockSimulateResponse();
201
+ mockRpcError(-32602, 'Invalid transaction');
202
+ const dummyIx = {
203
+ programAddress: address('11111111111111111111111111111111'),
204
+ accounts: [],
205
+ data: new Uint8Array(4),
206
+ };
207
+ // Kit's executor wraps errors — the original RPC error is in the cause chain
208
+ await expect(client.sendTransaction([dummyIx])).rejects.toThrow();
209
+ const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
210
+ expect(calls).toContain('estimateTransactionFee');
211
+ });
212
+ it('should propagate signAndSendTransaction errors', async () => {
213
+ mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
214
+ mockSimulateResponse();
215
+ mockRpcResponse({
216
+ fee_in_lamports: 5000,
217
+ fee_in_token: 50000,
218
+ signer_pubkey: MOCK_PAYER_ADDRESS,
219
+ payment_address: MOCK_PAYMENT_ADDRESS,
220
+ });
221
+ mockRpcError(-32003, 'Transaction failed');
222
+ const dummyIx = {
223
+ programAddress: address('11111111111111111111111111111111'),
224
+ accounts: [],
225
+ data: new Uint8Array(4),
226
+ };
227
+ // Kit's executor wraps errors — the original RPC error is the cause
228
+ await expect(client.sendTransaction([dummyIx])).rejects.toThrow();
229
+ // Verify signAndSendTransaction was attempted
230
+ const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
231
+ expect(calls).toContain('signAndSendTransaction');
232
+ });
233
+ });
234
+ describe('planTransaction', () => {
235
+ let client;
236
+ beforeEach(async () => {
237
+ mockRpcResponse({
238
+ signer_address: MOCK_PAYER_ADDRESS,
239
+ payment_address: MOCK_PAYMENT_ADDRESS,
240
+ });
241
+ client = await createKitKoraClient({
242
+ endpoint: MOCK_ENDPOINT,
243
+ rpcUrl: MOCK_RPC_URL,
244
+ feeToken: MOCK_FEE_TOKEN,
245
+ feePayerWallet: MOCK_WALLET,
246
+ });
247
+ mockFetch.mockClear();
248
+ });
249
+ it('should return a transaction message without sending', async () => {
250
+ const dummyIx = {
251
+ programAddress: address('11111111111111111111111111111111'),
252
+ accounts: [],
253
+ data: new Uint8Array(4),
254
+ };
255
+ const result = await client.planTransaction([dummyIx]);
256
+ // Returns a transaction message (has version, instructions, feePayer)
257
+ expect(result).toBeDefined();
258
+ expect('version' in result).toBe(true);
259
+ expect('instructions' in result).toBe(true);
260
+ // Should NOT call any RPC methods (planner is local)
261
+ expect(mockFetch).toHaveBeenCalledTimes(0);
262
+ });
263
+ });
264
+ describe('plugin composition', () => {
265
+ it('should support .use() for extending the client with a Kit plugin', async () => {
266
+ mockRpcResponse({
267
+ signer_address: MOCK_PAYER_ADDRESS,
268
+ payment_address: MOCK_PAYMENT_ADDRESS,
269
+ });
270
+ const client = await createKitKoraClient({
271
+ endpoint: MOCK_ENDPOINT,
272
+ rpcUrl: MOCK_RPC_URL,
273
+ feeToken: MOCK_FEE_TOKEN,
274
+ feePayerWallet: MOCK_WALLET,
275
+ });
276
+ // Kit plugins must spread the client to preserve existing properties
277
+ const extended = client.use((c) => ({
278
+ ...c,
279
+ custom: {
280
+ hello: () => 'world',
281
+ },
282
+ }));
283
+ expect(extended.custom.hello()).toBe('world');
284
+ // Original methods preserved via spread
285
+ expect(extended.kora).toBeDefined();
286
+ expect(typeof extended.sendTransaction).toBe('function');
287
+ expect(typeof extended.planTransaction).toBe('function');
288
+ });
289
+ it('should preserve existing properties when extending via plugin spread', async () => {
290
+ mockRpcResponse({
291
+ signer_address: MOCK_PAYER_ADDRESS,
292
+ payment_address: MOCK_PAYMENT_ADDRESS,
293
+ });
294
+ const client = await createKitKoraClient({
295
+ endpoint: MOCK_ENDPOINT,
296
+ rpcUrl: MOCK_RPC_URL,
297
+ feeToken: MOCK_FEE_TOKEN,
298
+ feePayerWallet: MOCK_WALLET,
299
+ });
300
+ // Plugin that adds extra property (with spread)
301
+ const extended = client.use((c) => ({
302
+ ...c,
303
+ extra: 42,
304
+ }));
305
+ expect(extended.extra).toBe(42);
306
+ expect(extended.payer.address).toBe(MOCK_PAYER_ADDRESS);
307
+ expect(extended.paymentAddress).toBe(MOCK_PAYMENT_ADDRESS);
308
+ });
309
+ });
310
+ describe('auth passthrough', () => {
311
+ it('should pass apiKey to underlying KoraClient', async () => {
312
+ mockRpcResponse({
313
+ signer_address: MOCK_PAYER_ADDRESS,
314
+ payment_address: MOCK_PAYMENT_ADDRESS,
315
+ });
316
+ await createKitKoraClient({
317
+ endpoint: MOCK_ENDPOINT,
318
+ rpcUrl: MOCK_RPC_URL,
319
+ feeToken: MOCK_FEE_TOKEN,
320
+ feePayerWallet: MOCK_WALLET,
321
+ apiKey: 'test-api-key',
322
+ });
323
+ // The init call should include the API key header
324
+ const headers = mockFetch.mock.calls[0][1].headers;
325
+ expect(headers['x-api-key']).toBe('test-api-key');
326
+ });
327
+ it('should pass getRecaptchaToken to underlying KoraClient', async () => {
328
+ const mockGetToken = jest.fn().mockResolvedValue('test-recaptcha-token');
329
+ mockRpcResponse({
330
+ signer_address: MOCK_PAYER_ADDRESS,
331
+ payment_address: MOCK_PAYMENT_ADDRESS,
332
+ });
333
+ await createKitKoraClient({
334
+ endpoint: MOCK_ENDPOINT,
335
+ rpcUrl: MOCK_RPC_URL,
336
+ feeToken: MOCK_FEE_TOKEN,
337
+ feePayerWallet: MOCK_WALLET,
338
+ getRecaptchaToken: mockGetToken,
339
+ });
340
+ // The init call should include the reCAPTCHA token header
341
+ expect(mockGetToken).toHaveBeenCalledTimes(1);
342
+ const headers = mockFetch.mock.calls[0][1].headers;
343
+ expect(headers['x-recaptcha-token']).toBe('test-recaptcha-token');
344
+ });
345
+ });
346
+ describe('Token-2022 support', () => {
347
+ it('should accept tokenProgramId in config', async () => {
348
+ mockRpcResponse({
349
+ signer_address: MOCK_PAYER_ADDRESS,
350
+ payment_address: MOCK_PAYMENT_ADDRESS,
351
+ });
352
+ const TOKEN_2022_PROGRAM_ADDRESS = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
353
+ const client = await createKitKoraClient({
354
+ endpoint: MOCK_ENDPOINT,
355
+ rpcUrl: MOCK_RPC_URL,
356
+ feeToken: MOCK_FEE_TOKEN,
357
+ feePayerWallet: MOCK_WALLET,
358
+ tokenProgramId: TOKEN_2022_PROGRAM_ADDRESS,
359
+ });
360
+ expect(client).toBeDefined();
361
+ expect(typeof client.sendTransaction).toBe('function');
362
+ });
363
+ });
364
+ describe('compute budget instructions', () => {
365
+ const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
366
+ // SetComputeUnitLimit discriminator = 0x02, SetComputeUnitPrice discriminator = 0x03
367
+ const CU_LIMIT_DISCRIMINATOR = 2;
368
+ const CU_PRICE_DISCRIMINATOR = 3;
369
+ const DUMMY_IX = {
370
+ programAddress: address('11111111111111111111111111111111'),
371
+ accounts: [],
372
+ data: new Uint8Array(4),
373
+ };
374
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
375
+ function getComputeBudgetIxs(planned) {
376
+ return planned.instructions.filter((ix) => ix.programAddress === COMPUTE_BUDGET_PROGRAM);
377
+ }
378
+ it('should include provisory CU limit by default (simulation-based estimation)', async () => {
379
+ mockRpcResponse({
380
+ signer_address: MOCK_PAYER_ADDRESS,
381
+ payment_address: MOCK_PAYMENT_ADDRESS,
382
+ });
383
+ const client = await createKitKoraClient({
384
+ endpoint: MOCK_ENDPOINT,
385
+ rpcUrl: MOCK_RPC_URL,
386
+ feeToken: MOCK_FEE_TOKEN,
387
+ feePayerWallet: MOCK_WALLET,
388
+ });
389
+ const planned = await client.planTransaction([DUMMY_IX]);
390
+ const cbIxs = getComputeBudgetIxs(planned);
391
+ expect(cbIxs).toHaveLength(1);
392
+ expect(cbIxs[0].data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
393
+ const units = new DataView(cbIxs[0].data.buffer, cbIxs[0].data.byteOffset).getUint32(1, true);
394
+ expect(units).toBe(0); // Provisory — resolved via simulation in executor
395
+ });
396
+ it('should include SetComputeUnitLimit with correct units when computeUnitLimit is set', async () => {
397
+ mockRpcResponse({
398
+ signer_address: MOCK_PAYER_ADDRESS,
399
+ payment_address: MOCK_PAYMENT_ADDRESS,
400
+ });
401
+ const client = await createKitKoraClient({
402
+ endpoint: MOCK_ENDPOINT,
403
+ rpcUrl: MOCK_RPC_URL,
404
+ feeToken: MOCK_FEE_TOKEN,
405
+ feePayerWallet: MOCK_WALLET,
406
+ computeUnitLimit: 200_000,
407
+ });
408
+ const planned = await client.planTransaction([DUMMY_IX]);
409
+ const cbIxs = getComputeBudgetIxs(planned);
410
+ expect(cbIxs).toHaveLength(1);
411
+ const ix = cbIxs[0];
412
+ // discriminator 0x02 = SetComputeUnitLimit
413
+ expect(ix.data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
414
+ // 200_000 in u32 LE = [0x40, 0x0D, 0x03, 0x00]
415
+ const units = new DataView(ix.data.buffer, ix.data.byteOffset).getUint32(1, true);
416
+ expect(units).toBe(200_000);
417
+ });
418
+ it('should include SetComputeUnitPrice and provisory CU limit when computeUnitPrice is set', async () => {
419
+ mockRpcResponse({
420
+ signer_address: MOCK_PAYER_ADDRESS,
421
+ payment_address: MOCK_PAYMENT_ADDRESS,
422
+ });
423
+ const client = await createKitKoraClient({
424
+ endpoint: MOCK_ENDPOINT,
425
+ rpcUrl: MOCK_RPC_URL,
426
+ feeToken: MOCK_FEE_TOKEN,
427
+ feePayerWallet: MOCK_WALLET,
428
+ computeUnitPrice: 1000n,
429
+ });
430
+ const planned = await client.planTransaction([DUMMY_IX]);
431
+ const cbIxs = getComputeBudgetIxs(planned);
432
+ // Price instruction + provisory CU limit (simulation-based estimation always on)
433
+ expect(cbIxs).toHaveLength(2);
434
+ const priceIx = cbIxs.find(ix => ix.data[0] === CU_PRICE_DISCRIMINATOR);
435
+ expect(priceIx).toBeDefined();
436
+ const ix = priceIx;
437
+ // discriminator 0x03 = SetComputeUnitPrice
438
+ expect(ix.data[0]).toBe(CU_PRICE_DISCRIMINATOR);
439
+ // 1000 in u64 LE
440
+ const view = new DataView(ix.data.buffer, ix.data.byteOffset);
441
+ const microLamports = view.getBigUint64(1, true);
442
+ expect(microLamports).toBe(1000n);
443
+ });
444
+ it('should include both CU limit and price instructions when both are set', async () => {
445
+ mockRpcResponse({
446
+ signer_address: MOCK_PAYER_ADDRESS,
447
+ payment_address: MOCK_PAYMENT_ADDRESS,
448
+ });
449
+ const client = await createKitKoraClient({
450
+ endpoint: MOCK_ENDPOINT,
451
+ rpcUrl: MOCK_RPC_URL,
452
+ feeToken: MOCK_FEE_TOKEN,
453
+ feePayerWallet: MOCK_WALLET,
454
+ computeUnitLimit: 150_000,
455
+ computeUnitPrice: 500n,
456
+ });
457
+ const planned = await client.planTransaction([DUMMY_IX]);
458
+ const cbIxs = getComputeBudgetIxs(planned);
459
+ expect(cbIxs).toHaveLength(2);
460
+ // First should be SetComputeUnitLimit
461
+ expect(cbIxs[0].data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
462
+ const units = new DataView(cbIxs[0].data.buffer, cbIxs[0].data.byteOffset).getUint32(1, true);
463
+ expect(units).toBe(150_000);
464
+ // Second should be SetComputeUnitPrice
465
+ expect(cbIxs[1].data[0]).toBe(CU_PRICE_DISCRIMINATOR);
466
+ const microLamports = new DataView(cbIxs[1].data.buffer, cbIxs[1].data.byteOffset).getBigUint64(1, true);
467
+ expect(microLamports).toBe(500n);
468
+ });
469
+ it('should use explicit computeUnitLimit over simulation when computeUnitLimit is set', async () => {
470
+ mockRpcResponse({
471
+ signer_address: MOCK_PAYER_ADDRESS,
472
+ payment_address: MOCK_PAYMENT_ADDRESS,
473
+ });
474
+ const client = await createKitKoraClient({
475
+ endpoint: MOCK_ENDPOINT,
476
+ rpcUrl: MOCK_RPC_URL,
477
+ feeToken: MOCK_FEE_TOKEN,
478
+ feePayerWallet: MOCK_WALLET,
479
+ computeUnitLimit: 200_000,
480
+ });
481
+ const planned = await client.planTransaction([DUMMY_IX]);
482
+ const cbIxs = getComputeBudgetIxs(planned);
483
+ expect(cbIxs).toHaveLength(1);
484
+ const ix = cbIxs[0];
485
+ expect(ix.data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
486
+ // Should be the explicit 200_000, not 0 (provisory)
487
+ const units = new DataView(ix.data.buffer, ix.data.byteOffset).getUint32(1, true);
488
+ expect(units).toBe(200_000);
489
+ });
490
+ });
491
+ });
@@ -1,4 +1,4 @@
1
- import { Address, Commitment, KeyPairSigner } from '@solana/kit';
1
+ import { Address, KeyPairSigner } from '@solana/kit';
2
2
  import { KoraClient } from '../src/index.js';
3
3
  interface TestSuite {
4
4
  destinationAddress: Address<string>;
@@ -9,14 +9,11 @@ interface TestSuite {
9
9
  usdcMint: Address<string>;
10
10
  }
11
11
  export declare function loadEnvironmentVariables(): {
12
- commitment: Commitment;
13
12
  destinationAddress: Address<string>;
14
13
  koraAddress: Address<string>;
15
14
  koraRpcUrl: string;
16
15
  koraSignerType: string;
17
16
  solDropAmount: bigint;
18
- solanaRpcUrl: string;
19
- solanaWsUrl: string;
20
17
  testUsdcMintSecret: string;
21
18
  testWalletSecret: string;
22
19
  tokenDecimals: number;