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