@solana/kora 0.2.0-beta.6 → 0.3.0-beta.0

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,4 +1,4 @@
1
- import { assertIsAddress, createNoopSigner } from '@solana/kit';
1
+ import { assertIsAddress, isTransactionSigner } from '@solana/kit';
2
2
  import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
3
3
  import crypto from 'crypto';
4
4
  /**
@@ -303,7 +303,9 @@ export class KoraClient {
303
303
  * ```
304
304
  */
305
305
  async getPaymentInstruction({ transaction, fee_token, source_wallet, token_program_id = TOKEN_PROGRAM_ADDRESS, signer_key, sig_verify, }) {
306
- assertIsAddress(source_wallet);
306
+ const isSigner = typeof source_wallet !== 'string' && isTransactionSigner(source_wallet);
307
+ const walletAddress = isSigner ? source_wallet.address : source_wallet;
308
+ assertIsAddress(walletAddress);
307
309
  assertIsAddress(fee_token);
308
310
  assertIsAddress(token_program_id);
309
311
  const { fee_in_token, payment_address, signer_pubkey } = await this.estimateTransactionFee({
@@ -315,7 +317,7 @@ export class KoraClient {
315
317
  assertIsAddress(payment_address);
316
318
  const [sourceTokenAccount] = await findAssociatedTokenPda({
317
319
  mint: fee_token,
318
- owner: source_wallet,
320
+ owner: walletAddress,
319
321
  tokenProgram: token_program_id,
320
322
  });
321
323
  const [destinationTokenAccount] = await findAssociatedTokenPda({
@@ -328,7 +330,7 @@ export class KoraClient {
328
330
  }
329
331
  const paymentInstruction = getTransferInstruction({
330
332
  amount: fee_in_token,
331
- authority: createNoopSigner(source_wallet),
333
+ authority: isSigner ? source_wallet : walletAddress,
332
334
  destination: destinationTokenAccount,
333
335
  source: sourceTokenAccount,
334
336
  });
@@ -96,8 +96,11 @@ export interface GetPaymentInstructionRequest {
96
96
  sig_verify?: boolean;
97
97
  /** Optional signer address for the transaction */
98
98
  signer_key?: string;
99
- /** The wallet owner (not token account) that will be making the token payment */
100
- source_wallet: string;
99
+ /** The wallet owner that will be making the token payment.
100
+ * Accepts a plain address string or a TransactionSigner. When a TransactionSigner is provided,
101
+ * it is used as the transfer authority on the payment instruction, preserving signer identity
102
+ * and avoiding conflicts with other instructions that reference the same address. */
103
+ source_wallet: TransactionSigner | string;
101
104
  /** The token program id to use for the payment (defaults to TOKEN_PROGRAM_ID) */
102
105
  token_program_id?: string;
103
106
  /** Base64-encoded transaction to estimate fees for */
@@ -1,4 +1,5 @@
1
- import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
1
+ import { getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
2
+ import { appendTransactionMessageInstructions, createNoopSigner, createTransactionMessage, generateKeyPairSigner, partiallySignTransactionMessageWithSigners, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
2
3
  import { KoraClient } from '../src/client.js';
3
4
  import { getInstructionsFromBase64Message } from '../src/utils/transaction.js';
4
5
  // Mock fetch globally
@@ -402,12 +403,9 @@ describe('KoraClient Unit Tests', () => {
402
403
  role: 1, // writable
403
404
  }), // Destination token account
404
405
  expect.objectContaining({
405
- // readonly-signer
406
+ // readonly (plain address, no signer attached)
406
407
  address: validRequest.source_wallet,
407
- role: 2,
408
- signer: expect.objectContaining({
409
- address: validRequest.source_wallet,
410
- }),
408
+ role: 0,
411
409
  }), // Authority
412
410
  ],
413
411
  data: expect.any(Uint8Array),
@@ -473,6 +471,70 @@ describe('KoraClient Unit Tests', () => {
473
471
  mockFetch.mockRejectedValueOnce(new Error('Network error'));
474
472
  await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('Network error');
475
473
  });
474
+ it('should produce a payment instruction compatible with a real signer for the same address', async () => {
475
+ // Generate a real KeyPairSigner (simulates a user's wallet)
476
+ const userSigner = await generateKeyPairSigner();
477
+ // Mock estimateTransactionFee to return the user's address as source_wallet context
478
+ const feeEstimate = {
479
+ fee_in_lamports: 5000,
480
+ fee_in_token: 50000,
481
+ payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
482
+ signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
483
+ };
484
+ mockSuccessfulResponse(feeEstimate);
485
+ // Get payment instruction — authority is a plain address (no signer attached)
486
+ const result = await client.getPaymentInstruction({
487
+ ...validRequest,
488
+ source_wallet: userSigner.address,
489
+ });
490
+ // Build another instruction that references the same address with the REAL signer
491
+ // (simulates a program instruction like makePurchase where the user is a signer)
492
+ const userOwnedIx = getTransferInstruction({
493
+ amount: 1000n,
494
+ authority: userSigner, // <-- real KeyPairSigner
495
+ destination: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
496
+ source: '11111111111111111111111111111111',
497
+ });
498
+ // Combine both instructions in a transaction — previously this would throw
499
+ // "Multiple distinct signers" because the payment instruction had a NoopSigner.
500
+ // Now the payment instruction uses a plain address, so no conflict.
501
+ const feePayer = createNoopSigner('DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7');
502
+ const txMessage = appendTransactionMessageInstructions([userOwnedIx, result.payment_instruction], setTransactionMessageLifetimeUsingBlockhash({ blockhash: '11111111111111111111111111111111', lastValidBlockHeight: 0n }, setTransactionMessageFeePayerSigner(feePayer, createTransactionMessage({ version: 0 }))));
503
+ // This should NOT throw "Multiple distinct signers"
504
+ await expect(partiallySignTransactionMessageWithSigners(txMessage)).resolves.toBeDefined();
505
+ });
506
+ it('should accept a TransactionSigner as source_wallet and preserve signer identity', async () => {
507
+ const userSigner = await generateKeyPairSigner();
508
+ const feeEstimate = {
509
+ fee_in_lamports: 5000,
510
+ fee_in_token: 50000,
511
+ payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
512
+ signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
513
+ };
514
+ mockSuccessfulResponse(feeEstimate);
515
+ // Pass the signer directly as source_wallet
516
+ const result = await client.getPaymentInstruction({
517
+ ...validRequest,
518
+ source_wallet: userSigner,
519
+ });
520
+ // The authority account meta should carry the signer
521
+ const authorityMeta = result.payment_instruction.accounts?.[2];
522
+ expect(authorityMeta).toEqual(expect.objectContaining({
523
+ address: userSigner.address,
524
+ role: 2, // readonly-signer
525
+ signer: userSigner,
526
+ }));
527
+ // Combining with another instruction using the same signer should work
528
+ const userOwnedIx = getTransferInstruction({
529
+ amount: 1000n,
530
+ authority: userSigner,
531
+ destination: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
532
+ source: '11111111111111111111111111111111',
533
+ });
534
+ const feePayer = createNoopSigner('DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7');
535
+ const txMessage = appendTransactionMessageInstructions([userOwnedIx, result.payment_instruction], setTransactionMessageLifetimeUsingBlockhash({ blockhash: '11111111111111111111111111111111', lastValidBlockHeight: 0n }, setTransactionMessageFeePayerSigner(feePayer, createTransactionMessage({ version: 0 }))));
536
+ await expect(partiallySignTransactionMessageWithSigners(txMessage)).resolves.toBeDefined();
537
+ });
476
538
  it('should return correct payment details in response', async () => {
477
539
  mockFetch.mockResolvedValueOnce({
478
540
  json: jest.fn().mockResolvedValueOnce({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/kora",
3
- "version": "0.2.0-beta.6",
3
+ "version": "0.3.0-beta.0",
4
4
  "description": "TypeScript SDK for Kora RPC",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",