@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.
- package/dist/src/client.d.ts +7 -201
- package/dist/src/client.js +18 -221
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/kit/executor.js +5 -18
- package/dist/src/kit/index.d.ts +1 -4
- package/dist/src/kit/index.js +2 -10
- package/dist/src/kit/payment.js +2 -3
- package/dist/src/kit/plugin.d.ts +31 -0
- package/dist/src/{plugin.js → kit/plugin.js} +18 -88
- package/dist/src/types/index.d.ts +79 -170
- package/dist/test/auth-setup.js +4 -4
- package/dist/test/integration.test.js +291 -168
- package/dist/test/kit-client.test.js +0 -18
- package/dist/test/plugin.test.js +71 -126
- package/dist/test/setup.d.ts +15 -8
- package/dist/test/setup.js +152 -52
- package/dist/test/unit.test.js +158 -256
- package/package.json +16 -8
- package/dist/src/plugin.d.ts +0 -85
package/dist/test/auth-setup.js
CHANGED
|
@@ -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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
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 });
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
//
|
|
209
|
-
|
|
210
|
-
const
|
|
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
|
|
221
|
-
amount:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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 {
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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 () => {
|