@solana/kora 0.2.0-beta.6 → 0.2.1

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.
@@ -5,34 +5,34 @@ export function runAuthenticationTests() {
5
5
  describe('Authentication', () => {
6
6
  it('should fail with incorrect API key', async () => {
7
7
  const client = new KoraClient({
8
- apiKey: 'WRONG-API-KEY',
9
8
  rpcUrl: koraRpcUrl,
9
+ apiKey: 'WRONG-API-KEY',
10
10
  });
11
11
  // Auth failure should result in an error (empty response body causes JSON parse error)
12
12
  await expect(client.getConfig()).rejects.toThrow();
13
13
  });
14
14
  it('should fail with incorrect HMAC secret', async () => {
15
15
  const client = new KoraClient({
16
- hmacSecret: 'WRONG-HMAC-SECRET',
17
16
  rpcUrl: koraRpcUrl,
17
+ hmacSecret: 'WRONG-HMAC-SECRET',
18
18
  });
19
19
  // Auth failure should result in an error
20
20
  await expect(client.getConfig()).rejects.toThrow();
21
21
  });
22
22
  it('should fail with both incorrect credentials', async () => {
23
23
  const client = new KoraClient({
24
+ rpcUrl: koraRpcUrl,
24
25
  apiKey: 'WRONG-API-KEY',
25
26
  hmacSecret: 'WRONG-HMAC-SECRET',
26
- rpcUrl: koraRpcUrl,
27
27
  });
28
28
  // Auth failure should result in an error
29
29
  await expect(client.getConfig()).rejects.toThrow();
30
30
  });
31
31
  it('should succeed with correct credentials', async () => {
32
32
  const client = new KoraClient({
33
+ rpcUrl: koraRpcUrl,
33
34
  apiKey: 'test-api-key-123',
34
35
  hmacSecret: 'test-hmac-secret-456',
35
- rpcUrl: koraRpcUrl,
36
36
  });
37
37
  const config = await client.getConfig();
38
38
  expect(config).toBeDefined();
@@ -1,60 +1,15 @@
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';
3
- import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
4
1
  import { createKitKoraClient } from '../src/index.js';
5
- import { runAuthenticationTests } from './auth-setup.js';
6
2
  import setupTestSuite from './setup.js';
3
+ import { runAuthenticationTests } from './auth-setup.js';
4
+ import { address, generateKeyPairSigner, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, signTransaction, } from '@solana/kit';
5
+ import { getTransferSolInstruction } from '@solana-program/system';
6
+ import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, parseTransferInstruction, TOKEN_PROGRAM_ADDRESS, tokenProgram, TRANSFER_DISCRIMINATOR, } from '@solana-program/token';
7
7
  function transactionFromBase64(base64) {
8
8
  const encoder = getBase64Encoder();
9
9
  const decoder = getTransactionDecoder();
10
10
  const messageBytes = encoder.encode(base64);
11
11
  return decoder.decode(messageBytes);
12
12
  }
13
- function transactionToBase64(transaction) {
14
- const txEncoder = getTransactionEncoder();
15
- const txBytes = txEncoder.encode(transaction);
16
- const base64Decoder = getBase64Decoder();
17
- return base64Decoder.decode(txBytes);
18
- }
19
- /**
20
- * Helper to build a SPL token transfer transaction.
21
- * This replaces the deprecated transferTransaction endpoint.
22
- */
23
- async function buildTokenTransferTransaction(params) {
24
- const { client, amount, mint, sourceWallet, destinationWallet } = params;
25
- // Get the payer signer from Kora (fee payer)
26
- const { signer_address } = await client.getPayerSigner();
27
- // Get blockhash
28
- const { blockhash } = await client.getBlockhash();
29
- // Find source and destination ATAs
30
- const [sourceAta] = await findAssociatedTokenPda({
31
- mint,
32
- owner: sourceWallet.address,
33
- tokenProgram: TOKEN_PROGRAM_ADDRESS,
34
- });
35
- const [destinationAta] = await findAssociatedTokenPda({
36
- mint,
37
- owner: destinationWallet,
38
- tokenProgram: TOKEN_PROGRAM_ADDRESS,
39
- });
40
- // Build transfer instruction
41
- const transferIx = getTransferInstruction({
42
- amount,
43
- authority: sourceWallet,
44
- destination: destinationAta,
45
- source: sourceAta,
46
- });
47
- // Build transaction message with Kora as fee payer
48
- // We create a mock signer for the fee payer address since we only need the address
49
- const feePayerSigner = {
50
- address: signer_address,
51
- };
52
- 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));
53
- // Compile to transaction
54
- const transaction = compileTransaction(transactionMessage);
55
- const base64Transaction = getBase64EncodedWireTransaction(transaction);
56
- return { blockhash: blockhash, transaction: base64Transaction };
57
- }
58
13
  const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
59
14
  const FREE_PRICING = process.env.FREE_PRICING === 'true';
60
15
  const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
@@ -62,15 +17,21 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
62
17
  let client;
63
18
  let testWallet;
64
19
  let testWalletAddress;
20
+ let destinationAddress;
65
21
  let usdcMint;
66
22
  let koraAddress;
23
+ let koraRpcUrl;
24
+ let authConfig;
67
25
  beforeAll(async () => {
68
26
  const testSuite = await setupTestSuite();
69
27
  client = testSuite.koraClient;
70
28
  testWallet = testSuite.testWallet;
71
29
  testWalletAddress = testWallet.address;
30
+ destinationAddress = testSuite.destinationAddress;
72
31
  usdcMint = testSuite.usdcMint;
73
32
  koraAddress = testSuite.koraAddress;
33
+ koraRpcUrl = testSuite.koraRpcUrl;
34
+ authConfig = testSuite.authConfig;
74
35
  }, 90000); // allow adequate time for airdrops and token initialization
75
36
  // Run authentication tests only when auth is enabled
76
37
  if (AUTH_ENABLED) {
@@ -132,9 +93,9 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
132
93
  expect(config.enabled_methods.get_supported_tokens).toBeDefined();
133
94
  expect(config.enabled_methods.sign_transaction).toBeDefined();
134
95
  expect(config.enabled_methods.sign_and_send_transaction).toBeDefined();
96
+ expect(config.enabled_methods.transfer_transaction).toBeDefined();
135
97
  expect(config.enabled_methods.get_blockhash).toBeDefined();
136
98
  expect(config.enabled_methods.get_config).toBeDefined();
137
- expect(config.enabled_methods.get_version).toBeDefined();
138
99
  });
139
100
  it('should get payer signer', async () => {
140
101
  const { signer_address, payment_address } = await client.getPayerSigner();
@@ -154,25 +115,54 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
154
115
  expect(blockhash.length).toBeGreaterThanOrEqual(43);
155
116
  expect(blockhash.length).toBeLessThanOrEqual(44); // Base58 encoded hash length
156
117
  });
157
- it('should get version', async () => {
158
- const { version } = await client.getVersion();
159
- expect(version).toBeDefined();
160
- expect(typeof version).toBe('string');
161
- expect(version.length).toBeGreaterThan(0);
162
- // Version should follow semver format (e.g., "2.1.0" or "2.1.0-beta.0")
163
- expect(version).toMatch(/^\d+\.\d+\.\d+/);
164
- });
165
118
  });
