本文介绍了 Kora,一个允许用户使用 SPL 代币支付 Solana 交易费用的工具,从而实现 gasless 交易。文章详细指导了如何安装、配置 Kora RPC 服务器,定义验证规则,构建 TypeScript 客户端,并使用 SPL 代币测试完整的费用支付流程。
Solana 交易手续费以 SOL 支付,这意味着用户钱包中需要有一些 SOL 才能完成交易。当用户持有 USDC 或其他 SPL token 但没有可用的 SOL 时,这可能会给应用程序带来摩擦。Kora 消除了这一障碍,允许以支持的 SPL token 支付交易和优先级费用(有时称为“gas”),或者完全赞助费用,以便用户无需支付任何费用。
在幕后,Kora 充当 费用支付者,在签名之前根据你的安全规则验证交易。你可以完全控制允许的程序、token 和支出限制。
本指南将引导你完成设置本地 Kora 服务器、配置验证规则以及构建 TypeScript 单元测试的过程,从而创建真正 gasless 的用户体验。
本指南假定你基本熟悉 Solana 交易、SPL token、关联 Token 帐户 (ATA) 和 TypeScript。
在你开始之前,这些指南可以提供快速回顾:
本指南将使用以下包和库:
| 依赖 | 版本 |
|---|---|
| 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 由 3 个主要组件组成:

