@solana/kora 0.2.0 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/client.d.ts +201 -7
- package/dist/src/client.js +223 -18
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/kit/executor.js +18 -5
- package/dist/src/kit/index.d.ts +4 -1
- package/dist/src/kit/index.js +10 -2
- package/dist/src/kit/payment.js +3 -2
- package/dist/src/plugin.d.ts +85 -0
- package/dist/src/{kit/plugin.js → plugin.js} +88 -18
- package/dist/src/types/index.d.ts +173 -78
- package/dist/test/auth-setup.js +4 -4
- package/dist/test/integration.test.js +168 -291
- package/dist/test/kit-client.test.js +18 -0
- package/dist/test/plugin.test.js +126 -71
- package/dist/test/setup.d.ts +8 -15
- package/dist/test/setup.js +52 -152
- package/dist/test/unit.test.js +318 -166
- package/package.json +8 -16
- package/dist/src/kit/plugin.d.ts +0 -31
package/dist/test/unit.test.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
2
|
+
import { appendTransactionMessageInstructions, createNoopSigner, createTransactionMessage, generateKeyPairSigner, partiallySignTransactionMessageWithSigners, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
|
|
1
3
|
import { KoraClient } from '../src/client.js';
|
|
2
|
-
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
3
4
|
import { getInstructionsFromBase64Message } from '../src/utils/transaction.js';
|
|
4
5
|
// Mock fetch globally
|
|
5
6
|
const mockFetch = jest.fn();
|
|
@@ -11,8 +12,8 @@ describe('KoraClient Unit Tests', () => {
|
|
|
11
12
|
const mockSuccessfulResponse = (result) => {
|
|
12
13
|
mockFetch.mockResolvedValueOnce({
|
|
13
14
|
json: jest.fn().mockResolvedValueOnce({
|
|
14
|
-
jsonrpc: '2.0',
|
|
15
15
|
id: 1,
|
|
16
|
+
jsonrpc: '2.0',
|
|
16
17
|
result,
|
|
17
18
|
}),
|
|
18
19
|
});
|
|
@@ -20,9 +21,9 @@ describe('KoraClient Unit Tests', () => {
|
|
|
20
21
|
const mockErrorResponse = (error) => {
|
|
21
22
|
mockFetch.mockResolvedValueOnce({
|
|
22
23
|
json: jest.fn().mockResolvedValueOnce({
|
|
23
|
-
jsonrpc: '2.0',
|
|
24
|
-
id: 1,
|
|
25
24
|
error,
|
|
25
|
+
id: 1,
|
|
26
|
+
jsonrpc: '2.0',
|
|
26
27
|
}),
|
|
27
28
|
});
|
|
28
29
|
};
|
|
@@ -78,70 +79,81 @@ describe('KoraClient Unit Tests', () => {
|
|
|
78
79
|
describe('getConfig', () => {
|
|
79
80
|
it('should return configuration', async () => {
|
|
80
81
|
const mockConfig = {
|
|
82
|
+
enabled_methods: {
|
|
83
|
+
estimate_bundle_fee: true,
|
|
84
|
+
estimate_transaction_fee: true,
|
|
85
|
+
get_blockhash: true,
|
|
86
|
+
get_config: true,
|
|
87
|
+
get_payer_signer: true,
|
|
88
|
+
get_supported_tokens: true,
|
|
89
|
+
get_version: true,
|
|
90
|
+
liveness: true,
|
|
91
|
+
sign_and_send_bundle: true,
|
|
92
|
+
sign_and_send_transaction: true,
|
|
93
|
+
sign_bundle: true,
|
|
94
|
+
sign_transaction: true,
|
|
95
|
+
transfer_transaction: true,
|
|
96
|
+
},
|
|
81
97
|
fee_payers: ['test_fee_payer_address'],
|
|
82
98
|
validation_config: {
|
|
83
|
-
max_allowed_lamports: 1000000,
|
|
84
|
-
max_signatures: 10,
|
|
85
|
-
price_source: 'Jupiter',
|
|
86
99
|
allowed_programs: ['program1', 'program2'],
|
|
87
|
-
allowed_tokens: ['token1', 'token2'],
|
|
88
100
|
allowed_spl_paid_tokens: ['spl_token1'],
|
|
101
|
+
allowed_tokens: ['token1', 'token2'],
|
|
89
102
|
disallowed_accounts: ['account1'],
|
|
90
103
|
fee_payer_policy: {
|
|
91
|
-
|
|
104
|
+
spl_token: {
|
|
105
|
+
allow_approve: true,
|
|
106
|
+
allow_burn: true,
|
|
107
|
+
allow_close_account: true,
|
|
108
|
+
allow_freeze_account: true,
|
|
109
|
+
allow_initialize_account: true,
|
|
110
|
+
allow_initialize_mint: true,
|
|
111
|
+
allow_initialize_multisig: true,
|
|
112
|
+
allow_mint_to: true,
|
|
113
|
+
allow_revoke: true,
|
|
114
|
+
allow_set_authority: true,
|
|
115
|
+
allow_thaw_account: true,
|
|
92
116
|
allow_transfer: true,
|
|
117
|
+
},
|
|
118
|
+
system: {
|
|
119
|
+
allow_allocate: true,
|
|
93
120
|
allow_assign: true,
|
|
94
121
|
allow_create_account: true,
|
|
95
|
-
|
|
122
|
+
allow_transfer: true,
|
|
96
123
|
nonce: {
|
|
97
|
-
allow_initialize: true,
|
|
98
124
|
allow_advance: true,
|
|
99
125
|
allow_authorize: true,
|
|
126
|
+
allow_initialize: true,
|
|
100
127
|
allow_withdraw: true,
|
|
101
128
|
},
|
|
102
129
|
},
|
|
103
|
-
spl_token: {
|
|
104
|
-
allow_transfer: true,
|
|
105
|
-
allow_burn: true,
|
|
106
|
-
allow_close_account: true,
|
|
107
|
-
allow_approve: true,
|
|
108
|
-
allow_revoke: true,
|
|
109
|
-
allow_set_authority: true,
|
|
110
|
-
allow_mint_to: true,
|
|
111
|
-
allow_freeze_account: true,
|
|
112
|
-
allow_thaw_account: true,
|
|
113
|
-
},
|
|
114
130
|
token_2022: {
|
|
115
|
-
|
|
131
|
+
allow_approve: true,
|
|
116
132
|
allow_burn: true,
|
|
117
133
|
allow_close_account: true,
|
|
118
|
-
|
|
134
|
+
allow_freeze_account: true,
|
|
135
|
+
allow_initialize_account: true,
|
|
136
|
+
allow_initialize_mint: true,
|
|
137
|
+
allow_initialize_multisig: true,
|
|
138
|
+
allow_mint_to: true,
|
|
119
139
|
allow_revoke: true,
|
|
120
140
|
allow_set_authority: true,
|
|
121
|
-
allow_mint_to: true,
|
|
122
|
-
allow_freeze_account: true,
|
|
123
141
|
allow_thaw_account: true,
|
|
142
|
+
allow_transfer: false,
|
|
124
143
|
},
|
|
125
144
|
},
|
|
145
|
+
max_allowed_lamports: 1000000,
|
|
146
|
+
max_signatures: 10,
|
|
126
147
|
price: {
|
|
127
|
-
type: 'margin',
|
|
128
148
|
margin: 0.1,
|
|
149
|
+
type: 'margin',
|
|
129
150
|
},
|
|
151
|
+
price_source: 'Jupiter',
|
|
130
152
|
token2022: {
|
|
131
|
-
blocked_mint_extensions: ['extension1', 'extension2'],
|
|
132
153
|
blocked_account_extensions: ['account_extension1', 'account_extension2'],
|
|
154
|
+
blocked_mint_extensions: ['extension1', 'extension2'],
|
|
133
155
|
},
|
|
134
156
|
},
|
|
135
|
-
enabled_methods: {
|
|
136
|
-
liveness: true,
|
|
137
|
-
estimate_transaction_fee: true,
|
|
138
|
-
get_supported_tokens: true,
|
|
139
|
-
sign_transaction: true,
|
|
140
|
-
sign_and_send_transaction: true,
|
|
141
|
-
transfer_transaction: true,
|
|
142
|
-
get_blockhash: true,
|
|
143
|
-
get_config: true,
|
|
144
|
-
},
|
|
145
157
|
};
|
|
146
158
|
await testSuccessfulRpcMethod('getConfig', () => client.getConfig(), mockConfig);
|
|
147
159
|
});
|
|
@@ -154,6 +166,14 @@ describe('KoraClient Unit Tests', () => {
|
|
|
154
166
|
await testSuccessfulRpcMethod('getBlockhash', () => client.getBlockhash(), mockResponse);
|
|
155
167
|
});
|
|
156
168
|
});
|
|
169
|
+
describe('getVersion', () => {
|
|
170
|
+
it('should return server version', async () => {
|
|
171
|
+
const mockResponse = {
|
|
172
|
+
version: '2.1.0-beta.0',
|
|
173
|
+
};
|
|
174
|
+
await testSuccessfulRpcMethod('getVersion', () => client.getVersion(), mockResponse);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
157
177
|
describe('getSupportedTokens', () => {
|
|
158
178
|
it('should return supported tokens list', async () => {
|
|
159
179
|
const mockResponse = {
|
|
@@ -165,15 +185,15 @@ describe('KoraClient Unit Tests', () => {
|
|
|
165
185
|
describe('getPayerSigner', () => {
|
|
166
186
|
it('should return payer signer and payment destination', async () => {
|
|
167
187
|
const mockResponse = {
|
|
168
|
-
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
169
188
|
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
189
|
+
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
170
190
|
};
|
|
171
191
|
await testSuccessfulRpcMethod('getPayerSigner', () => client.getPayerSigner(), mockResponse);
|
|
172
192
|
});
|
|
173
193
|
it('should return same address for signer and payment_destination when no separate paymaster', async () => {
|
|
174
194
|
const mockResponse = {
|
|
175
|
-
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
176
195
|
payment_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
196
|
+
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
177
197
|
};
|
|
178
198
|
await testSuccessfulRpcMethod('getPayerSigner', () => client.getPayerSigner(), mockResponse);
|
|
179
199
|
expect(mockResponse.signer_address).toBe(mockResponse.payment_address);
|
|
@@ -182,14 +202,14 @@ describe('KoraClient Unit Tests', () => {
|
|
|
182
202
|
describe('estimateTransactionFee', () => {
|
|
183
203
|
it('should estimate transaction fee', async () => {
|
|
184
204
|
const request = {
|
|
185
|
-
transaction: 'base64_encoded_transaction',
|
|
186
205
|
fee_token: 'SOL',
|
|
206
|
+
transaction: 'base64_encoded_transaction',
|
|
187
207
|
};
|
|
188
208
|
const mockResponse = {
|
|
189
209
|
fee_in_lamports: 5000,
|
|
190
210
|
fee_in_token: 25,
|
|
191
|
-
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
192
211
|
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
212
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
193
213
|
};
|
|
194
214
|
await testSuccessfulRpcMethod('estimateTransactionFee', () => client.estimateTransactionFee(request), mockResponse, request);
|
|
195
215
|
});
|
|
@@ -219,148 +239,139 @@ describe('KoraClient Unit Tests', () => {
|
|
|
219
239
|
await testSuccessfulRpcMethod('signAndSendTransaction', () => client.signAndSendTransaction(request), mockResponse, request);
|
|
220
240
|
});
|
|
221
241
|
});
|
|
222
|
-
describe('
|
|
223
|
-
it('should
|
|
242
|
+
describe('signBundle', () => {
|
|
243
|
+
it('should sign bundle of transactions', async () => {
|
|
224
244
|
const request = {
|
|
225
|
-
|
|
226
|
-
token: 'SOL',
|
|
227
|
-
source: 'source_address',
|
|
228
|
-
destination: 'destination_address',
|
|
245
|
+
transactions: ['base64_tx_1', 'base64_tx_2'],
|
|
229
246
|
};
|
|
230
247
|
const mockResponse = {
|
|
231
|
-
|
|
232
|
-
message: 'Transfer transaction created',
|
|
233
|
-
blockhash: 'test_blockhash',
|
|
248
|
+
signed_transactions: ['base64_signed_tx_1', 'base64_signed_tx_2'],
|
|
234
249
|
signer_pubkey: 'test_signer_pubkey',
|
|
235
|
-
instructions: [],
|
|
236
250
|
};
|
|
237
|
-
await testSuccessfulRpcMethod('
|
|
251
|
+
await testSuccessfulRpcMethod('signBundle', () => client.signBundle(request), mockResponse, request);
|
|
238
252
|
});
|
|
239
|
-
it('should
|
|
253
|
+
it('should handle RPC error', async () => {
|
|
240
254
|
const request = {
|
|
241
|
-
|
|
242
|
-
token: 'SOL',
|
|
243
|
-
source: 'source_address',
|
|
244
|
-
destination: 'destination_address',
|
|
255
|
+
transactions: ['base64_tx_1'],
|
|
245
256
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
signer_pubkey: 'test_signer_pubkey',
|
|
254
|
-
instructions: [],
|
|
255
|
-
};
|
|
256
|
-
mockSuccessfulResponse(mockResponse);
|
|
257
|
-
const result = await client.transferTransaction(request);
|
|
258
|
-
expect(result.instructions).toBeDefined();
|
|
259
|
-
expect(Array.isArray(result.instructions)).toBe(true);
|
|
260
|
-
// The instructions array should be populated from the parsed message
|
|
261
|
-
expect(result.instructions).not.toBeNull();
|
|
262
|
-
});
|
|
263
|
-
it('should handle transfer transaction with empty message gracefully', async () => {
|
|
257
|
+
const mockError = { code: -32000, message: 'Bundle validation failed' };
|
|
258
|
+
mockErrorResponse(mockError);
|
|
259
|
+
await expect(client.signBundle(request)).rejects.toThrow('RPC Error -32000: Bundle validation failed');
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('signAndSendBundle', () => {
|
|
263
|
+
it('should sign and send bundle of transactions', async () => {
|
|
264
264
|
const request = {
|
|
265
|
-
|
|
266
|
-
token: 'SOL',
|
|
267
|
-
source: 'source_address',
|
|
268
|
-
destination: 'destination_address',
|
|
265
|
+
transactions: ['base64_tx_1', 'base64_tx_2'],
|
|
269
266
|
};
|
|
270
267
|
const mockResponse = {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
blockhash: 'test_blockhash',
|
|
268
|
+
bundle_uuid: 'test-bundle-uuid-123',
|
|
269
|
+
signed_transactions: ['base64_signed_tx_1', 'base64_signed_tx_2'],
|
|
274
270
|
signer_pubkey: 'test_signer_pubkey',
|
|
275
|
-
instructions: [],
|
|
276
271
|
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
272
|
+
await testSuccessfulRpcMethod('signAndSendBundle', () => client.signAndSendBundle(request), mockResponse, request);
|
|
273
|
+
});
|
|
274
|
+
it('should handle RPC error', async () => {
|
|
275
|
+
const request = {
|
|
276
|
+
transactions: ['base64_tx_1'],
|
|
277
|
+
};
|
|
278
|
+
const mockError = { code: -32000, message: 'Jito submission failed' };
|
|
279
|
+
mockErrorResponse(mockError);
|
|
280
|
+
await expect(client.signAndSendBundle(request)).rejects.toThrow('RPC Error -32000: Jito submission failed');
|
|
281
281
|
});
|
|
282
282
|
});
|
|
283
283
|
describe('getPaymentInstruction', () => {
|
|
284
|
-
const
|
|
284
|
+
const _mockConfig = {
|
|
285
|
+
enabled_methods: {
|
|
286
|
+
estimate_bundle_fee: true,
|
|
287
|
+
estimate_transaction_fee: true,
|
|
288
|
+
get_blockhash: true,
|
|
289
|
+
get_config: true,
|
|
290
|
+
get_payer_signer: true,
|
|
291
|
+
get_supported_tokens: true,
|
|
292
|
+
get_version: true,
|
|
293
|
+
liveness: true,
|
|
294
|
+
sign_and_send_bundle: true,
|
|
295
|
+
sign_and_send_transaction: true,
|
|
296
|
+
sign_bundle: true,
|
|
297
|
+
sign_transaction: true,
|
|
298
|
+
transfer_transaction: true,
|
|
299
|
+
},
|
|
285
300
|
fee_payers: ['11111111111111111111111111111111'],
|
|
286
301
|
validation_config: {
|
|
287
|
-
max_allowed_lamports: 1000000,
|
|
288
|
-
max_signatures: 10,
|
|
289
|
-
price_source: 'Jupiter',
|
|
290
302
|
allowed_programs: ['program1'],
|
|
291
|
-
allowed_tokens: ['token1'],
|
|
292
303
|
allowed_spl_paid_tokens: ['4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'],
|
|
304
|
+
allowed_tokens: ['token1'],
|
|
293
305
|
disallowed_accounts: [],
|
|
294
306
|
fee_payer_policy: {
|
|
295
|
-
|
|
307
|
+
spl_token: {
|
|
308
|
+
allow_approve: true,
|
|
309
|
+
allow_burn: true,
|
|
310
|
+
allow_close_account: true,
|
|
311
|
+
allow_freeze_account: true,
|
|
312
|
+
allow_initialize_account: true,
|
|
313
|
+
allow_initialize_mint: true,
|
|
314
|
+
allow_initialize_multisig: true,
|
|
315
|
+
allow_mint_to: true,
|
|
316
|
+
allow_revoke: true,
|
|
317
|
+
allow_set_authority: true,
|
|
318
|
+
allow_thaw_account: true,
|
|
296
319
|
allow_transfer: true,
|
|
320
|
+
},
|
|
321
|
+
system: {
|
|
322
|
+
allow_allocate: true,
|
|
297
323
|
allow_assign: true,
|
|
298
324
|
allow_create_account: true,
|
|
299
|
-
|
|
325
|
+
allow_transfer: true,
|
|
300
326
|
nonce: {
|
|
301
|
-
allow_initialize: true,
|
|
302
327
|
allow_advance: true,
|
|
303
328
|
allow_authorize: true,
|
|
329
|
+
allow_initialize: true,
|
|
304
330
|
allow_withdraw: true,
|
|
305
331
|
},
|
|
306
332
|
},
|
|
307
|
-
spl_token: {
|
|
308
|
-
allow_transfer: true,
|
|
309
|
-
allow_burn: true,
|
|
310
|
-
allow_close_account: true,
|
|
311
|
-
allow_approve: true,
|
|
312
|
-
allow_revoke: true,
|
|
313
|
-
allow_set_authority: true,
|
|
314
|
-
allow_mint_to: true,
|
|
315
|
-
allow_freeze_account: true,
|
|
316
|
-
allow_thaw_account: true,
|
|
317
|
-
},
|
|
318
333
|
token_2022: {
|
|
319
|
-
|
|
334
|
+
allow_approve: true,
|
|
320
335
|
allow_burn: true,
|
|
321
336
|
allow_close_account: true,
|
|
322
|
-
|
|
337
|
+
allow_freeze_account: true,
|
|
338
|
+
allow_initialize_account: true,
|
|
339
|
+
allow_initialize_mint: true,
|
|
340
|
+
allow_initialize_multisig: true,
|
|
341
|
+
allow_mint_to: true,
|
|
323
342
|
allow_revoke: true,
|
|
324
343
|
allow_set_authority: true,
|
|
325
|
-
allow_mint_to: true,
|
|
326
|
-
allow_freeze_account: true,
|
|
327
344
|
allow_thaw_account: true,
|
|
345
|
+
allow_transfer: true,
|
|
328
346
|
},
|
|
329
347
|
},
|
|
348
|
+
max_allowed_lamports: 1000000,
|
|
349
|
+
max_signatures: 10,
|
|
330
350
|
price: {
|
|
331
|
-
type: 'margin',
|
|
332
351
|
margin: 0.1,
|
|
352
|
+
type: 'margin',
|
|
333
353
|
},
|
|
354
|
+
price_source: 'Jupiter',
|
|
334
355
|
token2022: {
|
|
335
|
-
blocked_mint_extensions: [],
|
|
336
356
|
blocked_account_extensions: [],
|
|
357
|
+
blocked_mint_extensions: [],
|
|
337
358
|
},
|
|
338
359
|
},
|
|
339
|
-
enabled_methods: {
|
|
340
|
-
liveness: true,
|
|
341
|
-
estimate_transaction_fee: true,
|
|
342
|
-
get_supported_tokens: true,
|
|
343
|
-
sign_transaction: true,
|
|
344
|
-
sign_and_send_transaction: true,
|
|
345
|
-
transfer_transaction: true,
|
|
346
|
-
get_blockhash: true,
|
|
347
|
-
get_config: true,
|
|
348
|
-
},
|
|
349
360
|
};
|
|
350
361
|
const mockFeeEstimate = {
|
|
351
362
|
fee_in_lamports: 5000,
|
|
352
363
|
fee_in_token: 50000,
|
|
353
|
-
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
354
364
|
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
365
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
355
366
|
};
|
|
356
367
|
// Create a mock base64-encoded transaction
|
|
357
368
|
// This is a minimal valid transaction structure
|
|
358
369
|
const mockTransactionBase64 = 'Aoq7ymA5OGP+gmDXiY5m3cYXlY2Rz/a/gFjOgt9ZuoCS7UzuiGGaEnW2OOtvHvMQHkkD7Z4LRF5B63ftu+1oZwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgECB1urjQEjgFgzqYhJ8IXJeSg4cJP1j1g2CJstOQTDchOKUzqH3PxgGW3c4V3vZV05A5Y30/MggOBs0Kd00s1JEwg5TaEeaV4+KL2y7fXIAuf6cN0ZQitbhY+G9ExtBSChspOXPgNcy9pYpETe4bmB+fg4bfZx1tnicA/kIyyubczAmbcIKIuniNOOQYG2ggKCz8NjEsHVezrWMatndu1wk6J5miGP26J6Vwp31AljiAajAFuP0D9mWJwSeFuA7J5rPwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpd/O36SW02zRtNtqk6GFeip2+yBQsVTeSbLL4rWJRkd4CBgQCBQQBCgxAQg8AAAAAAAYGBAIFAwEKDBAnAAAAAAAABg==';
|
|
359
370
|
const validRequest = {
|
|
360
|
-
transaction: mockTransactionBase64,
|
|
361
371
|
fee_token: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
|
|
362
372
|
source_wallet: '11111111111111111111111111111111',
|
|
363
373
|
token_program_id: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
|
374
|
+
transaction: mockTransactionBase64,
|
|
364
375
|
};
|
|
365
376
|
beforeEach(() => {
|
|
366
377
|
// Mock console.log to avoid noise in tests
|
|
@@ -373,16 +384,17 @@ describe('KoraClient Unit Tests', () => {
|
|
|
373
384
|
// Mock estimateTransactionFee call
|
|
374
385
|
mockFetch.mockResolvedValueOnce({
|
|
375
386
|
json: jest.fn().mockResolvedValueOnce({
|
|
376
|
-
jsonrpc: '2.0',
|
|
377
387
|
id: 1,
|
|
388
|
+
jsonrpc: '2.0',
|
|
378
389
|
result: mockFeeEstimate,
|
|
379
390
|
}),
|
|
380
391
|
});
|
|
381
392
|
const result = await client.getPaymentInstruction(validRequest);
|
|
382
393
|
expect(result).toEqual({
|
|
383
394
|
original_transaction: validRequest.transaction,
|
|
395
|
+
payment_address: mockFeeEstimate.payment_address,
|
|
396
|
+
payment_amount: mockFeeEstimate.fee_in_token,
|
|
384
397
|
payment_instruction: expect.objectContaining({
|
|
385
|
-
programAddress: TOKEN_PROGRAM_ADDRESS,
|
|
386
398
|
accounts: [
|
|
387
399
|
expect.objectContaining({
|
|
388
400
|
role: 1, // writable
|
|
@@ -391,25 +403,17 @@ describe('KoraClient Unit Tests', () => {
|
|
|
391
403
|
role: 1, // writable
|
|
392
404
|
}), // Destination token account
|
|
393
405
|
expect.objectContaining({
|
|
394
|
-
|
|
406
|
+
// readonly (plain address, no signer attached)
|
|
395
407
|
address: validRequest.source_wallet,
|
|
396
|
-
|
|
397
|
-
address: validRequest.source_wallet,
|
|
398
|
-
}),
|
|
408
|
+
role: 0,
|
|
399
409
|
}), // Authority
|
|
400
410
|
],
|
|
401
411
|
data: expect.any(Uint8Array),
|
|
412
|
+
programAddress: TOKEN_PROGRAM_ADDRESS,
|
|
402
413
|
}),
|
|
403
|
-
payment_amount: mockFeeEstimate.fee_in_token,
|
|
404
414
|
payment_token: validRequest.fee_token,
|
|
405
|
-
payment_address: mockFeeEstimate.payment_address,
|
|
406
415
|
signer_address: mockFeeEstimate.signer_pubkey,
|
|
407
|
-
signer: expect.objectContaining({
|
|
408
|
-
address: validRequest.source_wallet,
|
|
409
|
-
}),
|
|
410
416
|
});
|
|
411
|
-
expect(result.signer).toBeDefined();
|
|
412
|
-
expect(result.signer.address).toBe(validRequest.source_wallet);
|
|
413
417
|
// Verify only estimateTransactionFee was called
|
|
414
418
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
415
419
|
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
@@ -432,8 +436,8 @@ describe('KoraClient Unit Tests', () => {
|
|
|
432
436
|
// Mock estimateTransactionFee call
|
|
433
437
|
mockFetch.mockResolvedValueOnce({
|
|
434
438
|
json: jest.fn().mockResolvedValueOnce({
|
|
435
|
-
jsonrpc: '2.0',
|
|
436
439
|
id: 1,
|
|
440
|
+
jsonrpc: '2.0',
|
|
437
441
|
result: mockFeeEstimate,
|
|
438
442
|
}),
|
|
439
443
|
});
|
|
@@ -456,9 +460,9 @@ describe('KoraClient Unit Tests', () => {
|
|
|
456
460
|
const mockError = { code: -32602, message: 'Invalid transaction' };
|
|
457
461
|
mockFetch.mockResolvedValueOnce({
|
|
458
462
|
json: jest.fn().mockResolvedValueOnce({
|
|
459
|
-
jsonrpc: '2.0',
|
|
460
|
-
id: 1,
|
|
461
463
|
error: mockError,
|
|
464
|
+
id: 1,
|
|
465
|
+
jsonrpc: '2.0',
|
|
462
466
|
}),
|
|
463
467
|
});
|
|
464
468
|
await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('RPC Error -32602: Invalid transaction');
|
|
@@ -467,21 +471,85 @@ describe('KoraClient Unit Tests', () => {
|
|
|
467
471
|
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
468
472
|
await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('Network error');
|
|
469
473
|
});
|
|
474
|
+
it('should produce a payment instruction compatible with a real signer for the same address', async () => {
|
|
475
|
+
// Generate a real KeyPairSigner (simulates a user's wallet)
|
|
476
|
+
const userSigner = await generateKeyPairSigner();
|
|
477
|
+
// Mock estimateTransactionFee to return the user's address as source_wallet context
|
|
478
|
+
const feeEstimate = {
|
|
479
|
+
fee_in_lamports: 5000,
|
|
480
|
+
fee_in_token: 50000,
|
|
481
|
+
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
482
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
483
|
+
};
|
|
484
|
+
mockSuccessfulResponse(feeEstimate);
|
|
485
|
+
// Get payment instruction — authority is a plain address (no signer attached)
|
|
486
|
+
const result = await client.getPaymentInstruction({
|
|
487
|
+
...validRequest,
|
|
488
|
+
source_wallet: userSigner.address,
|
|
489
|
+
});
|
|
490
|
+
// Build another instruction that references the same address with the REAL signer
|
|
491
|
+
// (simulates a program instruction like makePurchase where the user is a signer)
|
|
492
|
+
const userOwnedIx = getTransferInstruction({
|
|
493
|
+
amount: 1000n,
|
|
494
|
+
authority: userSigner, // <-- real KeyPairSigner
|
|
495
|
+
destination: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
496
|
+
source: '11111111111111111111111111111111',
|
|
497
|
+
});
|
|
498
|
+
// Combine both instructions in a transaction — previously this would throw
|
|
499
|
+
// "Multiple distinct signers" because the payment instruction had a NoopSigner.
|
|
500
|
+
// Now the payment instruction uses a plain address, so no conflict.
|
|
501
|
+
const feePayer = createNoopSigner('DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7');
|
|
502
|
+
const txMessage = appendTransactionMessageInstructions([userOwnedIx, result.payment_instruction], setTransactionMessageLifetimeUsingBlockhash({ blockhash: '11111111111111111111111111111111', lastValidBlockHeight: 0n }, setTransactionMessageFeePayerSigner(feePayer, createTransactionMessage({ version: 0 }))));
|
|
503
|
+
// This should NOT throw "Multiple distinct signers"
|
|
504
|
+
await expect(partiallySignTransactionMessageWithSigners(txMessage)).resolves.toBeDefined();
|
|
505
|
+
});
|
|
506
|
+
it('should accept a TransactionSigner as source_wallet and preserve signer identity', async () => {
|
|
507
|
+
const userSigner = await generateKeyPairSigner();
|
|
508
|
+
const feeEstimate = {
|
|
509
|
+
fee_in_lamports: 5000,
|
|
510
|
+
fee_in_token: 50000,
|
|
511
|
+
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
512
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
513
|
+
};
|
|
514
|
+
mockSuccessfulResponse(feeEstimate);
|
|
515
|
+
// Pass the signer directly as source_wallet
|
|
516
|
+
const result = await client.getPaymentInstruction({
|
|
517
|
+
...validRequest,
|
|
518
|
+
source_wallet: userSigner,
|
|
519
|
+
});
|
|
520
|
+
// The authority account meta should carry the signer
|
|
521
|
+
const authorityMeta = result.payment_instruction.accounts?.[2];
|
|
522
|
+
expect(authorityMeta).toEqual(expect.objectContaining({
|
|
523
|
+
address: userSigner.address,
|
|
524
|
+
role: 2, // readonly-signer
|
|
525
|
+
signer: userSigner,
|
|
526
|
+
}));
|
|
527
|
+
// Combining with another instruction using the same signer should work
|
|
528
|
+
const userOwnedIx = getTransferInstruction({
|
|
529
|
+
amount: 1000n,
|
|
530
|
+
authority: userSigner,
|
|
531
|
+
destination: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
532
|
+
source: '11111111111111111111111111111111',
|
|
533
|
+
});
|
|
534
|
+
const feePayer = createNoopSigner('DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7');
|
|
535
|
+
const txMessage = appendTransactionMessageInstructions([userOwnedIx, result.payment_instruction], setTransactionMessageLifetimeUsingBlockhash({ blockhash: '11111111111111111111111111111111', lastValidBlockHeight: 0n }, setTransactionMessageFeePayerSigner(feePayer, createTransactionMessage({ version: 0 }))));
|
|
536
|
+
await expect(partiallySignTransactionMessageWithSigners(txMessage)).resolves.toBeDefined();
|
|
537
|
+
});
|
|
470
538
|
it('should return correct payment details in response', async () => {
|
|
471
539
|
mockFetch.mockResolvedValueOnce({
|
|
472
540
|
json: jest.fn().mockResolvedValueOnce({
|
|
473
|
-
jsonrpc: '2.0',
|
|
474
541
|
id: 1,
|
|
542
|
+
jsonrpc: '2.0',
|
|
475
543
|
result: mockFeeEstimate,
|
|
476
544
|
}),
|
|
477
545
|
});
|
|
478
546
|
const result = await client.getPaymentInstruction(validRequest);
|
|
479
547
|
expect(result).toMatchObject({
|
|
480
548
|
original_transaction: validRequest.transaction,
|
|
481
|
-
|
|
549
|
+
payment_address: mockFeeEstimate.payment_address,
|
|
482
550
|
payment_amount: mockFeeEstimate.fee_in_token,
|
|
551
|
+
payment_instruction: expect.any(Object),
|
|
483
552
|
payment_token: validRequest.fee_token,
|
|
484
|
-
payment_address: mockFeeEstimate.payment_address,
|
|
485
553
|
signer_address: mockFeeEstimate.signer_pubkey,
|
|
486
554
|
});
|
|
487
555
|
});
|
|
@@ -503,28 +571,112 @@ describe('KoraClient Unit Tests', () => {
|
|
|
503
571
|
await expect(client.getConfig()).rejects.toThrow('RPC Error undefined: undefined');
|
|
504
572
|
});
|
|
505
573
|
});
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
574
|
+
describe('reCAPTCHA Authentication', () => {
|
|
575
|
+
it('should include x-recaptcha-token header when getRecaptchaToken callback is provided (sync)', async () => {
|
|
576
|
+
const recaptchaClient = new KoraClient({
|
|
577
|
+
getRecaptchaToken: () => 'test-recaptcha-token',
|
|
578
|
+
rpcUrl: mockRpcUrl,
|
|
579
|
+
});
|
|
580
|
+
mockSuccessfulResponse({ version: '1.0.0' });
|
|
581
|
+
await recaptchaClient.getVersion();
|
|
582
|
+
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
583
|
+
body: JSON.stringify({
|
|
584
|
+
id: 1,
|
|
585
|
+
jsonrpc: '2.0',
|
|
586
|
+
method: 'getVersion',
|
|
587
|
+
params: undefined,
|
|
588
|
+
}),
|
|
589
|
+
headers: {
|
|
590
|
+
'Content-Type': 'application/json',
|
|
591
|
+
'x-recaptcha-token': 'test-recaptcha-token',
|
|
592
|
+
},
|
|
593
|
+
method: 'POST',
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
it('should include x-recaptcha-token header when getRecaptchaToken callback returns Promise', async () => {
|
|
597
|
+
const recaptchaClient = new KoraClient({
|
|
598
|
+
getRecaptchaToken: () => Promise.resolve('async-recaptcha-token'),
|
|
599
|
+
rpcUrl: mockRpcUrl,
|
|
600
|
+
});
|
|
601
|
+
mockSuccessfulResponse({ version: '1.0.0' });
|
|
602
|
+
await recaptchaClient.getVersion();
|
|
603
|
+
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
604
|
+
body: JSON.stringify({
|
|
605
|
+
id: 1,
|
|
606
|
+
jsonrpc: '2.0',
|
|
607
|
+
method: 'getVersion',
|
|
608
|
+
params: undefined,
|
|
609
|
+
}),
|
|
610
|
+
headers: {
|
|
611
|
+
'Content-Type': 'application/json',
|
|
612
|
+
'x-recaptcha-token': 'async-recaptcha-token',
|
|
613
|
+
},
|
|
614
|
+
method: 'POST',
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
it('should NOT include x-recaptcha-token header when getRecaptchaToken is not provided', async () => {
|
|
618
|
+
mockSuccessfulResponse({ version: '1.0.0' });
|
|
619
|
+
await client.getVersion();
|
|
620
|
+
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
621
|
+
body: JSON.stringify({
|
|
622
|
+
id: 1,
|
|
623
|
+
jsonrpc: '2.0',
|
|
624
|
+
method: 'getVersion',
|
|
625
|
+
params: undefined,
|
|
626
|
+
}),
|
|
627
|
+
headers: {
|
|
628
|
+
'Content-Type': 'application/json',
|
|
629
|
+
},
|
|
630
|
+
method: 'POST',
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
it('should include x-recaptcha-token along with other auth headers', async () => {
|
|
634
|
+
const combinedAuthClient = new KoraClient({
|
|
635
|
+
apiKey: 'test-api-key',
|
|
636
|
+
getRecaptchaToken: () => 'test-recaptcha-token',
|
|
637
|
+
rpcUrl: mockRpcUrl,
|
|
638
|
+
});
|
|
639
|
+
mockSuccessfulResponse({ version: '1.0.0' });
|
|
640
|
+
await combinedAuthClient.getVersion();
|
|
641
|
+
const callArgs = mockFetch.mock.calls[0][1];
|
|
642
|
+
expect(callArgs.headers).toMatchObject({
|
|
643
|
+
'Content-Type': 'application/json',
|
|
644
|
+
'x-api-key': 'test-api-key',
|
|
645
|
+
'x-recaptcha-token': 'test-recaptcha-token',
|
|
646
|
+
});
|
|
647
|
+
});
|
|
648
|
+
it('should call getRecaptchaToken callback for each request', async () => {
|
|
649
|
+
let callCount = 0;
|
|
650
|
+
const recaptchaClient = new KoraClient({
|
|
651
|
+
getRecaptchaToken: () => `token-${++callCount}`,
|
|
652
|
+
rpcUrl: mockRpcUrl,
|
|
653
|
+
});
|
|
654
|
+
mockSuccessfulResponse({ version: '1.0.0' });
|
|
655
|
+
await recaptchaClient.getVersion();
|
|
656
|
+
mockSuccessfulResponse({ blockhash: 'test-blockhash' });
|
|
657
|
+
await recaptchaClient.getBlockhash();
|
|
658
|
+
expect(callCount).toBe(2);
|
|
659
|
+
const calls = mockFetch.mock.calls;
|
|
660
|
+
expect(calls[0][1].headers['x-recaptcha-token']).toBe('token-1');
|
|
661
|
+
expect(calls[1][1].headers['x-recaptcha-token']).toBe('token-2');
|
|
662
|
+
});
|
|
663
|
+
it('should propagate errors when getRecaptchaToken callback throws', async () => {
|
|
664
|
+
const recaptchaClient = new KoraClient({
|
|
665
|
+
getRecaptchaToken: () => {
|
|
666
|
+
throw new Error('reCAPTCHA failed to load');
|
|
667
|
+
},
|
|
668
|
+
rpcUrl: mockRpcUrl,
|
|
669
|
+
});
|
|
670
|
+
await expect(recaptchaClient.getVersion()).rejects.toThrow('reCAPTCHA failed to load');
|
|
671
|
+
});
|
|
672
|
+
it('should propagate errors when getRecaptchaToken returns rejected Promise', async () => {
|
|
673
|
+
const recaptchaClient = new KoraClient({
|
|
674
|
+
getRecaptchaToken: () => Promise.reject(new Error('Token generation failed')),
|
|
675
|
+
rpcUrl: mockRpcUrl,
|
|
676
|
+
});
|
|
677
|
+
await expect(recaptchaClient.getVersion()).rejects.toThrow('Token generation failed');
|
|
678
|
+
});
|
|
679
|
+
});
|
|
528
680
|
});
|
|
529
681
|
describe('Transaction Utils', () => {
|
|
530
682
|
describe('getInstructionsFromBase64Message', () => {
|