Skip to content

Commit 76707ee

Browse files
amilzdev-jodeeellipsis-dev[bot]
authored
feat:(PRO-144) Add getPaymentInstruction SDK method (#192)
* Update typescript-integration.yml * Revert "Update typescript-integration.yml" This reverts commit e97ebce. * chore: test error codes * chore: update commitment * chore: finalize mint setup * feat: add getPaymentInstruction - adds a helper instruction to get a payment instruction based on a transaction - adds a couple of private helper methods * unit tests * chore: add todo * chore: update estimate txn fee response add payment address to est fee response for better devex Update rpc_integration_tests.rs * chore: update ts tests * chore: remove unused methods * Apply suggestion from @ellipsis-dev[bot] Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --------- Co-authored-by: jo <17280917+dev-jodee@users.noreply.github.com> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 80b7fc3 commit 76707ee

File tree

7 files changed

+405
-5
lines changed

7 files changed

+405
-5
lines changed

crates/lib/src/config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ impl ValidationConfig {
7979
pub fn is_payment_required(&self) -> bool {
8080
!matches!(&self.price.model, PriceModel::Free)
8181
}
82+
83+
pub fn supports_token(&self, token: &str) -> bool {
84+
self.allowed_spl_paid_tokens.iter().any(|s| s == token)
85+
}
8286
}
8387

8488
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]

crates/lib/src/rpc_server/method/estimate_transaction_fee.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ pub struct EstimateTransactionFeeResponse {
3030
pub fee_in_token: Option<f64>,
3131
/// Public key of the signer used for fee estimation (for client consistency)
3232
pub signer_pubkey: String,
33+
/// Public key of the payment destination
34+
pub payment_address: String,
3335
}
3436

3537
pub async fn estimate_transaction_fee(
@@ -39,7 +41,10 @@ pub async fn estimate_transaction_fee(
3941
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
4042

4143
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;
42-
let validation_config = &get_config()?.validation;
44+
let config = get_config()?;
45+
let payment_destination = config.kora.get_payment_address(&signer.solana_pubkey())?;
46+
47+
let validation_config = &config.validation;
4348
let fee_payer = signer.solana_pubkey();
4449

4550
let mut resolved_transaction =
@@ -61,6 +66,10 @@ pub async fn estimate_transaction_fee(
6166
KoraError::InvalidTransaction("Invalid fee token mint address".to_string())
6267
})?;
6368

69+
if !validation_config.supports_token(fee_token) {
70+
return Err(KoraError::InvalidRequest(format!("Token {fee_token} is not supported")));
71+
}
72+
6473
let fee_value_in_token = TokenUtil::calculate_lamports_value_in_token(
6574
fee_in_lamports,
6675
&token_mint,
@@ -76,5 +85,6 @@ pub async fn estimate_transaction_fee(
7685
fee_in_lamports,
7786
fee_in_token,
7887
signer_pubkey: fee_payer.to_string(),
88+
payment_address: payment_destination.to_string(),
7989
})
8090
}