166
119
  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
+ });
167
156
  it('should estimate transaction fee', async () => {
168
- const { transaction } = await buildTokenTransferTransaction({
169
- amount: 1000000n,
170
- client,
171
- destinationWallet: koraAddress,
172
- mint: usdcMint,
173
- sourceWallet: testWallet,
174
- });
175
- const fee = await client.estimateTransactionFee({ fee_token: usdcMint, transaction });
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 });
176
166
  expect(fee).toBeDefined();
177
167
  expect(typeof fee.fee_in_lamports).toBe('number');
178
168
  expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
@@ -183,13 +173,15 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
183
173
  }
184
174
  });
185
175
  it('should sign transaction', async () => {
186
- const { transaction } = await buildTokenTransferTransaction({
187
- amount: 1000000n,
188
- client,
189
- destinationWallet: koraAddress,
190
- mint: usdcMint,
191
- sourceWallet: testWallet,
192
- });
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);
193
185
  const signResult = await client.signTransaction({
194
186
  transaction,
195
187
  });
@@ -197,47 +189,47 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
197
189
  expect(signResult.signed_transaction).toBeDefined();
198
190
  });
199
191
  it('should sign and send transaction', async () => {
200
- const { transaction: transactionString } = await buildTokenTransferTransaction({
201
- amount: 1000000n,
202
- client,
203
- destinationWallet: koraAddress,
204
- mint: usdcMint,
205
- sourceWallet: testWallet,
206
- });
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);
207
201
  const transaction = transactionFromBase64(transactionString);
208
- // Partially sign transaction with test wallet before sending
209
- // Kora will add fee payer signature via signAndSendTransaction
210
- const signedTransaction = await partiallySignTransaction([testWallet.keyPair], transaction);
211
- const base64SignedTransaction = transactionToBase64(signedTransaction);
202
+ // Sign transaction with test wallet before sending
203
+ const signedTransaction = await signTransaction([testWallet.keyPair], transaction);
204
+ const base64SignedTransaction = getBase64EncodedWireTransaction(signedTransaction);
212
205
  const signResult = await client.signAndSendTransaction({
213
206
  transaction: base64SignedTransaction,
214
207
  });
215
208
  expect(signResult).toBeDefined();
216
209
  expect(signResult.signed_transaction).toBeDefined();
217
- expect(signResult.signature).toBeDefined();
218
210
  });
