@solana/kora 0.2.0-beta.3 → 0.2.0-beta.6
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/README.md +29 -27
- package/dist/src/client.d.ts +3 -1
- package/dist/src/client.js +10 -3
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/kit/executor.d.ts +7 -0
- package/dist/src/kit/executor.js +68 -0
- package/dist/src/kit/index.d.ts +53 -0
- package/dist/src/kit/index.js +75 -0
- package/dist/src/kit/payment.d.ts +18 -0
- package/dist/src/kit/payment.js +70 -0
- package/dist/src/kit/planner.d.ts +4 -0
- package/dist/src/kit/planner.js +23 -0
- package/dist/src/plugin.js +7 -5
- package/dist/src/types/index.d.ts +37 -1
- package/dist/test/integration.test.js +43 -16
- package/dist/test/kit-client.test.d.ts +1 -0
- package/dist/test/kit-client.test.js +491 -0
- package/dist/test/setup.d.ts +1 -4
- package/dist/test/setup.js +34 -136
- package/dist/test/unit.test.js +106 -22
- package/package.json +26 -16
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { appendTransactionMessageInstruction, compileTransaction, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
|
|
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';
|
|
2
3
|
import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
4
|
+
import { createKitKoraClient } from '../src/index.js';
|
|
3
5
|
import { runAuthenticationTests } from './auth-setup.js';
|
|
4
6
|
import setupTestSuite from './setup.js';
|
|
5
7
|
function transactionFromBase64(base64) {
|
|
@@ -54,6 +56,7 @@ async function buildTokenTransferTransaction(params) {
|
|
|
54
56
|
return { blockhash: blockhash, transaction: base64Transaction };
|
|
55
57
|
}
|
|
56
58
|
const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
|
|
59
|
+
const FREE_PRICING = process.env.FREE_PRICING === 'true';
|
|
57
60
|
const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
|
|
58
61
|
describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without auth'} | signer type: ${KORA_SIGNER_TYPE})`, () => {
|
|
59
62
|
let client;
|
|
@@ -172,9 +175,12 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
172
175
|
const fee = await client.estimateTransactionFee({ fee_token: usdcMint, transaction });
|
|
173
176
|
expect(fee).toBeDefined();
|
|
174
177
|
expect(typeof fee.fee_in_lamports).toBe('number');
|
|
175
|
-
expect(fee.fee_in_lamports).
|
|
178
|
+
expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
|
|
176
179
|
expect(typeof fee.fee_in_token).toBe('number');
|
|
177
|
-
|
|
180
|
+
if (!FREE_PRICING) {
|
|
181
|
+
expect(fee.fee_in_lamports).toBeGreaterThan(0);
|
|
182
|
+
expect(fee.fee_in_token).toBeGreaterThan(0);
|
|
183
|
+
}
|
|
178
184
|
});
|
|
179
185
|
it('should sign transaction', async () => {
|
|
180
186
|
const { transaction } = await buildTokenTransferTransaction({
|
|
@@ -246,7 +252,8 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
246
252
|
expect(original_transaction).toBe(transaction);
|
|
247
253
|
});
|
|
248
254
|
});
|
|
249
|
-
|
|
255
|
+
// Bundle tests require bundle.enabled = true in the Kora config
|
|
256
|
+
(FREE_PRICING ? describe.skip : describe)('Bundle Operations', () => {
|
|
250
257
|
it('should sign bundle of transactions', async () => {
|
|
251
258
|
// Create two transfer transactions for the bundle
|
|
252
259
|
const { transaction: tx1String } = await buildTokenTransferTransaction({
|
|
@@ -324,17 +331,37 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
|
|
|
324
331
|
await expect(client.estimateTransactionFee({ fee_token: usdcMint, transaction: 'invalid_transaction' })).rejects.toThrow();
|
|
325
332
|
});
|
|
326
333
|
});
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
if (FREE_PRICING) {
|
|
335
|
+
describe('Kit Client (free pricing)', () => {
|
|
336
|
+
let freeClient;
|
|
337
|
+
beforeAll(async () => {
|
|
338
|
+
const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
|
|
339
|
+
freeClient = await createKitKoraClient({
|
|
340
|
+
endpoint: koraRpcUrl,
|
|
341
|
+
rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
|
|
342
|
+
feeToken: usdcMint,
|
|
343
|
+
feePayerWallet: testWallet,
|
|
344
|
+
});
|
|
345
|
+
}, 30000);
|
|
346
|
+
it('should send transaction without payment instruction when fee is 0', async () => {
|
|
347
|
+
const ix = getTransferSolInstruction({
|
|
348
|
+
source: testWallet,
|
|
349
|
+
destination: address(koraAddress),
|
|
350
|
+
amount: 1000,
|
|
351
|
+
});
|
|
352
|
+
const result = await freeClient.sendTransaction([ix]);
|
|
353
|
+
expect(result.status).toBe('successful');
|
|
354
|
+
expect(result.context.signature).toBeDefined();
|
|
355
|
+
}, 30000);
|
|
356
|
+
it('should strip placeholder from planned message when fee is 0', async () => {
|
|
357
|
+
const ix = getTransferSolInstruction({
|
|
358
|
+
source: testWallet,
|
|
359
|
+
destination: address(koraAddress),
|
|
360
|
+
amount: 1000,
|
|
361
|
+
});
|
|
362
|
+
const result = await freeClient.sendTransaction([ix]);
|
|
363
|
+
expect(result.status).toBe('successful');
|
|
364
|
+
}, 30000);
|
|
338
365
|
});
|
|
339
|
-
}
|
|
366
|
+
}
|
|
340
367
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { createKitKoraClient } from '../src/kit/index.js';
|
|
2
|
+
import { address, createNoopSigner } from '@solana/kit';
|
|
3
|
+
// Mock fetch globally
|
|
4
|
+
const mockFetch = jest.fn();
|
|
5
|
+
global.fetch = mockFetch;
|
|
6
|
+
const MOCK_ENDPOINT = 'http://localhost:8080';
|
|
7
|
+
const MOCK_RPC_URL = 'http://127.0.0.1:8899';
|
|
8
|
+
const MOCK_PAYER_ADDRESS = 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7';
|
|
9
|
+
const MOCK_PAYMENT_ADDRESS = 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7';
|
|
10
|
+
const MOCK_FEE_TOKEN = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU';
|
|
11
|
+
const MOCK_WALLET_ADDRESS = 'BrEe1Xjy2Ky72doGBAhyUPCxMm5b4bRTm3AD6MNMfKmq';
|
|
12
|
+
const MOCK_WALLET = createNoopSigner(MOCK_WALLET_ADDRESS);
|
|
13
|
+
const MOCK_SIGNATURE = '5wBzExmp8yR5M6m4KjV8WT9T6B1NMQkaMbsFWqBoDPBMYWxDx6EuSGxNqKfXnBhDhAkEqMiGRjEwKnGhSN3pi3n';
|
|
14
|
+
function mockRpcResponse(result) {
|
|
15
|
+
mockFetch.mockResolvedValueOnce({
|
|
16
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
17
|
+
jsonrpc: '2.0',
|
|
18
|
+
id: 1,
|
|
19
|
+
result,
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function mockSimulateResponse(unitsConsumed = 50000) {
|
|
24
|
+
const body = JSON.stringify({
|
|
25
|
+
jsonrpc: '2.0',
|
|
26
|
+
id: 1,
|
|
27
|
+
result: {
|
|
28
|
+
context: { slot: 1 },
|
|
29
|
+
value: { err: null, logs: [], unitsConsumed },
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
mockFetch.mockResolvedValueOnce({
|
|
33
|
+
ok: true,
|
|
34
|
+
status: 200,
|
|
35
|
+
statusText: 'OK',
|
|
36
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
37
|
+
text: jest.fn().mockResolvedValueOnce(body),
|
|
38
|
+
json: jest.fn().mockResolvedValueOnce(JSON.parse(body)),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function mockRpcError(code, message) {
|
|
42
|
+
mockFetch.mockResolvedValueOnce({
|
|
43
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
44
|
+
jsonrpc: '2.0',
|
|
45
|
+
id: 1,
|
|
46
|
+
error: { code, message },
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
describe('createKitKoraClient', () => {
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
mockFetch.mockClear();
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
jest.resetAllMocks();
|
|
56
|
+
});
|
|
57
|
+
describe('initialization', () => {
|
|
58
|
+
it('should fetch payer info on creation', async () => {
|
|
59
|
+
mockRpcResponse({
|
|
60
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
61
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
62
|
+
});
|
|
63
|
+
const client = await createKitKoraClient({
|
|
64
|
+
endpoint: MOCK_ENDPOINT,
|
|
65
|
+
rpcUrl: MOCK_RPC_URL,
|
|
66
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
67
|
+
feePayerWallet: MOCK_WALLET,
|
|
68
|
+
});
|
|
69
|
+
expect(client.paymentAddress).toBe(MOCK_PAYMENT_ADDRESS);
|
|
70
|
+
// ClientWithPayer: payer is a NoopSigner for the Kora fee payer
|
|
71
|
+
expect(client.payer.address).toBe(MOCK_PAYER_ADDRESS);
|
|
72
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
73
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
74
|
+
expect(body.method).toBe('getPayerSigner');
|
|
75
|
+
});
|
|
76
|
+
it('should throw if getPayerSigner fails', async () => {
|
|
77
|
+
mockRpcError(-32000, 'Server error');
|
|
78
|
+
await expect(createKitKoraClient({
|
|
79
|
+
endpoint: MOCK_ENDPOINT,
|
|
80
|
+
rpcUrl: MOCK_RPC_URL,
|
|
81
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
82
|
+
feePayerWallet: MOCK_WALLET,
|
|
83
|
+
})).rejects.toThrow('RPC Error -32000: Server error');
|
|
84
|
+
});
|
|
85
|
+
it('should expose kora namespace for raw RPC access', async () => {
|
|
86
|
+
mockRpcResponse({
|
|
87
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
88
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
89
|
+
});
|
|
90
|
+
const client = await createKitKoraClient({
|
|
91
|
+
endpoint: MOCK_ENDPOINT,
|
|
92
|
+
rpcUrl: MOCK_RPC_URL,
|
|
93
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
94
|
+
feePayerWallet: MOCK_WALLET,
|
|
95
|
+
});
|
|
96
|
+
expect(client.kora).toBeDefined();
|
|
97
|
+
expect(typeof client.kora.getConfig).toBe('function');
|
|
98
|
+
expect(typeof client.kora.getBlockhash).toBe('function');
|
|
99
|
+
expect(typeof client.kora.estimateTransactionFee).toBe('function');
|
|
100
|
+
});
|
|
101
|
+
it('should implement Kit plugin interfaces', async () => {
|
|
102
|
+
mockRpcResponse({
|
|
103
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
104
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
105
|
+
});
|
|
106
|
+
const client = await createKitKoraClient({
|
|
107
|
+
endpoint: MOCK_ENDPOINT,
|
|
108
|
+
rpcUrl: MOCK_RPC_URL,
|
|
109
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
110
|
+
feePayerWallet: MOCK_WALLET,
|
|
111
|
+
});
|
|
112
|
+
// ClientWithPayer
|
|
113
|
+
expect(client.payer).toBeDefined();
|
|
114
|
+
expect(client.payer.address).toBe(MOCK_PAYER_ADDRESS);
|
|
115
|
+
// ClientWithTransactionPlanning
|
|
116
|
+
expect(typeof client.planTransaction).toBe('function');
|
|
117
|
+
expect(typeof client.planTransactions).toBe('function');
|
|
118
|
+
// ClientWithTransactionSending
|
|
119
|
+
expect(typeof client.sendTransaction).toBe('function');
|
|
120
|
+
expect(typeof client.sendTransactions).toBe('function');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
describe('sendTransaction', () => {
|
|
124
|
+
let client;
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
// Mock getPayerSigner for init
|
|
127
|
+
mockRpcResponse({
|
|
128
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
129
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
130
|
+
});
|
|
131
|
+
client = await createKitKoraClient({
|
|
132
|
+
endpoint: MOCK_ENDPOINT,
|
|
133
|
+
rpcUrl: MOCK_RPC_URL,
|
|
134
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
135
|
+
feePayerWallet: MOCK_WALLET,
|
|
136
|
+
});
|
|
137
|
+
mockFetch.mockClear();
|
|
138
|
+
});
|
|
139
|
+
it('should call getBlockhash, simulateTransaction, estimateTransactionFee, and signAndSendTransaction', async () => {
|
|
140
|
+
// Mock getBlockhash
|
|
141
|
+
mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
|
|
142
|
+
// Mock simulateTransaction (CU estimation)
|
|
143
|
+
mockSimulateResponse();
|
|
144
|
+
// Mock estimateTransactionFee
|
|
145
|
+
mockRpcResponse({
|
|
146
|
+
fee_in_lamports: 5000,
|
|
147
|
+
fee_in_token: 50000,
|
|
148
|
+
signer_pubkey: MOCK_PAYER_ADDRESS,
|
|
149
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
150
|
+
});
|
|
151
|
+
// Mock signAndSendTransaction
|
|
152
|
+
mockRpcResponse({
|
|
153
|
+
signature: MOCK_SIGNATURE,
|
|
154
|
+
signed_transaction: 'base64signedtx',
|
|
155
|
+
signer_pubkey: MOCK_PAYER_ADDRESS,
|
|
156
|
+
});
|
|
157
|
+
const dummyIx = {
|
|
158
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
159
|
+
accounts: [],
|
|
160
|
+
data: new Uint8Array(4),
|
|
161
|
+
};
|
|
162
|
+
const result = await client.sendTransaction([dummyIx]);
|
|
163
|
+
expect(result.status).toBe('successful');
|
|
164
|
+
expect(result.context.signature).toBe(MOCK_SIGNATURE);
|
|
165
|
+
expect(mockFetch).toHaveBeenCalledTimes(4);
|
|
166
|
+
const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
|
|
167
|
+
expect(calls).toEqual([
|
|
168
|
+
'getBlockhash',
|
|
169
|
+
'simulateTransaction',
|
|
170
|
+
'estimateTransactionFee',
|
|
171
|
+
'signAndSendTransaction',
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
it('should skip payment instruction when fee is 0', async () => {
|
|
175
|
+
mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
|
|
176
|
+
mockSimulateResponse();
|
|
177
|
+
mockRpcResponse({
|
|
178
|
+
fee_in_lamports: 0,
|
|
179
|
+
fee_in_token: 0,
|
|
180
|
+
signer_pubkey: MOCK_PAYER_ADDRESS,
|
|
181
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
182
|
+
});
|
|
183
|
+
// Mock signAndSendTransaction
|
|
184
|
+
mockRpcResponse({
|
|
185
|
+
signature: MOCK_SIGNATURE,
|
|
186
|
+
signed_transaction: 'base64signedtx',
|
|
187
|
+
signer_pubkey: MOCK_PAYER_ADDRESS,
|
|
188
|
+
});
|
|
189
|
+
const dummyIx = {
|
|
190
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
191
|
+
accounts: [],
|
|
192
|
+
data: new Uint8Array(4),
|
|
193
|
+
};
|
|
194
|
+
const result = await client.sendTransaction([dummyIx]);
|
|
195
|
+
expect(result.status).toBe('successful');
|
|
196
|
+
expect(result.context.signature).toBe(MOCK_SIGNATURE);
|
|
197
|
+
});
|
|
198
|
+
it('should propagate fee estimation errors', async () => {
|
|
199
|
+
mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
|
|
200
|
+
mockSimulateResponse();
|
|
201
|
+
mockRpcError(-32602, 'Invalid transaction');
|
|
202
|
+
const dummyIx = {
|
|
203
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
204
|
+
accounts: [],
|
|
205
|
+
data: new Uint8Array(4),
|
|
206
|
+
};
|
|
207
|
+
// Kit's executor wraps errors — the original RPC error is in the cause chain
|
|
208
|
+
await expect(client.sendTransaction([dummyIx])).rejects.toThrow();
|
|
209
|
+
const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
|
|
210
|
+
expect(calls).toContain('estimateTransactionFee');
|
|
211
|
+
});
|
|
212
|
+
it('should propagate signAndSendTransaction errors', async () => {
|
|
213
|
+
mockRpcResponse({ blockhash: '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi' });
|
|
214
|
+
mockSimulateResponse();
|
|
215
|
+
mockRpcResponse({
|
|
216
|
+
fee_in_lamports: 5000,
|
|
217
|
+
fee_in_token: 50000,
|
|
218
|
+
signer_pubkey: MOCK_PAYER_ADDRESS,
|
|
219
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
220
|
+
});
|
|
221
|
+
mockRpcError(-32003, 'Transaction failed');
|
|
222
|
+
const dummyIx = {
|
|
223
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
224
|
+
accounts: [],
|
|
225
|
+
data: new Uint8Array(4),
|
|
226
|
+
};
|
|
227
|
+
// Kit's executor wraps errors — the original RPC error is the cause
|
|
228
|
+
await expect(client.sendTransaction([dummyIx])).rejects.toThrow();
|
|
229
|
+
// Verify signAndSendTransaction was attempted
|
|
230
|
+
const calls = mockFetch.mock.calls.map(c => JSON.parse(c[1].body).method);
|
|
231
|
+
expect(calls).toContain('signAndSendTransaction');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('planTransaction', () => {
|
|
235
|
+
let client;
|
|
236
|
+
beforeEach(async () => {
|
|
237
|
+
mockRpcResponse({
|
|
238
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
239
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
240
|
+
});
|
|
241
|
+
client = await createKitKoraClient({
|
|
242
|
+
endpoint: MOCK_ENDPOINT,
|
|
243
|
+
rpcUrl: MOCK_RPC_URL,
|
|
244
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
245
|
+
feePayerWallet: MOCK_WALLET,
|
|
246
|
+
});
|
|
247
|
+
mockFetch.mockClear();
|
|
248
|
+
});
|
|
249
|
+
it('should return a transaction message without sending', async () => {
|
|
250
|
+
const dummyIx = {
|
|
251
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
252
|
+
accounts: [],
|
|
253
|
+
data: new Uint8Array(4),
|
|
254
|
+
};
|
|
255
|
+
const result = await client.planTransaction([dummyIx]);
|
|
256
|
+
// Returns a transaction message (has version, instructions, feePayer)
|
|
257
|
+
expect(result).toBeDefined();
|
|
258
|
+
expect('version' in result).toBe(true);
|
|
259
|
+
expect('instructions' in result).toBe(true);
|
|
260
|
+
// Should NOT call any RPC methods (planner is local)
|
|
261
|
+
expect(mockFetch).toHaveBeenCalledTimes(0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('plugin composition', () => {
|
|
265
|
+
it('should support .use() for extending the client with a Kit plugin', async () => {
|
|
266
|
+
mockRpcResponse({
|
|
267
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
268
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
269
|
+
});
|
|
270
|
+
const client = await createKitKoraClient({
|
|
271
|
+
endpoint: MOCK_ENDPOINT,
|
|
272
|
+
rpcUrl: MOCK_RPC_URL,
|
|
273
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
274
|
+
feePayerWallet: MOCK_WALLET,
|
|
275
|
+
});
|
|
276
|
+
// Kit plugins must spread the client to preserve existing properties
|
|
277
|
+
const extended = client.use((c) => ({
|
|
278
|
+
...c,
|
|
279
|
+
custom: {
|
|
280
|
+
hello: () => 'world',
|
|
281
|
+
},
|
|
282
|
+
}));
|
|
283
|
+
expect(extended.custom.hello()).toBe('world');
|
|
284
|
+
// Original methods preserved via spread
|
|
285
|
+
expect(extended.kora).toBeDefined();
|
|
286
|
+
expect(typeof extended.sendTransaction).toBe('function');
|
|
287
|
+
expect(typeof extended.planTransaction).toBe('function');
|
|
288
|
+
});
|
|
289
|
+
it('should preserve existing properties when extending via plugin spread', async () => {
|
|
290
|
+
mockRpcResponse({
|
|
291
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
292
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
293
|
+
});
|
|
294
|
+
const client = await createKitKoraClient({
|
|
295
|
+
endpoint: MOCK_ENDPOINT,
|
|
296
|
+
rpcUrl: MOCK_RPC_URL,
|
|
297
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
298
|
+
feePayerWallet: MOCK_WALLET,
|
|
299
|
+
});
|
|
300
|
+
// Plugin that adds extra property (with spread)
|
|
301
|
+
const extended = client.use((c) => ({
|
|
302
|
+
...c,
|
|
303
|
+
extra: 42,
|
|
304
|
+
}));
|
|
305
|
+
expect(extended.extra).toBe(42);
|
|
306
|
+
expect(extended.payer.address).toBe(MOCK_PAYER_ADDRESS);
|
|
307
|
+
expect(extended.paymentAddress).toBe(MOCK_PAYMENT_ADDRESS);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
describe('auth passthrough', () => {
|
|
311
|
+
it('should pass apiKey to underlying KoraClient', async () => {
|
|
312
|
+
mockRpcResponse({
|
|
313
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
314
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
315
|
+
});
|
|
316
|
+
await createKitKoraClient({
|
|
317
|
+
endpoint: MOCK_ENDPOINT,
|
|
318
|
+
rpcUrl: MOCK_RPC_URL,
|
|
319
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
320
|
+
feePayerWallet: MOCK_WALLET,
|
|
321
|
+
apiKey: 'test-api-key',
|
|
322
|
+
});
|
|
323
|
+
// The init call should include the API key header
|
|
324
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
325
|
+
expect(headers['x-api-key']).toBe('test-api-key');
|
|
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
|
+
});
|
|
346
|
+
describe('Token-2022 support', () => {
|
|
347
|
+
it('should accept tokenProgramId in config', async () => {
|
|
348
|
+
mockRpcResponse({
|
|
349
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
350
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
351
|
+
});
|
|
352
|
+
const TOKEN_2022_PROGRAM_ADDRESS = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
|
|
353
|
+
const client = await createKitKoraClient({
|
|
354
|
+
endpoint: MOCK_ENDPOINT,
|
|
355
|
+
rpcUrl: MOCK_RPC_URL,
|
|
356
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
357
|
+
feePayerWallet: MOCK_WALLET,
|
|
358
|
+
tokenProgramId: TOKEN_2022_PROGRAM_ADDRESS,
|
|
359
|
+
});
|
|
360
|
+
expect(client).toBeDefined();
|
|
361
|
+
expect(typeof client.sendTransaction).toBe('function');
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
describe('compute budget instructions', () => {
|
|
365
|
+
const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
366
|
+
// SetComputeUnitLimit discriminator = 0x02, SetComputeUnitPrice discriminator = 0x03
|
|
367
|
+
const CU_LIMIT_DISCRIMINATOR = 2;
|
|
368
|
+
const CU_PRICE_DISCRIMINATOR = 3;
|
|
369
|
+
const DUMMY_IX = {
|
|
370
|
+
programAddress: address('11111111111111111111111111111111'),
|
|
371
|
+
accounts: [],
|
|
372
|
+
data: new Uint8Array(4),
|
|
373
|
+
};
|
|
374
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
375
|
+
function getComputeBudgetIxs(planned) {
|
|
376
|
+
return planned.instructions.filter((ix) => ix.programAddress === COMPUTE_BUDGET_PROGRAM);
|
|
377
|
+
}
|
|
378
|
+
it('should include provisory CU limit by default (simulation-based estimation)', async () => {
|
|
379
|
+
mockRpcResponse({
|
|
380
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
381
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
382
|
+
});
|
|
383
|
+
const client = await createKitKoraClient({
|
|
384
|
+
endpoint: MOCK_ENDPOINT,
|
|
385
|
+
rpcUrl: MOCK_RPC_URL,
|
|
386
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
387
|
+
feePayerWallet: MOCK_WALLET,
|
|
388
|
+
});
|
|
389
|
+
const planned = await client.planTransaction([DUMMY_IX]);
|
|
390
|
+
const cbIxs = getComputeBudgetIxs(planned);
|
|
391
|
+
expect(cbIxs).toHaveLength(1);
|
|
392
|
+
expect(cbIxs[0].data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
|
|
393
|
+
const units = new DataView(cbIxs[0].data.buffer, cbIxs[0].data.byteOffset).getUint32(1, true);
|
|
394
|
+
expect(units).toBe(0); // Provisory — resolved via simulation in executor
|
|
395
|
+
});
|
|
396
|
+
it('should include SetComputeUnitLimit with correct units when computeUnitLimit is set', async () => {
|
|
397
|
+
mockRpcResponse({
|
|
398
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
399
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
400
|
+
});
|
|
401
|
+
const client = await createKitKoraClient({
|
|
402
|
+
endpoint: MOCK_ENDPOINT,
|
|
403
|
+
rpcUrl: MOCK_RPC_URL,
|
|
404
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
405
|
+
feePayerWallet: MOCK_WALLET,
|
|
406
|
+
computeUnitLimit: 200_000,
|
|
407
|
+
});
|
|
408
|
+
const planned = await client.planTransaction([DUMMY_IX]);
|
|
409
|
+
const cbIxs = getComputeBudgetIxs(planned);
|
|
410
|
+
expect(cbIxs).toHaveLength(1);
|
|
411
|
+
const ix = cbIxs[0];
|
|
412
|
+
// discriminator 0x02 = SetComputeUnitLimit
|
|
413
|
+
expect(ix.data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
|
|
414
|
+
// 200_000 in u32 LE = [0x40, 0x0D, 0x03, 0x00]
|
|
415
|
+
const units = new DataView(ix.data.buffer, ix.data.byteOffset).getUint32(1, true);
|
|
416
|
+
expect(units).toBe(200_000);
|
|
417
|
+
});
|
|
418
|
+
it('should include SetComputeUnitPrice and provisory CU limit when computeUnitPrice is set', async () => {
|
|
419
|
+
mockRpcResponse({
|
|
420
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
421
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
422
|
+
});
|
|
423
|
+
const client = await createKitKoraClient({
|
|
424
|
+
endpoint: MOCK_ENDPOINT,
|
|
425
|
+
rpcUrl: MOCK_RPC_URL,
|
|
426
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
427
|
+
feePayerWallet: MOCK_WALLET,
|
|
428
|
+
computeUnitPrice: 1000n,
|
|
429
|
+
});
|
|
430
|
+
const planned = await client.planTransaction([DUMMY_IX]);
|
|
431
|
+
const cbIxs = getComputeBudgetIxs(planned);
|
|
432
|
+
// Price instruction + provisory CU limit (simulation-based estimation always on)
|
|
433
|
+
expect(cbIxs).toHaveLength(2);
|
|
434
|
+
const priceIx = cbIxs.find(ix => ix.data[0] === CU_PRICE_DISCRIMINATOR);
|
|
435
|
+
expect(priceIx).toBeDefined();
|
|
436
|
+
const ix = priceIx;
|
|
437
|
+
// discriminator 0x03 = SetComputeUnitPrice
|
|
438
|
+
expect(ix.data[0]).toBe(CU_PRICE_DISCRIMINATOR);
|
|
439
|
+
// 1000 in u64 LE
|
|
440
|
+
const view = new DataView(ix.data.buffer, ix.data.byteOffset);
|
|
441
|
+
const microLamports = view.getBigUint64(1, true);
|
|
442
|
+
expect(microLamports).toBe(1000n);
|
|
443
|
+
});
|
|
444
|
+
it('should include both CU limit and price instructions when both are set', async () => {
|
|
445
|
+
mockRpcResponse({
|
|
446
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
447
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
448
|
+
});
|
|
449
|
+
const client = await createKitKoraClient({
|
|
450
|
+
endpoint: MOCK_ENDPOINT,
|
|
451
|
+
rpcUrl: MOCK_RPC_URL,
|
|
452
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
453
|
+
feePayerWallet: MOCK_WALLET,
|
|
454
|
+
computeUnitLimit: 150_000,
|
|
455
|
+
computeUnitPrice: 500n,
|
|
456
|
+
});
|
|
457
|
+
const planned = await client.planTransaction([DUMMY_IX]);
|
|
458
|
+
const cbIxs = getComputeBudgetIxs(planned);
|
|
459
|
+
expect(cbIxs).toHaveLength(2);
|
|
460
|
+
// First should be SetComputeUnitLimit
|
|
461
|
+
expect(cbIxs[0].data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
|
|
462
|
+
const units = new DataView(cbIxs[0].data.buffer, cbIxs[0].data.byteOffset).getUint32(1, true);
|
|
463
|
+
expect(units).toBe(150_000);
|
|
464
|
+
// Second should be SetComputeUnitPrice
|
|
465
|
+
expect(cbIxs[1].data[0]).toBe(CU_PRICE_DISCRIMINATOR);
|
|
466
|
+
const microLamports = new DataView(cbIxs[1].data.buffer, cbIxs[1].data.byteOffset).getBigUint64(1, true);
|
|
467
|
+
expect(microLamports).toBe(500n);
|
|
468
|
+
});
|
|
469
|
+
it('should use explicit computeUnitLimit over simulation when computeUnitLimit is set', async () => {
|
|
470
|
+
mockRpcResponse({
|
|
471
|
+
signer_address: MOCK_PAYER_ADDRESS,
|
|
472
|
+
payment_address: MOCK_PAYMENT_ADDRESS,
|
|
473
|
+
});
|
|
474
|
+
const client = await createKitKoraClient({
|
|
475
|
+
endpoint: MOCK_ENDPOINT,
|
|
476
|
+
rpcUrl: MOCK_RPC_URL,
|
|
477
|
+
feeToken: MOCK_FEE_TOKEN,
|
|
478
|
+
feePayerWallet: MOCK_WALLET,
|
|
479
|
+
computeUnitLimit: 200_000,
|
|
480
|
+
});
|
|
481
|
+
const planned = await client.planTransaction([DUMMY_IX]);
|
|
482
|
+
const cbIxs = getComputeBudgetIxs(planned);
|
|
483
|
+
expect(cbIxs).toHaveLength(1);
|
|
484
|
+
const ix = cbIxs[0];
|
|
485
|
+
expect(ix.data[0]).toBe(CU_LIMIT_DISCRIMINATOR);
|
|
486
|
+
// Should be the explicit 200_000, not 0 (provisory)
|
|
487
|
+
const units = new DataView(ix.data.buffer, ix.data.byteOffset).getUint32(1, true);
|
|
488
|
+
expect(units).toBe(200_000);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
package/dist/test/setup.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Address,
|
|
1
|
+
import { Address, KeyPairSigner } from '@solana/kit';
|
|
2
2
|
import { KoraClient } from '../src/index.js';
|
|
3
3
|
interface TestSuite {
|
|
4
4
|
destinationAddress: Address<string>;
|
|
@@ -9,14 +9,11 @@ interface TestSuite {
|
|
|
9
9
|
usdcMint: Address<string>;
|
|
10
10
|
}
|
|
11
11
|
export declare function loadEnvironmentVariables(): {
|
|
12
|
-
commitment: Commitment;
|
|
13
12
|
destinationAddress: Address<string>;
|
|
14
13
|
koraAddress: Address<string>;
|
|
15
14
|
koraRpcUrl: string;
|
|
16
15
|
koraSignerType: string;
|
|
17
16
|
solDropAmount: bigint;
|
|
18
|
-
solanaRpcUrl: string;
|
|
19
|
-
solanaWsUrl: string;
|
|
20
17
|
testUsdcMintSecret: string;
|
|
21
18
|
testWalletSecret: string;
|
|
22
19
|
tokenDecimals: number;
|