@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.
- package/dist/src/client.js +6 -4
- package/dist/src/types/index.d.ts +5 -2
- package/dist/test/unit.test.js +68 -6
- package/package.json +1 -1
package/dist/src/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertIsAddress,
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
100
|
-
|
|
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 */
|
package/dist/test/unit.test.js
CHANGED
|
@@ -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
|
|
406
|
+
// readonly (plain address, no signer attached)
|
|
406
407
|
address: validRequest.source_wallet,
|
|
407
|
-
role:
|
|
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({
|