-
Notifications
You must be signed in to change notification settings - Fork 247
Expand file tree
/
Copy pathclient.ts
More file actions
411 lines (383 loc) · 16.3 KB
/
client.ts
File metadata and controls
411 lines (383 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
import { Address, assertIsAddress, Instruction, isTransactionSigner } from '@solana/kit';
import { findAssociatedTokenPda, getTransferInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
import crypto from 'crypto';
import {
AuthenticationHeaders,
Config,
EstimateBundleFeeRequest,
EstimateBundleFeeResponse,
EstimateTransactionFeeRequest,
EstimateTransactionFeeResponse,
GetBlockhashResponse,
GetPayerSignerResponse,
GetPaymentInstructionRequest,
GetPaymentInstructionResponse,
GetSupportedTokensResponse,
GetVersionResponse,
KoraClientOptions,
RpcError,
SignAndSendBundleRequest,
SignAndSendBundleResponse,
SignAndSendTransactionRequest,
SignAndSendTransactionResponse,
SignBundleRequest,
SignBundleResponse,
SignTransactionRequest,
SignTransactionResponse,
} from './types/index.js';
/**
* Kora RPC client for interacting with the Kora paymaster service.
*
* Provides methods to estimate fees, sign transactions, and perform gasless transfers
* on Solana as specified by the Kora paymaster operator.
*
* @example Kora Initialization
* ```typescript
* const client = new KoraClient({
* rpcUrl: 'http://localhost:8080',
* // apiKey may be required by some operators
* // apiKey: 'your-api-key',
* // hmacSecret may be required by some operators
* // hmacSecret: 'your-hmac-secret'
* });
*
* // Sample usage: Get config
* const config = await client.getConfig();
* ```
*/
export class KoraClient {
private rpcUrl: string;
private apiKey?: string;
private hmacSecret?: string;
private getRecaptchaToken?: () => Promise<string> | string;
/**
* Creates a new Kora client instance.
* @param options - Client configuration options
* @param options.rpcUrl - The Kora RPC server URL
* @param options.apiKey - Optional API key for authentication
* @param options.hmacSecret - Optional HMAC secret for signature-based authentication
* @param options.getRecaptchaToken - Optional callback to get reCAPTCHA token for bot protection
*/
constructor({ rpcUrl, apiKey, hmacSecret, getRecaptchaToken }: KoraClientOptions) {
this.rpcUrl = rpcUrl;
this.apiKey = apiKey;
this.hmacSecret = hmacSecret;
this.getRecaptchaToken = getRecaptchaToken;
}
private getHmacSignature({ timestamp, body }: { body: string; timestamp: string }): string {
if (!this.hmacSecret) {
throw new Error('HMAC secret is not set');
}
const message = timestamp + body;
return crypto.createHmac('sha256', this.hmacSecret).update(message).digest('hex');
}
private async getHeaders({ body }: { body: string }): Promise<AuthenticationHeaders> {
const headers: AuthenticationHeaders = {};
if (this.apiKey) {
headers['x-api-key'] = this.apiKey;
}
if (this.hmacSecret) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = this.getHmacSignature({ body, timestamp });
headers['x-timestamp'] = timestamp;
headers['x-hmac-signature'] = signature;
}
if (this.getRecaptchaToken) {
const token = await Promise.resolve(this.getRecaptchaToken());
headers['x-recaptcha-token'] = token;
}
return headers;
}
private async rpcRequest<T, U>(method: string, params: U): Promise<T> {
const body = JSON.stringify({
id: 1,
jsonrpc: '2.0',
method,
params,
});
const headers = await this.getHeaders({ body });
const response = await fetch(this.rpcUrl, {
body,
headers: { ...headers, 'Content-Type': 'application/json' },
method: 'POST',
});
const json = (await response.json()) as { error?: RpcError; result: T };
if (json.error) {
const error = json.error;
throw new Error(`RPC Error ${error.code}: ${error.message}`);
}
return json.result;
}
/**
* Retrieves the current Kora server configuration.
* @returns The server configuration including fee payer address and validation rules
* @throws {Error} When the RPC call fails
*
* @example
* ```typescript
* const config = await client.getConfig();
* console.log('Fee payer:', config.fee_payer);
* console.log('Validation config:', JSON.stringify(config.validation_config, null, 2));
* ```
*/
async getConfig(): Promise<Config> {
return await this.rpcRequest<Config, undefined>('getConfig', undefined);
}
/**
* Retrieves the payer signer and payment destination from the Kora server.
* @returns Object containing the payer signer and payment destination
* @throws {Error} When the RPC call fails
*
* @example
*/
async getPayerSigner(): Promise<GetPayerSignerResponse> {
return await this.rpcRequest<GetPayerSignerResponse, undefined>('getPayerSigner', undefined);
}
/**
* Gets the latest blockhash from the Solana RPC that the Kora server is connected to.
* @returns Object containing the current blockhash
* @throws {Error} When the RPC call fails
*
* @example
* ```typescript
* const { blockhash } = await client.getBlockhash();
* console.log('Current blockhash:', blockhash);
* ```
*/
async getBlockhash(): Promise<GetBlockhashResponse> {
return await this.rpcRequest<GetBlockhashResponse, undefined>('getBlockhash', undefined);
}
/**
* Gets the version of the Kora server.
* @returns Object containing the server version
* @throws {Error} When the RPC call fails
*
* @example
* ```typescript
* const { version } = await client.getVersion();
* console.log('Server version:', version);
* ```
*/
async getVersion(): Promise<GetVersionResponse> {
return await this.rpcRequest<GetVersionResponse, undefined>('getVersion', undefined);
}
/**
* Retrieves the list of tokens supported for fee payment.
* @returns Object containing an array of supported token mint addresses
* @throws {Error} When the RPC call fails
*
* @example
* ```typescript
* const { tokens } = await client.getSupportedTokens();
* console.log('Supported tokens:', tokens);
* // Output: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', ...]
* ```
*/
async getSupportedTokens(): Promise<GetSupportedTokensResponse> {
return await this.rpcRequest<GetSupportedTokensResponse, undefined>('getSupportedTokens', undefined);
}
/**
* Estimates the transaction fee in both lamports and the specified token.
* @param request - Fee estimation request parameters
* @param request.transaction - Base64-encoded transaction to estimate fees for
* @param request.fee_token - Mint address of the token to calculate fees in
* @returns Fee amounts in both lamports and the specified token
* @throws {Error} When the RPC call fails, the transaction is invalid, or the token is not supported
*
* @example
* ```typescript
* const fees = await client.estimateTransactionFee({
* transaction: 'base64EncodedTransaction',
* fee_token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC
* });
* console.log('Fee in lamports:', fees.fee_in_lamports);
* console.log('Fee in USDC:', fees.fee_in_token);
* ```
*/
async estimateTransactionFee(request: EstimateTransactionFeeRequest): Promise<EstimateTransactionFeeResponse> {
return await this.rpcRequest<EstimateTransactionFeeResponse, EstimateTransactionFeeRequest>(
'estimateTransactionFee',
request,
);
}
/**
* Estimates the bundle fee in both lamports and the specified token.
* @param request - Bundle fee estimation request parameters
* @param request.transactions - Array of base64-encoded transactions to estimate fees for
* @param request.fee_token - Mint address of the token to calculate fees in
* @returns Total fee amounts across all transactions in both lamports and the specified token
* @throws {Error} When the RPC call fails, the bundle is invalid, or the token is not supported
*
* @example
* ```typescript
* const fees = await client.estimateBundleFee({
* transactions: ['base64EncodedTransaction1', 'base64EncodedTransaction2'],
* fee_token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC
* });
* console.log('Total fee in lamports:', fees.fee_in_lamports);
* console.log('Total fee in USDC:', fees.fee_in_token);
* ```
*/
async estimateBundleFee(request: EstimateBundleFeeRequest): Promise<EstimateBundleFeeResponse> {
return await this.rpcRequest<EstimateBundleFeeResponse, EstimateBundleFeeRequest>('estimateBundleFee', request);
}
/**
* Signs a transaction with the Kora fee payer without broadcasting it.
* @param request - Sign request parameters
* @param request.transaction - Base64-encoded transaction to sign
* @returns Signature and the signed transaction
* @throws {Error} When the RPC call fails or transaction validation fails
*
* @example
* ```typescript
* const result = await client.signTransaction({
* transaction: 'base64EncodedTransaction'
* });
* console.log('Signature:', result.signature);
* console.log('Signed tx:', result.signed_transaction);
* ```
*/
async signTransaction(request: SignTransactionRequest): Promise<SignTransactionResponse> {
return await this.rpcRequest<SignTransactionResponse, SignTransactionRequest>('signTransaction', request);
}
/**
* Signs a transaction and immediately broadcasts it to the Solana network.
* @param request - Sign and send request parameters
* @param request.transaction - Base64-encoded transaction to sign and send
* @returns Signature and the signed transaction
* @throws {Error} When the RPC call fails, validation fails, or broadcast fails
*
* @example
* ```typescript
* const result = await client.signAndSendTransaction({
* transaction: 'base64EncodedTransaction'
* });
* console.log('Transaction signature:', result.signature);
* ```
*/
async signAndSendTransaction(request: SignAndSendTransactionRequest): Promise<SignAndSendTransactionResponse> {
return await this.rpcRequest<SignAndSendTransactionResponse, SignAndSendTransactionRequest>(
'signAndSendTransaction',
request,
);
}
/**
* Signs a bundle of transactions with the Kora fee payer without broadcasting.
* @param request - Sign bundle request parameters
* @param request.transactions - Array of base64-encoded transactions to sign
* @param request.signer_key - Optional signer address for the transactions
* @param request.sig_verify - Optional signature verification (defaults to false)
* @param request.sign_only_indices - Optional indices of transactions to sign (defaults to all)
* @returns Array of signed transactions and signer public key
* @throws {Error} When the RPC call fails or validation fails
*
* @example
* ```typescript
* const result = await client.signBundle({
* transactions: ['base64Tx1', 'base64Tx2']
* });
* console.log('Signed transactions:', result.signed_transactions);
* console.log('Signer:', result.signer_pubkey);
* ```
*/
async signBundle(request: SignBundleRequest): Promise<SignBundleResponse> {
return await this.rpcRequest<SignBundleResponse, SignBundleRequest>('signBundle', request);
}
/**
* Signs a bundle of transactions and sends them to Jito block engine.
* @param request - Sign and send bundle request parameters
* @param request.transactions - Array of base64-encoded transactions to sign and send
* @param request.signer_key - Optional signer address for the transactions
* @param request.sig_verify - Optional signature verification (defaults to false)
* @param request.sign_only_indices - Optional indices of transactions to sign (defaults to all)
* @returns Array of signed transactions, signer public key, and Jito bundle UUID
* @throws {Error} When the RPC call fails, validation fails, or Jito submission fails
*
* @example
* ```typescript
* const result = await client.signAndSendBundle({
* transactions: ['base64Tx1', 'base64Tx2']
* });
* console.log('Bundle UUID:', result.bundle_uuid);
* console.log('Signed transactions:', result.signed_transactions);
* ```
*/
async signAndSendBundle(request: SignAndSendBundleRequest): Promise<SignAndSendBundleResponse> {
return await this.rpcRequest<SignAndSendBundleResponse, SignAndSendBundleRequest>('signAndSendBundle', request);
}
/**
* Creates a payment instruction to append to a transaction for fee payment to the Kora paymaster.
*
* This method estimates the required fee and generates a token transfer instruction
* from the source wallet to the Kora payment address. The server handles decimal
* conversion internally, so the raw token amount is used directly.
*
* @param request - Payment instruction request parameters
* @param request.transaction - Base64-encoded transaction to estimate fees for
* @param request.fee_token - Mint address of the token to use for payment
* @param request.source_wallet - Public key of the wallet paying the fees
* @param request.token_program_id - Optional token program ID (defaults to TOKEN_PROGRAM_ADDRESS)
* @param request.signer_key - Optional signer address for the transaction
* @param request.sig_verify - Optional signer verification during transaction simulation (defaults to false)
* @returns Payment instruction details including the instruction, amount, and addresses
* @throws {Error} When the token is not supported, payment is not required, or invalid addresses are provided
*
* @example
* ```typescript
* const paymentInfo = await client.getPaymentInstruction({
* transaction: 'base64EncodedTransaction',
* fee_token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
* source_wallet: 'sourceWalletPublicKey'
* });
* // Append paymentInfo.payment_instruction to your transaction
* ```
*/
async getPaymentInstruction({
transaction,
fee_token,
source_wallet,
token_program_id = TOKEN_PROGRAM_ADDRESS,
signer_key,
sig_verify,
}: GetPaymentInstructionRequest): Promise<GetPaymentInstructionResponse> {
const isSigner = typeof source_wallet !== 'string' && isTransactionSigner(source_wallet);
const walletAddress: Address = isSigner ? source_wallet.address : (source_wallet as Address);
assertIsAddress(walletAddress);
assertIsAddress(fee_token);
assertIsAddress(token_program_id);
const { fee_in_token, payment_address, signer_pubkey } = await this.estimateTransactionFee({
fee_token,
sig_verify,
signer_key,
transaction,
});
assertIsAddress(payment_address);
const [sourceTokenAccount] = await findAssociatedTokenPda({
mint: fee_token,
owner: walletAddress,
tokenProgram: token_program_id,
});
const [destinationTokenAccount] = await findAssociatedTokenPda({
mint: fee_token,
owner: payment_address,
tokenProgram: token_program_id,
});
if (fee_in_token === undefined) {
throw new Error('Fee token was specified but fee_in_token was not returned from server');
}
const paymentInstruction: Instruction = getTransferInstruction({
amount: fee_in_token,
authority: isSigner ? source_wallet : walletAddress,
destination: destinationTokenAccount,
source: sourceTokenAccount,
});
return {
original_transaction: transaction,
payment_address,
payment_amount: fee_in_token,
payment_instruction: paymentInstruction,
payment_token: fee_token,
signer_address: signer_pubkey,
};
}
}