@solana/kora 0.2.1 → 0.3.0-beta.2

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,15 +1,61 @@
1
- import { createKitKoraClient } from '../src/index.js';
2
- import setupTestSuite from './setup.js';
3
- import { runAuthenticationTests } from './auth-setup.js';
4
- import { address, generateKeyPairSigner, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, signTransaction, } from '@solana/kit';
1
+ import { address, appendTransactionMessageInstruction, compileTransaction, createClient, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
2
+ import { identity } from '@solana/kit-plugin-signer';
5
3
  import { getTransferSolInstruction } from '@solana-program/system';
6
- import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, parseTransferInstruction, TOKEN_PROGRAM_ADDRESS, tokenProgram, TRANSFER_DISCRIMINATOR, } from '@solana-program/token';
4
+ import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS, tokenProgram, } from '@solana-program/token';
5
+ import { kora } from '../src/kit/index.js';
6
+ import { runAuthenticationTests } from './auth-setup.js';
7
+ import setupTestSuite from './setup.js';
7
8
  function transactionFromBase64(base64) {
8
9
  const encoder = getBase64Encoder();
9
10
  const decoder = getTransactionDecoder();
10
11
  const messageBytes = encoder.encode(base64);
11
12
  return decoder.decode(messageBytes);
12
13
  }
14
+ function transactionToBase64(transaction) {
15
+ const txEncoder = getTransactionEncoder();
16
+ const txBytes = txEncoder.encode(transaction);
17
+ const base64Decoder = getBase64Decoder();
18
+ return base64Decoder.decode(txBytes);
19
+ }
20
+ /**
21
+ * Helper to build a SPL token transfer transaction.
22
+ * This replaces the deprecated transferTransaction endpoint.
23
+ */
24
+ async function buildTokenTransferTransaction(params) {
25
+ const { client, amount, mint, sourceWallet, destinationWallet } = params;
26
+ // Get the payer signer from Kora (fee payer)
27
+ const { signer_address } = await client.getPayerSigner();
28
+ // Get blockhash
29
+ const { blockhash } = await client.getBlockhash();
30
+ // Find source and destination ATAs
31
+ const [sourceAta] = await findAssociatedTokenPda({
32
+ mint,
33
+ owner: sourceWallet.address,
34
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
35
+ });
36
+ const [destinationAta] = await findAssociatedTokenPda({
37
+ mint,
38
+ owner: destinationWallet,
39
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
40
+ });
41
+ // Build transfer instruction
42
+ const transferIx = getTransferInstruction({
43
+ amount,
44
+ authority: sourceWallet,
45
+ destination: destinationAta,
46
+ source: sourceAta,
47
+ });
48
+ // Build transaction message with Kora as fee payer
49
+ // We create a mock signer for the fee payer address since we only need the address
50
+ const feePayerSigner = {
51
+ address: signer_address,
52
+ };
53
+ const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayerSigner(feePayerSigner, tx), tx => setTransactionMessageLifetimeUsingBlockhash({ blockhash: blockhash, lastValidBlockHeight: BigInt(Number.MAX_SAFE_INTEGER) }, tx), tx => appendTransactionMessageInstruction(transferIx, tx));
54
+ // Compile to transaction
55
+ const transaction = compileTransaction(transactionMessage);
56
+ const base64Transaction = getBase64EncodedWireTransaction(transaction);
57
+ return { blockhash: blockhash, transaction: base64Transaction };
58
+ }
13
59
  const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
14
60
  const FREE_PRICING = process.env.FREE_PRICING === 'true';
15
61
  const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
@@ -17,21 +63,15 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
17
63
  let client;
18
64
  let testWallet;
19
65
  let testWalletAddress;
20
- let destinationAddress;
21
66
  let usdcMint;
22
67
  let koraAddress;
23
- let koraRpcUrl;
24
- let authConfig;
25
68
  beforeAll(async () => {
26
69
  const testSuite = await setupTestSuite();
27
70
  client = testSuite.koraClient;
28
71
  testWallet = testSuite.testWallet;
29
72
  testWalletAddress = testWallet.address;
30
- destinationAddress = testSuite.destinationAddress;
31
73
  usdcMint = testSuite.usdcMint;
32
74
  koraAddress = testSuite.koraAddress;
33
- koraRpcUrl = testSuite.koraRpcUrl;
34
- authConfig = testSuite.authConfig;
35
75
  }, 90000); // allow adequate time for airdrops and token initialization
