在学习 Solana 开发的过程中,你一定会遇到 SPL 代币转账 这个关键环节:从给社区空投打卡 NFT,到批量分发白名单通证,乃至在多签托管账户之间划拨资金,无一例外都要借助 SPL Token Program。抓住本文,你会用最短时间完成「发送 SPL 代币」从概念到脚本的全部环节。
本文你会做什么?
本次实操将基于 Solana Devnet,用 TypeScript 编写一段 40 行核心代码,完成以下任务:
- 创建钱包并领取测试 SOL
- 计算正确的小数位
- 自动检测并创建接收方 ATA
- 发起链上转账并获取 交易签名
- 在终端查看绿色成功提示 ✅
一分钟了解 SPL 账户模型
SPL 代币转账与原生 SOL 转账的最大差异在于——账户结构。牢牢记住下面两个小概念,后面的代码就水到渠成。
Mint ID:每个代币的身份证
无论你用的是 USDC、SAMO,还是独创的游戏币,都有一条独一无二 Mint ID。NFT 也一样:每只 Famous Fox 均对应专属的 Mint ID,所以才能保持“非同质化”。
ATA(Associated Token Account):一对一的收纳格
把钱包地址想象成房间,ATA 就是房间里给某个特定代币 单独预留的抽屉。必须「币种相同」的 ATA 才能互转,否则 Solana 会直接报错。若接收方首次接触该代币,还需 顺带创建 ATA 并预存租金。
动手前:环境与资产
- Node.js ≥ 20.0
- npm 或 yarn
- 已在 Devnet 拥有至少 1 枚测试 SPL 代币(可通过水龙头或自行铸造获得)
👉 亲手领 1 亿枚测试 $DUMMY,五分钟就能开箱实验!
步骤 1:初始化项目
mkdir spl-transfer-demo && cd spl-transfer-demo
npm init -y
npm i @solana/kit @solana-program/token ts-node
npx tsc --init在 tsconfig.json 中加入:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmit": true,
"target": "ESNext"
}
}步骤 2:变量声明与助记词
新建文件 app.ts,头几行引入依赖并声明变量(真实开发请改用 .env):
import { address } from '@solana/kit';
const SECRET = [...]; // 32 字节助记词
const DESTINATION = address('DemoKMZWkk483hX4mUrcJoo3zVvsKhm8XXs28TuwZw9H');
const MINT = address('DoJuta7joTSuuoozqQtjtnASRYiVsT435gh4srh5LLGK'); // 换成你的 Mint ID
const AMOUNT = 1n; // 注意使用 BigInt若不确定代币 Mint ID,可打开 Solana Explorer → Tokens 粘贴钱包地址查看并复制。
步骤 3:获取小数位(decimals)
链上记录的是 最小单位整数,需要换算:
async function getDecimals(mint: string) {
const info = await rpc.getAccountInfo(address(mint)).send();
if (!info.value?.data) throw new Error('找不到 Mint');
const data = Uint8Array.from(Buffer.from(info.value.data[0], 'base64'));
return data[44];
}使用场景:USDC 通常 6 位小数,因此转账 1 USDC 实际填 1_000_000 单位。
步骤 4:完整转账函数
下面用函数式写法拼装交易:
async function sendTokens() {
console.log(`转账 ${AMOUNT} 枚代币至 ${DESTINATION}`);
// 1. 创建并充值钱包
const fromKP = await createKeyPairFromBytes(new Uint8Array(SECRET));
const fromSigner = createSignerFromKeyPair(fromKP);
// 2. 查找或创建 ATA
const [srcATA] = await findAssociatedTokenPda({
owner: fromSigner.address,
mint: MINT,
});
const [dstATA] = await findAssociatedTokenPda({
owner: DESTINATION,
mint: MINT,
});
// 3. 判断目标 ATA 是否已存在
let createDstATA = false;
try {
await rpc.getAccountInfo(dstATA).send();
} catch {
createDstATA = true;
}
// 4. 计算带小数位的总量
const decimals = await getDecimals(MINT);
const amountWithDecimals = AMOUNT * BigInt(10 ** decimals);
// 5. 拼装并发送交易
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const tx = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(fromSigner, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) =>
createDstATA
? appendTransactionMessageInstruction(
getCreateAssociatedTokenIdempotentInstruction({
payer: fromSigner,
ata: dstATA,
owner: DESTINATION,
mint: MINT,
}),
m
)
: m,
(m) =>
appendTransactionMessageInstruction(
getTransferInstruction({
source: srcATA,
destination: dstATA,
authority: fromSigner,
amount: amountWithDecimals,
}),
m
)
);
const signed = await signTransactionMessageWithSigners(tx);
const sig = await sendAndConfirmTransaction(signed);
console.log(`\x1B[32m转账成功!\x1B[0m\nhttps://explorer.solana.com/tx/${sig}?cluster=devnet`);
}
sendTokens().catch(console.error);一条命令跑起来:
npx ts-node app.ts看到绿色 转账成功! 加上交易哈希即大功告成。
深度扩展:你还可以做什么?
- NFT 一键整理:利用
getTokenAccountsByOwner,批量把 NFT 从多个钱包汇集到主号 - 空投脚本:同时替上千地址检查并创建 ATA,再用循环完成批量交易
- 批量监测:通过区块订阅接口,实时监听 SPL 代币流入/流出合约
常见问题 FAQ
Q1: 为什么会出现「0x11 - Insufficient funds for transaction」报错?
A: 检查 SOL 余额是否 >= 0.001 SOL(包含 ATA rent + compute units)。若不足,请重新领水后再试。
Q2: 代币明明拿到手,为什么搜索不到?
A: 大多数钱包默认隐藏 零余额 ATA。打开钱包设置里的「显示隐藏资产」即可。
Q3: BigInt 不支持小数点,如何转账 0.5 枚代币?
A: 第一次计算完整单位,再乘以 10 位小数次方即可。例如 USDC 的 0.5 等效 BigInt(500000)。
Q4: 能否一次性往几千地址空投?
A: 可以,但强烈采用 指令打包批量交易 的方式优化;否则每笔单交易,上千笔会出现速率瓶颈。
Q5: 生产环境应如何保管私钥?
A: 建议将密钥写入 环境变量 或安全硬件(Ledger、Trezor),并开启 白名单 IP 访问节点,降低泄露风险。
现在就打开编辑器,亲手跑一次脚本——你将深刻体会到 SPL Token Program 的简洁与强大!