@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 +29 -27
- package/dist/src/client.d.ts +3 -1
- package/dist/src/client.js +10 -3
- 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 -5
- package/dist/src/types/index.d.ts +37 -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/dist/test/unit.test.js +106 -22
- package/package.json +26 -16
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
|
-
|
|
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)**
|
package/dist/src/client.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/client.js
CHANGED
|
@@ -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' },
|
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,11 +31,13 @@ import { KoraClient } from './client.js';
|
|
|
31
31
|
* ```
|
|
32
32
|
*/
|
|
33
33
|
export function koraPlugin(config) {
|
|
34
|
-
const client =
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|