@solana/kora 0.2.0-beta.3 → 0.2.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,11 @@
1
- import { airdropFactory, appendTransactionMessageInstructions, assertIsAddress, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcSubscriptions, createTransactionMessage, getBase58Encoder, getSignatureFromTransaction, lamports, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit';
2
- import { MAX_COMPUTE_UNIT_LIMIT, updateOrAppendSetComputeUnitLimitInstruction, updateOrAppendSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
3
- import { getCreateAccountInstruction } from '@solana-program/system';
4
- import { findAssociatedTokenPda, getCreateAssociatedTokenIdempotentInstructionAsync, getInitializeMintInstruction, getMintSize, getMintToInstruction, TOKEN_PROGRAM_ADDRESS, } from '@solana-program/token';
1
+ import { assertIsAddress, createKeyPairSignerFromBytes, getBase58Encoder, lamports, } from '@solana/kit';
2
+ import { createClient } from '@solana/kit-client-litesvm';
3
+ import { tokenProgram, associatedTokenProgram } from '@solana-program/token';
5
4
  import { config } from 'dotenv';
6
5
  import path from 'path';
7
6
  import { KoraClient } from '../src/index.js';
8
7
  config({ path: path.resolve(process.cwd(), '.env') });
9
8
  const DEFAULTS = {
10
- COMMITMENT: 'processed',
11
9
  DECIMALS: 6,
12
10
  // Make sure this matches the USDC mint in kora.toml (9BgeTKqmFsPVnfYscfM6NvsgmZxei7XfdciShQ6D3bxJ)
13
11
  DESTINATION_ADDRESS: 'AVmDft8deQEo78bRKcGN5ZMf3hyjeLBK4Rd4xGB46yQM',
@@ -17,8 +15,6 @@ const DEFAULTS = {
17
15
  KORA_SIGNER_TYPE: 'memory',
18
16
  // Make sure this matches the kora-rpc signer address on launch (root .env)
19
17
  SENDER_SECRET: 'tzgfgSWTE3KUA6qfRoFYLaSfJm59uUeZRDy4ybMrLn1JV2drA1mftiaEcVFvq1Lok6h6EX2C4Y9kSKLvQWyMpS5',
20
- SOLANA_RPC_URL: 'http://127.0.0.1:8899',
21
- SOLANA_WS_URL: 'ws://127.0.0.1:8900',
22
18
  SOL_DROP_AMOUNT: 1_000_000_000,
23
19
  // HhA5j2rRiPbMrpF2ZD36r69FyZf3zWmEHRNSZbbNdVjf
24
20
  TEST_USDC_MINT_SECRET: '59kKmXphL5UJANqpFFjtH17emEq3oRNmYsx6a3P3vSGJRmhMgVdzH77bkNEi9bArRViT45e8L2TsuPxKNFoc3Qfg',
@@ -54,9 +50,6 @@ export function loadEnvironmentVariables() {
54
50
  }
55
51
  }
56
52
  const koraRpcUrl = process.env.KORA_RPC_URL || DEFAULTS.KORA_RPC_URL;
57
- const solanaRpcUrl = process.env.SOLANA_RPC_URL || DEFAULTS.SOLANA_RPC_URL;
58
- const solanaWsUrl = process.env.SOLANA_WS_URL || DEFAULTS.SOLANA_WS_URL;
59
- const commitment = (process.env.COMMITMENT || DEFAULTS.COMMITMENT);
60
53
  const tokenDecimals = Number(process.env.TOKEN_DECIMALS || DEFAULTS.DECIMALS);
61
54
  const tokenDropAmount = Number(process.env.TOKEN_DROP_AMOUNT || DEFAULTS.TOKEN_DROP_AMOUNT);
62
55
  const solDropAmount = BigInt(process.env.SOL_DROP_AMOUNT || DEFAULTS.SOL_DROP_AMOUNT);
@@ -66,14 +59,11 @@ export function loadEnvironmentVariables() {
66
59
  assertIsAddress(destinationAddress);
67
60
  assertIsAddress(koraAddress);
68
61
  return {
69
- commitment,
70
62
  destinationAddress,
71
63
  koraAddress,
72
64
  koraRpcUrl,
73
65
  koraSignerType,
74
66
  solDropAmount,
75
- solanaRpcUrl,
76
- solanaWsUrl,
77
67
  testUsdcMintSecret,
78
68
  testWalletSecret,
79
69
  tokenDecimals,
@@ -90,129 +80,46 @@ async function createKeyPairSigners() {
90
80
  usdcMint,
91
81
  };
92
82
  }
93
- const createDefaultTransaction = async (client, feePayer, computeLimit = MAX_COMPUTE_UNIT_LIMIT, feeMicroLamports = 1n) => {
94
- const { value: latestBlockhash } = await client.rpc.getLatestBlockhash().send();
95
- return pipe(createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayerSigner(feePayer, tx), tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), tx => updateOrAppendSetComputeUnitPriceInstruction(feeMicroLamports, tx), tx => updateOrAppendSetComputeUnitLimitInstruction(computeLimit, tx));
96
- };
97
- const signAndSendTransaction = async (client, transactionMessage, commitment) => {
98
- const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
99
- const signature = getSignatureFromTransaction(signedTransaction);
100
- assertIsSendableTransaction(signedTransaction);
101
- assertIsTransactionWithBlockhashLifetime(signedTransaction);
102
- await sendAndConfirmTransactionFactory(client)(signedTransaction, { commitment, skipPreflight: true });
103
- return signature;
104
- };
105
- function safeStringify(obj) {
106
- return JSON.stringify(obj, (key, value) => {
107
- if (typeof value === 'bigint') {
108
- return value.toString();
109
- }
110
- return value;
111
- }, 2);
112
- }
113
- async function sendAndConfirmInstructions(client, payer, instructions, description, commitment = loadEnvironmentVariables().commitment) {
114
- try {
115
- const signature = await pipe(await createDefaultTransaction(client, payer, 200_000), tx => appendTransactionMessageInstructions(instructions, tx), tx => signAndSendTransaction(client, tx, commitment));
116
- return signature;
117
- }
118
- catch (error) {
119
- console.error(safeStringify(error));
120
- throw new Error(`Failed to ${description.toLowerCase()}: ${error instanceof Error ? error.message : 'Unknown error'}`);
121
- }
122
- }
123
- async function initializeToken({ client, mintAuthority, payer, owner, mint, dropAmount, decimals, otherAtaWallets, }) {
124
- // Get Owner ATA
125
- const [ata] = await findAssociatedTokenPda({
126
- mint: mint.address,
127
- owner: owner.address,
128
- tokenProgram: TOKEN_PROGRAM_ADDRESS,
129
- });
130
- // Get Mint size & rent
131
- const mintSpace = BigInt(getMintSize());
132
- const mintRent = await client.rpc.getMinimumBalanceForRentExemption(mintSpace).send();
133
- // Create instructions for new token mint
134
- const baseInstructions = [
135
- // Create the Mint Account
136
- getCreateAccountInstruction({
137
- lamports: mintRent,
138
- newAccount: mint,
139
- payer,
140
- programAddress: TOKEN_PROGRAM_ADDRESS,
141
- space: mintSpace,
142
- }),
143
- // Initialize the Mint
144
- getInitializeMintInstruction({
145
- decimals,
146
- mint: mint.address,
147
- mintAuthority: mintAuthority.address,
148
- }),
149
- // Create Associated Token Account
150
- await getCreateAssociatedTokenIdempotentInstructionAsync({
151
- mint: mint.address,
152
- owner: owner.address,
153
- payer,
154
- }),
155
- // Mint To the Destination Associated Token Account
156
- getMintToInstruction({
157
- amount: BigInt(dropAmount * 10 ** decimals),
158
- mint: mint.address,
159
- mintAuthority,
160
- token: ata,
161
- }),
162
- ];
163
- // Generate Create ATA instructions for other token accounts we wish to add
164
- const otherAtaInstructions = otherAtaWallets
165
- ? await Promise.all(otherAtaWallets.map(async (wallet) => await getCreateAssociatedTokenIdempotentInstructionAsync({
166
- mint: mint.address,
167
- owner: wallet,
168
- payer,
169
- })))
170
- : [];
171
- const alreadyExists = await mintExists(client, mint.address);
172
- const instructions = alreadyExists ? [...otherAtaInstructions] : [...baseInstructions, ...otherAtaInstructions];
173
- await sendAndConfirmInstructions(client, payer, instructions, 'Initialize token and ATAs', 'finalized');
174
- }
175
83
  async function setupTestSuite() {
176
- const { koraAddress, koraRpcUrl, tokenDecimals, tokenDropAmount, solDropAmount, solanaRpcUrl, solanaWsUrl } = loadEnvironmentVariables();
177
- // Load auth config from environment if not provided
84
+ const { koraAddress, koraRpcUrl, tokenDecimals, tokenDropAmount, solDropAmount } = loadEnvironmentVariables();
178
85
  const authConfig = process.env.ENABLE_AUTH === 'true'
179
86
  ? {
180
87
  apiKey: process.env.KORA_API_KEY || 'test-api-key-123',
181
88
  hmacSecret: process.env.KORA_HMAC_SECRET || 'test-hmac-secret-456',
182
89
  }
183
90
  : undefined;
184
- // Create Solana client
185
- const rpc = createSolanaRpc(solanaRpcUrl);
186
- const rpcSubscriptions = createSolanaRpcSubscriptions(solanaWsUrl);
187
- const airdrop = airdropFactory({ rpc, rpcSubscriptions });
188
- const client = { rpc, rpcSubscriptions };
189
- // Get or create keypairs
190
91
  const { testWallet, usdcMint, destinationAddress } = await createKeyPairSigners();
191
- const mintAuthority = testWallet; // test wallet can be used as mint authority for the test
192
- // Airdrop SOL to test sender and kora wallets
193
- await Promise.all([
194
- airdrop({
195
- commitment: 'finalized',
196
- lamports: lamports(solDropAmount),
197
- recipientAddress: koraAddress,
198
- }),
199
- airdrop({
200
- commitment: 'finalized',
201
- lamports: lamports(solDropAmount),
202
- recipientAddress: testWallet.address,
203
- }),
204
- ]);
205
- // Initialize token and ATAs
206
- await initializeToken({
207
- client,
92
+ const client = await createClient({ payer: testWallet }).use(tokenProgram()).use(associatedTokenProgram());
93
+ // Airdrop SOL via LiteSVM
94
+ await client.airdrop(koraAddress, lamports(solDropAmount));
95
+ await client.airdrop(testWallet.address, lamports(solDropAmount));
96
+ // Create mint
97
+ await client.token.instructions
98
+ .createMint({
99
+ newMint: usdcMint,
100
+ decimals: tokenDecimals,
101
+ mintAuthority: testWallet.address,
102
+ })
103
+ .sendTransaction();
104
+ // Mint tokens to testWallet's ATA (auto-creates ATA)
105
+ await client.token.instructions
106
+ .mintToATA({
107
+ mint: usdcMint.address,
108
+ owner: testWallet.address,
109
+ mintAuthority: testWallet,
110
+ amount: BigInt(tokenDropAmount * 10 ** tokenDecimals),
208
111
  decimals: tokenDecimals,
209
- dropAmount: tokenDropAmount,
210
- mint: usdcMint,
211
- mintAuthority,
212
- otherAtaWallets: [testWallet.address, koraAddress, destinationAddress],
213
- owner: testWallet,
214
- payer: mintAuthority,
215
- });
112
+ })
113
+ .sendTransaction();
114
+ // Create ATAs for kora and destination wallets
115
+ for (const owner of [koraAddress, destinationAddress]) {
116
+ await client.associatedToken.instructions
117
+ .createAssociatedTokenIdempotent({
118
+ owner,
119
+ mint: usdcMint.address,
120
+ })
121
+ .sendTransaction();
122
+ }
216
123
  return {
217
124
  destinationAddress,
218
125
  koraAddress,
@@ -222,13 +129,4 @@ async function setupTestSuite() {
222
129
  usdcMint: usdcMint.address,
223
130
  };
224
131
  }
225
- const mintExists = async (client, mint) => {
226
- try {
227
- const mintAccount = await client.rpc.getAccountInfo(mint).send();
228
- return mintAccount.value !== null;
229
- }
230
- catch {
231
- return false;
232
- }
233
- };
234
132
  export default setupTestSuite;
@@ -509,28 +509,112 @@ describe('KoraClient Unit Tests', () => {
509
509
  await expect(client.getConfig()).rejects.toThrow('RPC Error undefined: undefined');
510
510
  });
511
511
  });
512
- // TODO: Add Authentication Tests (separate PR)
513
- //
514
- // describe('Authentication', () => {
515
- // describe('API Key Authentication', () => {
516
- // - Test that x-api-key header is included when apiKey is provided
517
- // - Test requests work without apiKey when not provided
518
- // - Test all RPC methods include the header
519
- // });
520
- //
521
- // describe('HMAC Authentication', () => {
522
- // - Test x-timestamp and x-hmac-signature headers are included when hmacSecret is provided
523
- // - Test HMAC signature calculation is correct (SHA256 of timestamp + body)
524
- // - Test timestamp is current (within reasonable bounds)
525
- // - Test requests work without HMAC when not provided
526
- // - Test all RPC methods include the headers
527
- // });
528
- //
529
- // describe('Combined Authentication', () => {
530
- // - Test both API key and HMAC headers are included when both are provided
531
- // - Test headers are correctly combined
532
- // });
533
- // });
512
+ describe('reCAPTCHA Authentication', () => {
513
+ it('should include x-recaptcha-token header when getRecaptchaToken callback is provided (sync)', async () => {
514
+ const recaptchaClient = new KoraClient({
515
+ getRecaptchaToken: () => 'test-recaptcha-token',
516
+ rpcUrl: mockRpcUrl,
517
+ });
518
+ mockSuccessfulResponse({ version: '1.0.0' });
519
+ await recaptchaClient.getVersion();
520
+ expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
521
+ body: JSON.stringify({
522
+ id: 1,
523
+ jsonrpc: '2.0',
524
+ method: 'getVersion',
525
+ params: undefined,
526
+ }),
527
+ headers: {
528
+ 'Content-Type': 'application/json',
529
+ 'x-recaptcha-token': 'test-recaptcha-token',
530
+ },
531
+ method: 'POST',
532
+ });
533
+ });
534
+ it('should include x-recaptcha-token header when getRecaptchaToken callback returns Promise', async () => {
535
+ const recaptchaClient = new KoraClient({
536
+ getRecaptchaToken: () => Promise.resolve('async-recaptcha-token'),
537
+ rpcUrl: mockRpcUrl,
538
+ });
539
+ mockSuccessfulResponse({ version: '1.0.0' });
540
+ await recaptchaClient.getVersion();
541
+ expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
542
+ body: JSON.stringify({
543
+ id: 1,
544
+ jsonrpc: '2.0',
545
+ method: 'getVersion',
546
+ params: undefined,
547
+ }),
548
+ headers: {
549
+ 'Content-Type': 'application/json',
550
+ 'x-recaptcha-token': 'async-recaptcha-token',
551
+ },
552
+ method: 'POST',
553
+ });
554
+ });
555
+ it('should NOT include x-recaptcha-token header when getRecaptchaToken is not provided', async () => {
556
+ mockSuccessfulResponse({ version: '1.0.0' });
557
+ await client.getVersion();
558
+ expect(mockFetch).toHaveBeenCalledWith(mockRpcUrl, {
559
+ body: JSON.stringify({
560
+ id: 1,
561
+ jsonrpc: '2.0',
562
+ method: 'getVersion',
563
+ params: undefined,
564
+ }),
565
+ headers: {
566
+ 'Content-Type': 'application/json',
567
+ },
568
+ method: 'POST',
569
+ });
570
+ });
571
+ it('should include x-recaptcha-token along with other auth headers', async () => {
572
+ const combinedAuthClient = new KoraClient({
573
+ apiKey: 'test-api-key',
574
+ getRecaptchaToken: () => 'test-recaptcha-token',
575
+ rpcUrl: mockRpcUrl,
576
+ });
577
+ mockSuccessfulResponse({ version: '1.0.0' });
578
+ await combinedAuthClient.getVersion();
579
+ const callArgs = mockFetch.mock.calls[0][1];
580
+ expect(callArgs.headers).toMatchObject({
581
+ 'Content-Type': 'application/json',
582
+ 'x-api-key': 'test-api-key',
583
+ 'x-recaptcha-token': 'test-recaptcha-token',
584
+ });
585
+ });
586
+ it('should call getRecaptchaToken callback for each request', async () => {
587
+ let callCount = 0;
588
+ const recaptchaClient = new KoraClient({
589
+ getRecaptchaToken: () => `token-${++callCount}`,
590
+ rpcUrl: mockRpcUrl,
591
+ });
592
+ mockSuccessfulResponse({ version: '1.0.0' });
593
+ await recaptchaClient.getVersion();
594
+ mockSuccessfulResponse({ blockhash: 'test-blockhash' });
595
+ await recaptchaClient.getBlockhash();
596
+ expect(callCount).toBe(2);
597
+ const calls = mockFetch.mock.calls;
598
+ expect(calls[0][1].headers['x-recaptcha-token']).toBe('token-1');
599
+ expect(calls[1][1].headers['x-recaptcha-token']).toBe('token-2');
600
+ });
601
+ it('should propagate errors when getRecaptchaToken callback throws', async () => {
602
+ const recaptchaClient = new KoraClient({
603
+ getRecaptchaToken: () => {
604
+ throw new Error('reCAPTCHA failed to load');
605
+ },
606
+ rpcUrl: mockRpcUrl,
607
+ });
608
+ await expect(recaptchaClient.getVersion()).rejects.toThrow('reCAPTCHA failed to load');
609
+ });
610
+ it('should propagate errors when getRecaptchaToken returns rejected Promise', async () => {
611
+ const recaptchaClient = new KoraClient({
612
+ getRecaptchaToken: () => Promise.reject(new Error('Token generation failed')),
613
+ rpcUrl: mockRpcUrl,
614
+ });
615
+ await expect(recaptchaClient.getVersion()).rejects.toThrow('Token generation failed');
616
+ });
617
+ });
534
618
  });