sdks/ts/src/client.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// TODO Make sure to change necessary deps from devdeps to deps
2+
import { assertIsAddress, createNoopSigner, Instruction } from '@solana/kit';
13
import {
24
Config,
35
EstimateTransactionFeeRequest,
@@ -17,8 +19,17 @@ import {
1719
AuthenticationHeaders,
1820
KoraClientOptions,
1921
GetPayerSignerResponse,
22+
GetPaymentInstructionRequest,
23+
GetPaymentInstructionResponse,
2024
} from './types/index.js';
2125
import crypto from 'crypto';
26+
import {
27+
findAssociatedTokenPda,
28+
getTransferCheckedInstruction,
29+
TOKEN_PROGRAM_ADDRESS,
30+
fetchMaybeMint,
31+
getTransferInstruction,
32+
} from '@solana-program/token';
2233

2334
/**
2435
* Kora RPC client for interacting with the Kora paymaster service.
@@ -281,4 +292,74 @@ export class KoraClient {
281292
async transferTransaction(request: TransferTransactionRequest): Promise<TransferTransactionResponse> {
282293
return this.rpcRequest<TransferTransactionResponse, TransferTransactionRequest>('transferTransaction', request);
283294
}
295+
296+
/**
297+
* Creates a payment instruction to append to a transaction for fee payment to the Kora paymaster.
298+
*
299+
* This method estimates the required fee and generates a token transfer instruction
300+
* from the source wallet to the Kora payment address. The server handles decimal
301+
* conversion internally, so the raw token amount is used directly.
302+
*
303+
* @param request - Payment instruction request parameters
304+
* @param request.transaction - Base64-encoded transaction to estimate fees for
305+
* @param request.fee_token - Mint address of the token to use for payment
306+
* @param request.source_wallet - Public key of the wallet paying the fees
307+
* @param request.token_program_id - Optional token program ID (defaults to TOKEN_PROGRAM_ADDRESS)
308+
* @returns Payment instruction details including the instruction, amount, and addresses
309+
* @throws {Error} When the token is not supported, payment is not required, or invalid addresses are provided
310+
*
311+
* @example
312+
* ```typescript
313+
* const paymentInfo = await client.getPaymentInstruction({
314+
* transaction: 'base64EncodedTransaction',
315+
* fee_token: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
316+
* source_wallet: 'sourceWalletPublicKey'
317+
* });
318+
* // Append paymentInfo.payment_instruction to your transaction
319+
* ```
320+
*/
321+
async getPaymentInstruction({
322+
transaction,
323+
fee_token,
324+
source_wallet,
325+
token_program_id = TOKEN_PROGRAM_ADDRESS,
326+
}: GetPaymentInstructionRequest): Promise<GetPaymentInstructionResponse> {
327+
assertIsAddress(source_wallet);
328+
assertIsAddress(fee_token);
329+
assertIsAddress(token_program_id);
330+
331+
const { fee_in_token, payment_address, signer_pubkey } = await this.estimateTransactionFee({
332+
transaction,
333+
fee_token,
334+
});
335+
assertIsAddress(payment_address);
336+
337+
const [sourceTokenAccount] = await findAssociatedTokenPda({
338+
owner: source_wallet,
339+
tokenProgram: token_program_id,
340+
mint: fee_token,
341+
});
342+
343+
const [destinationTokenAccount] = await findAssociatedTokenPda({
344+
owner: payment_address,
345+
tokenProgram: token_program_id,
346+
mint: fee_token,
347+
});
348+
349+
const paymentInstruction: Instruction = getTransferInstruction({
350+
source: sourceTokenAccount,
351+
destination: destinationTokenAccount,
352+
authority: createNoopSigner(source_wallet),
353+
amount: fee_in_token,
354+
});
355+
356+
return {
357+
original_transaction: transaction,
358+
payment_instruction: paymentInstruction,
359+
payment_amount: fee_in_token,
360+
payment_token: fee_token,
361+
payment_address,
362+
signer_address: signer_pubkey,
363+
};
364+
}
284365
}

sdks/ts/src/types/index.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Instruction, Address } from '@solana/kit';
2+
13
/**
24
* Request Types
35
*/
@@ -60,6 +62,20 @@ export interface EstimateTransactionFeeRequest {
6062
signer_key?: string;
6163
}
6264

65+
/**
66+
* Parameters for getting a payment instruction.
67+
*/
68+
export interface GetPaymentInstructionRequest {
69+
/** Base64-encoded transaction to estimate fees for */
70+
transaction: string;
71+
/** Mint address of the token to calculate fees in */
72+
fee_token: string;
73+
/** The wallet owner (not token account) that will be making the token payment */
74+
source_wallet: string;
75+
/** The token program id to use for the payment (defaults to TOKEN_PROGRAM_ID) */
76+
token_program_id?: string;
77+
}
78+
6379
/**
6480
* Response Types
6581
*/
@@ -142,6 +158,8 @@ export interface EstimateTransactionFeeResponse {
142158
fee_in_token: number;
143159
/** Public key of the signer used to estimate the fee */
144160
signer_pubkey: string;
161+
/** Public key of the payment destination */
162+
payment_address: string;
145163
}
146164

