@solana/kora 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
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
+ signature: 'transaction_signature',
216
+ signed_transaction: 'base64_signed_transaction',
217
+ signer_pubkey: 'test_signer_pubkey',
218
+ };
219
+ await testSuccessfulRpcMethod('signAndSendTransaction', () => client.signAndSendTransaction(request), mockResponse, request);
220
+ });
221
+ });
222
+ describe('transferTransaction', () => {
223
+ it('should create transfer transaction', async () => {
224
+ const request = {
225
+ amount: 1000000,
226
+ token: 'SOL',
227
+ source: 'source_address',
228
+ destination: 'destination_address',
229
+ };
230
+ const mockResponse = {
231
+ transaction: 'base64_encoded_transaction',
232
+ message: 'Transfer transaction created',
233
+ blockhash: 'test_blockhash',
234
+ signer_pubkey: 'test_signer_pubkey',
235
+ instructions: [],
236
+ };
237
+ await testSuccessfulRpcMethod('transferTransaction', () => client.transferTransaction(request), mockResponse, request);
238
+ });
239
+ it('should parse instructions from transfer transaction message', async () => {
240
+ const request = {
241
+ amount: 1000000,
242
+ token: 'SOL',
243
+ source: 'source_address',
244
+ destination: 'destination_address',
245
+ };
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
+ const mockResponse = {
250
+ transaction: 'base64_encoded_transaction',
251
+ message: mockMessage,
252
+ blockhash: 'test_blockhash',
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 () => {
264
+ const request = {
265
+ amount: 1000000,
266
+ token: 'SOL',
267
+ source: 'source_address',
268
+ destination: 'destination_address',
269
+ };
270
+ const mockResponse = {
271
+ transaction: 'base64_encoded_transaction',
272
+ message: '',
273
+ blockhash: 'test_blockhash',
274
+ signer_pubkey: 'test_signer_pubkey',
275
+ instructions: [],
276
+ };
277
+ mockSuccessfulResponse(mockResponse);
278
+ const result = await client.transferTransaction(request);
279
+ // Should handle empty message gracefully
280
+ expect(result.instructions).toEqual([]);
281
+ });
282
+ });
283
+ describe('getPaymentInstruction', () => {
284
+ const mockConfig = {
285
+ fee_payers: ['11111111111111111111111111111111'],
286
+ validation_config: {
287
+ max_allowed_lamports: 1000000,
288
+ max_signatures: 10,
289
+ price_source: 'Jupiter',
290
+ allowed_programs: ['program1'],
291
+ allowed_tokens: ['token1'],
292
+ allowed_spl_paid_tokens: ['4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'],
293
+ disallowed_accounts: [],
294
+ fee_payer_policy: {
295
+ system: {
296
+ allow_transfer: true,
297
+ allow_assign: true,
298
+ allow_create_account: true,
299
+ allow_allocate: true,
300
+ nonce: {
301
+ allow_initialize: true,
302
+ allow_advance: true,
303
+ allow_authorize: true,
304
+ allow_withdraw: true,
305
+ },
306
+ },
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
+ token_2022: {
319
+ allow_transfer: true,
320
+ allow_burn: true,
321
+ allow_close_account: true,
322
+ allow_approve: true,
323
+ allow_revoke: true,
324
+ allow_set_authority: true,
325
+ allow_mint_to: true,
326
+ allow_freeze_account: true,
327
+ allow_thaw_account: true,
328
+ },
329
+ },
330
+ price: {
331
+ type: 'margin',
332
+ margin: 0.1,
333
+ },
334
+ token2022: {
335
+ blocked_mint_extensions: [],
336
+ blocked_account_extensions: [],
337
+ },
338
+ },
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
+ };
350
+ const mockFeeEstimate = {
351
+ fee_in_lamports: 5000,
352
+ fee_in_token: 50000,
353
+ signer_pubkey: 'DemoKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
354
+ payment_address: 'PayKMZWkk483QoFPLRPQ2XVKB7bWnuXwSjvDE1JsWk7',
355
+ };
356
+ // Create a mock base64-encoded transaction
357
+ // This is a minimal valid transaction structure
358
+ const mockTransactionBase64 = 'Aoq7ymA5OGP+gmDXiY5m3cYXlY2Rz/a/gFjOgt9ZuoCS7UzuiGGaEnW2OOtvHvMQHkkD7Z4LRF5B63ftu+1oZwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgECB1urjQEjgFgzqYhJ8IXJeSg4cJP1j1g2CJstOQTDchOKUzqH3PxgGW3c4V3vZV05A5Y30/MggOBs0Kd00s1JEwg5TaEeaV4+KL2y7fXIAuf6cN0ZQitbhY+G9ExtBSChspOXPgNcy9pYpETe4bmB+fg4bfZx1tnicA/kIyyubczAmbcIKIuniNOOQYG2ggKCz8NjEsHVezrWMatndu1wk6J5miGP26J6Vwp31AljiAajAFuP0D9mWJwSeFuA7J5rPwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpd/O36SW02zRtNtqk6GFeip2+yBQsVTeSbLL4rWJRkd4CBgQCBQQBCgxAQg8AAAAAAAYGBAIFAwEKDBAnAAAAAAAABg==';
359
+ const validRequest = {
360
+ transaction: mockTransactionBase64,
361
+ fee_token: '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU',
362
+ source_wallet: '11111111111111111111111111111111',
363
+ token_program_id: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
364
+ };
365
+ beforeEach(() => {
366
+ // Mock console.log to avoid noise in tests
367
+ jest.spyOn(console, 'log').mockImplementation();
368
+ });
369
+ afterEach(() => {
370
+ jest.restoreAllMocks();
371
+ });
372
+ it('should successfully append payment instruction', async () => {
373
+ // Mock estimateTransactionFee call
374
+ mockFetch.mockResolvedValueOnce({
375
+ json: jest.fn().mockResolvedValueOnce({
376
+ jsonrpc: '2.0',
377
+ id: 1,
378
+ result: mockFeeEstimate,
379
+ }),
380
+ });
381
+ const result = await client.getPaymentInstruction(validRequest);
382
+ expect(result).toEqual({
383
+ original_transaction: validRequest.transaction,
384
+ payment_instruction: expect.objectContaining({
385
+ programAddress: TOKEN_PROGRAM_ADDRESS,
386
+ accounts: [
387
+ expect.objectContaining({
388
+ role: 1, // writable
389
+ }), // Source token account
390
+ expect.objectContaining({
391
+ role: 1, // writable
392
+ }), // Destination token account
393
+ expect.objectContaining({
394
+ role: 2, // readonly-signer
395
+ address: validRequest.source_wallet,
396
+ signer: expect.objectContaining({
397
+ address: validRequest.source_wallet,
398
+ }),
399
+ }), // Authority
400
+ ],
401
+ data: expect.any(Uint8Array),
402
+ }),
403
+ payment_amount: mockFeeEstimate.fee_in_token,
404
+ payment_token: validRequest.fee_token,
405
+ payment_address: mockFeeEstimate.payment_address,
406
+ signer_address: mockFeeEstimate.signer_pubkey,
407
+ });
408
+ // Verify only estimateTransactionFee was called
409
+ expect(mockFetch).toHaveBeenCalledTimes(1);
410
+ expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
411
+ method: 'POST',
412
+ headers: {
413
+ 'Content-Type': 'application/json',
414
+ },
415
+ body: JSON.stringify({
416
+ jsonrpc: '2.0',
417
+ id: 1,
418
+ method: 'estimateTransactionFee',
419
+ params: {
420
+ transaction: validRequest.transaction,
421
+ fee_token: validRequest.fee_token,
422
+ },
423
+ }),
424
+ });
425
+ });
426
+ it('should handle fixed pricing configuration', async () => {
427
+ // Mock estimateTransactionFee call
428
+ mockFetch.mockResolvedValueOnce({
429
+ json: jest.fn().mockResolvedValueOnce({
430
+ jsonrpc: '2.0',
431
+ id: 1,
432
+ result: mockFeeEstimate,
433
+ }),
434
+ });
435
+ const result = await client.getPaymentInstruction(validRequest);
436
+ expect(result.payment_amount).toBe(mockFeeEstimate.fee_in_token);
437
+ expect(result.payment_token).toBe(validRequest.fee_token);
438
+ });
439
+ it('should throw error for invalid addresses', async () => {
440
+ const invalidRequests = [
441
+ { ...validRequest, source_wallet: 'invalid_address' },
442
+ { ...validRequest, fee_token: 'invalid_token' },
443
+ { ...validRequest, token_program_id: 'invalid_program' },
444
+ ];
445
+ for (const invalidRequest of invalidRequests) {
446
+ await expect(client.getPaymentInstruction(invalidRequest)).rejects.toThrow();
447
+ }
448
+ });
449
+ it('should handle estimateTransactionFee RPC error', async () => {
450
+ // Mock failed estimateTransactionFee
451
+ const mockError = { code: -32602, message: 'Invalid transaction' };
452
+ mockFetch.mockResolvedValueOnce({
453
+ json: jest.fn().mockResolvedValueOnce({
454
+ jsonrpc: '2.0',
455
+ id: 1,
456
+ error: mockError,
457
+ }),
458
+ });
459
+ await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('RPC Error -32602: Invalid transaction');
460
+ });
461
+ it('should handle network errors', async () => {
462
+ mockFetch.mockRejectedValueOnce(new Error('Network error'));
463
+ await expect(client.getPaymentInstruction(validRequest)).rejects.toThrow('Network error');
464
+ });
465
+ it('should return correct payment details in response', async () => {
466
+ mockFetch.mockResolvedValueOnce({
467
+ json: jest.fn().mockResolvedValueOnce({
468
+ jsonrpc: '2.0',
469
+ id: 1,
470
+ result: mockFeeEstimate,
471
+ }),
472
+ });
473
+ const result = await client.getPaymentInstruction(validRequest);
474
+ expect(result).toMatchObject({
475
+ original_transaction: validRequest.transaction,
476
+ payment_instruction: expect.any(Object),
477
+ payment_amount: mockFeeEstimate.fee_in_token,
478
+ payment_token: validRequest.fee_token,
479
+ payment_address: mockFeeEstimate.payment_address,
480
+ signer_address: mockFeeEstimate.signer_pubkey,
481
+ });
482
+ });
483
+ });
484
+ describe('Error Handling Edge Cases', () => {
485
+ it('should handle malformed JSON responses', async () => {
486
+ mockFetch.mockResolvedValueOnce({
487
+ json: jest.fn().mockRejectedValueOnce(new Error('Invalid JSON')),
488
+ });
489
+ await expect(client.getConfig()).rejects.toThrow('Invalid JSON');
490
+ });
491
+ it('should handle responses with an error object', async () => {
492
+ const mockError = { code: -32602, message: 'Invalid params' };
493
+ mockErrorResponse(mockError);
494
+ await expect(client.getConfig()).rejects.toThrow('RPC Error -32602: Invalid params');
495
+ });
496
+ it('should handle empty error object', async () => {
497
+ mockErrorResponse({});
498
+ await expect(client.getConfig()).rejects.toThrow('RPC Error undefined: undefined');
499
+ });
500
+ });
501
+ // TODO: Add Authentication Tests (separate PR)
502
+ //
503
+ // describe('Authentication', () => {
504
+ // describe('API Key Authentication', () => {
505
+ // - Test that x-api-key header is included when apiKey is provided
506
+ // - Test requests work without apiKey when not provided
507
+ // - Test all RPC methods include the header
508
+ // });
509
+ //
510
+ // describe('HMAC Authentication', () => {
511
+ // - Test x-timestamp and x-hmac-signature headers are included when hmacSecret is provided
512
+ // - Test HMAC signature calculation is correct (SHA256 of timestamp + body)
513
+ // - Test timestamp is current (within reasonable bounds)
514
+ // - Test requests work without HMAC when not provided
515
+ // - Test all RPC methods include the headers
516
+ // });
517
+ //
518
+ // describe('Combined Authentication', () => {
519
+ // - Test both API key and HMAC headers are included when both are provided
520
+ // - Test headers are correctly combined
521
+ // });
522
+ // });
523
+ });
524
+ describe('Transaction Utils', () => {
525
+ describe('getInstructionsFromBase64Message', () => {
526
+ it('should parse instructions from a valid base64 message', () => {
527
+ // This is a sample base64 encoded transaction message
528
+ const validMessage = 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQABAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIDAAEMAgAAAAEAAAAAAAAA';
529
+ const instructions = getInstructionsFromBase64Message(validMessage);
530
+ expect(Array.isArray(instructions)).toBe(true);
531
+ expect(instructions).not.toBeNull();
532
+ });
533
+ it('should return empty array for invalid base64 message', () => {
534
+ const invalidMessage = 'invalid_base64_message';
535
+ const instructions = getInstructionsFromBase64Message(invalidMessage);
536
+ expect(Array.isArray(instructions)).toBe(true);
537
+ expect(instructions).toEqual([]);
538
+ });
539
+ it('should return empty array for empty message', () => {
540
+ const emptyMessage = '';
541
+ const instructions = getInstructionsFromBase64Message(emptyMessage);
542
+ expect(Array.isArray(instructions)).toBe(true);
543
+ expect(instructions).toEqual([]);
544
+ });
545
+ it('should handle malformed transaction messages gracefully', () => {
546
+ // Valid base64 but not a valid transaction message
547
+ const malformedMessage = 'SGVsbG8gV29ybGQh'; // "Hello World!" in base64
548
+ const instructions = getInstructionsFromBase64Message(malformedMessage);
549
+ expect(Array.isArray(instructions)).toBe(true);
550
+ expect(instructions).toEqual([]);
551
+ });
552
+ });
553
+ });
package/package.json CHANGED
@@ -1,12 +1,69 @@
1
1
  {
2
2
  "name": "@solana/kora",
3
- "version": "0.0.0",
4
- "description": "",
5
- "license": "ISC",
3
+ "version": "0.1.1",
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
- "type": "commonjs",
8
- "main": "index.js",
18
+ "license": "MIT",
19
+ "peerDependencies": {
20
+ "@solana-program/token": "^0.9.0",
21
+ "@solana/kit": "^5.0.0"
22
+ },
23
+ "devDependencies": {
24
+ "@solana-program/compute-budget": "^0.11.0",
25
+ "@solana-program/system": "^0.10.0",
26
+ "@solana-program/token": "^0.9.0",
27
+ "@solana/kit": "^5.1.0",
28
+ "@solana/prettier-config-solana": "^0.0.6",
29
+ "@types/jest": "^29.5.12",
30
+ "@types/node": "^20.17.27",
31
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
32
+ "@typescript-eslint/parser": "^8.38.0",
33
+ "dotenv": "^16.4.5",
34
+ "eslint": "^9.31.0",
35
+ "jest": "^29.7.0",
36
+ "prettier": "^3.2.5",
37
+ "ts-jest": "^29.1.2",
38
+ "ts-node": "^10.9.2",
39
+ "typedoc": "^0.28.9",
40
+ "typedoc-plugin-markdown": "^4.8.0",
41
+ "typescript": "^5.3.3",
42
+ "ws": "^8.18.3"
43
+ },
9
44
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
45
+ "build": "tsc",
46
+ "test": "jest",
47
+ "test:auth": "ENABLE_AUTH=true pnpm test integration.test.ts",
48
+ "test:watch": "jest --watch",
49
+ "test:coverage": "jest --coverage",
50
+ "test:integration": "pnpm test integration.test.ts",
51
+ "test:integration:auth": "ENABLE_AUTH=true pnpm test integration.test.ts",
52
+ "test:integration:privy": "KORA_SIGNER_TYPE=privy pnpm test integration.test.ts",
53
+ "test:integration:turnkey": "KORA_SIGNER_TYPE=turnkey pnpm test integration.test.ts",
54
+ "test:unit": "pnpm test unit.test.ts",
55
+ "test:ci:integration": "node scripts/test-with-validator.js",
56
+ "test:ci:integration:auth": "ENABLE_AUTH=true node scripts/test-with-validator.js",
57
+ "test:ci:unit": "jest test/unit.test.ts",
58
+ "lint": "eslint src --ext .ts",
59
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
60
+ "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
61
+ "type-check": "tsc --noEmit",
62
+ "docs": "typedoc --readme none && node scripts/add-toc.js",
63
+ "docs:html": "typedoc --options typedoc.html.json",
64
+ "docs:watch": "typedoc --watch",
65
+ "docs:serve": "pnpm run docs:html && npx serve docs-html",
66
+ "docs:api": "node scripts/generate-api-docs.js",
67
+ "docs:all": "pnpm run docs && pnpm run docs:api"
11
68
  }
12
- }
69
+ }