@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.
- package/dist/src/client.d.ts +212 -7
- package/dist/src/client.js +234 -17
- package/dist/src/error.d.ts +41 -0
- package/dist/src/error.js +54 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +2 -2
- package/dist/src/kit/executor.d.ts +2 -2
- package/dist/src/kit/executor.js +20 -7
- package/dist/src/kit/index.d.ts +85 -25
- package/dist/src/kit/index.js +79 -49
- package/dist/src/kit/payment.js +3 -2
- package/dist/src/kit/planner.d.ts +2 -2
- package/dist/src/plugin.d.ts +85 -0
- package/dist/src/plugin.js +179 -0
- package/dist/src/types/index.d.ts +200 -80
- package/dist/test/auth-setup.js +4 -4
- package/dist/test/integration.test.js +223 -298
- package/dist/test/kit-client.test.js +66 -3
- package/dist/test/plugin.test.js +169 -79
- package/dist/test/setup.d.ts +8 -15
- package/dist/test/setup.js +76 -153
- package/dist/test/unit.test.js +365 -161
- package/package.json +37 -34
- package/dist/src/kit/plugin.d.ts +0 -31
- package/dist/src/kit/plugin.js +0 -107
|
@@ -1,15 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
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 {
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
const
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
const
|
|
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
|
|
213
|
-
amount:
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 {
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
expect(result.
|
|
357
|
-
expect(result.
|
|
358
|
-
expect(
|
|
359
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
403
|
-
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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(
|
|
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(
|
|
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
|
});
|