31 min read
Overview
Solana transaction fees are paid in SOL, which means users need some SOL in their wallet to complete transactions. This can create friction for apps when users hold USDC or other SPL tokens but don’t have SOL available. Kora removes this barrier by allowing transaction and priority fees (sometimes called “gas”) to be paid in supported SPL tokens, or by sponsoring fees entirely so users pay nothing.
Behind the scenes, Kora acts as a fee payer that validates transactions against your security rules before signing. You maintain complete control over allowed programs, tokens, and spending limits.
This guide walks you through setting up a local Kora server, configuring validation rules, and building TypeScript unit tests that create a genuinely gasless user experience.
What You Will Do
- Install and configure a local Kora RPC server
- Define validation rules for programs, tokens, and spending limits
- Initialize a test environment with local tokens and Associated Token Accounts (ATAs)
- Build a TypeScript client that creates gasless transactions
- Test complete fee-payment flows using SPL tokens instead of SOL
What You Will Need
This guide assumes basic familiarity with Solana transactions, SPL tokens, Associated Token Accounts (ATAs), and TypeScript.
For a quick recap before you begin, these guides can help:
- Comprehensive Guide to Optimizing Solana Transactions
- Get All Token Accounts Held by a Solana Wallet
- Five Ways to Find the Associated Token Address for a Solana Wallet and Mint
- How to Send Transactions with Solana Kit
This guide will use the following packages and libraries:
| Dependency | Version |
|---|---|
| Node | 22+ |
| Solana CLI | 3.0.6+ |
| Kora CLI | 2.0.1 |
| @solana/kit | 5.5.1 |
| @solana/kora | 0.1.0 |
| @solana/codecs | 6.0.1 |
| @solana-program/system | 0.10.0 |
| @solana-program/token | 0.9.0 |
| tsx | 4.7.0 |
Kora's Architecture
Kora consists of 3 main components:
- Your client (dApp/wallet) that builds the unsigned transaction
- The Kora server that validates it against your rules and signs as the fee payer
- A Solana RPC endpoint that broadcasts the signed transaction to the network

