每笔 Solana 交易都需要使用 SOL 支付网络手续费。但用户在使用你的支付应用时,通常希望用稳定币进行交易,而不想管理第二种代币余额。手续费抽象通过让其他人代为支付手续费,消除了这一障碍。
本指南涵盖两个层面:
- 手续费赞助的工作原理 — Solana 的底层机制
- 使用 Kora 实现大规模手续费抽象 — 可用于生产环境的手续费抽象服务
手续费赞助的工作原理
Solana 交易有一个指定的手续费支付者——即负责支付网络手续费的账户。默认情况下,这个账户是第一个签名者。但你也可以指定其他账户作为手续费支付者,从而让第三方(即“赞助者”)代表发送方支付手续费。
发送方和赞助者都必须对交易进行签名:
- 发送方 签名以授权其代币的转账
- 赞助者 签名以授权支付网络手续费
参见 Solana 支付原理 了解核心支付概念。
以下步骤展示了核心流程。完整可运行代码请参见 演示。
创建赞助者账户
为将要支付交易手续费的赞助者生成一个独立的 keypair。赞助者需要持有用于支付手续费的 SOL,但无需持有被转账的代币。
const sponsor = await generateKeyPairSigner();
创建转账指令
创建以发送方为授权人的代币转账指令。发送方拥有代币,必须对转账进行签名。
const sponsor = await generateKeyPairSigner();const transferInstruction = getTransferCheckedInstruction({source: senderAta,mint: mint.address,destination: recipientAta,authority: sender, // Sender signs for the transferamount: 250_000n, // adjusted for the mint's decimalsdecimals: 6});
由赞助方作为手续费支付者进行发送
通过 payer(sponsor) 将赞助商设置为手续费支付方来创建客户端,然后调用
client.sendTransaction。赞助商签名以支付网络费用,而发送方签名以授权转账——sendTransaction
会自动收集所有被引用的签名者。
const sponsor = await generateKeyPairSigner();const transferInstruction = getTransferCheckedInstruction({source: senderAta,mint: mint.address,destination: recipientAta,authority: sender,amount: 250_000n,decimals: 6});const client = createClient().use(payer(sponsor)) // Sponsor pays the transaction fees.use(solanaLocalRpc());const { context } = await client.sendTransaction([transferInstruction]);
演示
// Generate keypairs for sender, recipient, and sponsor (fee payer)const sender = await generateKeyPairSigner();const recipient = await generateKeyPairSigner();const sponsor = await generateKeyPairSigner();console.log("Sender Address:", sender.address);console.log("Recipient Address:", recipient.address);console.log("Sponsor Address (Fee Payer):", sponsor.address);// Demo Setup: Create client, mint account, token accounts, and fund with initial tokensconst { client, mint } = await demoSetup(sender, recipient, sponsor);console.log("\nMint Address:", mint.address);// Derive the Associated Token Accounts addresses (ATAs) for sender and recipientconst [senderAta] = await findAssociatedTokenPda({mint: mint.address,owner: sender.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});const [recipientAta] = await findAssociatedTokenPda({mint: mint.address,owner: recipient.address,tokenProgram: TOKEN_2022_PROGRAM_ADDRESS});console.log("Sender Token Account:", senderAta.toString());console.log("Recipient Token Account:", recipientAta.toString());// =============================================================================// Sponsored Token Payment Demo// =============================================================================// Create instruction to transfer tokens from sender to recipient// Transferring 250,000 base units = 0.25 tokens (with 6 decimals)const transferInstruction = getTransferCheckedInstruction({source: senderAta,mint: mint.address,destination: recipientAta,authority: sender, // Sender signs to authorize the transferamount: 250_000n, // 0.25 tokensdecimals: 6});// Send transaction with sponsor as fee payer using @solana/kit// The sponsor (client.payer) pays fees; the sender signs for the transferconst { context } = await client.sendTransaction([transferInstruction]);const signature = context.signature;console.log("\n=== Sponsored Token Payment Complete ===");console.log("Transaction Signature:", signature.toString());// Fetch final token account balances from the RPCconst senderBalance = await client.rpc.getTokenAccountBalance(senderAta).send();const recipientBalance = await client.rpc.getTokenAccountBalance(recipientAta).send();console.log("\nSender Token Account Balance:", senderBalance.value.uiAmount);console.log("Recipient Token Account Balance:",recipientBalance.value.uiAmount);// Fetch transaction detailsconst transaction = await client.rpc.getTransaction(signature, {encoding: "jsonParsed",maxSupportedTransactionVersion: 0}).send();const feePayer = transaction?.transaction.message.accountKeys?.[0];console.log("\nNote: The first account in accountKeys is always the fee payer");console.log("Fee Payer Address:", feePayer);// =============================================================================// Demo Setup Helper Function// =============================================================================
当你为终端用户创建 token account 时,他们可以关闭该账户并取回用于 rent 的 SOL。建议向用户收取账户创建费用(如以稳定币结算),或将此成本计入你的产品经济模型中。
使用 Kora 实现大规模手续费抽象
fee payer 原语非常强大,但要构建生产级的免 gas 系统,还需要更多功能:管理赞助钱包、处理 token 兑换(让用户可以用 USDC "支付" 手续费)、限流和安全控制。
Kora 处理这种复杂性。它是一个 JSON-RPC 服务器,提供费用抽象功能,使用户永远不需要 SOL。您可以完全赞助费用或接受以任何代币支付费用。
只需一条命令即可部署 Kora:
cargo install kora-clikora --config path/to/kora.toml rpc start --signers-config path/to/signers.toml
然后将 Kit 客户端指向您的 Kora 服务器,并使用与上述相同的 API 发送交易:
pnpm add @solana/kora
import { address } from "@solana/kit";import { createKitKoraClient } from "@solana/kora";// A Kora-backed Kit client: Kora is the fee payer, and users pay// fees in the SPL token of your choice instead of SOLconst kora = await createKitKoraClient({endpoint: "https://your-kora-instance",rpcUrl: "https://api.mainnet-beta.solana.com",feeToken: address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), // USDCfeePayerWallet: sender // Signs to authorize the token fee payment});// Same instruction, same call—Kora sponsors the network feeconst { context } = await kora.sendTransaction([transferInstruction]);console.log("Signature:", context.signature);
Kora 资源
Is this page helpful?