535
619
  describe('Transaction Utils', () => {
536
620
  describe('getInstructionsFromBase64Message', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solana/kora",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.2.0-beta.6",
4
4
  "description": "TypeScript SDK for Kora RPC",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -16,31 +16,42 @@
16
16
  ],
17
17
  "author": "",
18
18
  "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/solana-foundation/kora",
22
+ "directory": "sdks/ts"
23
+ },
19
24
  "peerDependencies": {
20
- "@solana-program/token": "^0.9.0",
21
- "@solana/kit": "^5.0.0"
25
+ "@solana-program/compute-budget": "^0.15.0",
26
+ "@solana-program/token": "^0.12.0",
27
+ "@solana/kit": "^6.3.0",
28
+ "@solana/kit-plugin-instruction-plan": "^0.7.0",
29
+ "@solana/kit-plugin-payer": "^0.6.0",
30
+ "@solana/kit-plugin-rpc": "^0.7.0"
22
31
  },
23
32
  "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/eslint-config-solana": "^6.0.0",
28
- "@solana/kit": "^5.4.0",
33
+ "@eslint/js": "^9.39.3",
34
+ "@solana-program/system": "^0.12.0",
35
+ "@solana/eslint-config-solana": "6.0.0",
36
+ "@solana/kit-client-litesvm": "^0.7.0",
29
37
  "@solana/prettier-config-solana": "^0.0.6",
30
38
  "@types/jest": "^29.5.12",
31
39
  "@types/node": "^20.17.27",
32
- "@typescript-eslint/eslint-plugin": "^8.38.0",
33
- "@typescript-eslint/parser": "^8.38.0",
34
40
  "dotenv": "^16.4.5",
35
- "eslint": "^9.31.0",
41
+ "eslint": "^9.39.3",
42
+ "eslint-plugin-jest": "^29.15.0",
43
+ "eslint-plugin-simple-import-sort": "^12.1.1",
44
+ "eslint-plugin-sort-keys-fix": "^1.1.2",
45
+ "eslint-plugin-typescript-sort-keys": "^3.3.0",
46
+ "globals": "^16.5.0",
36
47
  "jest": "^29.7.0",
37
48
  "prettier": "^3.2.5",
38
49
  "ts-jest": "^29.1.2",
39
50
  "ts-node": "^10.9.2",
40
51
  "typedoc": "^0.28.9",
41
52
  "typedoc-plugin-markdown": "^4.8.0",
42
- "typescript": "^5.3.3",
43
- "ws": "^8.18.3"
53
+ "typescript": "^5.9.3",
54
+ "typescript-eslint": "^8.56.1"
44
55
  },
45
56
  "scripts": {
46
57
  "build": "tsc",
@@ -50,13 +61,12 @@
50
61
  "test:coverage": "jest --coverage",
51
62
  "test:integration": "pnpm test integration.test.ts",
52
63
  "test:integration:auth": "ENABLE_AUTH=true pnpm test integration.test.ts",
64
+ "test:integration:free": "FREE_PRICING=true pnpm test integration.test.ts",
53
65
  "test:integration:privy": "KORA_SIGNER_TYPE=privy pnpm test integration.test.ts",
54
66
  "test:integration:turnkey": "KORA_SIGNER_TYPE=turnkey pnpm test integration.test.ts",
55
67
  "test:unit": "pnpm test unit.test.ts",
56
- "test:ci:integration": "node scripts/test-with-validator.js",
57
- "test:ci:integration:auth": "ENABLE_AUTH=true node scripts/test-with-validator.js",
58
68
  "test:ci:unit": "jest test/unit.test.ts",
59
- "lint": "eslint src test",
69
+ "lint": "eslint src/",
60
70
  "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
61
71
  "format:check": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\"",
62
72
  "type-check": "tsc --noEmit",