147165
/**
@@ -154,6 +172,24 @@ export interface GetPayerSignerResponse {
154172
payment_address: string;
155173
}
156174

175+
/**
176+
* Response containing a payment instruction.
177+
*/
178+
export interface GetPaymentInstructionResponse {
179+
/** Base64-encoded original transaction */
180+
original_transaction: string;
181+
/** Base64-encoded payment instruction */
182+
payment_instruction: Instruction;
183+
/** Payment amount in the requested token */
184+
payment_amount: number;
185+
/** Mint address of the token used for payment */
186+
payment_token: string;
187+
/** Public key of the payment destination */
188+
payment_address: string;
189+
/** Public key of the payer signer */
190+
signer_address: string;
191+
}
192+
157193
/**
158194
* Configuration Types
159195
*/

sdks/ts/test/integration.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { KoraClient } from '../src/index.js';
22
import setupTestSuite from './setup.js';
33
import { runAuthenticationTests } from './auth-setup.js';
4-
54
import {
65
Address,
76
getBase64EncodedWireTransaction,
87
getBase64Encoder,
98
getTransactionDecoder,
10-
partiallySignTransaction,
119
signTransaction,
1210
type KeyPairSigner,
1311
type Transaction,
1412
} from '@solana/kit';
13+
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';
1514

1615
function transactionFromBase64(base64: string): Transaction {
1716
const encoder = getBase64Encoder();
@@ -193,6 +192,50 @@ describe(`KoraClient Integration Tests (${AUTH_ENABLED ? 'with auth' : 'without
193192
expect(signResult.transaction).toBeDefined();
194193
expect(signResult.signed_transaction).toBeDefined();
195194
});
195+
196+
it('should get payment instruction', async () => {
197+
const transferRequest = {
198+
amount: 1000000,
199+
token: usdcMint,
200+
source: testWalletAddress,
201+
destination: destinationAddress,
202+
};
203+
const [expectedSenderAta] = await findAssociatedTokenPda({
204+
owner: testWalletAddress,
205+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
206+
mint: usdcMint,
207+
});
208+
const [koraAta] = await findAssociatedTokenPda({
209+
owner: koraAddress,
210+
tokenProgram: TOKEN_PROGRAM_ADDRESS,
211+
mint: usdcMint,
212+
});
213+
214+
const { transaction } = await client.transferTransaction(transferRequest);
215+
const {
216+
payment_instruction,
217+
payment_amount,
218+
payment_token,
219+
payment_address,
220+
signer_address,
221+
original_transaction,
222+
} = await client.getPaymentInstruction({
223+
transaction,
224+
fee_token: usdcMint,
225+
source_wallet: testWalletAddress,
226+
});
227+
expect(payment_instruction).toBeDefined();
228+
expect(payment_instruction.programAddress).toBe(TOKEN_PROGRAM_ADDRESS);
229+
expect(payment_instruction.accounts?.[0].address).toBe(expectedSenderAta);
230+
expect(payment_instruction.accounts?.[1].address).toBe(koraAta);
231+
expect(payment_instruction.accounts?.[2].address).toBe(testWalletAddress);
232+
// todo math to verify payment amount
233+
// expect(payment_amount).toBe(1000000);
234+
expect(payment_token).toBe(usdcMint);
235+
expect(payment_address).toBe(koraAddress);
236+
expect(signer_address).toBe(koraAddress);
237+
expect(original_transaction).toBe(transaction);
238+
});
196239
});
197240

198241
describe('Error Handling', () => {

0 commit comments

Comments
 (0)