@solana/kora 0.2.0-beta.4 → 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,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,12 +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
- getRecaptchaToken: config.getRecaptchaToken,
37
- hmacSecret: config.hmacSecret,
38
- rpcUrl: config.endpoint,
39
- });
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
+ });
40
41
  return (c) => ({
41
42
  ...c,
42
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
  */
@@ -479,6 +479,7 @@ export interface KoraClientOptions {
479
479
  * Plugin Types - Kit-typed responses for the Kora plugin
480
480
  */
481
481
  import type { Address, Base64EncodedWireTransaction, Blockhash, Instruction as KitInstruction, Signature } from '@solana/kit';
482
+ import { KoraClient } from '../client.js';
482
483
  /** Configuration options for the Kora Kit plugin */
483
484
  export interface KoraPluginConfig {
484
485
  /** Optional API key for authentication */
@@ -494,6 +495,8 @@ export interface KoraPluginConfig {
494
495
  getRecaptchaToken?: () => Promise<string> | string;
495
496
  /** Optional HMAC secret for signature-based authentication */
496
497
  hmacSecret?: string;
498
+ /** Existing Kora Client for reusing existing instance */
499
+ koraClient?: KoraClient;
497
500
  }
498
501
  /** Plugin response for getPayerSigner with Kit Address types */
499
502
  export interface KitPayerSignerResponse {
@@ -613,3 +616,20 @@ export interface KitValidationConfig {
613
616
  /** Token2022 configuration */
614
617
  token2022: Token2022Config;
615
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
+ }
@@ -1,5 +1,7 @@
1
- import { appendTransactionMessageInstruction, compileTransaction, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
1
+ import { address, appendTransactionMessageInstruction, compileTransaction, createTransactionMessage, getBase64Decoder, getBase64EncodedWireTransaction, getBase64Encoder, getTransactionDecoder, getTransactionEncoder, partiallySignTransaction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit';
2
+ import { getTransferSolInstruction } from '@solana-program/system';
2
3
  import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
4
+ import { createKitKoraClient } from '../src/index.js';
3
5
  import { runAuthenticationTests } from './auth-setup.js';
4
6
  import setupTestSuite from './setup.js';
5
7
  function transactionFromBase64(base64) {
@@ -54,6 +56,7 @@ async function buildTokenTransferTransaction(params) {
54
56
  return { blockhash: blockhash, transaction: base64Transaction };
55
57
  }
56
58
  const AUTH_ENABLED = process.env.ENABLE_AUTH === 'true';
59
+ const FREE_PRICING = process.env.FREE_PRICING === 'true';
57
60
  const KORA_SIGNER_TYPE = process.env.KORA_SIGNER_TYPE || 'memory';
58
61
  describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without auth'} | signer type: ${KORA_SIGNER_TYPE})`, () => {
59
62
  let client;
@@ -172,9 +175,12 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
172
175
  const fee = await client.estimateTransactionFee({ fee_token: usdcMint, transaction });
173
176
  expect(fee).toBeDefined();
174
177
  expect(typeof fee.fee_in_lamports).toBe('number');
175
- expect(fee.fee_in_lamports).toBeGreaterThan(0);
178
+ expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
176
179
  expect(typeof fee.fee_in_token).toBe('number');
177
- expect(fee.fee_in_token).toBeGreaterThan(0);
180
+ if (!FREE_PRICING) {
181
+ expect(fee.fee_in_lamports).toBeGreaterThan(0);
182
+ expect(fee.fee_in_token).toBeGreaterThan(0);
183
+ }
178
184
  });
179
185
  it('should sign transaction', async () => {
180
186
  const { transaction } = await buildTokenTransferTransaction({
@@ -246,7 +252,8 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
246
252
  expect(original_transaction).toBe(transaction);
247
253
  });
248
254
  });
249
- describe('Bundle Operations', () => {
255
+ // Bundle tests require bundle.enabled = true in the Kora config
256
+ (FREE_PRICING ? describe.skip : describe)('Bundle Operations', () => {
250
257
  it('should sign bundle of transactions', async () => {
251
258
  // Create two transfer transactions for the bundle
252
259
  const { transaction: tx1String } = await buildTokenTransferTransaction({
@@ -324,17 +331,37 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
324
331
  await expect(client.estimateTransactionFee({ fee_token: usdcMint, transaction: 'invalid_transaction' })).rejects.toThrow();
325
332
  });
326
333
  });
327
- describe('End-to-End Flows', () => {
328
- it('should handle transfer and sign flow', async () => {
329
- const { transaction } = await buildTokenTransferTransaction({
330
- amount: 1000000n,
331
- client,
332
- destinationWallet: koraAddress,
333
- mint: usdcMint,
334
- sourceWallet: testWallet,
335
- });
336
- const signResult = await client.signTransaction({ transaction });
337
- expect(signResult.signed_transaction).toBeDefined();
334
+ if (FREE_PRICING) {
335
+ describe('Kit Client (free pricing)', () => {
336
+ let freeClient;
337
+ beforeAll(async () => {
338
+ const koraRpcUrl = process.env.KORA_RPC_URL || 'http://127.0.0.1:8080';
339
+ freeClient = await createKitKoraClient({
340
+ endpoint: koraRpcUrl,
341
+ rpcUrl: process.env.SOLANA_RPC_URL || 'http://127.0.0.1:8899',
342
+ feeToken: usdcMint,
343
+ feePayerWallet: testWallet,
344
+ });
345
+ }, 30000);
346
+ it('should send transaction without payment instruction when fee is 0', async () => {
347
+ const ix = getTransferSolInstruction({
348
+ source: testWallet,
349
+ destination: address(koraAddress),
350
+ amount: 1000,
351
+ });
352
+ const result = await freeClient.sendTransaction([ix]);
353
+ expect(result.status).toBe('successful');
354
+ expect(result.context.signature).toBeDefined();
355
+ }, 30000);
356
+ it('should strip placeholder from planned message when fee is 0', async () => {
357
+ const ix = getTransferSolInstruction({
358
+ source: testWallet,
359
+ destination: address(koraAddress),
360
+ amount: 1000,
361
+ });
362
+ const result = await freeClient.sendTransaction([ix]);
363
+ expect(result.status).toBe('successful');
364
+ }, 30000);
338
365
  });
339
- });
366
+ }
340
367
  });
@@ -0,0 +1 @@
1
+ export {};