@solana/kora 0.0.0 → 0.1.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/README.md +60 -0
- package/dist/src/client.d.ts +187 -0
- package/dist/src/client.js +287 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +2 -0
- package/dist/src/types/index.d.ts +383 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/utils/transaction.d.ts +8 -0
- package/dist/src/utils/transaction.js +32 -0
- package/dist/test/auth-setup.d.ts +1 -0
- package/dist/test/auth-setup.js +51 -0
- package/dist/test/integration.test.d.ts +1 -0
- package/dist/test/integration.test.js +307 -0
- package/dist/test/setup.d.ts +26 -0
- package/dist/test/setup.js +229 -0
- package/dist/test/unit.test.d.ts +1 -0
- package/dist/test/unit.test.js +552 -0
- package/package.json +59 -7
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { KoraClient } from '../src/client.js';
|
|
2
|
+
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
|
|
3
|
+
import { getInstructionsFromBase64Message } from '../src/utils/transaction.js';
|
|
4
|
+
// Mock fetch globally
|
|
5
|
+
const mockFetch = jest.fn();
|
|
6
|
+
global.fetch = mockFetch;
|
|
7
|
+
describe('KoraClient Unit Tests', () => {
|
|
8
|
+
let client;
|
|
9
|
+
const mockRpcUrl = 'http://localhost:8080';
|
|
10
|
+
// Helper Functions
|
|
11
|
+
const mockSuccessfulResponse = (result) => {
|
|
12
|
+
mockFetch.mockResolvedValueOnce({
|
|
13
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
14
|
+
jsonrpc: '2.0',
|
|
15
|
+
id: 1,
|
|
16
|
+
result,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
const mockErrorResponse = (error) => {
|
|
21
|
+
mockFetch.mockResolvedValueOnce({
|
|
22
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
23
|
+
jsonrpc: '2.0',
|
|
24
|
+
id: 1,
|
|
25
|
+
error,
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
const expectRpcCall = (method, params = undefined) => {
|
|
30
|
+
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
},
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
jsonrpc: '2.0',
|
|
37
|
+
id: 1,
|
|
38
|
+
method,
|
|
39
|
+
params,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
const testSuccessfulRpcMethod = async (methodName, clientMethod, expectedResult, params = undefined) => {
|
|
44
|
+
mockSuccessfulResponse(expectedResult);
|
|
45
|
+
const result = await clientMethod();
|
|
46
|
+
expect(result).toEqual(expectedResult);
|
|
47
|
+
expectRpcCall(methodName, params);
|
|
48
|
+
};
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
client = new KoraClient({ rpcUrl: mockRpcUrl });
|
|
51
|
+
mockFetch.mockClear();
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
jest.resetAllMocks();
|
|
55
|
+
});
|
|
56
|
+
describe('Constructor', () => {
|
|
57
|
+
it('should create KoraClient instance with provided RPC URL', () => {
|
|
58
|
+
const testUrl = 'https://api.example.com';
|
|
59
|
+
const testClient = new KoraClient({ rpcUrl: testUrl });
|
|
60
|
+
expect(testClient).toBeInstanceOf(KoraClient);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('RPC Request Handling', () => {
|
|
64
|
+
it('should handle successful RPC responses', async () => {
|
|
65
|
+
const mockResult = { value: 'test' };
|
|
66
|
+
await testSuccessfulRpcMethod('getConfig', () => client.getConfig(), mockResult);
|
|
67
|
+
});
|
|
68
|
+
it('should handle RPC error responses', async () => {
|
|
69
|
+
const mockError = { code: -32601, message: 'Method not found' };
|
|
70
|
+
mockErrorResponse(mockError);
|
|
71
|
+
await expect(client.getConfig()).rejects.toThrow('RPC Error -32601: Method not found');
|
|
72
|
+
});
|
|
73
|
+
it('should handle network errors', async () => {
|
|
74
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
75
|
+
await expect(client.getConfig()).rejects.toThrow('Network error');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('getConfig', () => {
|
|
79
|
+
it('should return configuration', async () => {
|
|
80
|
+
const mockConfig = {
|
|
81
|
+
fee_payers: ['test_fee_payer_address'],
|
|
82
|
+
validation_config: {
|
|
83
|
+
max_allowed_lamports: 1000000,
|
|
84
|
+
max_signatures: 10,
|
|
85
|
+
price_source: 'Jupiter',
|
|
86
|
+
allowed_programs: ['program1', 'program2'],
|
|
87
|
+
allowed_tokens: ['token1', 'token2'],
|
|
88
|
+
allowed_spl_paid_tokens: ['spl_token1'],
|
|
89
|
+
disallowed_accounts: ['account1'],
|
|
90
|
+
fee_payer_policy: {
|
|
91
|
+
system: {
|
|
92
|
+
allow_transfer: true,
|
|
93
|
+
allow_assign: true,
|
|
94
|
+
allow_create_account: true,
|
|
95
|
+
allow_allocate: true,
|
|
96
|
+
nonce: {
|
|
97
|
+
allow_initialize: true,
|
|
98
|
+
allow_advance: true,
|
|
99
|
+
allow_authorize: true,
|
|
100
|
+
allow_withdraw: true,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
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
|
+
token_2022: {
|
|
115
|
+
allow_transfer: false,
|
|
116
|
+
allow_burn: true,
|
|
117
|
+
allow_close_account: true,
|
|
118
|
+
allow_approve: true,
|
|
119
|
+
allow_revoke: true,
|
|
120
|
+
allow_set_authority: true,
|
|
121
|
+
allow_mint_to: true,
|
|
122
|
+
allow_freeze_account: true,
|
|
123
|
+
allow_thaw_account: true,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
price: {
|
|
127
|
+
type: 'margin',
|
|
128
|
+
margin: 0.1,
|
|
129
|
+
},
|
|
130
|
+
token2022: {
|
|
131
|
+
blocked_mint_extensions: ['extension1', 'extension2'],
|
|
132
|
+
blocked_account_extensions: ['account_extension1', 'account_extension2'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
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
|
+
};
|
|
146
|
+
await testSuccessfulRpcMethod('getConfig', () => client.getConfig(), mockConfig);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe('getBlockhash', () => {
|
|
150
|
+
it('should return blockhash', async () => {
|
|
151
|
+
const mockResponse = {
|
|
152
|
+
blockhash: 'test_blockhash_value',
|
|
153
|
+
};
|
|
154
|
+
await testSuccessfulRpcMethod('getBlockhash', () => client.getBlockhash(), mockResponse);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('getSupportedTokens', () => {
|
|
158
|
+
it('should return supported tokens list', async () => {
|
|
159
|
+
const mockResponse = {
|
|
160
|
+
tokens: ['SOL', 'USDC', 'USDT'],
|
|
161
|
+
};
|
|
162
|
+
await testSuccessfulRpcMethod('getSupportedTokens', () => client.getSupportedTokens(), mockResponse);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('getPayerSigner', () => {
|
|
166
|
+
it('should return payer signer and payment destination', async () => {
|
|
167
|
+
const mockResponse = {
|
|
168
|
+
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
169
|
+
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
170
|
+
};
|
|
171
|
+
await testSuccessfulRpcMethod('getPayerSigner', () => client.getPayerSigner(), mockResponse);
|
|
172
|
+
});
|
|
173
|
+
it('should return same address for signer and payment_destination when no separate paymaster', async () => {
|
|
174
|
+
const mockResponse = {
|
|
175
|
+
signer_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
176
|
+
payment_address: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
177
|
+
};
|
|
178
|
+
await testSuccessfulRpcMethod('getPayerSigner', () => client.getPayerSigner(), mockResponse);
|
|
179
|
+
expect(mockResponse.signer_address).toBe(mockResponse.payment_address);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
describe('estimateTransactionFee', () => {
|
|
183
|
+
it('should estimate transaction fee', async () => {
|
|
184
|
+
const request = {
|
|
185
|
+
transaction: 'base64_encoded_transaction',
|
|
186
|
+
fee_token: 'SOL',
|
|
187
|
+
};
|
|
188
|
+
const mockResponse = {
|
|
189
|
+
fee_in_lamports: 5000,
|
|
190
|
+
fee_in_token: 25,
|
|
191
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
192
|
+
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
193
|
+
};
|
|
194
|
+
await testSuccessfulRpcMethod('estimateTransactionFee', () => client.estimateTransactionFee(request), mockResponse, request);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe('signTransaction', () => {
|
|
198
|
+
it('should sign transaction', async () => {
|
|
199
|
+
const request = {
|
|
200
|
+
transaction: 'base64_encoded_transaction',
|
|
201
|
+
};
|
|
202
|
+
const mockResponse = {
|
|
203
|
+
signed_transaction: 'base64_signed_transaction',
|
|
204
|
+
signer_pubkey: 'test_signer_pubkey',
|
|
205
|
+
};
|
|
206
|
+
await testSuccessfulRpcMethod('signTransaction', () => client.signTransaction(request), mockResponse, request);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
describe('signAndSendTransaction', () => {
|
|
210
|
+
it('should sign and send transaction', async () => {
|
|
211
|
+
const request = {
|
|
212
|
+
transaction: 'base64_encoded_transaction',
|
|
213
|
+
};
|
|
214
|
+
const mockResponse = {
|
|
215
|
+
signed_transaction: 'base64_signed_transaction',
|
|
216
|
+
signer_pubkey: 'test_signer_pubkey',
|
|
217
|
+
};
|
|
218
|
+
await testSuccessfulRpcMethod('signAndSendTransaction', () => client.signAndSendTransaction(request), mockResponse, request);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('transferTransaction', () => {
|
|
222
|
+
it('should create transfer transaction', async () => {
|
|
223
|
+
const request = {
|
|
224
|
+
amount: 1000000,
|
|
225
|
+
token: 'SOL',
|
|
226
|
+
source: 'source_address',
|
|
227
|
+
destination: 'destination_address',
|
|
228
|
+
};
|
|
229
|
+
const mockResponse = {
|
|
230
|
+
transaction: 'base64_encoded_transaction',
|
|
231
|
+
message: 'Transfer transaction created',
|
|
232
|
+
blockhash: 'test_blockhash',
|
|
233
|
+
signer_pubkey: 'test_signer_pubkey',
|
|
234
|
+
instructions: [],
|
|
235
|
+
};
|
|
236
|
+
await testSuccessfulRpcMethod('transferTransaction', () => client.transferTransaction(request), mockResponse, request);
|
|
237
|
+
});
|
|
238
|
+
it('should parse instructions from transfer transaction message', async () => {
|
|
239
|
+
const request = {
|
|
240
|
+
amount: 1000000,
|
|
241
|
+
token: 'SOL',
|
|
242
|
+
source: 'source_address',
|
|
243
|
+
destination: 'destination_address',
|
|
244
|
+
};
|
|
245
|
+
// This is a real base64 encoded message for testing
|
|
246
|
+
// In production, this would come from the RPC response
|
|
247
|
+
const mockMessage = 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDAAEMAgAAAAEAAAAAAAAA';
|
|
248
|
+
const mockResponse = {
|
|
249
|
+
transaction: 'base64_encoded_transaction',
|
|
250
|
+
message: mockMessage,
|
|
251
|
+
blockhash: 'test_blockhash',
|
|
252
|
+
signer_pubkey: 'test_signer_pubkey',
|
|
253
|
+
instructions: [],
|
|
254
|
+
};
|
|
255
|
+
mockSuccessfulResponse(mockResponse);
|
|
256
|
+
const result = await client.transferTransaction(request);
|
|
257
|
+
expect(result.instructions).toBeDefined();
|
|
258
|
+
expect(Array.isArray(result.instructions)).toBe(true);
|
|
259
|
+
// The instructions array should be populated from the parsed message
|
|
260
|
+
expect(result.instructions).not.toBeNull();
|
|
261
|
+
});
|
|
262
|
+
it('should handle transfer transaction with empty message gracefully', async () => {
|
|
263
|
+
const request = {
|
|
264
|
+
amount: 1000000,
|
|
265
|
+
token: 'SOL',
|
|
266
|
+
source: 'source_address',
|
|
267
|
+
destination: 'destination_address',
|
|
268
|
+
};
|
|
269
|
+
const mockResponse = {
|
|
270
|
+
transaction: 'base64_encoded_transaction',
|
|
271
|
+
message: '',
|
|
272
|
+
blockhash: 'test_blockhash',
|
|
273
|
+
signer_pubkey: 'test_signer_pubkey',
|
|
274
|
+
instructions: [],
|
|
275
|
+
};
|
|
276
|
+
mockSuccessfulResponse(mockResponse);
|
|
277
|
+
const result = await client.transferTransaction(request);
|
|
278
|
+
// Should handle empty message gracefully
|
|
279
|
+
expect(result.instructions).toEqual([]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('getPaymentInstruction', () => {
|
|
283
|
+
const mockConfig = {
|
|
284
|
+
fee_payers: ['11111111111111111111111111111111'],
|
|
285
|
+
validation_config: {
|
|
286
|
+
max_allowed_lamports: 1000000,
|
|
287
|
+
max_signatures: 10,
|
|
288
|
+
price_source: 'Jupiter',
|
|
289
|
+
allowed_programs: ['program1'],
|
|
290
|
+
allowed_tokens: ['token1'],
|
|
291
|
+
allowed_spl_paid_tokens: ['4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'],
|
|
292
|
+
disallowed_accounts: [],
|
|
293
|
+
fee_payer_policy: {
|
|
294
|
+
system: {
|
|
295
|
+
allow_transfer: true,
|
|
296
|
+
allow_assign: true,
|
|
297
|
+
allow_create_account: true,
|
|
298
|
+
allow_allocate: true,
|
|
299
|
+
nonce: {
|
|
300
|
+
allow_initialize: true,
|
|
301
|
+
allow_advance: true,
|
|
302
|
+
allow_authorize: true,
|
|
303
|
+
allow_withdraw: true,
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
spl_token: {
|
|
307
|
+
allow_transfer: true,
|
|
308
|
+
allow_burn: true,
|
|
309
|
+
allow_close_account: true,
|
|
310
|
+
allow_approve: true,
|
|
311
|
+
allow_revoke: true,
|
|
312
|
+
allow_set_authority: true,
|
|
313
|
+
allow_mint_to: true,
|
|
314
|
+
allow_freeze_account: true,
|
|
315
|
+
allow_thaw_account: true,
|
|
316
|
+
},
|
|
317
|
+
token_2022: {
|
|
318
|
+
allow_transfer: true,
|
|
319
|
+
allow_burn: true,
|
|
320
|
+
allow_close_account: true,
|
|
321
|
+
allow_approve: true,
|
|
322
|
+
allow_revoke: true,
|
|
323
|
+
allow_set_authority: true,
|
|
324
|
+
allow_mint_to: true,
|
|
325
|
+
allow_freeze_account: true,
|
|
326
|
+
allow_thaw_account: true,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
price: {
|
|
330
|
+
type: 'margin',
|
|
331
|
+
margin: 0.1,
|
|
332
|
+
},
|
|
333
|
+
token2022: {
|
|
334
|
+
blocked_mint_extensions: [],
|
|
335
|
+
blocked_account_extensions: [],
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
enabled_methods: {
|
|
339
|
+
liveness: true,
|
|
340
|
+
estimate_transaction_fee: true,
|
|
341
|
+
get_supported_tokens: true,
|
|
342
|
+
sign_transaction: true,
|
|
343
|
+
sign_and_send_transaction: true,
|
|
344
|
+
transfer_transaction: true,
|
|
345
|
+
get_blockhash: true,
|
|
346
|
+
get_config: true,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
const mockFeeEstimate = {
|
|
350
|
+
fee_in_lamports: 5000,
|
|
351
|
+
fee_in_token: 50000,
|
|
352
|
+
signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
353
|
+
payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
|
|
354
|
+
};
|
|
355
|
+
// Create a mock base64-encoded transaction
|
|
356
|
+
// This is a minimal valid transaction structure
|
|
357
|
+
const mockTransactionBase64 = 'Aoq7ymA5OGP+gmDXiY5m3cYXlY2Rz/a/gFjOgt9ZuoCS7UzuiGGaEnW2OOtvHvMQHkkD7Z4LRF5B63ftu+1oZwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgECB1urjQEjgFgzqYhJ8IXJeSg4cJP1j1g2CJstOQTDchOKUzqH3PxgGW3c4V3vZV05A5Y30/MggOBs0Kd00s1JEwg5TaEeaV4+KL2y7fXIAuf6cN0ZQitbhY+G9ExtBSChspOXPgNcy9pYpETe4bmB+fg4bfZx1tnicA/kIyyubczAmbcIKIuniNOOQYG2ggKCz8NjEsHVezrWMatndu1wk6J5miGP26J6Vwp31AljiAajAFuP0D9mWJwSeFuA7J5rPwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpd/O36SW02zRtNtqk6GFeip2+yBQsVTeSbLL4rWJRkd4CBgQCBQQBCgxAQg8AAAAAAAYGBAIFAwEKDBAnAAAAAAAABg==';
|
|
358
|
+
const validRequest = {
|
|
359
|
+
transaction: mockTransactionBase64,
|
|
360
|
+
fee_token: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
|
|
361
|
+
source_wallet: '11111111111111111111111111111111',
|
|
362
|
+
token_program_id: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
|
|
363
|
+
};
|
|
364
|
+
beforeEach(() => {
|
|
365
|
+
// Mock console.log to avoid noise in tests
|
|
366
|
+
jest.spyOn(console, 'log').mockImplementation();
|
|
367
|
+
});
|
|
368
|
+
afterEach(() => {
|
|
369
|
+
jest.restoreAllMocks();
|
|
370
|
+
});
|
|
371
|
+
it('should successfully append payment instruction', async () => {
|
|
372
|
+
// Mock estimateTransactionFee call
|
|
373
|
+
mockFetch.mockResolvedValueOnce({
|
|
374
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
375
|
+
jsonrpc: '2.0',
|
|
376
|
+
id: 1,
|
|
377
|
+
result: mockFeeEstimate,
|
|
378
|
+
}),
|
|
379
|
+
});
|
|
380
|
+
const result = await client.getPaymentInstruction(validRequest);
|
|
381
|
+
expect(result).toEqual({
|
|
382
|
+
original_transaction: validRequest.transaction,
|
|
383
|
+
payment_instruction: expect.objectContaining({
|
|
384
|
+
programAddress: TOKEN_PROGRAM_ADDRESS,
|
|
385
|
+
accounts: [
|
|
386
|
+
expect.objectContaining({
|
|
387
|
+
role: 1, // writable
|
|
388
|
+
}), // Source token account
|
|
389
|
+
expect.objectContaining({
|
|
390
|
+
role: 1, // writable
|
|
391
|
+
}), // Destination token account
|
|
392
|
+
expect.objectContaining({
|
|
393
|
+
role: 2, // readonly-signer
|
|
394
|
+
address: validRequest.source_wallet,
|
|
395
|
+
signer: expect.objectContaining({
|
|
396
|
+
address: validRequest.source_wallet,
|
|
397
|
+
}),
|
|
398
|
+
}), // Authority
|
|
399
|
+
],
|
|
400
|
+
data: expect.any(Uint8Array),
|
|
401
|
+
}),
|
|
402
|
+
payment_amount: mockFeeEstimate.fee_in_token,
|
|
403
|
+
payment_token: validRequest.fee_token,
|
|
404
|
+
payment_address: mockFeeEstimate.payment_address,
|
|
405
|
+
signer_address: mockFeeEstimate.signer_pubkey,
|
|
406
|
+
});
|
|
407
|
+
// Verify only estimateTransactionFee was called
|
|
408
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
409
|
+
expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: {
|
|
412
|
+
'Content-Type': 'application/json',
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
jsonrpc: '2.0',
|
|
416
|
+
id: 1,
|
|
417
|
+
method: 'estimateTransactionFee',
|
|
418
|
+
params: {
|
|
419
|
+
transaction: validRequest.transaction,
|
|
420
|
+
fee_token: validRequest.fee_token,
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
it('should handle fixed pricing configuration', async () => {
|
|
426
|
+
// Mock estimateTransactionFee call
|
|
427
|
+
mockFetch.mockResolvedValueOnce({
|
|
428
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
429
|
+
jsonrpc: '2.0',
|
|
430
|
+
id: 1,
|
|
431
|
+
result: mockFeeEstimate,
|
|
432
|
+
}),
|
|
433
|
+
});
|
|
434
|
+
const result = await client.getPaymentInstruction(validRequest);
|
|
435
|
+
expect(result.payment_amount).toBe(mockFeeEstimate.fee_in_token);
|
|
436
|
+
expect(result.payment_token).toBe(validRequest.fee_token);
|
|
437
|
+
});
|
|
438
|
+
it('should throw error for invalid addresses', async () => {
|
|
439
|
+
const invalidRequests = [
|
|
440
|
+
{ ...validRequest, source_wallet: 'invalid_address' },
|
|
441
|
+
{ ...validRequest, fee_token: 'invalid_token' },
|
|
442
|
+
{ ...validRequest, token_program_id: 'invalid_program' },
|
|
443
|
+
];
|
|
444
|
+
for (const invalidRequest of invalidRequests) {
|
|
445
|
+
await expect(client.getPaymentInstruction(invalidRequest)).rejects.toThrow();
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
it('should handle estimateTransactionFee RPC error', async () => {
|
|
449
|
+
// Mock failed estimateTransactionFee
|
|
450
|
+
const mockError = { code: -32602, message: 'Invalid transaction' };
|
|
451
|
+
mockFetch.mockResolvedValueOnce({
|
|
452
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
453
|
+
jsonrpc: '2.0',
|
|
454
|
+
id: 1,
|
|
455
|
+
error: mockError,
|
|
456
|
+
}),
|
|
457
|
+
});
|
|
458
|
+
await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('RPC Error -32602: Invalid transaction');
|
|
459
|
+
});
|
|
460
|
+
it('should handle network errors', async () => {
|
|
461
|
+
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
|
462
|
+
await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('Network error');
|
|
463
|
+
});
|
|
464
|
+
it('should return correct payment details in response', async () => {
|
|
465
|
+
mockFetch.mockResolvedValueOnce({
|
|
466
|
+
json: jest.fn().mockResolvedValueOnce({
|
|
467
|
+
jsonrpc: '2.0',
|
|
468
|
+
id: 1,
|
|
469
|
+
result: mockFeeEstimate,
|
|
470
|
+
}),
|
|
471
|
+
});
|
|
472
|
+
const result = await client.getPaymentInstruction(validRequest);
|
|
473
|
+
expect(result).toMatchObject({
|
|
474
|
+
original_transaction: validRequest.transaction,
|
|
475
|
+
payment_instruction: expect.any(Object),
|
|
476
|
+
payment_amount: mockFeeEstimate.fee_in_token,
|
|
477
|
+
payment_token: validRequest.fee_token,
|
|
478
|
+
payment_address: mockFeeEstimate.payment_address,
|
|
479
|
+
signer_address: mockFeeEstimate.signer_pubkey,
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
describe('Error Handling Edge Cases', () => {
|
|
484
|
+
it('should handle malformed JSON responses', async () => {
|
|
485
|
+
mockFetch.mockResolvedValueOnce({
|
|
486
|
+
json: jest.fn().mockRejectedValueOnce(new Error('Invalid JSON')),
|
|
487
|
+
});
|
|
488
|
+
await expect(client.getConfig()).rejects.toThrow('Invalid JSON');
|
|
489
|
+
});
|
|
490
|
+
it('should handle responses with an error object', async () => {
|
|
491
|
+
const mockError = { code: -32602, message: 'Invalid params' };
|
|
492
|
+
mockErrorResponse(mockError);
|
|
493
|
+
await expect(client.getConfig()).rejects.toThrow('RPC Error -32602: Invalid params');
|
|
494
|
+
});
|
|
495
|
+
it('should handle empty error object', async () => {
|
|
496
|
+
mockErrorResponse({});
|
|
497
|
+
await expect(client.getConfig()).rejects.toThrow('RPC Error undefined: undefined');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
// TODO: Add Authentication Tests (separate PR)
|
|
501
|
+
//
|
|
502
|
+
// describe('Authentication', () => {
|
|
503
|
+
// describe('API Key Authentication', () => {
|
|
504
|
+
// - Test that x-api-key header is included when apiKey is provided
|
|
505
|
+
// - Test requests work without apiKey when not provided
|
|
506
|
+
// - Test all RPC methods include the header
|
|
507
|
+
// });
|
|
508
|
+
//
|
|
509
|
+
// describe('HMAC Authentication', () => {
|
|
510
|
+
// - Test x-timestamp and x-hmac-signature headers are included when hmacSecret is provided
|
|
511
|
+
// - Test HMAC signature calculation is correct (SHA256 of timestamp + body)
|
|
512
|
+
// - Test timestamp is current (within reasonable bounds)
|
|
513
|
+
// - Test requests work without HMAC when not provided
|
|
514
|
+
// - Test all RPC methods include the headers
|
|
515
|
+
// });
|
|
516
|
+
//
|
|
517
|
+
// describe('Combined Authentication', () => {
|
|
518
|
+
// - Test both API key and HMAC headers are included when both are provided
|
|
519
|
+
// - Test headers are correctly combined
|
|
520
|
+
// });
|
|
521
|
+
// });
|
|
522
|
+
});
|
|
523
|
+
describe('Transaction Utils', () => {
|
|
524
|
+
describe('getInstructionsFromBase64Message', () => {
|
|
525
|
+
it('should parse instructions from a valid base64 message', () => {
|
|
526
|
+
// This is a sample base64 encoded transaction message
|
|
527
|
+
const validMessage = 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDAAEMAgAAAAEAAAAAAAAA';
|
|
528
|
+
const instructions = getInstructionsFromBase64Message(validMessage);
|
|
529
|
+
expect(Array.isArray(instructions)).toBe(true);
|
|
530
|
+
expect(instructions).not.toBeNull();
|
|
531
|
+
});
|
|
532
|
+
it('should return empty array for invalid base64 message', () => {
|
|
533
|
+
const invalidMessage = 'invalid_base64_message';
|
|
534
|
+
const instructions = getInstructionsFromBase64Message(invalidMessage);
|
|
535
|
+
expect(Array.isArray(instructions)).toBe(true);
|
|
536
|
+
expect(instructions).toEqual([]);
|
|
537
|
+
});
|
|
538
|
+
it('should return empty array for empty message', () => {
|
|
539
|
+
const emptyMessage = '';
|
|
540
|
+
const instructions = getInstructionsFromBase64Message(emptyMessage);
|
|
541
|
+
expect(Array.isArray(instructions)).toBe(true);
|
|
542
|
+
expect(instructions).toEqual([]);
|
|
543
|
+
});
|
|
544
|
+
it('should handle malformed transaction messages gracefully', () => {
|
|
545
|
+
// Valid base64 but not a valid transaction message
|
|
546
|
+
const malformedMessage = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64
|
|
547
|
+
const instructions = getInstructionsFromBase64Message(malformedMessage);
|
|
548
|
+
expect(Array.isArray(instructions)).toBe(true);
|
|
549
|
+
expect(instructions).toEqual([]);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana/kora",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript SDK for Kora RPC",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "dist/src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"keywords": [
|
|
12
|
+
"kora",
|
|
13
|
+
"solana",
|
|
14
|
+
"blockchain",
|
|
15
|
+
"sdk"
|
|
16
|
+
],
|
|
6
17
|
"author": "",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@solana-program/compute-budget": "^0.8.0",
|
|
21
|
+
"@solana-program/system": "^0.7.0",
|
|
22
|
+
"@solana-program/token": "^0.5.1",
|
|
23
|
+
"@solana/kit": "^2.3.0",
|
|
24
|
+
"@solana/prettier-config-solana": "^0.0.5",
|
|
25
|
+
"@types/jest": "^29.5.12",
|
|
26
|
+
"@types/node": "^20.17.27",
|
|
27
|
+
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
28
|
+
"@typescript-eslint/parser": "^8.38.0",
|
|
29
|
+
"dotenv": "^16.4.5",
|
|
30
|
+
"eslint": "^9.31.0",
|
|
31
|
+
"jest": "^29.7.0",
|
|
32
|
+
"prettier": "^3.2.5",
|
|
33
|
+
"ts-jest": "^29.1.2",
|
|
34
|
+
"ts-node": "^10.9.2",
|
|
35
|
+
"typedoc": "^0.28.9",
|
|
36
|
+
"typedoc-plugin-markdown": "^4.8.0",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
38
|
+
},
|
|
9
39
|
"scripts": {
|
|
10
|
-
"
|
|
40
|
+
"build": "tsc",
|
|
41
|
+
"test": "jest",
|
|
42
|
+
"test:auth": "ENABLE_AUTH=true pnpm test integration.test.ts",
|
|
43
|
+
"test:watch": "jest --watch",
|
|
44
|
+
"test:coverage": "jest --coverage",
|
|
45
|
+
"test:integration": "pnpm test integration.test.ts",
|
|
46
|
+
"test:integration:auth": "ENABLE_AUTH=true pnpm test integration.test.ts",
|
|
47
|
+
"test:integration:privy": "KORA_SIGNER_TYPE=privy pnpm test integration.test.ts",
|
|
48
|
+
"test:integration:turnkey": "KORA_SIGNER_TYPE=turnkey pnpm test integration.test.ts",
|
|
49
|
+
"test:unit": "pnpm test unit.test.ts",
|
|
50
|
+
"test:ci:integration": "node scripts/test-with-validator.js",
|
|
51
|
+
"test:ci:integration:auth": "ENABLE_AUTH=true node scripts/test-with-validator.js",
|
|
52
|
+
"test:ci:unit": "jest test/unit.test.ts",
|
|
53
|
+
"lint": "eslint src --ext .ts",
|
|
54
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
55
|
+
"format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
56
|
+
"type-check": "tsc --noEmit",
|
|
57
|
+
"docs": "typedoc --readme none && node scripts/add-toc.js",
|
|
58
|
+
"docs:html": "typedoc --options typedoc.html.json",
|
|
59
|
+
"docs:watch": "typedoc --watch",
|
|
60
|
+
"docs:serve": "pnpm run docs:html && npx serve docs-html",
|
|
61
|
+
"docs:api": "node scripts/generate-api-docs.js",
|
|
62
|
+
"docs:all": "pnpm run docs && pnpm run docs:api"
|
|
11
63
|
}
|
|
12
|
-
}
|
|
64
|
+
}
|