36
76
  // Run authentication tests only when auth is enabled
37
77
  if (AUTH_ENABLED) {
@@ -93,9 +133,9 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
93
133
  expect(config.enabled_methods.get_supported_tokens).toBeDefined();
94
134
  expect(config.enabled_methods.sign_transaction).toBeDefined();
95
135
  expect(config.enabled_methods.sign_and_send_transaction).toBeDefined();
96
- expect(config.enabled_methods.transfer_transaction).toBeDefined();
97
136
  expect(config.enabled_methods.get_blockhash).toBeDefined();
98
137
  expect(config.enabled_methods.get_config).toBeDefined();
138
+ expect(config.enabled_methods.get_version).toBeDefined();
99
139
  });
100
140
  it('should get payer signer', async () => {
101
141
  const { signer_address, payment_address } = await client.getPayerSigner();
@@ -115,54 +155,25 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
115
155
  expect(blockhash.length).toBeGreaterThanOrEqual(43);
116
156
  expect(blockhash.length).toBeLessThanOrEqual(44); // Base58 encoded hash length
117
157
  });
158
+ it('should get version', async () => {
159
+ const { version } = await client.getVersion();
160
+ expect(version).toBeDefined();
161
+ expect(typeof version).toBe('string');
162
+ expect(version.length).toBeGreaterThan(0);
163
+ // Version should follow semver format (e.g., "2.1.0" or "2.1.0-beta.0")
164
+ expect(version).toMatch(/^\d+\.\d+\.\d+/);
165
+ });
118
166
  });