219
211
  it('should get payment instruction', async () => {
220
- const { transaction } = await buildTokenTransferTransaction({
221
- amount: 1000000n,
222
- client,
223
- destinationWallet: koraAddress,
224
- mint: usdcMint,
225
- sourceWallet: testWallet,
226
- });
212
+ const transferRequest = {
213
+ amount: 1000000,
214
+ token: usdcMint,
215
+ source: testWalletAddress,
216
+ destination: destinationAddress,
217
+ };
227
218
  const [expectedSenderAta] = await findAssociatedTokenPda({
228
- mint: usdcMint,
229
219
  owner: testWalletAddress,
230
220
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
221
+ mint: usdcMint,
231
222
  });
232
223
  const [koraAta] = await findAssociatedTokenPda({
233
- mint: usdcMint,
234
224
  owner: koraAddress,
235
225
  tokenProgram: TOKEN_PROGRAM_ADDRESS,
226
+ mint: usdcMint,
236
227
  });
237
- const { payment_instruction, payment_amount: _payment_amount, payment_token, payment_address, signer_address, original_transaction, } = await client.getPaymentInstruction({
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
231
  fee_token: usdcMint,
239
232
  source_wallet: testWalletAddress,
240
- transaction,
241
233
  });
242
234
  expect(payment_instruction).toBeDefined();
243
235
  expect(payment_instruction.programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
@@ -252,101 +244,228 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
252
244
  expect(original_transaction).toBe(transaction);
253
245
  });
254
246
  });