This validation-first approach ensures Kora only signs transactions that comply with your security rules. You specify exactly which programs can be called, set spending limits per transaction and account, and define which tokens are acceptable for payment. You can even block specific accounts.
The complete transaction flow works like this:
- Your client constructs a transaction with Kora's address as the fee payer
- Client sends the unsigned transaction to Kora RPC server
- Kora validates the transaction against configured rules
- If valid, Kora signs the transaction with its fee payer keypair
- Kora either returns the signed transaction to your client or submits it directly to Solana
This architecture creates a clear separation of concerns: your client manages user interaction and transaction construction, Kora handles validation and signing, and Solana executes the transaction on-chain. The result is a system that enables gasless experiences without sacrificing security or control over spending.
Install Kora
Kora is distributed as a Rust CLI tool. Install it globally using Cargo:
cargo install kora-cli
After installation completes, verify it's available:
kora --version
The kora binary provides commands for running the RPC server, initializing payment accounts, and managing configuration.
Set Up Project
You'll build a complete gasless transaction implementation from scratch, creating all the necessary files and configuration. By the end of this guide, you'll have a working example that demonstrates how to integrate Kora into your applications.
Create a new project directory:
mkdir kora-gasless-demo
cd kora-gasless-demo
Initialize a Node.js project and install dependencies:
npm init -y
npm install @solana/kit @solana/kora @solana-program/token @solana-program/system @solana/codecs
npm install --save-dev typescript tsx @types/node
Add package.json type and scripts:
npm pkg set type="module"
npm pkg set scripts.env-setup="tsx --env-file=.env setup.ts"
npm pkg set scripts.test="tsx --env-file=.env --test test/gasless-transfer.test.ts"
For setup, config, and end-to-end tests, we'll create these files:
- .env: Environment configuration for connecting to Kora and Solana
- setup.ts: Script that creates test tokens, initializes ATAs, and funds accounts
- kora.toml: Kora server configuration with validation rules
- signers.toml: Kora Signer configuration for the fee payer
- test/gasless-transfer.test.ts: Complete test suite demonstrating Kora integration
Configure Kora
Before running the setup script, you need to create Kora's configuration files. These define the security rules Kora enforces and how it manages signing keys.
Create kora.toml
Create a file named kora.toml in your project root. This file is the heart of Kora's security model. It defines exactly what Kora will allow to be signed.
# Kora RPC Server Configuration
[kora]
rate_limit = 100 # Global rate limit (requests per second) across all clients
[kora.auth]
# Authentication disabled for local testing
[kora.cache]
enabled = false
default_ttl = 300
account_ttl = 60
[kora.enabled_methods]
liveness = false
estimate_transaction_fee = true
get_supported_tokens = true
sign_transaction = true
sign_and_send_transaction = true
transfer_transaction = true
get_blockhash = true
get_config = true
get_payer_signer = true
[validation]
max_allowed_lamports = 1000000000 # 1 SOL
max_signatures = 10
price_source = "Mock"
allow_durable_transactions = false
allowed_programs = [
"11111111111111111111111111111111", # System Program
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", # Token Program
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program
]
allowed_tokens = ["YOUR_USDC_MINT_ADDRESS"]
allowed_spl_paid_tokens = ["YOUR_USDC_MINT_ADDRESS"]
disallowed_accounts = []
[validation.fee_payer_policy]
[validation.fee_payer_policy.system]
allow_transfer = true
allow_assign = true
allow_create_account = true
allow_allocate = true
[validation.fee_payer_policy.system.nonce]
allow_initialize = true
allow_advance = true
allow_authorize = true
allow_withdraw = true
[validation.fee_payer_policy.spl_token]
allow_transfer = true
allow_burn = true
allow_close_account = true
allow_approve = true
allow_revoke = true
allow_set_authority = true
allow_mint_to = true
allow_initialize_mint = true
allow_initialize_account = true
allow_initialize_multisig = true
allow_freeze_account = true
allow_thaw_account = true
[validation.fee_payer_policy.token_2022]
allow_transfer = true
allow_burn = true
allow_close_account = true
allow_approve = true
allow_revoke = true
allow_set_authority = true
allow_mint_to = true
allow_initialize_mint = true
allow_initialize_account = true
allow_initialize_multisig = true
allow_freeze_account = true
allow_thaw_account = true
[validation.price]
type = "margin"
margin = 0.1 # 10% margin
[validation.token2022]
blocked_mint_extensions = []
blocked_account_extensions = []
[kora.usage_limit]
enabled = false
cache_url = "redis://localhost:6379"
max_transactions = 2
fallback_if_unavailable = false
Validation Rules control which transactions Kora will sign:
max_allowed_lamports sets the maximum fee per transaction. This protects against expensive transactions or bugs that could drain your fee payer account.
allowed_programs is an allowlist of program IDs that transactions can invoke. Kora rejects any transaction attempting to call programs not in this list. Start with System Program, Token Program, and Associated Token Program, then add your application's programs as needed.
allowed_tokens specifies which token mints can be transferred in transactions. You'll add your test USDC mint address here after running the setup script.
allowed_spl_paid_tokens is a subset of allowed_tokens that Kora accepts as fee payment. Typically includes stablecoins or your native token. You'll add your test USDC mint address here.
price_source = "Mock" uses fixed conversion rates defined in your config. Ideal for local development and testing. For production deployments you would use price_source = "Jupiter".
margin = 0.1 adds a 10% buffer above the actual cost to protect against price fluctuations between fee estimation and transaction execution.
Fee Payer Policies define which Solana operations Kora allows across System Program, SPL Token, and Token Extensions instructions. These granular controls let you permit specific instruction types (transfers, mints, burns, etc.) while blocking others.
Create signers.toml
Create a file named signers.toml in your project root. This configures how Kora manages its signing keys:
# Kora Signers Configuration
[signer_pool]
strategy = "round_robin"
[[signers]]
name = "fee_payer"
type = "memory"
private_key_env = "KORA_PRIVATE_KEY"
weight = 1
This configuration uses a memory-based signer that loads the keypair from the KORA_PRIVATE_KEY environment variable. The signer pool uses a round_robin strategy, which distributes transactions evenly across multiple signers (though we only have one signer in this example).
For production, you would replace the memory signer with a secure key management service like Turnkey, Vault, or Privy to keep private keys in hardware security modules (HSMs) and provide signing through secure APIs.
Generate Required Keypairs
Start a local Solana test validator in a separate terminal:
solana-test-validator
Generate the four required keypairs:
solana-keygen new --outfile kora-fee-payer.json --no-bip39-passphrase
solana-keygen new --outfile user-keypair.json --no-bip39-passphrase
solana-keygen new --outfile recipient-keypair.json --no-bip39-passphrase
solana-keygen new --outfile mint-authority-keypair.json --no-bip39-passphrase
Fund these accounts with SOL from your local validator:
solana airdrop 1 -k kora-fee-payer.json
solana airdrop 1 -k user-keypair.json
solana airdrop 1 -k recipient-keypair.json
solana airdrop 1 -k mint-authority-keypair.json
Configure Environment Variables
Create a .env file to store your environment variables:
# Kora fee payer private key (path to keypair file or base58 string)
KORA_PRIVATE_KEY=kora-fee-payer.json
# USDC mint address (generated by setup script)
USDC_MINT_ADDRESS=MINT_ADDRESS_FROM_SETUP
# RPC URLs
SOLANA_RPC_URL=http://localhost:8899
SOLANA_WS_URL=ws://localhost:8900
KORA_RPC_URL=http://localhost:8080
Create Setup Script
Create a file named setup.ts in your project root. This script automates the entire test environment setup by loading the four keypairs you created earlier, generating a new SPL token to represent USDC, and initializing all necessary Associated Token Accounts.
It mints 1,000 USDC to the user's account for testing, creates Kora's ATA for receiving fee payments, and sets up the recipient's ATA for receiving transfers. When complete, the script outputs the USDC mint address you'll need to add to your configuration files.
import { readFile } from 'fs/promises';
import {
createKeyPairSignerFromBytes,
generateKeyPairSigner,
createSolanaRpc,
createSolanaRpcSubscriptions,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory
} from '@solana/kit';
import {
getInitializeMint2Instruction,
getMintToInstruction,
TOKEN_PROGRAM_ADDRESS
} from '@solana-program/token';
import { getCreateAccountInstruction } from '@solana-program/system';
import { getCreateAssociatedTokenInstructionAsync } from '@solana-program/token';
const RPC = process.env.SOLANA_RPC_URL || 'http://localhost:8899';
const WS = process.env.SOLANA_WS_URL || 'ws://localhost:8900';
async function main() {
console.log('\n🚀 Setup\n');
const rpc = createSolanaRpc(RPC);
const rpcSubs = createSolanaRpcSubscriptions(WS);
const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions: rpcSubs });
// Load keypairs
console.log('1️⃣ Loading keypairs...');
const loadKey = async (f: string) => createKeyPairSignerFromBytes(
new Uint8Array(JSON.parse(await readFile(f, 'utf-8')))
);
const [kora, user, recipient, mintAuth] = await Promise.all([
loadKey('kora-fee-payer.json'),
loadKey('user-keypair.json'),
loadKey('recipient-keypair.json'),
loadKey('mint-authority-keypair.json')
]);
console.log(' ✅ Done');
// Create mint
console.log('\n2️⃣ Creating USDC mint...');
const mint = await generateKeyPairSigner();
const latestBlockhash = await rpc.getLatestBlockhash().send();
const rentExempt = await rpc.getMinimumBalanceForRentExemption(BigInt(82)).send();
const createMintTx = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(mintAuth, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash.value, tx),
tx => appendTransactionMessageInstruction(
getCreateAccountInstruction({
payer: mintAuth,
newAccount: mint,
lamports: rentExempt,
space: 82n,
programAddress: TOKEN_PROGRAM_ADDRESS
}),
tx
),
tx => appendTransactionMessageInstruction(
getInitializeMint2Instruction({
mint: mint.address,
decimals: 6,
mintAuthority: mintAuth.address
}),
tx
)
);
const signedMintTx = await signTransactionMessageWithSigners(createMintTx);
await sendAndConfirm(signedMintTx, { commitment: 'confirmed' });
console.log(` ✅ ${mint.address}`);
// Mint to user (auto-creates ATA)
console.log('\n3️⃣ Minting 1000 USDC to user...');
const createUserAtaIx = await getCreateAssociatedTokenInstructionAsync({
payer: user,
owner: user.address,
mint: mint.address
});
const userAta = createUserAtaIx.accounts[1].address;
const bh2 = await rpc.getLatestBlockhash().send();
const mintToUserTx = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(user, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(bh2.value, tx),
tx => appendTransactionMessageInstruction(createUserAtaIx, tx),
tx => appendTransactionMessageInstruction(
getMintToInstruction({
mint: mint.address,
token: userAta,
mintAuthority: mintAuth,
amount: 1000_000000n
}),
tx
)
);
const signedUserTx = await signTransactionMessageWithSigners(mintToUserTx);
await sendAndConfirm(signedUserTx, { commitment: 'confirmed' });
console.log(' ✅ Done');
// Create Kora ATA
console.log('\n4️⃣ Creating Kora ATA...');
const createKoraAtaIx = await getCreateAssociatedTokenInstructionAsync({
payer: kora,
owner: kora.address,
mint: mint.address
});
const bh3 = await rpc.getLatestBlockhash().send();
const koraTx = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(kora, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(bh3.value, tx),
tx => appendTransactionMessageInstruction(createKoraAtaIx, tx)
);
const signedKoraTx = await signTransactionMessageWithSigners(koraTx);
await sendAndConfirm(signedKoraTx, { commitment: 'confirmed' });
console.log(' ✅ Done');
// Create recipient ATA
console.log('\n5️⃣ Creating recipient ATA...');
const createRecipientAtaIx = await getCreateAssociatedTokenInstructionAsync({
payer: recipient,
owner: recipient.address,
mint: mint.address
});
const bh4 = await rpc.getLatestBlockhash().send();
const recipientTx = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(recipient, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(bh4.value, tx),
tx => appendTransactionMessageInstruction(createRecipientAtaIx, tx)
);
const signedRecipientTx = await signTransactionMessageWithSigners(recipientTx);
await sendAndConfirm(signedRecipientTx, { commitment: 'confirmed' });
console.log(' ✅ Done');
// Output
console.log('\n✅ Setup complete!\n');
console.log('📋 Next steps:\n');
console.log('1. Update .env with the new USDC mint address:\n');
console.log(` USDC_MINT_ADDRESS=${mint.address}\n`);
console.log('2. Update kora.toml:\n');
console.log(` allowed_tokens = ["${mint.address}"]`);
console.log(` allowed_spl_paid_tokens = ["${mint.address}"]\n`);
console.log('3. Start Kora: kora rpc start --signers-config signers.toml\n');
console.log('4. Run tests: npm run test\n');
}
main().catch(console.error);
Run the setup script:
npm run env-setup
After you have run setup successfully, you will see the following (your USDC Mint address will be different):
🚀 Setup
1️⃣ Loading keypairs...
✅ Done
2️⃣ Creating USDC mint...
✅ GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk
3️⃣ Minting 1000 USDC to user...
✅ Done
4️⃣ Creating Kora ATA...
✅ Done
5️⃣ Creating recipient ATA...
✅ Done
✅ Setup complete!
📋 Next steps:
1. Update .env with the new USDC mint address:
USDC_MINT_ADDRESS=GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk
2. Update kora.toml:
allowed_tokens = ["GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk"]
allowed_spl_paid_tokens = ["GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk"]
3. Start Kora: kora rpc start --signers-config signers.toml
4. Run tests: npm run test
The script will output the address for your USDC mint. You'll need it for the Kora configuration files in the next steps.
Also be sure to update .env with your USDC mint address.
Update Kora Configuration
Open kora.toml and update the token allowlists with your mint address:
allowed_tokens = ["MINT_ADDRESS_FROM_SETUP"]
allowed_spl_paid_tokens = ["MINT_ADDRESS_FROM_SETUP"]
Replace MINT_ADDRESS_FROM_SETUP with your mint address. This tells Kora to accept your test USDC as both a transferable token and a fee payment token.
Start the Kora Server
With configuration complete and payment accounts initialized, start the Kora RPC server in a new terminal:
kora rpc start --signers-config signers.toml
The server reads kora.toml from the current directory. Once it starts, you'll see output indicating it's listening on http://localhost:8080. The server is now ready to accept JSON-RPC requests, validate transactions, and sign approved transactions as a fee payer.
Leave this terminal running with the Kora server active while you create and run the tests.
Create the Test Suite
Now you'll create a comprehensive test suite that demonstrates all of Kora's functionality. Create a directory for tests:
mkdir test
touch test/gasless-transfer.test.ts
We'll build this file section by section to understand how each part works.
Imports and Configuration
This section sets up the test infrastructure by importing dependencies and defining configuration constants. The code imports testing utilities, transaction building functions, and the Kora client. It then defines constants for RPC URLs, token mint addresses, and program IDs that will be used throughout the tests.
import { describe, it, before } from 'node:test';
import assert from 'node:assert';
import { readFile } from 'fs/promises';
import {
createKeyPairSignerFromBytes,
createSolanaRpc,
getAddressEncoder,
getProgramDerivedAddress,
address,
getBase64EncodedWireTransaction,
partiallySignTransactionMessageWithSigners,
partiallySignTransaction,
createNoopSigner,
setTransactionMessageFeePayerSigner,
createTransactionMessage,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
pipe,
type Address,
type KeyPairSigner
} from '@solana/kit';
import { KoraClient } from '@solana/kora';
import { getBase58Decoder } from "@solana/codecs";
const USDC_MINT_ADDRESS = process.env.USDC_MINT_ADDRESS!;
const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || 'http://localhost:8899';
const KORA_RPC_URL = process.env.KORA_RPC_URL || 'http://localhost:8080';
const LAMPORTS_PER_SOL = 1_000_000_000;
const USDC_DECIMALS = 6;
const TOKEN_PROGRAM_ID = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
const ASSOCIATED_TOKEN_PROGRAM_ID = address('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
interface TestEnvironment {
rpc: any;
koraClient: KoraClient;
userKeypair: KeyPairSigner;
recipientKeypair: KeyPairSigner;
usdcMint: Address;
}
let env: TestEnvironment;
Helper Functions
This section defines utility functions that will be used throughout the test suite to get keypairs from files, compute Associated Token Account (ATA) addresses, and get SOL and token balances for a wallet.
// Loads a keypair from a JSON file
async function loadKeypair(filepath: string): Promise<KeyPairSigner> {
const bytes = JSON.parse(await readFile(filepath, 'utf-8'));
return await createKeyPairSignerFromBytes(new Uint8Array(bytes));
}
// Derives the ATA address for a given mint and owner
async function deriveAssociatedTokenAddress(mint: Address, owner: Address): Promise<Address> {
const addressEncoder = getAddressEncoder();
const [ata] = await getProgramDerivedAddress({
programAddress: ASSOCIATED_TOKEN_PROGRAM_ID,
seeds: [
addressEncoder.encode(owner),
addressEncoder.encode(TOKEN_PROGRAM_ID),
addressEncoder.encode(mint)
]
});
return ata;
}
// Queries both SOL and USDC balances for a wallet (for testing)
async function getBalances(rpc: any, walletAddress: Address, usdcMint: Address) {
const solBalanceResponse = await rpc.getBalance(walletAddress).send();
const solBalance = Number(solBalanceResponse.value) / LAMPORTS_PER_SOL;
const usdcATA = await deriveAssociatedTokenAddress(usdcMint, walletAddress);
try {
const accountInfo = await rpc.getAccountInfo(usdcATA, { encoding: 'jsonParsed' }).send();
let usdcBalance = 0;
if (accountInfo.value) {
const parsedData = accountInfo.value.data as any;
const amount = parsedData.parsed?.info?.tokenAmount?.amount;
usdcBalance = amount ? Number(amount) / (10 ** USDC_DECIMALS) : 0;
}
return { sol: solBalance, usdc: usdcBalance };
} catch {
return { sol: solBalance, usdc: 0 };
}
}
Gasless Transfer Function
This section implements the core gasless transfer function that demonstrates the complete Kora integration workflow where the user pays the transaction fee in USDC instead of SOL.
The gasless transaction begins by calling transferTransaction to construct the transfer instructions and set up the transaction structure with Kora's address as the fee payer. The response includes all the instructions needed for the transfer along with the blockhash and signer information.
Next, we call getPaymentInstruction to calculate how much USDC the user needs to pay to cover the transaction fee, using the configured price source.
We then combine the transfer instructions from transferTransaction with the payment instruction from getPaymentInstruction into a single transaction using a noop signer as a placeholder for Kora's signature, since Kora will add its actual signature in the next step.
The user signs the transaction with their keypair, authorizing both the token transfer to the recipient and the USDC payment to Kora. The transaction now has the user's signature but still needs Kora's signature as the fee payer before it can be submitted to Solana.
Finally, we send the partially-signed transaction to Kora by calling signAndSendTransaction. Kora validates the transaction against its configuration rules, adds its signature as the fee payer, and broadcasts the fully-signed transaction to Solana. The function extracts the transaction signature from the response, which can be used to track the transaction on Solana explorers or implement monitoring and retry logic.
async function executeGaslessTransfer({
rpc,
koraClient,
userKeyPair,
recipientAddress,
usdcMint,
amount
}: {
koraClient: KoraClient;
userKeyPair: KeyPairSigner;
recipientAddress: Address;
usdcMint: Address;
amount: number;
}) {
console.log(`📝 Constructing transfer: ${amount} USDC`);
// Step 1: Use Kora's transferTransaction to build the gasless transfer
// This method automatically handles:
// - Building the transfer instruction
// - Adding payment instruction for fee in USDC
// - Getting blockhash
// - Setting up the transaction structure with Kora as fee payer
console.log('🔧 Building transaction with Kora');
const transferResponse = await koraClient.transferTransaction({
amount: amount * Math.pow(10, USDC_DECIMALS),
token: usdcMint,
source: userKeyPair.address, // Source wallet (not token account)
destination: recipientAddress // Destination wallet (not token account)
});
// Step 2: Get payment instruction from Kora
// transferTransaction() doesn't include payment, so we must add it manually
console.log('💵 Getting payment instruction from Kora');
const paymentResponse = await koraClient.getPaymentInstruction({
transaction: transferResponse.transaction,
fee_token: usdcMint,
source_wallet: userKeyPair.address
});
const feeInSol = Number(paymentResponse.payment_amount) / Math.pow(10, USDC_DECIMALS) / 140;
const feeInUsdc = Number(paymentResponse.payment_amount) / Math.pow(10, USDC_DECIMALS);
console.log(` Fee: ${feeInSol.toFixed(9)} SOL (~${feeInUsdc.toFixed(4)} USDC)`);
// Step 3: Build complete transaction with transfer + payment instructions
console.log('✍️ Building transaction with user signature');
// Get fresh blockhash with lastValidBlockHeight
const latestBlockhash = await rpc.getLatestBlockhash().send();
// Add transfer instructions from Kora + payment instruction
const allInstructions = [
...transferResponse.instructions,
paymentResponse.payment_instruction
];
// Build complete transaction with noop signer as fee payer placeholder
// (Kora will be the actual fee payer)
const noopSigner = createNoopSigner(address(transferResponse.signer_pubkey));
const txMessage: any = allInstructions.reduce(
(tx: any, instruction: any) => appendTransactionMessageInstruction(instruction, tx),
pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(noopSigner, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash.value, tx)
)
);
// Partially sign with noop signer (placeholder)
const partiallySigned = await partiallySignTransactionMessageWithSigners(txMessage);
// User signs with their keypair (authorizes token transfer and payment)
const userSigned = await partiallySignTransaction([userKeyPair.keyPair], partiallySigned);
// Encode to base64 for sending to Kora
const signedTxBase64 = getBase64EncodedWireTransaction(userSigned);
// Step 4: Send to Kora for co-signing and broadcasting
// Kora will add its signature as fee payer AND broadcast in one step
console.log('📤 Sending to Kora for co-signing and broadcast');
const response = await koraClient.signAndSendTransaction({
transaction: signedTxBase64
});
// Extract the transaction signature from the response
// signAndSendTransaction returns the signed transaction in base64 format
// Solana transaction format: [num_signatures: 1 byte][signatures: 64 bytes each][message: rest]
const signedTxBytes = Buffer.from(response.signed_transaction, 'base64');
// First signature starts at byte 1 (after the count byte) and is 64 bytes
const signatureBytes = signedTxBytes.slice(1, 65);
// Convert signature bytes to base58 (Solana's standard signature format)
const signature = getBase58Decoder().decode(signatureBytes);
console.log(`✅ Transaction completed successfully\n`);
return {
signature,
fee: BigInt(paymentResponse.payment_amount),
feeInUsdc,
amount
};
}
Test Setup (Before Hook)
This section defines the test initialization that runs once before all tests execute to set up the shared test environment.
The setup first verifies the Solana test validator is running by calling getHealth and checks that the Kora server is running by calling getConfig, then loads the user and recipient keypairs and stores all initialized objects in the env variable that all tests can access.
// Test Setup - Runs once before all tests
before(async () => {
console.log('\n🚀 Loading test environment...\n');
const rpc = createSolanaRpc(SOLANA_RPC_URL);
// Check validator is running
try {
await rpc.getHealth().send();
console.log('✅ Connected to Solana test validator');
} catch (error) {
console.error('❌ Cannot connect to Solana validator at', SOLANA_RPC_URL);
console.error(' Start with: solana-test-validator');
throw error;
}
// Check Kora server is running
const koraClient = new KoraClient({ rpcUrl: KORA_RPC_URL });
try {
await koraClient.getConfig();
console.log('✅ Connected to Kora RPC server\n');
} catch (error) {
console.error('