@solana/kora 0.2.0-beta.4 → 0.2.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.d.ts +7 -201
- package/dist/src/client.js +15 -218
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/kit/executor.d.ts +7 -0
- package/dist/src/kit/executor.js +55 -0
- package/dist/src/kit/index.d.ts +50 -0
- package/dist/src/kit/index.js +67 -0
- package/dist/src/kit/payment.d.ts +18 -0
- package/dist/src/kit/payment.js +69 -0
- package/dist/src/kit/planner.d.ts +4 -0
- package/dist/src/kit/planner.js +23 -0
- package/dist/src/kit/plugin.d.ts +31 -0
- package/dist/src/{plugin.js → kit/plugin.js} +13 -82
- package/dist/src/types/index.d.ts +89 -161
- package/dist/test/auth-setup.js +4 -4
- package/dist/test/integration.test.js +322 -172
- package/dist/test/kit-client.test.d.ts +1 -0
- package/dist/test/kit-client.test.js +473 -0
- package/dist/test/plugin.test.js +71 -126
- package/dist/test/setup.d.ts +13 -9
- package/dist/test/setup.js +36 -38
- package/dist/test/unit.test.js +163 -253
- package/package.json +31 -13
- package/dist/src/plugin.d.ts +0 -85
|
@@ -1,73 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
3
|
-
import { runAuthenticationTests } from './auth-setup.js';
|
|
1
|
+
import { createKitKoraClient } from '../src/index.js';
|
|
4
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';
|
|
5
7
|
function transactionFromBase64(base64) {
|
|
6
8
|
const encoder = getBase64Encoder();
|
|
7
9
|
const decoder = getTransactionDecoder();
|
|
8
10
|
const messageBytes = encoder.encode(base64);
|
|
9
11
|
return decoder.decode(messageBytes);
|
|
10
12
|
}
|
|
11
|
-
function transactionToBase64(transaction) {
|
|
12
|
-
const txEncoder = getTransactionEncoder();
|
|
13
|
-
const txBytes = txEncoder.encode(transaction);
|
|
14
|
-
const base64Decoder = getBase64Decoder();
|
|
15
|
-
return base64Decoder.decode(txBytes);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Helper to build a SPL token transfer transaction.
|
|
19
|
-
* This replaces the deprecated transferTransaction endpoint.
|
|
20
|
-
*/
|
|
21
|
-
async function buildTokenTransferTransaction(params) {
|
|
22
|
-
const { client, amount, mint, sourceWallet, destinationWallet } = params;
|
|
23
|
-
// Get the payer signer from Kora (fee payer)
|
|
24
|
-
const { signer_address } = await client.getPayerSigner();
|
|
25
|
-
// Get blockhash
|
|
26
|
-
const { blockhash } = await client.getBlockhash();
|
|
27
|
-
// Find source and destination ATAs
|
|
28
|
-
const [sourceAta] = await findAssociatedTokenPda({
|
|
29
|
-
mint,
|
|
30
|
-
owner: sourceWallet.address,
|
|
31
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
32
|
-
});
|
|
33
|
-
const [destinationAta] = await findAssociatedTokenPda({
|
|
34
|
-
mint,
|
|
35
|
-
owner: destinationWallet,
|
|
36
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
37
|
-
});
|
|
38
|
-
// Build transfer instruction
|
|
39
|
-
const transferIx = getTransferInstruction({
|
|
40
|
-
amount,
|
|
41
|
-
authority: sourceWallet,
|
|
42
|
-
destination: destinationAta,
|
|
43
|
-
source: sourceAta,
|
|
44
|
-
});
|
|
45
|
-
// Build transaction message with Kora as fee payer
|
|
46
|
-
// We create a mock signer for the fee payer address since we only need the address
|
|
47
|
-
const feePayerSigner = {
|
|
48
|
-
address: signer_address,
|
|
49
|
-
};
|
|
50
|
-
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));
|
|
51
|
-
// Compile to transaction
|
|
52
|
-
const transaction = compileTransaction(transactionMessage);
|
|
53
|
-
const base64Transaction = getBase64EncodedWireTransaction(transaction);
|
|
54
|
-
return { blockhash: blockhash, transaction: base64Transaction };
|
|
55
|
-
}
|
|
56
13
|
const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
|
|
14
|
+
const FREE_PRICING = process.env.FREE_PRICING === 'true';
|
|
57
15
|
const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
|
|
58
16
|
describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without auth'} | signer type: ${KORA_SIGNER_TYPE})`, () => {
|
|
59
17
|
let client;
|
|
60
18
|
let testWallet;
|
|
61
19
|
let testWalletAddress;
|
|
20
|
+
let destinationAddress;
|
|
62
21
|
let usdcMint;
|
|
63
22
|
let koraAddress;
|
|
23
|
+
let koraRpcUrl;
|
|
24
|
+
let authConfig;
|
|
64
25
|
beforeAll(async () => {
|
|
65
26
|
const testSuite = await setupTestSuite();
|
|
66
27
|
client = testSuite.koraClient;
|
|
67
28
|
testWallet = testSuite.testWallet;
|
|
68
29
|
testWalletAddress = testWallet.address;
|
|
30
|
+
destinationAddress = testSuite.destinationAddress;
|
|
69
31
|
usdcMint = testSuite.usdcMint;
|
|
70
32
|
koraAddress = testSuite.koraAddress;
|
|
33
|
+
koraRpcUrl = testSuite.koraRpcUrl;
|
|
34
|
+
authConfig = testSuite.authConfig;
|
|
71
35
|
}, 90000); // allow adequate time for airdrops and token initialization
|
|
72
36
|
// Run authentication tests only when auth is enabled
|
|
73
37
|
if (AUTH_ENABLED) {
|
|
@@ -129,9 +93,9 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
129
93
|
expect(config.enabled_methods.get_supported_tokens).toBeDefined();
|
|
130
94
|
expect(config.enabled_methods.sign_transaction).toBeDefined();
|
|
131
95
|
expect(config.enabled_methods.sign_and_send_transaction).toBeDefined();
|
|
96
|
+
expect(config.enabled_methods.transfer_transaction).toBeDefined();
|
|
132
97
|
expect(config.enabled_methods.get_blockhash).toBeDefined();
|
|
133
98
|
expect(config.enabled_methods.get_config).toBeDefined();
|
|
134
|
-
expect(config.enabled_methods.get_version).toBeDefined();
|
|
135
99
|
});
|
|
136
100
|
it('should get payer signer', async () => {
|
|
137
101
|
const { signer_address, payment_address } = await client.getPayerSigner();
|
|
@@ -151,39 +115,73 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
151
115
|
expect(blockhash.length).toBeGreaterThanOrEqual(43);
|
|
152
116
|
expect(blockhash.length).toBeLessThanOrEqual(44); // Base58 encoded hash length
|
|
153
117
|
});
|
|
154
|
-
it('should get version', async () => {
|
|
155
|
-
const { version } = await client.getVersion();
|
|
156
|
-
expect(version).toBeDefined();
|
|
157
|
-
expect(typeof version).toBe('string');
|
|
158
|
-
expect(version.length).toBeGreaterThan(0);
|
|
159
|
-
// Version should follow semver format (e.g., "2.1.0" or "2.1.0-beta.0")
|
|
160
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
161
|
-
});
|
|
162
118
|
});
|
|
163
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
|
+
});
|
|
164
156
|
it('should estimate transaction fee', async () => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
const
|
|
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 });
|
|
173
166
|
expect(fee).toBeDefined();
|
|
174
167
|
expect(typeof fee.fee_in_lamports).toBe('number');
|
|
175
|
-
expect(fee.fee_in_lamports).
|
|
168
|
+
expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
|
|
176
169
|
expect(typeof fee.fee_in_token).toBe('number');
|
|
177
|
-
|
|
170
|
+
if (!FREE_PRICING) {
|
|
171
|
+
expect(fee.fee_in_lamports).toBeGreaterThan(0);
|
|
172
|
+
expect(fee.fee_in_token).toBeGreaterThan(0);
|
|
173
|
+
}
|
|
178
174
|
});
|
|
179
175
|
it('should sign transaction', async () => {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
185
|
const signResult = await client.signTransaction({
|
|
188
186
|
transaction,
|
|
189
187
|
});
|
|
@@ -191,47 +189,47 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
191
189
|
expect(signResult.signed_transaction).toBeDefined();
|
|
192
190
|
});
|
|
193
191
|
it('should sign and send transaction', async () => {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
201
|
const transaction = transactionFromBase64(transactionString);
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
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);
|
|
206
205
|
const signResult = await client.signAndSendTransaction({
|
|
207
206
|
transaction: base64SignedTransaction,
|
|
208
207
|
});
|
|
209
208
|
expect(signResult).toBeDefined();
|
|
210
209
|
expect(signResult.signed_transaction).toBeDefined();
|
|
211
|
-
expect(signResult.signature).toBeDefined();
|
|
212
210
|
});
|
|
213
211
|
it('should get payment instruction', async () => {
|
|
214
|
-
const
|
|
215
|
-
amount:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
212
|
+
const transferRequest = {
|
|
213
|
+
amount: 1000000,
|
|
214
|
+
token: usdcMint,
|
|
215
|
+
source: testWalletAddress,
|
|
216
|
+
destination: destinationAddress,
|
|
217
|
+
};
|
|
221
218
|
const [expectedSenderAta] = await findAssociatedTokenPda({
|
|
222
|
-
mint: usdcMint,
|
|
223
219
|
owner: testWalletAddress,
|
|
224
220
|
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
221
|
+
mint: usdcMint,
|
|
225
222
|
});
|
|
226
223
|
const [koraAta] = await findAssociatedTokenPda({
|
|
227
|
-
mint: usdcMint,
|
|
228
224
|
owner: koraAddress,
|
|
229
225
|
tokenProgram: TOKEN_PROGRAM_ADDRESS,
|
|
226
|
+
mint: usdcMint,
|
|
230
227
|
});
|
|
231
|
-
const {
|
|
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,
|
|
232
231
|
fee_token: usdcMint,
|
|
233
232
|
source_wallet: testWalletAddress,
|
|
234
|
-
transaction,
|
|
235
233
|
});
|
|
236
234
|
expect(payment_instruction).toBeDefined();
|
|
237
235
|
expect(payment_instruction.programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
|
|
@@ -246,95 +244,247 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
246
244
|
expect(original_transaction).toBe(transaction);
|
|
247
245
|
});
|
|
248
246
|
});
|
|
249
|
-
describe('
|
|
250
|
-
it('should
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
});
|
|
259
|
-
const { transaction: tx2String } = await buildTokenTransferTransaction({
|
|
260
|
-
amount: 500000n,
|
|
261
|
-
client,
|
|
262
|
-
destinationWallet: koraAddress,
|
|
263
|
-
mint: usdcMint,
|
|
264
|
-
sourceWallet: testWallet,
|
|
265
|
-
});
|
|
266
|
-
// Partially sign both transactions with test wallet
|
|
267
|
-
const tx1 = transactionFromBase64(tx1String);
|
|
268
|
-
const tx2 = transactionFromBase64(tx2String);
|
|
269
|
-
const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
|
|
270
|
-
const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
|
|
271
|
-
const base64Tx1 = transactionToBase64(signedTx1);
|
|
272
|
-
const base64Tx2 = transactionToBase64(signedTx2);
|
|
273
|
-
const result = await client.signBundle({
|
|
274
|
-
transactions: [base64Tx1, base64Tx2],
|
|
275
|
-
});
|
|
276
|
-
expect(result).toBeDefined();
|
|
277
|
-
expect(result.signed_transactions).toBeDefined();
|
|
278
|
-
expect(Array.isArray(result.signed_transactions)).toBe(true);
|
|
279
|
-
expect(result.signed_transactions.length).toBe(2);
|
|
280
|
-
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();
|
|
281
256
|
});
|
|
282
|
-
it('should
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
const { transaction: tx2String } = await buildTokenTransferTransaction({
|
|
292
|
-
amount: 500000n,
|
|
293
|
-
client,
|
|
294
|
-
destinationWallet: koraAddress,
|
|
295
|
-
mint: usdcMint,
|
|
296
|
-
sourceWallet: testWallet,
|
|
297
|
-
});
|
|
298
|
-
// Partially sign both transactions with test wallet
|
|
299
|
-
const tx1 = transactionFromBase64(tx1String);
|
|
300
|
-
const tx2 = transactionFromBase64(tx2String);
|
|
301
|
-
const signedTx1 = await partiallySignTransaction([testWallet.keyPair], tx1);
|
|
302
|
-
const signedTx2 = await partiallySignTransaction([testWallet.keyPair], tx2);
|
|
303
|
-
const base64Tx1 = transactionToBase64(signedTx1);
|
|
304
|
-
const base64Tx2 = transactionToBase64(signedTx2);
|
|
305
|
-
const result = await client.signAndSendBundle({
|
|
306
|
-
transactions: [base64Tx1, base64Tx2],
|
|
307
|
-
});
|
|
308
|
-
expect(result).toBeDefined();
|
|
309
|
-
expect(result.signed_transactions).toBeDefined();
|
|
310
|
-
expect(Array.isArray(result.signed_transactions)).toBe(true);
|
|
311
|
-
expect(result.signed_transactions.length).toBe(2);
|
|
312
|
-
expect(result.signer_pubkey).toBeDefined();
|
|
313
|
-
expect(result.bundle_uuid).toBeDefined();
|
|
314
|
-
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();
|
|
315
265
|
});
|
|
316
|
-
});
|
|
317
|
-
describe('Error Handling', () => {
|
|
318
266
|
it('should handle invalid transaction for signing', async () => {
|
|
319
267
|
await expect(client.signTransaction({
|
|
320
268
|
transaction: 'invalid_transaction',
|
|
321
269
|
})).rejects.toThrow();
|
|
322
270
|
});
|
|
323
271
|
it('should handle invalid transaction for fee estimation', async () => {
|
|
324
|
-
await expect(client.estimateTransactionFee({
|
|
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
|
+
}
|
|
325
290
|
});
|
|
326
291
|
});
|
|
327
292
|
describe('End-to-End Flows', () => {
|
|
328
293
|
it('should handle transfer and sign flow', async () => {
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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);
|
|
336
304
|
const signResult = await client.signTransaction({ transaction });
|
|
337
305
|
expect(signResult.signed_transaction).toBeDefined();
|
|
338
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
|
+
});
|
|
339
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);
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
if (FREE_PRICING) {
|
|
454
|
+
describe('Kit Client (free pricing)', () => {
|
|
455
|
+
let freeClient;
|
|
456
|
+
beforeAll(async () => {
|
|
457
|
+
freeClient = await createKitKoraClient({
|
|
458
|
+
endpoint: koraRpcUrl,
|
|
459
|
+
rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
|
|
460
|
+
feeToken: usdcMint,
|
|
461
|
+
feePayerWallet: testWallet,
|
|
462
|
+
...authConfig,
|
|
463
|
+
});
|
|
464
|
+
}, 30000);
|
|
465
|
+
it('should send transaction without payment instruction when fee is 0', async () => {
|
|
466
|
+
const ix = getTransferSolInstruction({
|
|
467
|
+
source: testWallet,
|
|
468
|
+
destination: address(destinationAddress),
|
|
469
|
+
amount: 1000,
|
|
470
|
+
});
|
|
471
|
+
const result = await freeClient.sendTransaction([ix]);
|
|
472
|
+
expect(result.status).toBe('successful');
|
|
473
|
+
expect(result.context.signature).toBeDefined();
|
|
474
|
+
}, 30000);
|
|
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.
|
|
478
|
+
const ix = getTransferSolInstruction({
|
|
479
|
+
source: testWallet,
|
|
480
|
+
destination: address(destinationAddress),
|
|
481
|
+
amount: 1000,
|
|
482
|
+
});
|
|
483
|
+
// Sending should succeed regardless — either no placeholder is added,
|
|
484
|
+
// or it's added then stripped when fee estimation returns 0.
|
|
485
|
+
const result = await freeClient.sendTransaction([ix]);
|
|
486
|
+
expect(result.status).toBe('successful');
|
|
487
|
+
}, 30000);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
340
490
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|