255
- // Bundle tests require bundle.enabled = true in the Kora config
256
- (FREE_PRICING ? describe.skip : describe)('Bundle Operations', () => {
257
- it('should sign bundle of transactions', async () => {
258
- // Create two transfer transactions for the bundle
259
- const { transaction: tx1String } = await buildTokenTransferTransaction({
260
- amount: 1000000n,
261
- client,
262
- destinationWallet: koraAddress,
263
- mint: usdcMint,
264
- sourceWallet: testWallet,
265
- });
266
- const { transaction: tx2String } = await buildTokenTransferTransaction({
267
- amount: 500000n,
268
- client,
269
- destinationWallet: koraAddress,
270
- mint: usdcMint,
271
- sourceWallet: testWallet,
272
- });
273
- // Partially sign both transactions with test wallet
274
- const tx1 = transactionFromBase64(tx1String);
275
- const tx2 = transactionFromBase64(tx2String);
276
- const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
277
- const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
278
- const base64Tx1 = transactionToBase64(signedTx1);
279
- const base64Tx2 = transactionToBase64(signedTx2);
280
- const result = await client.signBundle({
281
- transactions: [base64Tx1, base64Tx2],
282
- });
283
- expect(result).toBeDefined();
284
- expect(result.signed_transactions).toBeDefined();
285
- expect(Array.isArray(result.signed_transactions)).toBe(true);
286
- expect(result.signed_transactions.length).toBe(2);
287
- expect(result.signer_pubkey).toBeDefined();
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();
288
256
  });
289
- it('should sign and send bundle of transactions', async () => {
290
- // Create two transfer transactions for the bundle
291
- const { transaction: tx1String } = await buildTokenTransferTransaction({
292
- amount: 1000000n,
293
- client,
294
- destinationWallet: koraAddress,
295
- mint: usdcMint,
296
- sourceWallet: testWallet,
297
- });
298
- const { transaction: tx2String } = await buildTokenTransferTransaction({
299
- amount: 500000n,
300
- client,
301
- destinationWallet: koraAddress,
302
- mint: usdcMint,
303
- sourceWallet: testWallet,
304
- });
305
- // Partially sign both transactions with test wallet
306
- const tx1 = transactionFromBase64(tx1String);
307
- const tx2 = transactionFromBase64(tx2String);
308
- const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
309
- const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
310
- const base64Tx1 = transactionToBase64(signedTx1);
311
- const base64Tx2 = transactionToBase64(signedTx2);
312
- const result = await client.signAndSendBundle({
313
- transactions: [base64Tx1, base64Tx2],
314
- });
315
- expect(result).toBeDefined();
316
- expect(result.signed_transactions).toBeDefined();
317
- expect(Array.isArray(result.signed_transactions)).toBe(true);
318
- expect(result.signed_transactions.length).toBe(2);
319
- expect(result.signer_pubkey).toBeDefined();
320
- expect(result.bundle_uuid).toBeDefined();
321
- expect(typeof result.bundle_uuid).toBe('string');
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();
322
265
  });
323
- });
324
- describe('Error Handling', () => {
325
266
  it('should handle invalid transaction for signing', async () => {
326
267
  await expect(client.signTransaction({
327
268
  transaction: 'invalid_transaction',
328
269
  })).rejects.toThrow();
329
270
  });
330
271
  it('should handle invalid transaction for fee estimation', async () => {
331
- await expect(client.estimateTransactionFee({ fee_token: usdcMint, transaction: 'invalid_transaction' })).rejects.toThrow();
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,
327
+ });
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
343
+ });
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
354
+ });
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');
371
+ });
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);
388
+ });
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();
400
+ });
401
+ });
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);
332
451
  });
333
452
  });
334
453
  if (FREE_PRICING) {
335
454
  describe('Kit Client (free pricing)', () => {
336
455
  let freeClient;
337
456
  beforeAll(async () => {
338
- const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
339
457
  freeClient = await createKitKoraClient({
340
458
  endpoint: koraRpcUrl,
341
459
  rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
342
460
  feeToken: usdcMint,
343
461
  feePayerWallet: testWallet,
462
+ ...authConfig,
344
463
  });
345
464
  }, 30000);
346
465
  it('should send transaction without payment instruction when fee is 0', async () => {
347
466
  const ix = getTransferSolInstruction({
348
467
  source: testWallet,
349
- destination: address(koraAddress),
468
+ destination: address(destinationAddress),
350
469
  amount: 1000,
351
470
  });
352
471
  const result = await freeClient.sendTransaction([ix]);
@@ -354,11 +473,15 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
354
473
  expect(result.context.signature).toBeDefined();
355
474
  }, 30000);
356
475
  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.
357
478
  const ix = getTransferSolInstruction({
358
479
  source: testWallet,
359
- destination: address(koraAddress),
480
+ destination: address(destinationAddress),
360
481
  amount: 1000,
361
482
  });
483
+ // Sending should succeed regardless — either no placeholder is added,
484
+ // or it's added then stripped when fee estimation returns 0.
362
485
  const result = await freeClient.sendTransaction([ix]);
363
486
  expect(result.status).toBe('successful');
364
487
  }, 30000);
@@ -324,24 +324,6 @@ describe('createKitKoraClient', () => {
324
324
  const headers = mockFetch.mock.calls[0][1].headers;
325
325
  expect(headers['x-api-key']).toBe('test-api-key');
326
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
327
  });
346
328
  describe('Token-2022 support', () => {
347
329
  it('should accept tokenProgramId in config', async () => {