@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.
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/kit/executor.d.ts +7 -0
- package/dist/src/kit/executor.js +68 -0
- package/dist/src/kit/index.d.ts +53 -0
- package/dist/src/kit/index.js +75 -0
- package/dist/src/kit/payment.d.ts +18 -0
- package/dist/src/kit/payment.js +70 -0
- package/dist/src/kit/planner.d.ts +4 -0
- package/dist/src/kit/planner.js +23 -0
- package/dist/src/plugin.js +7 -6
- package/dist/src/types/index.d.ts +21 -1
- package/dist/test/integration.test.js +43 -16
- package/dist/test/kit-client.test.d.ts +1 -0
- package/dist/test/kit-client.test.js +491 -0
- package/dist/test/setup.d.ts +1 -4
- package/dist/test/setup.js +34 -136
- package/package.json +26 -16
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -31,12 +31,13 @@ import { KoraClient } from './client.js';
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
export function koraPlugin(config) {
|
|
34
|
-
const client =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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).
|
|
178
|
+
expect(fee.fee_in_lamports).toBeGreaterThanOrEqual(0);
|
|
176
179
|
expect(typeof fee.fee_in_token).toBe('number');
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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 {};
|