这种 验证优先 的方法确保 Kora 仅对符合你的安全规则的交易进行签名。你可以准确指定可以调用的程序,设置每个交易和账户的支出限制,并定义哪些 token 可以接受付款。你甚至可以阻止特定帐户。
完整的交易流程如下所示:
这种架构创建了清晰的关注点分离:你的客户端管理用户交互和交易构建,Kora 处理验证和签名,Solana 在链上执行交易。结果是一个系统,它可以在不牺牲安全或对支出的控制的情况下实现 gasless 体验。
Kora 作为 Rust CLI 工具分发。使用 Cargo 全局安装它:
cargo install kora-cli
安装完成后,验证它是否可用:
kora --version
kora 二进制文件提供了用于运行 RPC 服务器、初始化支付帐户和管理配置的命令。
你将从头开始构建一个完整的 gasless 交易实现,创建所有必要的文件和配置。在本指南结束时,你将拥有一个工作示例,演示如何将 Kora 集成到你的应用程序中。
创建一个新的项目目录:
mkdir kora-gasless-demo
cd kora-gasless-demo
初始化一个 Node.js 项目并安装依赖项:
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
添加 package.json 类型和脚本:
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"
对于设置、配置和端到端测试,我们将创建以下文件:
在运行设置脚本之前,你需要创建 Kora 的配置文件。这些定义了 Kora 强制执行的安全规则以及它如何管理签名密钥。
在你的项目根目录中创建一个名为 kora.toml 的文件。此文件是 Kora 安全模型的中心。它精确地定义了 Kora 将允许签名的内容。
kora.toml
## Kora RPC Server Configuration
# Kora RPC 服务器配置
[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
# Token 程序\
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", # Associated Token Program
# 关联 Token 程序\
]
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
# 10% 的保证金
[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
验证规则 控制 Kora 将签名的交易:
max_allowed_lamports 设置每个交易的最大费用。这可以防止昂贵的交易或可能耗尽你的 费用支付者 帐户的错误。
allowed_programs 是交易可以调用的程序 ID 允许列表。Kora 拒绝任何尝试调用此列表中未包含的程序的交易。从系统计划、Token 计划和关联 Token 计划开始,然后根据需要添加你的应用程序的计划。
allowed_tokens 指定哪些 token 可以交易中转移。运行设置脚本后,你将在此处添加你的测试 USDC mint 地址。
allowed_spl_paid_tokens 是 allowed_tokens 的子集,Kora 接受其作为费用支付。通常包括稳定币或你的原生 token。你将在此处添加你的测试 USDC mint 地址。
price_source = "Mock" 使用在你的配置中定义的固定转换率。非常适合本地开发和测试。对于生产部署,你将使用 price_source = "Jupiter"。
margin = 0.1 在实际成本之上增加 10% 的缓冲区,以防止费用估算和交易执行之间的价格波动。
费用支付者策略 定义 Kora 允许跨系统程序、SPL Token 和 Token Extensions 指令执行哪些 Solana 操作。这些精细的控制使你可以允许特定的指令类型(转移、mint、burn 等),同时阻止其他指令类型。
在你的项目根目录中创建一个名为 signers.toml 的文件。此配置 Kora 如何管理其签名密钥:
signers.toml
## Kora Signers Configuration
# Kora 签名者配置
[signer_pool]
strategy = "round_robin"
[[signers]]
name = "fee_payer"
type = "memory"
private_key_env = "KORA_PRIVATE_KEY"
weight = 1
此配置使用基于内存的签名者,该签名者从 KORA_PRIVATE_KEY 环境变量加载密钥对。签名者池使用 round_robin 策略,该策略将交易均匀地分配给多个签名者(尽管在此示例中我们只有一个签名者)。
对于生产环境,你可以使用 Turnkey、Vault 或 Privy 等安全密钥管理服务替换 memory 签名者,以将私钥保存在硬件安全模块 (HSM) 中,并通过安全 API 提供签名。
在单独的终端中启动本地 Solana 测试验证器:
solana-test-validator
生成四个所需的密钥对:
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
从你的本地验证器使用 SOL 为这些帐户提供资金:
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
创建一个 .env 文件来存储你的环境变量:
.env
## Kora fee payer private key (path to keypair file or base58 string)
# Kora 费用支付者私钥(密钥对文件路径或 base58 字符串)
KORA_PRIVATE_KEY=kora-fee-payer.json
## USDC mint address (generated by setup script)
# USDC mint 地址(由设置脚本生成)
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
在你的项目根目录中创建一个名为 setup.ts 的文件。该脚本通过加载你之前创建的四个密钥对、生成一个新的 SPL token 来表示 USDC 以及初始化所有必要的关联 Token 帐户,从而自动完成整个测试环境设置。
它将 1,000 USDC mint 到用户帐户以进行测试,为 Kora 创建用于接收费用支付的 ATA,并为接收者设置用于接收转账的 ATA。完成后,该脚本会输出 USDC mint 地址,你需要将其添加到你的配置文件中。
setup.ts
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...');
// 1️⃣ 加载密钥对...
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
// 创建 mint
console.log('\n2️⃣ Creating USDC mint...');
// 2️⃣ 创建 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.address}
// Mint to user (auto-creates ATA)
// Mint给用户(自动创建 ATA)
console.log('\n3️⃣ Minting 1000 USDC to user...');
// 3️⃣ Minting 1000 USDC 给用户...
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
// 创建 Kora ATA
console.log('\n4️⃣ Creating Kora ATA...');
// 4️⃣ 创建 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
// 创建接收者 ATA
console.log('\n5️⃣ Creating recipient ATA...');
// 5️⃣ 创建接收者 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');
// 1. 使用新的 USDC mint 地址更新 .env:
console.log(` USDC_MINT_ADDRESS=${mint.address}\n`);
// USDC_MINT_ADDRESS=${mint.address}
console.log('2. Update kora.toml:\n');
// 2. 更新 kora.toml:
console.log(` allowed_tokens = ["${mint.address}"]`);
// allowed_tokens = ["${mint.address}"]
console.log(` allowed_spl_paid_tokens = ["${mint.address}"]\n`);
// allowed_spl_paid_tokens = ["${mint.address}"]
console.log('3. Start Kora: kora rpc start --signers-config signers.toml\n');
// 3. 启动 Kora:kora rpc start --signers-config signers.toml
console.log('4. Run tests: npm run test\n');
// 4. 运行 tests:npm run test
}
main().catch(console.error);
运行设置脚本:
npm run env-setup
成功运行设置后,你将看到以下内容(你的 USDC Mint 地址将有所不同):
🚀 Setup
# 🚀 设置
1️⃣ Loading keypairs...
# 1️⃣ 加载密钥对...
✅ Done
# ✅ 完成
2️⃣ Creating USDC mint...
# 2️⃣ 创建 USDC mint...
✅ GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk
3️⃣ Minting 1000 USDC to user...
# 3️⃣ 将 1000 USDC Mint 给用户...
✅ Done
# ✅ 完成
4️⃣ Creating Kora ATA...
# 4️⃣ 创建 Kora ATA...
✅ Done
# ✅ 完成
5️⃣ Creating recipient ATA...
# 5️⃣ 创建接收者 ATA...
✅ Done
# ✅ 完成
✅ Setup complete!
# ✅ 设置完成!
📋 Next steps:
# 📋 下一步:
1. Update .env with the new USDC mint address:
# 1. 使用新的 USDC mint 地址更新 .env:
USDC_MINT_ADDRESS=GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk
2. Update kora.toml:
# 2. 更新 kora.toml:
allowed_tokens = ["GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk"]
allowed_spl_paid_tokens = ["GK2oJXymkRtjp417b4MTMqouxxApBXbZn6Jmw2rZWxDk"]
3. Start Kora: kora rpc start --signers-config signers.toml
# 3. 启动 Kora:kora rpc start --signers-config signers.toml
4. Run tests: npm run test
# 4. 运行 tests:npm run test
该脚本将输出你的 USDC mint 的地址。你将在后续步骤中需要它来配置 Kora 文件。
另请务必使用你的 USDC mint 地址更新 .env。
打开 kora.toml 并使用你的 mint 地址更新 token 允许列表:
kora.toml
allowed_tokens = ["MINT_ADDRESS_FROM_SETUP"]
allowed_spl_paid_tokens = ["MINT_ADDRESS_FROM_SETUP"]
将 MINT_ADDRESS_FROM_SETUP 替换为你的 mint 地址。这告诉 Kora 接受你的测试 USDC 作为可转移 token 和费用支付 token。
配置完成后,初始化结算帐户,在新的终端中启动 Kora RPC 服务器:
kora rpc start --signers-config signers.toml
服务器从当前目录读取 kora.toml。启动后,你将看到指示它正在监听 http://localhost:8080 的输出。服务器现在可以接受 JSON-RPC 请求、验证交易,并对批准的交易进行签名作为 费用支付者。
保持此终端运行并激活 Kora 服务器,同时创建和运行测试。
现在,你将创建一个全面的测试套件,展示 Kora 的所有功能。创建一个用于测试的目录:
mkdir test
touch test/gasless-transfer.test.ts
我们将逐节构建此文件,以了解每个部分的工作原理。
本节通过导入依赖项和定义配置常量来设置测试基础架构。该代码导入测试实用程序、交易构建函数和 Kora 客户端。然后,它定义了 RPC URL、token mint 地址和将在整个测试中使用的程序 ID 的常量。
test/gasless-transfer.test.ts
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;
本节定义了实用程序函数,这些函数将在整个测试套件中使用,以从文件中获取密钥对、计算关联的 Token 帐户 (ATA) 地址以及获取钱包的 SOL 和 token 余额。
test/gasless-transfer.test.ts
// Loads a keypair from a JSON file
// 从 JSON 文件加载密钥对
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
// 获取给定 mint 和所有者的 ATA 地址
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)
// 查询钱包的 SOL 和 USDC 余额(用于测试)
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 转账函数,该函数演示了完整的 Kora 集成工作流程,用户在其中以 USDC 而不是 SOL 支付交易手续费。
该 gasless 交易首先调用 transferTransaction 来构建转账指令,并设置以 Kora 地址作为 费用支付者 的交易结构。响应包括转账所需的所有指令以及 区块哈希 和签名者信息。
接下来,我们调用 getPaymentInstruction,使用配置的价格来源计算用户需要支付多少 USDC 才能支付交易手续费。
然后,我们将来自 transferTransaction 的转账指令与来自 getPaymentInstruction 的付款指令组合到单个交易中,并使用 noop 签名者作为 Kora 签名的占位符,因为 Kora 将在下一步中添加其实际签名。
用户使用他们的密钥对签署交易,授权将 token 转账给接收者以及将 USDC 付款给 Kora。交易现在具有用户的签名,但仍需要 Kora 的签名作为 费用支付者 才能提交给 Solana。
最后,我们通过调用 signAndSendTransaction 将部分签名的交易发送给 Kora。Kora 根据其配置规则验证交易,添加其作为 费用支付者 的签名,并将完全签名的交易广播给 Solana。该函数从响应中提取交易签名,该签名可用于在 Solana 资源管理器上跟踪交易或实施监视和重试逻辑。
test/gasless-transfer.test.ts
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`);
// 📝 构造转账:${amount} USDC
// Step 1: Use Kora's transferTransaction to build the gasless transfer
// 步骤 1:使用 Kora 的 transferTransaction 构建 gasless 转账
// This method automatically handles:
// 此方法自动处理:
// - Building the transfer instruction
// - 构建转账指令
// - Adding payment instruction for fee in USDC
// - 以 USDC 为单位添加费用支付指令
// - Getting blockhash
// - 获取区块哈希
// - Setting up the transaction structure with Kora as fee payer
// - 设置以 Kora 作为费用支付者的交易结构
console.log('🔧 Building transaction with Kora');
// 🔧 使用 Kora 构建交易
const transferResponse = await koraClient.transferTransaction({
amount: amount * Math.pow(10, USDC_DECIMALS),
token: usdcMint,
source: userKeyPair.address, // Source wallet (not token account)
// 源钱包(不是 token 帐户)
destination: recipientAddress // Destination wallet (not token account)
// 目标钱包(不是 token 帐户)
});
// Step 2: Get payment instruction from Kora
// 步骤 2:从 Kora 获取付款指令
// transferTransaction() doesn't include payment, so we must add it manually
// transferTransaction() 不包括付款,因此我们必须手动添加它
console.log('💵 Getting payment instruction from Kora');
// 💵 从 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)`);
// 费用:${feeInSol.toFixed(9)} SOL (~${feeInUsdc.toFixed(4)} USDC)
// Step 3: Build complete transaction with transfer + payment instructions
// 步骤 3:使用转账 + 付款指令构建完整交易
console.log('✍️ Building transaction with user signature');
// ✍️ 使用用户签名构建交易
// Get fresh blockhash with lastValidBlockHeight
// 使用 lastValidBlockHeight 获取新的区块哈希
const latestBlockhash = await rpc.getLatestBlockhash().send();
// Add transfer instructions from Kora + payment instruction
// 从 Kora 添加转账```
// 从响应中提取交易签名
// signAndSendTransaction 以 base64 格式返回已签名的交易
// Solana 交易格式:[num_signatures: 1 字节][signatures: 每个 64 字节][message: 剩余部分]
const signedTxBytes = Buffer.from(response.signed_transaction, 'base64');
// 第一个签名从字节 1(在计数字节之后)开始,为 64 个字节
const signatureBytes = signedTxBytes.slice(1, 65);
// 将签名字节转换为 base58 (Solana 的标准签名格式)
const signature = getBase58Decoder().decode(signatureBytes);
console.log(`✅ 交易成功完成\n`);
return {
signature,
fee: BigInt(paymentResponse.payment_amount),
feeInUsdc,
amount
};
}
本节定义了测试初始化,该初始化在所有测试执行之抢跑一次,以设置共享测试环境。
该设置首先通过调用 getHealth 来验证 Solana 测试验证器是否正在运行,并通过调用 getConfig 来检查 Kora 服务器是否正在运行,然后加载用户和接收者的密钥对,并将所有初始化的对象存储在所有测试都可以访问的 env 变量中。
test/gasless-transfer.test.ts
// 测试设置 - 在所有测试之抢跑一次
before(async () => {
console.log('\n🚀 正在加载测试环境...\n');
const rpc = createSolanaRpc(SOLANA_RPC_URL);
// 检查验证器是否正在运行
try {
await rpc.getHealth().send();
console.log('✅ 已连接到 Solana 测试验证器');
} catch (error) {
console.error('❌ 无法连接到', SOLANA_RPC_URL, '的 Solana 验证器');
console.error(' 使用以下命令启动:solana-test-validator');
throw error;
}
// 检查 Kora 服务器是否正在运行
const koraClient = new KoraClient({ rpcUrl: KORA_RPC_URL });
try {
await koraClient.getConfig();
console.log('✅ 已连接到 Kora RPC 服务器\n');
} catch (error) {
console.error('❌ 无法连接到', KORA_RPC_URL, '的 Kora 服务器');
console.error(' 使用以下命令启动:kora rpc start --signers-config signers.toml');
throw error;
}
// 从文件加载密钥对(由 setup 创建)
console.log('📝 正在加载密钥对...');
const userKeypair = await loadKeypair('user-keypair.json');
const recipientKeypair = await loadKeypair('recipient-keypair.json');
console.log(` 用户:${userKeypair.address}`);
console.log(` 接收者:${recipientKeypair.address}\n`);
// 使用来自 setup 的 mint
const usdcMint = address(USDC_MINT_ADDRESS);
console.log(`💵 正在使用 USDC mint:${usdcMint}\n`);
console.log('✅ 测试环境准备就绪!\n');
env = {
rpc,
koraClient,
userKeypair,
recipientKeypair,
usdcMint
};
});
本节验证 Kora 服务器配置并演示配置 API 方法。
test/gasless-transfer.test.ts
// 配置测试 - 验证 Kora 服务器设置
describe('Kora 配置测试', () => {
it('应该连接到 Kora 服务器', async () => {
const config = await env.koraClient.getConfig();
assert.ok(config, '应该从 Kora 接收配置');
assert.ok(config.fee_payers, '配置应包括 fee payers 数组');
assert.ok(config.fee_payers.length > 0, '应该至少有一个 fee payer');
});
it('应该返回有效的 fee payer 地址', async () => {
const config = await env.koraClient.getConfig();
assert.doesNotThrow(
() => address(config.fee_payers[0]),
'Fee payer 应该是有效的 Solana 地址'
);
});
it('应该在允许的 tokens 中包括 USDC', async () => {
const config = await env.koraClient.getConfig();
assert.ok(
config.validation_config.allowed_tokens.includes(env.usdcMint),
'USDC 应该在允许的 tokens 列表中'
);
assert.ok(
config.validation_config.allowed_spl_paid_tokens.includes(env.usdcMint),
'USDC 应该在允许的 payment tokens 列表中'
);
});
it('应该从 Kora 获取 blockhash', async () => {
const { blockhash } = await env.koraClient.getBlockhash();
assert.ok(blockhash, '应该从 Kora 接收 blockhash');
assert.strictEqual(typeof blockhash, 'string', 'Blockhash 应该是字符串');
assert.ok(blockhash.length > 0, 'Blockhash 不应为空');
});
it('应该获取支持的 tokens', async () => {
const response = await env.koraClient.getSupportedTokens();
assert.ok(response.tokens, '应该返回 tokens 对象');
assert.ok(Array.isArray(response.tokens), 'Tokens 应该是数组');
assert.ok(response.tokens.length > 0, '应该至少有一个支持的 token');
assert.ok(
response.tokens.includes(env.usdcMint),
'USDC 应该在支持的 tokens 列表中'
);
});
it('应该获取 payer signer 信息', async () => {
const payerInfo = await env.koraClient.getPayerSigner();
assert.ok(payerInfo.signer_address, '应该返回 signer 地址');
assert.ok(payerInfo.payment_address, '应该返回 payment 地址');
assert.doesNotThrow(
() => address(payerInfo.signer_address),
'Signer 地址应该是有效的 Solana 地址'
);
});
});
本节演示如何估算交易费用并将其转换为 token 金额,这对于在执行交易之前向用户展示他们将支付多少费用至关重要。
这些测试首先调用 transferTransaction 来构建一个示例 token 转账交易,然后使用该交易和所需的 payment token 调用 estimateTransactionFee。该方法同时返回 fee_in_lamports 和 fee_in_token,因此客户端可以用他们喜欢的面额向用户显示成本。
test/gasless-transfer.test.ts
// 费用估算测试 - 计算交易成本
describe('费用估算测试', () => {
it('应该以 lamports 估算费用', async () => {
// 使用 Kora 的 transferTransaction 构建一个测试交易
const transferResponse = await env.koraClient.transferTransaction({
amount: 1000, // 0.001 USDC
token: env.usdcMint,
source: env.userKeypair.address,
destination: env.recipientKeypair.address
});
// 使用 Kora 的 estimateTransactionFee 方法估算费用
const { fee_in_lamports } = await env.koraClient.estimateTransactionFee({
transaction: transferResponse.transaction,
fee_token: env.usdcMint
});
assert.ok(fee_in_lamports > 0n, '费用应大于 0');
assert.ok(fee_in_lamports < 10_000_000n, '费用应小于 0.01 SOL');
});
it('应该将费用转换为 USDC 等价物', async () => {
// 使用 Kora 的 transferTransaction 构建一个测试交易
const transferResponse = await env.koraClient.transferTransaction({
amount: 1000, // 0.001 USDC
token: env.usdcMint,
source: env.userKeypair.address,
destination: env.recipientKeypair.address
});
// 使用 Kora 的 estimateTransactionFee 方法估算费用
const { fee_in_lamports, fee_in_token } = await env.koraClient.estimateTransactionFee({
transaction: transferResponse.transaction,
fee_token: env.usdcMint
});
const feeInSol = Number(fee_in_lamports) / LAMPORTS_PER_SOL;
const feeInUsdc = Number(fee_in_token) / Math.pow(10, USDC_DECIMALS);
assert.ok(feeInSol > 0, 'SOL 中的费用应大于 0');
assert.ok(feeInUsdc > 0, 'USDC 中的费用应大于 0');
// 为 Mock price source(本地测试)放宽 - 使用 Jupiter 的生产环境将具有真实的费用
assert.ok(feeInUsdc < 10, 'USDC 中的费用应该是合理的(Mock 模式下 < $10)');
});
});
本节包含端到端集成测试,该测试执行完整的 gasless 交易并验证其是否正常工作。
测试首先记录交易前用户的 SOL 和 USDC 余额,以建立验证的基线。然后,它调用 executeGaslessTransfer 来构建、签名和广播完整的交易。
交易完成后,测试再次查询余额并验证两个关键结果:
test/gasless-transfer.test.ts
// Gasless 转账测试 - 端到端验证
describe('Gasless 转账测试', () => {
it('应该执行 gasless USDC 转账,费用以 USDC 支付,并且 SOL 余额保持不变', async () => {
const beforeBalances = await getBalances(env.rpc, env.userKeypair.address, env.usdcMint);
console.log('\n💰 初始余额:');
console.log(` 用户 USDC:${beforeBalances.usdc.toFixed(4)} USDC`);
console.log(` 用户 SOL:${beforeBalances.sol.toFixed(2)} SOL\n`);
const transferAmount = 10;
const result = await executeGaslessTransfer({
rpc: env.rpc,
koraClient: env.koraClient,
userKeyPair: env.userKeypair,
recipientAddress: env.recipientKeypair.address,
usdcMint: env.usdcMint,
amount: transferAmount
});
console.log(`\n✅ 交易:${result.signature.slice(0, 8)}...${result.signature.slice(-4)}\n`);
const afterBalances = await getBalances(env.rpc, env.userKeypair.address, env.usdcMint);
console.log('📊 最终余额:');
console.log(` 用户 USDC:${afterBalances.usdc.toFixed(4)} USDC`);
console.log(` 用户 SOL:${afterBalances.sol.toFixed(2)} SOL(不变!)\n`);
// 验证交易成功
assert.ok(result.signature, '交易应具有签名');
assert.ok(result.signature.length > 0, '签名不应为空');
// 验证 USDC 减少了转账金额 + 费用
const expectedDecrease = transferAmount + result.feeInUsdc;
const actualDecrease = beforeBalances.usdc - afterBalances.usdc;
assert.ok(
Math.abs(actualDecrease - expectedDecrease) < 0.02,
`USDC 应减少转账 + 费用(预期:${expectedDecrease.toFixed(4)},实际:${actualDecrease.toFixed(4)})`
);
// 验证 SOL 余额不变 (gasless!)
assert.strictEqual(
afterBalances.sol,
beforeBalances.sol,
'用户 SOL 余额应保持不变 - gasless!'
);
console.log(`✅ 已确认:USDC 减少了 ${actualDecrease.toFixed(4)}(${transferAmount} 转账 + ${result.feeInUsdc.toFixed(4)} 费用)`);
console.log(`✅ 已确认:SOL 余额不变 (gasless 验证!)\n`);
});
});
创建所有文件后,运行测试套件:
npm run test
你应该看到如下输出:
✅ 测试环境准备就绪!
▶ Kora 配置测试
✔ 应该连接到 Kora 服务器
✔ 应该返回有效的 fee payer 地址
✔ 应该在允许的 tokens 中包括 USDC
✔ 应该从 Kora 获取 blockhash
✔ 应该获取支持的 tokens
✔ 应该获取 payer signer 信息
▶ 费用估算测试
✔ 应该以 lamports 估算费用
✔ 应该将费用转换为 USDC 等价物
💰 初始余额:
用户 USDC:1000.00 USDC
用户 SOL:0.99 SOL
📝 正在构造转账:10 USDC
🔧 正在使用 Kora 构建交易
💵 正在从 Kora 获取 payment 指令
费用:0.0111 USDC
✍️ 正在使用用户签名构建交易
📤 正在发送到 Kora 以进行共同签名和广播
✅ 交易成功完成
✅ 交易:5XqK8G3n...Mv2d
📊 最终余额:
用户 USDC:989.99 USDC
用户 SOL:0.99 SOL(不变!)
✅ 已确认:USDC 减少了 10.0111(10 转账 + 0.0111 费用)
✅ 已确认:SOL 余额不变 (gasless 验证!)
▶ Gasless 转账测试
✔ 应该执行 gasless USDC 转账,费用以 USDC 支付,并且 SOL 余额保持不变
ℹ tests 9
ℹ pass 9
如果所有测试都通过,恭喜!你已成功配置 Kora RPC 服务器并执行了 gasless 交易,其中用户以 USDC 而不是 SOL 支付费用。
虽然 gasless 交易允许用户使用 SOL 以外的 tokens 支付,但你可以通过完全赞助交易费用来更进一步。你的应用程序涵盖所有交易成本,而不是用户以 USDC 或其他 tokens 支付费用,从而为最终用户创造完全免费的体验。
Kora 使 sponsored 交易易于实施。你只需要在 kora.toml 中进行两个配置更改:
空的 SPL Payment Tokens:
allowed_spl_paid_tokens = []
Free 定价:
[validation.price]
type = "free"
## margin = 0.1
此测试演示了完全 sponsored 的交易,其中你的应用程序涵盖所有交易成本,用户无需支付任何费用。与 gasless 转账测试的主要区别是没有 getPaymentInstruction 调用。
该测试验证两个关键结果:
test/gasless-transfer.test.ts
describe('Sponsored 转账', () => {
it('应该转账 USDC 而不收取任何费用', async () => {
const beforeBalances = await getBalances(env.rpc, env.userKeypair.address, env.usdcMint);
const transferAmount = 10;
console.log(`\n💰 初始余额:`);
console.log(` 用户 USDC:${beforeBalances.usdc.toFixed(4)} USDC`);
console.log(` 用户 SOL:${beforeBalances.sol.toFixed(2)} SOL\n`);
// 构建转账交易
console.log(`📝 正在构建 sponsored 转账:${transferAmount} USDC`);
const transferResponse = await env.koraClient.transferTransaction({
amount: transferAmount * Math.pow(10, USDC_DECIMALS),
token: env.usdcMint,
source: env.userKeypair.address,
destination: env.recipientKeypair.address
});
// 关键:在 sponsored 模式下,跳过 getPaymentInstruction() - 不收取费用
const allInstructions = transferResponse.instructions;
// 从 RPC 获取具有 lastValidBlockHeight 的新 blockhash
// Kora 的 transferResponse 只有 blockhash 字符串,没有 lastValidBlockHeight
const latestBlockhash = await env.rpc.getLatestBlockhash().send();
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)
)
);
const partiallySigned = await partiallySignTransactionMessageWithSigners(txMessage);
const userSigned = await partiallySignTransaction([env.userKeypair.keyPair], partiallySigned);
const signedTxBase64 = getBase64EncodedWireTransaction(userSigned);
// 发送到 Kora 以进行共同签名和广播
console.log(`📤 正在发送到 Kora 以进行签名和广播`);
await env.koraClient.signAndSendTransaction({ transaction: signedTxBase64 });
console.log(`✅ 交易完成\n`);
const afterBalances = await getBalances(env.rpc, env.userKeypair.address, env.usdcMint);
const usdcDecrease = beforeBalances.usdc - afterBalances.usdc;
console.log(`📊 最终余额:`);
console.log(` 用户 USDC:${afterBalances.usdc.toFixed(4)} USDC(${usdcDecrease.toFixed(4)} 减少)`);
console.log(` 用户 SOL:${afterBalances.sol.toFixed(2)} SOL(不变)\n`);
// 验证:用户只支付了转账金额(USDC 中没有费用)
assert.strictEqual(usdcDecrease, transferAmount, 'USDC 应仅减少精确的转账金额');
// 验证:SOL 余额不变(SOL 中没有费用)
assert.strictEqual(afterBalances.sol, beforeBalances.sol, 'SOL 余额应保持不变');
});
});
更新 kora.toml 以启用 sponsored 模式后,重新启动 Kora 服务器以应用配置更改:
kora rpc start --signers-config signers.toml
由于该配置现在使用 sponsored 模式而不是用户支付模式,因此一些原始测试将失败,因为它们期望以 USDC 收取费用。为了使测试保持简单并专注于 sponsored 交易行为,我们将跳过这些不兼容的测试。
在 test/gasless-transfer.test.ts 中,添加 describe.skip() 以禁用这些测试套件:
// 跳过整个配置测试套件,因为它们与 sponsored 模式无关。
describe.skip('Kora 配置测试', () => {
// 所有测试都检查用户支付配置
});
// 跳过整个费用估算测试套件,因为它们与 sponsored 模式无关。
describe.skip('费用估算测试', () => {
// 这些尝试使用 fee_token 参数估算费用
// 失败,因为 USDC 不在 allowed_spl_paid_tokens 中
});
// 跳过整个 Gasless 转账测试套件,因为它们与 sponsored 模式无关。
describe.skip('Gasless 转账测试', () => {
// 测试用户付费模式,其中费用以 USDC 收取
});
更新 kora.toml 以启用 sponsored 模式后,重新启动 Kora 服务器以应用配置更改:
kora rpc start --signers-config signers.toml
然后运行 sponsored 交易测试,以验证交易是否成功完成,所有费用都由你的应用程序承担:
npm run test
测试成功运行时,你将看到以下输出:
初始余额:
用户 USDC:915.0000 USDC
用户 SOL:5.00 SOL
📝 正在构建 sponsored 转账:10 USDC
📤 正在发送到 Kora 以进行签名和广播
✅ 交易完成
📊 最终余额:
用户 USDC:905.0000 USDC(10.0000 减少)
用户 SOL:5.00 SOL(不变)
warning
截至 2026 年 1 月,Kora 使用未经审计的 solana-keychain 软件包。 虽然 Kora v2.0.3 已通过提交 8c592591 经过 Runtime Verification 的审计,但请对生产主网部署进行额外的安全审查。
部署到生产环境时,请从基于文件的密钥对转换为安全密钥管理服务:
有关每个提供商的完整配置示例和所需凭据,请参阅官方 Kora signers 文档。
仔细检查你的程序允许列表,仅包含你的应用程序合法需要调用的程序。如果你不确定某个程序是否必要,请监视你应用程序的交易,并且只有在看到合法操作的验证失败时才添加程序。
Kora 为生产部署提供了多项高级功能:
要运行 Kora 节点,你需要一个 fee payer signer,并且必须保持其资金充足,以便赞助你批准的交易。 这使你可以控制接受哪些 tokens、启用完全赞助的交易,并让你设置自己的策略,例如速率限制、验证规则和身份验证。
从总体上看,部署是使用你的 kora.toml 和 signers.toml 安装 Kora CLI,将节点指向 Solana RPC 端点,然后启动 Kora 服务器。 你可以在本地运行以进行测试、通过 Docker 部署(可以选择使用 Redis 缓存),或使用托管流程,例如 Railway。
如果我的 fee payer 帐户中的 SOL 用完了会发生什么?
当你的 fee payer 余额过低时,Kora 将无法签署新交易,因为 Solana 要求 fee payers 拥有足够的余额来支付交易成本。 使用 Kora 的指标监控你的 fee payer 余额,并在余额低于阈值时设置警报。
如果用户愿意,他们仍然可以用 SOL 支付费用吗?
是的。 Kora 对你的用户来说是一项可选服务。 你可以构建你的客户端以检测用户是否拥有足够的 SOL,如果是,则允许他们以传统方式支付费用,而无需 Kora 参与。 你的客户端逻辑根据用户的 token 持有量和偏好来确定要使用的流程。
如何处理 token 价格波动?
在你的定价设置中配置一个保证金,以在实际 SOL 成本之上添加一个缓冲区。 例如,10% 的保证金意味着用户支付的 token 价值比 SOL 成本高 10%,从而保护你免受你报价费用和交易执行之间的价格波动的影响。 对于生产部署,请使用 Jupiter 的价格预言机 (price_source = "Jupiter") 来获取实时市场价格,而不是模拟定价。 你还可以限制你接受的 tokens,仅限于流动性更深、波动性更小的稳定币或蓝筹 tokens。
Kora 适合高频应用程序吗?
Kora 为交易签名增加了最小的延迟,即一次 RPC 往返。 对于大多数应用程序而言,与网络延迟和 Solana 的区块时间相比,这种开销可以忽略不计。
我可以使用具有不同安全级别的多个签名者吗?
是的。 在 signers.toml 中配置多个签名者并定义选择策略。 例如,你可以将高价值交易路由到由 Turnkey 支持的更安全但速度较慢的签名者,或者实施轮询选择以在多个 fee payer 帐户之间分配支出,以避免每个帐户的速率限制。
你已成功从头开始构建了完整的 gasless 交易系统! 更重要的是,你了解 Kora 的总体架构:它如何在签名之前验证交易、它如何位于你的客户端和 Solana 网络之间,以及它如何实现传统的费用支付方式无法实现的业务模型。
尝试为新用户的前五个交互提供 sponsored 交易。 测试接受你的原生 token 作为支付方式,从而创建直接的 token 效用。 实施分层费用结构,其中不同的用户群体支付不同的金额。 Kora 让你可以灵活地迭代费用模型,而无需更改你的核心应用程序逻辑。 你只需要更新 Kora 的配置即可。
如果你有任何反馈或新主题请求,请告诉我们。 我们很乐意听取你的意见。
>- 原文链接: [quicknode.com/guides/sol...](https://www.quicknode.com/guides/solana-development/transactions/kora)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~ 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!