119
167
  describe('Transaction Operations', () => {
120
- it('should create transfer transaction', async () => {
121
- const request = {
122
- amount: 1000000, // 1 USDC
123
- token: usdcMint,
124
- source: testWalletAddress,
125
- destination: destinationAddress,
126
- };
127
- const response = await client.transferTransaction(request);
128
- expect(response).toBeDefined();
129
- expect(response.transaction).toBeDefined();
130
- expect(response.blockhash).toBeDefined();
131
- expect(response.message).toBeDefined();
132
- expect(response.instructions).toBeDefined();
133
- // since setup created ATA for destination, we should not expect ata instruction, only transfer instruction
134
- expect(response.instructions?.length).toBe(1);
135
- expect(response.instructions?.[0].programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
136
- });
137
- it('should create transfer transaction to address with no ATA', async () => {
138
- const randomDestination = await generateKeyPairSigner();
139
- const request = {
140
- amount: 1000000, // 1 USDC
141
- token: usdcMint,
142
- source: testWalletAddress,
143
- destination: randomDestination.address,
144
- };
145
- const response = await client.transferTransaction(request);
146
- expect(response).toBeDefined();
147
- expect(response.transaction).toBeDefined();
148
- expect(response.blockhash).toBeDefined();
149
- expect(response.message).toBeDefined();
150
- expect(response.instructions).toBeDefined();
151
- // since setup created ATA for destination, we should not expect ata instruction, only transfer instruction
152
- expect(response.instructions?.length).toBe(2);
153
- expect(response.instructions?.[0].programAddress).toBe(ASSOCIATED_TOKEN_PROGRAM_ADDRESS);
154
- expect(response.instructions?.[1].programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
155
- });
156
168
  it('should estimate transaction fee', async () => {
157
- // First create a transaction
158
- const transferRequest = {
159
- amount: 1000000,
160
- token: usdcMint,
161
- source: testWalletAddress,
162
- destination: testWalletAddress,
163
- };
164
- const { transaction } = await client.transferTransaction(transferRequest);
165
- const fee = await client.estimateTransactionFee({ transaction, fee_token: usdcMint });
169
+ const { transaction } = await buildTokenTransferTransaction({
170
+ amount: 1000000n,
171
+ client,
172
+ destinationWallet: koraAddress,
173
+ mint: usdcMint,
174
+ sourceWallet: testWallet,
175
+ });
176
+ const fee = await client.estimateTransactionFee({ fee_token: usdcMint, transaction });
166
177
  expect(fee).toBeDefined();
167
178
  expect(typeof fee.fee_in_lamports).toBe('number');
168
179
  expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
@@ -173,15 +184,13 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
173
184
  }
174
185
  });
175
186
  it('should sign transaction', async () => {
176
- const config = await client.getConfig();
177
- const paymentAddress = config.fee_payers[0];
178
- const transferRequest = {
179
- amount: 1000000,
180
- token: usdcMint,
181
- source: testWalletAddress,
182
- destination: paymentAddress,
183
- };
184
- const { transaction } = await client.transferTransaction(transferRequest);
187
+ const { transaction } = await buildTokenTransferTransaction({
188
+ amount: 1000000n,
189
+ client,
190
+ destinationWallet: koraAddress,
191
+ mint: usdcMint,
192
+ sourceWallet: testWallet,
193
+ });
185
194
  const signResult = await client.signTransaction({
186
195
  transaction,
187
196
  });
@@ -189,47 +198,47 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
189
198
  expect(signResult.signed_transaction).toBeDefined();
190
199
  });
191
200
  it('should sign and send transaction', async () => {
192
- const config = await client.getConfig();
193
- const paymentAddress = config.fee_payers[0];
194
- const transferRequest = {
195
- amount: 1000000,
196
- token: usdcMint,
197
- source: testWalletAddress,
198
- destination: paymentAddress,
199
- };
200
- const { transaction: transactionString } = await client.transferTransaction(transferRequest);
201
+ const { transaction: transactionString } = await buildTokenTransferTransaction({
202
+ amount: 1000000n,
203
+ client,
204
+ destinationWallet: koraAddress,
205
+ mint: usdcMint,
206
+ sourceWallet: testWallet,
207
+ });
201
208
  const transaction = transactionFromBase64(transactionString);
202
- // Sign transaction with test wallet before sending
203
- const signedTransaction = await signTransaction([testWallet.keyPair], transaction);
204
- const base64SignedTransaction = getBase64EncodedWireTransaction(signedTransaction);
209
+ // Partially sign transaction with test wallet before sending
210
+ // Kora will add fee payer signature via signAndSendTransaction
211
+ const signedTransaction = await partiallySignTransaction([testWallet.keyPair], transaction);
212
+ const base64SignedTransaction = transactionToBase64(signedTransaction);
205
213
  const signResult = await client.signAndSendTransaction({
206
214
  transaction: base64SignedTransaction,
207
215
  });
208
216
  expect(signResult).toBeDefined();
209
217
  expect(signResult.signed_transaction).toBeDefined();
210
- });
218
+ expect(signResult.signature).toBeDefined();
219
+ }, 30000);
211
220
  it('should get payment instruction', async () => {
212
- const transferRequest = {
213
- amount: 1000000,
214
- token: usdcMint,
215
- source: testWalletAddress,
216
- destination: destinationAddress,
217
- };
221
+ const { transaction } = await buildTokenTransferTransaction({
222
+ amount: 1000000n,
223
+ client,
224
+ destinationWallet: koraAddress,
225
+ mint: usdcMint,
226
+ sourceWallet: testWallet,
227
+ });
218
228
  const [expectedSenderAta] = await findAssociatedTokenPda({
229
+ mint: usdcMint,
219
230
  owner: testWalletAddress,
220
231
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
221
- mint: usdcMint,
222
232
  });
223
233
  const [koraAta] = await findAssociatedTokenPda({
234
+ mint: usdcMint,
224
235
  owner: koraAddress,
225
236
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
226
- mint: usdcMint,
227
237
  });
228
- const { transaction } = await client.transferTransaction(transferRequest);
229
- const { payment_instruction, payment_amount, payment_token, payment_address, signer_address, original_transaction, } = await client.getPaymentInstruction({
230
- transaction,
238
+ const { payment_instruction, payment_amount: _payment_amount, payment_token, payment_address, signer_address, original_transaction, } = await client.getPaymentInstruction({
231
239
  fee_token: usdcMint,
232
240
  source_wallet: testWalletAddress,
241
+ transaction,
233
242
  });
234
243
  expect(payment_instruction).toBeDefined();
235
244
  expect(payment_instruction.programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
@@ -244,228 +253,105 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
244
253
  expect(original_transaction).toBe(transaction);
245
254
  });
246
255
  });
247
- describe('Error Handling', () => {
248
- it('should handle invalid token address', async () => {
249
- const request = {
250
- amount: 1000000,
251
- token: 'InvalidTokenAddress',
252
- source: testWalletAddress,
253
- destination: destinationAddress,
254
- };
255
- await expect(client.transferTransaction(request)).rejects.toThrow();
256
- });
257
- it('should handle invalid amount', async () => {
258
- const request = {
259
- amount: -1, // Invalid amount
260
- token: usdcMint,
261
- source: testWalletAddress,
262
- destination: destinationAddress,
263
- };
264
- await expect(client.transferTransaction(request)).rejects.toThrow();
265
- });
266
- it('should handle invalid transaction for signing', async () => {
267
- await expect(client.signTransaction({
268
- transaction: 'invalid_transaction',
269
- })).rejects.toThrow();
270
- });
271
- it('should handle invalid transaction for fee estimation', async () => {
272
- await expect(client.estimateTransactionFee({ transaction: 'invalid_transaction', fee_token: usdcMint })).rejects.toThrow();
273
- });
274
- it('should handle non-allowed token for fee payment', async () => {
275
- const transferRequest = {
276
- amount: 1000000,
277
- token: usdcMint,
278
- source: testWalletAddress,
279
- destination: destinationAddress,
280
- };
281
- // TODO: API has an error. this endpoint should verify the provided fee token is supported
282
- const { transaction } = await client.transferTransaction(transferRequest);
283
- const fee = await client.estimateTransactionFee({ transaction, fee_token: usdcMint });
284
- expect(fee).toBeDefined();
285
- expect(typeof fee.fee_in_lamports).toBe('number');
286
- expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
287
- if (!FREE_PRICING) {
288
- expect(fee.fee_in_lamports).toBeGreaterThan(0);
289
- }
290
- });
291
- });
292
- describe('End-to-End Flows', () => {
293
- it('should handle transfer and sign flow', async () => {
294
- const config = await client.getConfig();
295
- const paymentAddress = config.fee_payers[0];
296
- const request = {
297
- amount: 1000000,
298
- token: usdcMint,
299
- source: testWalletAddress,
300
- destination: paymentAddress,
301
- };
302
- // Create and sign the transaction
303
- const { transaction } = await client.transferTransaction(request);
304
- const signResult = await client.signTransaction({ transaction });
305
- expect(signResult.signed_transaction).toBeDefined();
306
- });
307
- it('should reject transaction with non-allowed token', async () => {
308
- const invalidTokenMint = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; // Mainnet USDC mint
309
- const request = {
310
- amount: 1000000,
311
- token: invalidTokenMint,
312
- source: testWalletAddress,
313
- destination: destinationAddress,
314
- };
315
- await expect(client.transferTransaction(request)).rejects.toThrow();
316
- });
317
- });
318
- describe('Kit Client (createKitKoraClient)', () => {
319
- let kitClient;
320
- beforeAll(async () => {
321
- kitClient = await createKitKoraClient({
322
- endpoint: koraRpcUrl,
323
- rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
324
- feeToken: usdcMint,
325
- feePayerWallet: testWallet,
326
- ...authConfig,
256
+ // Bundle tests require bundle.enabled = true in the Kora config
257
+ (FREE_PRICING ? describe.skip : describe)('Bundle Operations', () => {
258
+ it('should sign bundle of transactions', async () => {
259
+ // Create two transfer transactions for the bundle
260
+ const { transaction: tx1String } = await buildTokenTransferTransaction({
261
+ amount: 1000000n,
262
+ client,
263
+ destinationWallet: koraAddress,
264
+ mint: usdcMint,
265
+ sourceWallet: testWallet,
327
266
  });
328
- }, 30000);
329
- it('should initialize with correct payer info', () => {
330
- expect(kitClient.payer).toBeDefined();
331
- expect(kitClient.payer.address).toBeDefined();
332
- expect(kitClient.paymentAddress).toBeDefined();
333
- });
334
- it('should expose kora namespace', async () => {
335
- const config = await kitClient.kora.getConfig();
336
- expect(config.fee_payers.length).toBeGreaterThan(0);
337
- });
338
- it('should plan a transaction without sending', async () => {
339
- const ix = getTransferSolInstruction({
340
- source: testWallet,
341
- destination: address(destinationAddress),
342
- amount: 1000, // 1000 lamports
267
+ const { transaction: tx2String } = await buildTokenTransferTransaction({
268
+ amount: 500000n,
269
+ client,
270
+ destinationWallet: koraAddress,
271
+ mint: usdcMint,
272
+ sourceWallet: testWallet,
343
273
  });
344
- const message = await kitClient.planTransaction([ix]);
345
- expect(message).toBeDefined();
346
- expect('version' in message).toBe(true);
347
- expect('instructions' in message).toBe(true);
348
- });
349
- it('should send a transaction end-to-end', async () => {
350
- const ix = getTransferSolInstruction({
351
- source: testWallet,
352
- destination: address(destinationAddress),
353
- amount: 1000, // 1000 lamports
274
+ // Partially sign both transactions with test wallet
275
+ const tx1 = transactionFromBase64(tx1String);
276
+ const tx2 = transactionFromBase64(tx2String);
277
+ const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
278
+ const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
279
+ const base64Tx1 = transactionToBase64(signedTx1);
280
+ const base64Tx2 = transactionToBase64(signedTx2);
281
+ const result = await client.signBundle({
282
+ transactions: [base64Tx1, base64Tx2],
354
283
  });
355
- const result = await kitClient.sendTransaction([ix]);
356
- expect(result.status).toBe('successful');
357
- expect(result.context.signature).toBeDefined();
358
- expect(typeof result.context.signature).toBe('string');
359
- // Signature should be base58-encoded (43-88 chars)
360
- expect(result.context.signature.length).toBeGreaterThanOrEqual(43);
361
- }, 30000);
362
- it('should support plugin composition via .use()', () => {
363
- // Kit plugins must spread the client to preserve existing properties
364
- const extended = kitClient.use((c) => ({
365
- ...c,
366
- custom: { hello: () => 'world' },
367
- }));
368
- expect(extended.custom.hello()).toBe('world');
369
- expect(extended.kora).toBeDefined();
370
- expect(typeof extended.sendTransaction).toBe('function');
284
+ expect(result).toBeDefined();
285
+ expect(result.signed_transactions).toBeDefined();
286
+ expect(Array.isArray(result.signed_transactions)).toBe(true);
287
+ expect(result.signed_transactions.length).toBe(2);
288
+ expect(result.signer_pubkey).toBeDefined();
371
289
  });
372
- describe('planner', () => {
373
- it('should include placeholder payment instruction in planned message', async () => {
374
- const ix = getTransferSolInstruction({
375
- source: testWallet,
376
- destination: address(destinationAddress),
377
- amount: 1000,
378
- });
379
- const message = await kitClient.planTransaction([ix]);
380
- const instructions = message.instructions;
381
- // Find the transfer placeholder by discriminator + token program
382
- const paymentIx = instructions.find(ix => ix.programAddress === TOKEN_PROGRAM_ADDRESS && ix.data?.[0] === TRANSFER_DISCRIMINATOR);
383
- expect(paymentIx).toBeDefined();
384
- // Verify it's a placeholder (amount=0) with correct accounts
385
- const parsed = parseTransferInstruction(paymentIx);
386
- expect(parsed.data.amount).toBe(0n);
387
- expect(parsed.accounts.authority.address).toBe(testWallet.address);
290
+ it('should sign and send bundle of transactions', async () => {
291
+ // Create two transfer transactions for the bundle
292
+ const { transaction: tx1String } = await buildTokenTransferTransaction({
293
+ amount: 1000000n,
294
+ client,
295
+ destinationWallet: koraAddress,
296
+ mint: usdcMint,
297
+ sourceWallet: testWallet,
388
298
  });
389
- it('should include user instructions in planned message', async () => {
390
- const ix = getTransferSolInstruction({
391
- source: testWallet,
392
- destination: address(destinationAddress),
393
- amount: 1000,
394
- });
395
- const message = await kitClient.planTransaction([ix]);
396
- const instructions = message.instructions;
397
- // The user's system transfer instruction should be present
398
- const systemIx = instructions.find(ix => ix.programAddress === '11111111111111111111111111111111');
399
- expect(systemIx).toBeDefined();
299
+ const { transaction: tx2String } = await buildTokenTransferTransaction({
300
+ amount: 500000n,
301
+ client,
302
+ destinationWallet: koraAddress,
303
+ mint: usdcMint,
304
+ sourceWallet: testWallet,
400
305
  });
306
+ // Partially sign both transactions with test wallet
307
+ const tx1 = transactionFromBase64(tx1String);
308
+ const tx2 = transactionFromBase64(tx2String);
309
+ const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
310
+ const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
311
+ const base64Tx1 = transactionToBase64(signedTx1);
312
+ const base64Tx2 = transactionToBase64(signedTx2);
313
+ const result = await client.signAndSendBundle({
314
+ transactions: [base64Tx1, base64Tx2],
315
+ });
316
+ expect(result).toBeDefined();
317
+ expect(result.signed_transactions).toBeDefined();
318
+ expect(Array.isArray(result.signed_transactions)).toBe(true);
319
+ expect(result.signed_transactions.length).toBe(2);
320
+ expect(result.signer_pubkey).toBeDefined();
321
+ expect(result.bundle_uuid).toBeDefined();
322
+ expect(typeof result.bundle_uuid).toBe('string');
323
+ }, 30000);
324
+ });
325
+ describe('Error Handling', () => {
326
+ it('should handle invalid transaction for signing', async () => {
327
+ await expect(client.signTransaction({
328
+ transaction: 'invalid_transaction',
329
+ })).rejects.toThrow();
401
330
  });
402
- describe('executor', () => {
403
- it('should resolve placeholder with non-zero fee amount', async () => {
404
- // The test config uses margin pricing (margin=0.0), so fee = base SOL fee converted to token
405
- // This verifies the full planner→executor flow: placeholder→estimate→update→send
406
- const ix = getTransferSolInstruction({
407
- source: testWallet,
408
- destination: address(destinationAddress),
409
- amount: 1000,
410
- });
411
- const result = await kitClient.sendTransaction([ix]);
412
- expect(result.status).toBe('successful');
413
- expect(result.context.signature).toBeDefined();
414
- // Verify fee was estimated by checking the Kora RPC was called
415
- // (the transaction succeeded, which means the payment IX had a valid amount)
416
- }, 30000);
417
- it('should handle multiple instructions in a single transaction', async () => {
418
- const ix1 = getTransferSolInstruction({
419
- source: testWallet,
420
- destination: address(destinationAddress),
421
- amount: 500,
422
- });
423
- const ix2 = getTransferSolInstruction({
424
- source: testWallet,
425
- destination: address(destinationAddress),
426
- amount: 500,
427
- });
428
- const result = await kitClient.sendTransaction([ix1, ix2]);
429
- expect(result.status).toBe('successful');
430
- expect(result.context.signature).toBeDefined();
431
- }, 30000);
432
- });
433
- describe('plugin composition with tokenProgram()', () => {
434
- it('should send a token transfer via tokenProgram().transferToATA().send()', async () => {
435
- // tokenProgram() works out of the box — rpc is included in the Kora client
436
- const tokenClient = kitClient.use(tokenProgram());
437
- // Transfer USDC to destination via the token plugin's fluent API.
438
- // This flows through Kora's planner (placeholder) → executor (fee estimate, resolve, send).
439
- const result = await tokenClient.token.instructions
440
- .transferToATA({
441
- authority: testWallet,
442
- recipient: destinationAddress,
443
- mint: usdcMint,
444
- amount: 1000,
445
- decimals: 6,
446
- })
447
- .sendTransaction();
448
- expect(result.status).toBe('successful');
449
- expect(result.context.signature).toBeDefined();
450
- }, 30000);
331
+ it('should handle invalid transaction for fee estimation', async () => {
332
+ await expect(client.estimateTransactionFee({ fee_token: usdcMint, transaction: 'invalid_transaction' })).rejects.toThrow();
451
333
  });
452
334
  });
453
335
  if (FREE_PRICING) {
454
336
  describe('Kit Client (free pricing)', () => {
455
337
  let freeClient;
456
- beforeAll(async () => {
457
- freeClient = await createKitKoraClient({
338
+ async function buildFreeClient() {
339
+ const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
340
+ return createClient()
341
+ .use(identity(testWallet))
342
+ .use(kora({
458
343
  endpoint: koraRpcUrl,
459
344
  rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
460
345
  feeToken: usdcMint,
461
- feePayerWallet: testWallet,
462
- ...authConfig,
463
- });
346
+ }));
347
+ }
348
+ beforeAll(async () => {
349
+ freeClient = await buildFreeClient();
464
350
  }, 30000);
465
351
  it('should send transaction without payment instruction when fee is 0', async () => {
466
352
  const ix = getTransferSolInstruction({
467
353
  source: testWallet,
468
- destination: address(destinationAddress),
354
+ destination: address(koraAddress),
469
355
  amount: 1000,
470
356
  });
471
357
  const result = await freeClient.sendTransaction([ix]);
@@ -473,18 +359,57 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
473
359
  expect(result.context.signature).toBeDefined();
474
360
  }, 30000);
475
361
  it('should strip placeholder from planned message when fee is 0', async () => {
476
- // With free pricing, getPayerSigner may not return a payment address.
477
- // If it does, the placeholder should still be stripped in the executor.
478
362
  const ix = getTransferSolInstruction({
479
363
  source: testWallet,
480
- destination: address(destinationAddress),
364
+ destination: address(koraAddress),
481
365
  amount: 1000,
482
366
  });
483
- // Sending should succeed regardless — either no placeholder is added,
484
- // or it's added then stripped when fee estimation returns 0.
485
367
  const result = await freeClient.sendTransaction([ix]);
486
368
  expect(result.status).toBe('successful');
487
369
  }, 30000);
488
370
  });
489
371
  }
372
+ (FREE_PRICING ? describe.skip : describe)('Kit Client (standard pricing)', () => {
373
+ let paidClient;
374
+ async function buildPaidClient() {
375
+ const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
376
+ return createClient()
377
+ .use(identity(testWallet))
378
+ .use(kora({
379
+ endpoint: koraRpcUrl,
380
+ rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
381
+ feeToken: usdcMint,
382
+ ...(AUTH_ENABLED && {
383
+ apiKey: process.env.KORA_API_KEY || 'test-api-key-123',
384
+ hmacSecret: process.env.KORA_HMAC_SECRET || 'test-hmac-secret-456',
385
+ }),
386
+ }))
387
+ .use(tokenProgram());
388
+ }
389
+ beforeAll(async () => {
390
+ paidClient = await buildPaidClient();
391
+ }, 30000);
392
+ it('should send SPL token transfer with paid fee via kit client', async () => {
393
+ const [sourceAta] = await findAssociatedTokenPda({
394
+ mint: usdcMint,
395
+ owner: testWallet.address,
396
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
397
+ });
398
+ const [destinationAta] = await findAssociatedTokenPda({
399
+ mint: usdcMint,
400
+ owner: koraAddress,
401
+ tokenProgram: TOKEN_PROGRAM_ADDRESS,
402
+ });
403
+ const result = await paidClient.token.instructions
404
+ .transfer({
405
+ amount: 1000000n,
406
+ authority: testWallet,
407
+ destination: destinationAta,
408
+ source: sourceAta,
409
+ })
410
+ .sendTransaction();
411
+ expect(result.status).toBe('successful');
412
+ expect(result.context.signature).toBeDefined();
413
+ }, 30000);
414
+ });
490
415
  });