Transfer Fee 是 Solana Token Extensions 最常用也最直观的一项功能:每次链上转账都会自动扣除一笔手续费,无需额外合约逻辑,收取的仍是同一 Token——简单、高效、安全。本指南将以 Devnet 实战为例,带你从 0 到 1 创建带 Transfer Fee 并发手续费的 SPL Token,并演示如何 提取、归属、收割这些链上手续费。无论你正在为 GameFi、RWA,还是 DAO 打造经济模型,这篇都可以作为核心代币设计备忘录。
目录
起步
- 打开 Solana Playground 模板
- 自动生成 Playground 钱包(首次使用会提示)→ 复制钱包地址
- 在终端执行
solana airdrop 5获取 Devnet SOL - 启动初始脚本观察余额输出
安装依赖
删掉默认模板,贴上:
import {
Connection, Keypair, SystemProgram, Transaction,
clusterApiUrl, sendAndConfirmTransaction,
} from '@solana/web3.js'
import {
ExtensionType, TOKEN_2022_PROGRAM_ID,
createAccount, createInitializeMintInstruction,
createInitializeTransferFeeConfigInstruction,
getMintLen, mintTo, transferCheckedWithFee,
withdrawWithheldTokensFromAccounts,
harvestWithheldTokensToMint,
withdrawWithheldTokensFromMint,
} from '@solana/spl-token'
const connection = new Connection(clusterApiUrl('devnet'), 'confirmed')
const payer = pg.wallet.keypair
let transactionSignature: string初始化带 Transfer Fee 的 Mint
配置参数
| 参数 | 含义 | 示例值 |
|---|---|---|
| decimals | 小数位 | 2 |
| feeBasisPoints | 万分比(100 = 1%) | 100(1%) |
| maxFee | 单笔最高手续费 | 100(即 1 Token) |
const mintKeypair = Keypair.generate()
const mint = mintKeypair.publicKey
const decimals = 2
const feeBasisPoints = 100 // 1%
const maxFee = BigInt(100) // 1.00 Token
const transferFeeConfigAuthority = pg.wallet.keypair
const withdrawWithheldAuthority = pg.wallet.keypair计算所需空间
const mintLen = getMintLen([ExtensionType.TransferFeeConfig])
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen)发送交易:创建账户与铸造
组合三步指令
const ixCreate = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mint,
space: mintLen,
lamports,
programId: TOKEN_2022_PROGRAM_ID,
})
const ixFee = createInitializeTransferFeeConfigInstruction(
mint, // mint
transferFeeConfigAuthority.publicKey, // fee更新者
withdrawWithheldAuthority.publicKey, // 费资金提取者
feeBasisPoints,
maxFee,
TOKEN_2022_PROGRAM_ID,
)
const ixMint = createInitializeMintInstruction(
mint, decimals,
payer.publicKey, null,
TOKEN_2022_PROGRAM_ID,
)
const tx = new Transaction().add(ixCreate, ixFee, ixMint)
transactionSignature = await sendAndConfirmTransaction(connection, tx, [payer, mintKeypair])
console.log('创建 Mint:🔍 https://solana.fm/tx/', transactionSignature + '?cluster=devnet-solana')创建演示用的 Token Accounts
const sourceTokenAccount = await createAccount(connection, payer, mint, payer.publicKey, undefined, undefined, TOKEN_2022_PROGRAM_ID)
const randomKeypair = Keypair.generate()
const destTokenAccount = await createAccount(connection, payer, mint, randomKeypair.publicKey, undefined, undefined, TOKEN_2022_PROGRAM_ID)
// 铸造 2000.00 Tokens 至 sourceTokenAccount
transactionSignature = await mintTo(connection, payer, mint, sourceTokenAccount, payer.publicKey, 2000_00, undefined, undefined, TOKEN_2022_PROGRAM_ID)转账并查看手续费
总转账额:1000 Tokens
计算手续费:
const transferAmount = BigInt(1000_00)
const fee = (transferAmount * BigInt(feeBasisPoints)) / BigInt(10_000) // 10 Tokens
const feeCharged = fee > maxFee ? maxFee : fee发起转账:
transactionSignature = await transferCheckedWithFee(
connection, payer, sourceTokenAccount, mint, destTokenAccount,
payer.publicKey, transferAmount, decimals, feeCharged,
undefined, undefined, TOKEN_2022_PROGRAM_ID
)
console.log('转账含手续费:🔍 https://solana.fm/tx/', transactionSignature + '?cluster=devnet-solana')转账后:
- 接收方 实际到账 990 Tokens
- 手续费 10 Tokens 锁在 destTokenAccount 的 withheld 资产空间内,接收方自己无法动用
提取手续费到任意地址
// Step1 拉取所有关联账户
const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: 'confirmed',
filters: [{ memcmp: { offset: 0, bytes: mint.toString() } }]
})
// Step2 过滤含手续费的
const accountsToWithdrawFrom = allAccounts
.map((a) => ({ pubkey: a.pubkey, account: unpackAccount(a.pubkey, a.account, TOKEN_2022_PROGRAM_ID) }))
.filter((u) => {
const feeData = getTransferFeeAmount(u.account)
return feeData ? feeData.withheldAmount > 0 : false
})
.map((u) => u.pubkey)
// Step3 提取到 destTokenAccount
transactionSignature = await withdrawWithheldTokensFromAccounts(
connection, payer, mint, destTokenAccount, withdrawWithheldAuthority,
undefined, accountsToWithdrawFrom, undefined, TOKEN_2022_PROGRAM_ID
)收割手续费到 Mint 再统一支配
代币账户中含有 任意Token时不能直接关闭。若需关闭账户需先 harvest 质押的手续费到 Mint。
// 再转一次,制造手续费余额
await transferCheckedWithFee(/*同上*/)
// 糖果式调用,任何人都可收割
const harvestTxSig = await harvestWithheldTokensToMint(
connection, payer, mint, [destTokenAccount], undefined, TOKEN_2022_PROGRAM_ID
)
console.log('收割 Fee 到 mint:🔍 https://solana.fm/tx/', harvestTxSig + '?cluster=devnet-solana')
// 统一提取到指定账户
await withdrawWithheldTokensFromMint(
connection, payer, mint, destTokenAccount,
withdrawWithheldAuthority, undefined, undefined, TOKEN_2022_PROGRAM_ID
)常见问答
Q1:Transfer Fee 会不会成为整条链的性能瓶颈?
A:不会。手续费被锁定在每个接收账户,无需写同一个“手续费池”。消除了全局锁,依旧享受 Solana 并行化的速度与吞吐量。
Q2:我可以后期调整手续费率吗?
A:只要 transferFeeConfigAuthority 签名即可重新配置:feeBasisPoints、maxFee,全链即时生效。
Q3:能否收取别的 Token 作为手续费?
A:Transfer Fee 仅收取同一 Token。想收取 USDC 或 SOL 请改用 Transfer Hook extension。
Q4:所有账户都能强制收割别人的手续费?
A:是的。harvestWithheldTokensToMint 是公开指令,任何用户都能帮指定账户清理 withheld 余额,维持生态整洁。
Q5:接收方能拿到手续费返利吗?
A:不能。手续费进入 withheld 区域,只能在 Withdraw Authority 签名后提取。接收方不拥有这些余额的任何操作权限。
总结与下一步
Transfer Fee extension 用 原生指令级 取代了额外合约的复杂逻辑,让手续费收集在任何转账场景下都变得顺其自然。
如果你是产品经理:可将手续费用于 DAO 资金库、游戏治理代币销毁、NFT 双币赋能 等场景。如果你是开发者:下一章可以继续了解 Transfer Hook 实现灵活的多 Token 手续费模式。