@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.
package/README.md CHANGED
@@ -1,8 +1,35 @@
1
1
  # Kora TypeScript SDK
2
2
 
3
- A TypeScript SDK for interacting with the Kora RPC server. This SDK provides a type-safe interface to all Kora RPC methods.
3
+ A TypeScript SDK for interacting with the Kora RPC server. This SDK provides a type-safe interface to all Kora RPC methods (requires a Kora RPC server to be running).
4
4
 
5
- ## Development
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pnpm install @solana/kora
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```typescript
15
+ import { KoraClient } from '@solana/kora';
16
+
17
+ // Initialize the client with your RPC endpoint
18
+ const client = new KoraClient({ rpcUrl: 'http://localhost:8080' });
19
+
20
+ // Example: Get Kora to sign a transaction
21
+ const result = await client.signTransaction({
22
+ transaction: 'myBase64EncodedTransaction'
23
+ });
24
+
25
+ // Access the signed transaction (base64 encoded)
26
+ console.log('Signed transaction:', result.signed_transaction);
27
+ ```
28
+
29
+ **[→ API Reference](https://launch.solana.com/docs/kora/json-rpc-api)**
30
+ **[→ Quick Start](https://launch.solana.com/docs/kora/getting-started/quick-start)**
31
+
32
+ ## Local Development
6
33
 
7
34
  ### Building from Source
8
35
 
@@ -33,28 +60,3 @@ pnpm test:ci:integration
33
60
 
34
61
  This will start a local test validator and run all tests.
35
62
 
36
-
37
- ## Quick Start
38
-
39
- ```typescript
40
- import { KoraClient } from '@solana/kora';
41
-
42
- // Initialize the client with your RPC endpoint
43
- const client = new KoraClient({ rpcUrl: 'http://localhost:8080' });
44
-
45
- // Example: Transfer tokens
46
- const result = await client.transferTransaction({
47
- amount: 1000000, // 1 USDC (6 decimals)
48
- token: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
49
- source: "sourceAddress",
50
- destination: "destinationAddress"
51
- });
52
-
53
- // Access the base64 encoded transaction, base64 encoded message, and parsed instructions directly
54
- console.log('Transaction:', result.transaction);
55
- console.log('Message:', result.message);
56
- console.log('Instructions:', result.instructions);
57
- ```
58
-
59
- **[→ API Reference](https://launch.solana.com/docs/kora/json-rpc-api)**
60
- **[→ Quick Start](https://launch.solana.com/docs/kora/getting-started/quick-start)**
@@ -23,14 +23,16 @@ export declare class KoraClient {
23
23
  private rpcUrl;
24
24
  private apiKey?;
25
25
  private hmacSecret?;
26
+ private getRecaptchaToken?;
26
27
  /**
27
28
  * Creates a new Kora client instance.
28
29
  * @param options - Client configuration options
29
30
  * @param options.rpcUrl - The Kora RPC server URL
30
31
  * @param options.apiKey - Optional API key for authentication
31
32
  * @param options.hmacSecret - Optional HMAC secret for signature-based authentication
33
+ * @param options.getRecaptchaToken - Optional callback to get reCAPTCHA token for bot protection
32
34
  */
33
- constructor({ rpcUrl, apiKey, hmacSecret }: KoraClientOptions);
35
+ constructor({ rpcUrl, apiKey, hmacSecret, getRecaptchaToken }: KoraClientOptions);
34
36
  private getHmacSignature;
35
37
  private getHeaders;
36
38
  private rpcRequest;
@@ -25,17 +25,20 @@ export class KoraClient {
25
25
  rpcUrl;
26
26
  apiKey;
27
27
  hmacSecret;
28
+ getRecaptchaToken;
28
29
  /**
29
30
  * Creates a new Kora client instance.
30
31
  * @param options - Client configuration options
31
32
  * @param options.rpcUrl - The Kora RPC server URL
32
33
  * @param options.apiKey - Optional API key for authentication
33
34
  * @param options.hmacSecret - Optional HMAC secret for signature-based authentication
35
+ * @param options.getRecaptchaToken - Optional callback to get reCAPTCHA token for bot protection
34
36
  */
35
- constructor({ rpcUrl, apiKey, hmacSecret }) {
37
+ constructor({ rpcUrl, apiKey, hmacSecret, getRecaptchaToken }) {
36
38
  this.rpcUrl = rpcUrl;
37
39
  this.apiKey = apiKey;
38
40
  this.hmacSecret = hmacSecret;
41
+ this.getRecaptchaToken = getRecaptchaToken;
39
42
  }
40
43
  getHmacSignature({ timestamp, body }) {
41
44
  if (!this.hmacSecret) {
@@ -44,7 +47,7 @@ export class KoraClient {
44
47
  const message = timestamp + body;
45
48
  return crypto.createHmac('sha256', this.hmacSecret).update(message).digest('hex');
46
49
  }
47
- getHeaders({ body }) {
50
+ async getHeaders({ body }) {
48
51
  const headers = {};
49
52
  if (this.apiKey) {
50
53
  headers['x-api-key'] = this.apiKey;
@@ -55,6 +58,10 @@ export class KoraClient {
55
58
  headers['x-timestamp'] = timestamp;
56
59
  headers['x-hmac-signature'] = signature;
57
60
  }
61
+ if (this.getRecaptchaToken) {
62
+ const token = await Promise.resolve(this.getRecaptchaToken());
63
+ headers['x-recaptcha-token'] = token;
64
+ }
58
65
  return headers;
59
66
  }
60
67
  async rpcRequest(method, params) {
@@ -64,7 +71,7 @@ export class KoraClient {
64
71
  method,
65
72
  params,
66
73
  });
67
- const headers = this.getHeaders({ body });
74
+ const headers = await this.getHeaders({ body });
68
75
  const response = await fetch(this.rpcUrl, {
69
76
  body,
70
77
  headers: { ...headers, 'Content-Type': 'application/json' },
@@ -1,3 +1,4 @@
1
1
  export * from './types/index.js';
2
2
  export { KoraClient } from './client.js';
3
+ export { createKitKoraClient, type KoraKitClient } from './kit/index.js';
3
4
  export { koraPlugin, type KoraApi } from './plugin.js';
package/dist/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './types/index.js';
2
2
  export { KoraClient } from './client.js';
3
+ export { createKitKoraClient } from './kit/index.js';
3
4
  export { koraPlugin } from './plugin.js';
@@ -0,0 +1,7 @@
1
+ import { type Address, type TransactionMessage, type TransactionMessageWithFeePayer, type TransactionSigner } from '@solana/kit';
2
+ import { KoraClient } from '../client.js';
3
+ import type { KoraKitClientConfig } from '../types/index.js';
4
+ export declare function createKoraTransactionPlanExecutor(koraClient: KoraClient, config: KoraKitClientConfig, payerSigner: TransactionSigner, payment: {
5
+ destinationTokenAccount: Address;
6
+ sourceTokenAccount: Address;
7
+ } | undefined, resolveProvisoryComputeUnitLimit: (<T extends TransactionMessage & TransactionMessageWithFeePayer>(transactionMessage: T) => Promise<T>) | undefined): import("@solana/kit").TransactionPlanExecutor<import("@solana/kit").TransactionPlanResultContext>;
@@ -0,0 +1,68 @@
1
+ import { appendTransactionMessageInstructions, blockhash, createTransactionMessage, createTransactionPlanExecutor, getBase64EncodedWireTransaction, getBase64Encoder, getSignatureFromTransaction, getTransactionDecoder, partiallySignTransactionMessageWithSigners, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signature, } from '@solana/kit';
2
+ import { removePaymentInstruction, updatePaymentInstructionAmount } from './payment.js';
3
+ // TODO: Create a bundle-aware executor (e.g. `createKoraBundlePlanExecutor`) that collects
4
+ // multiple planned transaction messages into a single `signAndSendBundle` call instead of
5
+ // submitting each one individually via `signAndSendTransaction`. This would let users
6
+ // compose Jito bundles through the Kit plan/execute pipeline rather than manually encoding
7
+ // transactions and calling `client.kora.signAndSendBundle()`.
8
+ export function createKoraTransactionPlanExecutor(koraClient, config, payerSigner, payment, resolveProvisoryComputeUnitLimit) {
9
+ return createTransactionPlanExecutor({
10
+ async executeTransactionMessage(_context, transactionMessage) {
11
+ // Kora manages blockhash validity; set max height to avoid premature client-side expiry checks
12
+ const { blockhash: bh } = await koraClient.getBlockhash();
13
+ const msgWithLifetime = setTransactionMessageLifetimeUsingBlockhash({
14
+ blockhash: blockhash(bh),
15
+ lastValidBlockHeight: BigInt(Number.MAX_SAFE_INTEGER),
16
+ }, transactionMessage);
17
+ const msgForEstimation = resolveProvisoryComputeUnitLimit
18
+ ? await resolveProvisoryComputeUnitLimit(msgWithLifetime)
19
+ : msgWithLifetime;
20
+ const prePaymentTx = getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(msgForEstimation));
21
+ let finalTx;
22
+ if (payment) {
23
+ const { sourceTokenAccount, destinationTokenAccount } = payment;
24
+ const { fee_in_token } = await koraClient.estimateTransactionFee({
25
+ fee_token: config.feeToken,
26
+ transaction: prePaymentTx,
27
+ });
28
+ if (fee_in_token == null) {
29
+ console.warn('[kora] fee_in_token is undefined — defaulting to 0. ' +
30
+ 'If paid pricing is expected, check that the fee token is correctly configured on the server.');
31
+ }
32
+ const feeInToken = fee_in_token ?? 0;
33
+ if (feeInToken < 0) {
34
+ throw new Error(`Kora fee estimation returned a negative fee (${feeInToken}). This indicates a server-side error.`);
35
+ }
36
+ const currentIxs = 'instructions' in msgForEstimation
37
+ ? msgForEstimation.instructions
38
+ : undefined;
39
+ if (!currentIxs) {
40
+ throw new Error('Cannot extract instructions from transaction message. ' +
41
+ 'The message structure may be incompatible with this version of the Kora SDK.');
42
+ }
43
+ // Replace placeholder with real fee amount, or strip it if fee is 0
44
+ const finalIxs = feeInToken > 0
45
+ ? updatePaymentInstructionAmount(currentIxs, config.feePayerWallet, sourceTokenAccount, destinationTokenAccount, feeInToken, config.tokenProgramId)
46
+ : removePaymentInstruction(currentIxs, sourceTokenAccount, destinationTokenAccount, config.feePayerWallet, config.tokenProgramId);
47
+ const resolvedMsg = pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayerSigner(payerSigner, m), m => setTransactionMessageLifetimeUsingBlockhash({
48
+ blockhash: blockhash(bh),
49
+ lastValidBlockHeight: BigInt(Number.MAX_SAFE_INTEGER),
50
+ }, m), m => appendTransactionMessageInstructions(finalIxs, m));
51
+ finalTx = getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(resolvedMsg));
52
+ }
53
+ else {
54
+ finalTx = prePaymentTx;
55
+ }
56
+ const result = await koraClient.signAndSendTransaction({
57
+ transaction: finalTx,
58
+ user_id: config.userId,
59
+ });
60
+ if (result.signature) {
61
+ return signature(result.signature);
62
+ }
63
+ const signedTxBytes = getBase64Encoder().encode(result.signed_transaction);
64
+ const decodedTx = getTransactionDecoder().decode(signedTxBytes);
65
+ return getSignatureFromTransaction(decodedTx);
66
+ },
67
+ });
68
+ }
@@ -0,0 +1,53 @@
1
+ import type { KoraKitClientConfig } from '../types/index.js';
2
+ /** The type returned by {@link createKitKoraClient}. */
3
+ export type KoraKitClient = Awaited<ReturnType<typeof createKitKoraClient>>;
4
+ /**
5
+ * Creates a Kora Kit client composed from Kit plugins.
6
+ *
7
+ * The returned client satisfies `ClientWithPayer`, `ClientWithTransactionPlanning`,
8
+ * and `ClientWithTransactionSending`, making it composable with Kit program plugins.
9
+ *
10
+ * @beta This API is experimental and may change in future releases.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createKitKoraClient } from '@solana/kora';
15
+ * import { address } from '@solana/kit';
16
+ *
17
+ * const client = await createKitKoraClient({
18
+ * endpoint: 'https://kora.example.com',
19
+ * rpcUrl: 'https://api.mainnet-beta.solana.com',
20
+ * feeToken: address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
21
+ * feePayerWallet: userSigner,
22
+ * });
23
+ *
24
+ * const result = await client.sendTransaction([myInstruction]);
25
+ * ```
26
+ */
27
+ export declare function createKitKoraClient(config: KoraKitClientConfig): Promise<import("@solana/kit").Client<import("@solana/kit").ClientWithTransactionPlanning & import("@solana/kit").ClientWithTransactionSending & object & {
28
+ rpc: import("@solana/kit").Rpc<import("@solana/kit").RequestAirdropApi & import("@solana/kit").GetAccountInfoApi & import("@solana/kit").GetBalanceApi & import("@solana/kit").GetBlockApi & import("@solana/kit").GetBlockCommitmentApi & import("@solana/kit").GetBlockHeightApi & import("@solana/kit").GetBlockProductionApi & import("@solana/kit").GetBlocksApi & import("@solana/kit").GetBlocksWithLimitApi & import("@solana/kit").GetBlockTimeApi & import("@solana/kit").GetClusterNodesApi & import("@solana/kit").GetEpochInfoApi & import("@solana/kit").GetEpochScheduleApi & import("@solana/kit").GetFeeForMessageApi & import("@solana/kit").GetFirstAvailableBlockApi & import("@solana/kit").GetGenesisHashApi & import("@solana/kit").GetHealthApi & import("@solana/kit").GetHighestSnapshotSlotApi & import("@solana/kit").GetIdentityApi & import("@solana/kit").GetInflationGovernorApi & import("@solana/kit").GetInflationRateApi & import("@solana/kit").GetInflationRewardApi & import("@solana/kit").GetLargestAccountsApi & import("@solana/kit").GetLatestBlockhashApi & import("@solana/kit").GetLeaderScheduleApi & import("@solana/kit").GetMaxRetransmitSlotApi & import("@solana/kit").GetMaxShredInsertSlotApi & import("@solana/kit").GetMinimumBalanceForRentExemptionApi & import("@solana/kit").GetMultipleAccountsApi & import("@solana/kit").GetProgramAccountsApi & import("@solana/kit").GetRecentPerformanceSamplesApi & import("@solana/kit").GetRecentPrioritizationFeesApi & import("@solana/kit").GetSignaturesForAddressApi & import("@solana/kit").GetSignatureStatusesApi & import("@solana/kit").GetSlotApi & import("@solana/kit").GetSlotLeaderApi & import("@solana/kit").GetSlotLeadersApi & import("@solana/kit").GetStakeMinimumDelegationApi & import("@solana/kit").GetSupplyApi & import("@solana/kit").GetTokenAccountBalanceApi & import("@solana/kit").GetTokenAccountsByDelegateApi & import("@solana/kit").GetTokenAccountsByOwnerApi & import("@solana/kit").GetTokenLargestAccountsApi & import("@solana/kit").GetTokenSupplyApi & import("@solana/kit").GetTransactionApi & import("@solana/kit").GetTransactionCountApi & import("@solana/kit").GetVersionApi & import("@solana/kit").GetVoteAccountsApi & import("@solana/kit").IsBlockhashValidApi & import("@solana/kit").MinimumLedgerSlotApi & import("@solana/kit").SendTransactionApi & import("@solana/kit").SimulateTransactionApi>;
29
+ rpcSubscriptions: import("@solana/kit").RpcSubscriptions<import("@solana/kit").SolanaRpcSubscriptionsApi>;
30
+ } & {
31
+ kora: {
32
+ estimateBundleFee(request: import("../types/index.js").EstimateBundleFeeRequest): Promise<import("../types/index.js").KitEstimateBundleFeeResponse>;
33
+ estimateTransactionFee(request: import("../types/index.js").EstimateTransactionFeeRequest): Promise<import("../types/index.js").KitEstimateFeeResponse>;
34
+ getBlockhash(): Promise<import("../types/index.js").KitBlockhashResponse>;
35
+ getConfig(): Promise<import("../types/index.js").KitConfigResponse>;
36
+ getPayerSigner(): Promise<import("../types/index.js").KitPayerSignerResponse>;
37
+ getPaymentInstruction(request: import("../types/index.js").GetPaymentInstructionRequest): Promise<import("../types/index.js").KitPaymentInstructionResponse>;
38
+ getSupportedTokens(): Promise<import("../types/index.js").KitSupportedTokensResponse>;
39
+ getVersion(): Promise<import("../types/index.js").GetVersionResponse>;
40
+ signAndSendBundle(request: import("../types/index.js").SignAndSendBundleRequest): Promise<import("../types/index.js").KitSignAndSendBundleResponse>;
41
+ signAndSendTransaction(request: import("../types/index.js").SignAndSendTransactionRequest): Promise<import("../types/index.js").KitSignAndSendTransactionResponse>;
42
+ signBundle(request: import("../types/index.js").SignBundleRequest): Promise<import("../types/index.js").KitSignBundleResponse>;
43
+ signTransaction(request: import("../types/index.js").SignTransactionRequest): Promise<import("../types/index.js").KitSignTransactionResponse>;
44
+ };
45
+ } & {
46
+ payer: import("@solana/kit").TransactionSigner;
47
+ } & {
48
+ paymentAddress: import("@solana/kit").Address | undefined;
49
+ } & {
50
+ transactionPlanner: import("@solana/kit").TransactionPlanner;
51
+ } & {
52
+ transactionPlanExecutor: import("@solana/kit").TransactionPlanExecutor;
53
+ }>>;
@@ -0,0 +1,75 @@
1
+ import { address, createEmptyClient, createNoopSigner, createSolanaRpc } from '@solana/kit';
2
+ import { planAndSendTransactions, transactionPlanExecutor as transactionPlanExecutorPlugin, transactionPlanner as transactionPlannerPlugin, } from '@solana/kit-plugin-instruction-plan';
3
+ import { payer } from '@solana/kit-plugin-payer';
4
+ import { rpc } from '@solana/kit-plugin-rpc';
5
+ import { estimateAndUpdateProvisoryComputeUnitLimitFactory, estimateComputeUnitLimitFactory, } from '@solana-program/compute-budget';
6
+ import { KoraClient } from '../client.js';
7
+ import { koraPlugin } from '../plugin.js';
8
+ import { createKoraTransactionPlanExecutor } from './executor.js';
9
+ import { buildPlaceholderPaymentInstruction, koraPaymentAddress } from './payment.js';
10
+ import { buildComputeBudgetInstructions, createKoraTransactionPlanner } from './planner.js';
11
+ /**
12
+ * Creates a Kora Kit client composed from Kit plugins.
13
+ *
14
+ * The returned client satisfies `ClientWithPayer`, `ClientWithTransactionPlanning`,
15
+ * and `ClientWithTransactionSending`, making it composable with Kit program plugins.
16
+ *
17
+ * @beta This API is experimental and may change in future releases.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { createKitKoraClient } from '@solana/kora';
22
+ * import { address } from '@solana/kit';
23
+ *
24
+ * const client = await createKitKoraClient({
25
+ * endpoint: 'https://kora.example.com',
26
+ * rpcUrl: 'https://api.mainnet-beta.solana.com',
27
+ * feeToken: address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'),
28
+ * feePayerWallet: userSigner,
29
+ * });
30
+ *
31
+ * const result = await client.sendTransaction([myInstruction]);
32
+ * ```
33
+ */
34
+ // TODO: Bundle support — the plan/execute pipeline currently handles single transactions only.
35
+ // For Jito bundles, users must manually encode transactions and call `client.kora.signAndSendBundle()`.
36
+ // A future `createKitKoraBundleClient` (or a bundle-aware executor plugin) could extend this to
37
+ // plan multiple transaction messages and submit them as a single bundle.
38
+ export async function createKitKoraClient(config) {
39
+ const koraClient = new KoraClient({
40
+ apiKey: config.apiKey,
41
+ getRecaptchaToken: config.getRecaptchaToken,
42
+ hmacSecret: config.hmacSecret,
43
+ rpcUrl: config.endpoint,
44
+ });
45
+ const { signer_address, payment_address } = await koraClient.getPayerSigner();
46
+ const paymentAddr = payment_address ? address(payment_address) : undefined;
47
+ const payerSigner = createNoopSigner(address(signer_address));
48
+ const computeBudgetIxs = buildComputeBudgetInstructions(config);
49
+ const solanaRpc = createSolanaRpc(config.rpcUrl);
50
+ const hasCuEstimation = config.computeUnitLimit === undefined;
51
+ const resolveProvisoryComputeUnitLimit = hasCuEstimation
52
+ ? estimateAndUpdateProvisoryComputeUnitLimitFactory(estimateComputeUnitLimitFactory({ rpc: solanaRpc }))
53
+ : undefined;
54
+ const payment = paymentAddr
55
+ ? await buildPlaceholderPaymentInstruction(config.feePayerWallet, paymentAddr, config.feeToken, config.tokenProgramId)
56
+ : undefined;
57
+ const koraTransactionPlanner = createKoraTransactionPlanner(payerSigner, computeBudgetIxs, payment?.instruction, hasCuEstimation);
58
+ const koraTransactionPlanExecutor = createKoraTransactionPlanExecutor(koraClient, config, payerSigner, payment
59
+ ? {
60
+ destinationTokenAccount: payment.destinationTokenAccount,
61
+ sourceTokenAccount: payment.sourceTokenAccount,
62
+ }
63
+ : undefined, resolveProvisoryComputeUnitLimit);
64
+ return createEmptyClient()
65
+ .use(rpc(config.rpcUrl))
66
+ .use(koraPlugin({
67
+ endpoint: config.endpoint,
68
+ koraClient,
69
+ }))
70
+ .use(payer(payerSigner))
71
+ .use(koraPaymentAddress(paymentAddr))
72
+ .use(transactionPlannerPlugin(koraTransactionPlanner))
73
+ .use(transactionPlanExecutorPlugin(koraTransactionPlanExecutor))
74
+ .use(planAndSendTransactions());
75
+ }
@@ -0,0 +1,18 @@
1
+ import { type Address, type Instruction, type TransactionSigner } from '@solana/kit';
2
+ /** Plugin that adds a `paymentAddress` to the client. */
3
+ export declare function koraPaymentAddress(paymentAddress?: Address): <T extends object>(client: T) => T & {
4
+ paymentAddress: Address | undefined;
5
+ };
6
+ /**
7
+ * Builds a placeholder payment instruction (amount=0) to reserve transaction space
8
+ * during planning. The executor later replaces it with the real fee amount.
9
+ */
10
+ export declare function buildPlaceholderPaymentInstruction(feePayerWallet: TransactionSigner, paymentAddress: Address, feeToken: Address, tokenProgramId?: Address): Promise<{
11
+ destinationTokenAccount: Address;
12
+ instruction: Instruction;
13
+ sourceTokenAccount: Address;
14
+ }>;
15
+ /** Replaces the placeholder (amount=0) with the estimated fee amount. */
16
+ export declare function updatePaymentInstructionAmount(instructions: readonly Instruction[], feePayerWallet: TransactionSigner, sourceTokenAccount: Address, destinationTokenAccount: Address, amount: bigint | number, tokenProgramId?: Address): Instruction[];
17
+ /** Removes the placeholder payment instruction (used when fee is 0). */
18
+ export declare function removePaymentInstruction(instructions: readonly Instruction[], sourceTokenAccount: Address, destinationTokenAccount: Address, feePayerWallet: TransactionSigner, tokenProgramId?: Address): Instruction[];
@@ -0,0 +1,70 @@
1
+ import { findAssociatedTokenPda, getTransferInstruction, parseTransferInstruction, TOKEN_PROGRAM_ADDRESS, TRANSFER_DISCRIMINATOR, } from '@solana-program/token';
2
+ /** Plugin that adds a `paymentAddress` to the client. */
3
+ export function koraPaymentAddress(paymentAddress) {
4
+ return (client) => ({
5
+ ...client,
6
+ paymentAddress,
7
+ });
8
+ }
9
+ /**
10
+ * Builds a placeholder payment instruction (amount=0) to reserve transaction space
11
+ * during planning. The executor later replaces it with the real fee amount.
12
+ */
13
+ export async function buildPlaceholderPaymentInstruction(feePayerWallet, paymentAddress, feeToken, tokenProgramId) {
14
+ const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ADDRESS;
15
+ const [sourceTokenAccount] = await findAssociatedTokenPda({
16
+ mint: feeToken,
17
+ owner: feePayerWallet.address,
18
+ tokenProgram,
19
+ });
20
+ const [destinationTokenAccount] = await findAssociatedTokenPda({
21
+ mint: feeToken,
22
+ owner: paymentAddress,
23
+ tokenProgram,
24
+ });
25
+ const instruction = getTransferInstruction({
26
+ amount: 0,
27
+ authority: feePayerWallet,
28
+ destination: destinationTokenAccount,
29
+ source: sourceTokenAccount,
30
+ }, { programAddress: tokenProgram });
31
+ return { destinationTokenAccount, instruction, sourceTokenAccount };
32
+ }
33
+ function isPlaceholderPaymentInstruction(ix, sourceTokenAccount, destinationTokenAccount, feePayerWallet, tokenProgramId) {
34
+ const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ADDRESS;
35
+ if (ix.programAddress !== tokenProgram)
36
+ return false;
37
+ if (ix.data?.[0] !== TRANSFER_DISCRIMINATOR)
38
+ return false;
39
+ const parsed = parseTransferInstruction(ix);
40
+ return (parsed.accounts.source.address === sourceTokenAccount &&
41
+ parsed.accounts.destination.address === destinationTokenAccount &&
42
+ parsed.accounts.authority.address === feePayerWallet.address &&
43
+ parsed.data.amount === 0n);
44
+ }
45
+ /** Replaces the placeholder (amount=0) with the estimated fee amount. */
46
+ export function updatePaymentInstructionAmount(instructions, feePayerWallet, sourceTokenAccount, destinationTokenAccount, amount, tokenProgramId) {
47
+ let replaced = false;
48
+ const result = instructions.map(ix => {
49
+ if (!isPlaceholderPaymentInstruction(ix, sourceTokenAccount, destinationTokenAccount, feePayerWallet, tokenProgramId)) {
50
+ return ix;
51
+ }
52
+ replaced = true;
53
+ const tokenProgram = tokenProgramId ?? TOKEN_PROGRAM_ADDRESS;
54
+ return getTransferInstruction({
55
+ amount,
56
+ authority: feePayerWallet,
57
+ destination: destinationTokenAccount,
58
+ source: sourceTokenAccount,
59
+ }, { programAddress: tokenProgram });
60
+ });
61
+ if (!replaced) {
62
+ throw new Error('Failed to update payment instruction: no matching placeholder transfer instruction found. ' +
63
+ 'This is a Kora SDK internal error — the transaction message may have been modified between planning and execution.');
64
+ }
65
+ return result;
66
+ }
67
+ /** Removes the placeholder payment instruction (used when fee is 0). */
68
+ export function removePaymentInstruction(instructions, sourceTokenAccount, destinationTokenAccount, feePayerWallet, tokenProgramId) {
69
+ return instructions.filter(ix => !isPlaceholderPaymentInstruction(ix, sourceTokenAccount, destinationTokenAccount, feePayerWallet, tokenProgramId));
70
+ }
@@ -0,0 +1,4 @@
1
+ import { type Instruction, type TransactionSigner } from '@solana/kit';
2
+ import type { KoraKitClientConfig } from '../types/index.js';
3
+ export declare function buildComputeBudgetInstructions(config: KoraKitClientConfig): Instruction[];
4
+ export declare function createKoraTransactionPlanner(payerSigner: TransactionSigner, computeBudgetIxs: Instruction[], paymentInstruction: Instruction | undefined, hasCuEstimation: boolean): import("@solana/kit").TransactionPlanner;
@@ -0,0 +1,23 @@
1
+ import { appendTransactionMessageInstructions, createTransactionMessage, createTransactionPlanner, pipe, setTransactionMessageFeePayerSigner, } from '@solana/kit';
2
+ import { fillProvisorySetComputeUnitLimitInstruction, getSetComputeUnitLimitInstruction, getSetComputeUnitPriceInstruction, } from '@solana-program/compute-budget';
3
+ export function buildComputeBudgetInstructions(config) {
4
+ const instructions = [];
5
+ if (config.computeUnitLimit !== undefined) {
6
+ instructions.push(getSetComputeUnitLimitInstruction({ units: config.computeUnitLimit }));
7
+ }
8
+ if (config.computeUnitPrice !== undefined) {
9
+ instructions.push(getSetComputeUnitPriceInstruction({ microLamports: config.computeUnitPrice }));
10
+ }
11
+ return instructions;
12
+ }
13
+ export function createKoraTransactionPlanner(payerSigner, computeBudgetIxs, paymentInstruction, hasCuEstimation) {
14
+ return createTransactionPlanner({
15
+ createTransactionMessage: () => {
16
+ const allIxs = [...computeBudgetIxs];
17
+ if (paymentInstruction) {
18
+ allIxs.push(paymentInstruction);
19
+ }
20
+ return pipe(createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayerSigner(payerSigner, m), m => appendTransactionMessageInstructions(allIxs, m), m => (hasCuEstimation ? fillProvisorySetComputeUnitLimitInstruction(m) : m));
21
+ },
22
+ });
23
+ }
@@ -31,11 +31,13 @@ import { KoraClient } from './client.js';
31
31
  * ```
32
32
  */
33
33
  export function koraPlugin(config) {
34
- const client = new KoraClient({
35
- apiKey: config.apiKey,
36
- hmacSecret: config.hmacSecret,
37
- rpcUrl: config.endpoint,
38
- });
34
+ const client = config.koraClient ??
35
+ new KoraClient({
36
+ apiKey: config.apiKey,
37
+ getRecaptchaToken: config.getRecaptchaToken,
38
+ hmacSecret: config.hmacSecret,
39
+ rpcUrl: config.endpoint,
40
+ });
39
41
  return (c) => ({
40
42
  ...c,
41
43
  kora: {
@@ -1,4 +1,4 @@
1
- import { Instruction } from '@solana/kit';
1
+ import { Instruction, type MicroLamports, type TransactionSigner } from '@solana/kit';
2
2
  /**
3
3
  * Request Types
4
4
  */
@@ -452,6 +452,8 @@ export interface AuthenticationHeaders {
452
452
  'x-api-key'?: string;
453
453
  /** HMAC SHA256 signature of timestamp + body */
454
454
  'x-hmac-signature'?: string;
455
+ /** reCAPTCHA v3 token for bot protection */
456
+ 'x-recaptcha-token'?: string;
455
457
  /** Unix timestamp for HMAC authentication */
456
458
  'x-timestamp'?: string;
457
459
  }
@@ -461,6 +463,13 @@ export interface AuthenticationHeaders {
461
463
  export interface KoraClientOptions {
462
464
  /** Optional API key for authentication */
463
465
  apiKey?: string;
466
+ /**
467
+ * Optional callback to get a reCAPTCHA v3 token for bot protection.
468
+ * Called for every request when provided; server determines which methods require it.
469
+ * @example Browser: `() => grecaptcha.execute('site-key', { action: 'sign' })`
470
+ * @example Testing: `() => 'test-token'`
471
+ */
472
+ getRecaptchaToken?: () => Promise<string> | string;
464
473
  /** Optional HMAC secret for signature-based authentication */
465
474
  hmacSecret?: string;
466
475
  /** URL of the Kora RPC server */
@@ -470,14 +479,24 @@ export interface KoraClientOptions {
470
479
  * Plugin Types - Kit-typed responses for the Kora plugin
471
480
  */
472
481
  import type { Address, Base64EncodedWireTransaction, Blockhash, Instruction as KitInstruction, Signature } from '@solana/kit';
482
+ import { KoraClient } from '../client.js';
473
483
  /** Configuration options for the Kora Kit plugin */
474
484
  export interface KoraPluginConfig {
475
485
  /** Optional API key for authentication */
476
486
  apiKey?: string;
477
487
  /** Kora RPC endpoint URL */
478
488
  endpoint: string;
489
+ /**
490
+ * Optional callback to get a reCAPTCHA v3 token for bot protection.
491
+ * Called for every request when provided; server determines which methods require it.
492
+ * @example Browser: `() => grecaptcha.execute('site-key', { action: 'sign' })`
493
+ * @example Testing: `() => 'test-token'`
494
+ */
495
+ getRecaptchaToken?: () => Promise<string> | string;
479
496
  /** Optional HMAC secret for signature-based authentication */
480
497
  hmacSecret?: string;
498
+ /** Existing Kora Client for reusing existing instance */
499
+ koraClient?: KoraClient;
481
500
  }
482
501
  /** Plugin response for getPayerSigner with Kit Address types */
483
502
  export interface KitPayerSignerResponse {
@@ -597,3 +616,20 @@ export interface KitValidationConfig {
597
616
  /** Token2022 configuration */
598
617
  token2022: Token2022Config;
599
618
  }
619
+ /**
620
+ * Kit Client Types
621
+ */
622
+ /** Configuration for {@link createKitKoraClient}. */
623
+ export interface KoraKitClientConfig {
624
+ readonly apiKey?: string;
625
+ readonly computeUnitLimit?: number;
626
+ readonly computeUnitPrice?: MicroLamports;
627
+ readonly endpoint: string;
628
+ readonly feePayerWallet: TransactionSigner;
629
+ readonly feeToken: Address;
630
+ readonly getRecaptchaToken?: () => Promise<string> | string;
631
+ readonly hmacSecret?: string;
632
+ readonly rpcUrl: string;
633
+ readonly tokenProgramId?: Address;
634
+ readonly userId?: string;
635
+ }