做 Solana 开发这两年,spl-token approve 是我反复用到、却又反复在它身上栽跟头的一条命令。最早从以太坊的思维切过来,以为 approve 就是"发一笔带事件的交易完事",结果在授权单位、签名者权限、账户类型三个地方接连踩坑,白花了不少测试时间。这篇文章就当一本"踩坑笔记"来写,把我验证过的命令用法、链上行为,以及 DeFi 授权、托管账户、多签审批的真实场景都梳理出来。不管你是刚接触 Solana CLI 的新手,还是从 EVM 生态转过来的老手,照着这份笔记操作,能少走很多弯路。
1. 为什么 Solana 的授权额度要存在链上账户里而不是一次签名
1.1 一次真实的"我要授权"场景
先说一个我自己遇到的情况。之前帮一个做支付聚合的朋友写脚本,他需要一个"结算服务账户"定期从用户的托管账户里划走固定金额的 USDT。最开始的想法很简单:让用户直接把私钥给服务方。这显然不行,私钥给了就等于把整个账户都交出去了,对方想转走什么就转走什么,谁也拦不住。
后来退一步,用 spl-token approve 给服务账户一个"转账额度"。额度严格限制,比如每笔结算只授权 100 个 USDT,服务方账户只能在这个额度内操作,超出部分链上程序直接拒绝。这个思路和银行卡的"授权限额"很像——不是把卡给你,而是允许你在某个额度内代扣。这就是 approve 的核心场景:代币资产的所有权没有转移,但使用权限被部分委托了出去。
类似场景其实到处都是:DEX 上你把币授权给交易合约、NFT 市场授权给挂单合约、借贷协议授权给清算合约,本质都是同一个动作。我自己的原则是:凡是涉及"别人要动我的币"但又不想交出私钥,就优先考虑 approve 这套机制。
1.2 Solana 的 delegate 模型与 EVM approve 的本质区别
从以太坊转过来的开发者,很容易习惯性去找 "approve 事件""nonce 递增"这些概念。Solana 这套委托机制和 EVM 上最大的区别在于:EVM 的 approve 只是一个日志记录,合约是否真的兑现这个授权,完全依赖调用方合约自己的代码逻辑;而 Solana 的 approve 直接改变链上 Token Account 的状态,转账时由 SPL Token 原生程序亲自检查这个状态。
换句话说,以太坊上你 approve 给某个合约,合约内部如果不按规矩执行,这个 approve 可能就是个花架子;Solana 上只要 delegate 字段被设置,SPL Token 程序在执行转账时就会强制检查,不存在"授权了但合约不认"的问题。这个设计让链上委托更可靠,但也意味着你一旦授权,风险是实打实的,所以才有后面那一大段安全习惯。
另一个容易忽视的差异是:EVM 的 approve 通常按"合约地址"维度记录,而 Solana 的 approve 作用在具体的 Token Account 上。同一个钱包如果有多套代币账户,就得对每个账户分别授权,不存在"一次性把钱包里所有币都授权出去"的说法。这一点在 DEX 交互时特别明显,很多 UI 显示"授权全部"其实也只是对该代币的某个账户操作。
1.3 链上到底发生了什么:Account 里的 delegate 字段
SPL Token 的 Token Account 是一个可解析的结构体,核心字段有这么几个:
| 字段 | 作用 |
|---|---|
| mint | 这个账户对应的代币类型 |
| owner | 资产真正的主人 |
| amount | 账户里的可用余额 |
| delegate | 被委托的第三方公钥 |
| delegated_amount | 当前剩余委托额度 |
| state | 账户状态(初始化/冻结等) |
当你执行 spl-token approve <TOKEN_ADDRESS> <AMOUNT> <DELEGATE> 时,SPL Token 程序会调用 Approve 指令,把 Token Account 的 delegate 字段改写为指定的 delegate 公钥,同时把 delegated_amount 设置为你要授权的数量。整个过程是链上状态变更,不是简单的链下签名。
这里有个容易被忽略的点:delegated_amount 不是"总共能用多少",而是"还剩多少"。delegate 每转走一部分,这个数字就会相应减少,减到 0 之后 delegate 就不能再转。想重新授权,owner 再次调用 approve,直接用新额度覆盖旧的即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与 approve 命令参数全解
2.1 安装 spl-token CLI 并连接目标网络
要用 approve,先得有 solana 命令行工具和 spl-token 插件。安装方式很简单,macOS/Linux 官方推荐用脚本安装 solana 工具链,装完自带 spl-token;如果本地已经有 cargo,也可以 cargo install spl-token-cli 单独装。装完后两种方式都可以跑 spl-token --version 验证一下。
装完之后第一件事是确认本地钱包。spl-token 默认读取 ~/.config/solana/id.json 作为签名账户,也就是你在这条命令里扮演的 owner。用 solana config get 可以查看当前 RPC 地址和默认签名账户,确认你要操作的是 devnet、testnet 还是主网。开发测试阶段强烈建议先用 devnet,solana config set --url https://api.devnet.solana.com 切过去,再用 solana airdrop 2 领一点测试 SOL 当手续费。
注意:approve 本身只消耗一笔交易手续费,扣的是 SOL,不是被授权的那个代币。所以钱包里必须有一点 SOL 余额,否则交易会直接报 insufficient funds。
2.2 命令语法与参数对照
approve 的基本语法是:
bash复制spl-token approve [OPTIONS] <TOKEN_ADDRESS> <AMOUNT> <DELEGATE>
三个位置参数都是必填的,而且每一个都有坑:
- TOKEN_ADDRESS:被授权的 Token Account 地址,不是 token mint 地址。这个坑我在第 6 部分会专门展开,这里先记住:mint 是代币的"发行凭证",Token Account 才是具体放币的"账户",approve 作用在账户上。
- AMOUNT:授权的数量,这个坑特别大,下面单独讲。
- DELEGATE:接收委托的公钥。可以是某个钱包公钥,也可以是一个 program ID。当你授权给 DeFi 协议时,委托对象往往就是协议的 program 地址,因为实际执行转账的是那个程序。
常用选项整理成一张表,方便对照查:
| 选项 | 作用 |
|---|---|
--owner <OWNER_SIGNER> |
指定签名者钱包,默认是 id.json |
--token-owner <TOKEN_OWNER_PUBKEY> |
Token Account 的 owner 不是签名钱包时,用这个显式指定 |
--fee-payer <FEE_PAYER_SIGNER> |
单独指定手续费支付账户 |
--allow-empty |
允许授权 0 个代币,默认会报错 |
--use-unchecked-amount |
跳过"授权额不超过余额"的客户端检查 |
--multisig-signer <MULTISIG_SIGNER> |
多签场景下传入签名者,可多次传入 |
--url <URL> |
临时指定 RPC 地址,覆盖 config 里的设置 |
大多数场景下只需要前三个位置参数,其他选项都是特殊场景。但 --multisig-signer 和 --token-owner 这两条,如果你做的是多签钱包或者要管理别人的资产账户,就绕不开。
2.3 最容易错的一步:amount 是基础单位不是人性化数字
这是我认为整个 approve 命令里最反直觉的地方。spl-token transfer 允许你写 1.5 这种带小数的数量,CLI 会自动根据 token 的 decimals 换算;但 spl-token approve 的 AMOUNT 要求的是基础单位(raw unit),也就是这个代币的最小单位,它不会帮你做小数换算。
举个例子,某代币 decimals 是 6,你想授权 5 个代币,approve 里要写 5000000,不是 5。如果你直接写 5,链上只会给 0.000005 个代币的额度,等真正转账时发现额度不够,一脸懵。我在 devnet 上测试时还专门犯过这个错,授权完成后用 account-info 一看,delegated amount 只有个位数,当场就明白怎么回事了。
换算公式就一句话:授权数量 × 10^decimals = 传入的 AMOUNT。想知道一个 token 的 decimals,用 spl-token display <MINT> 或 spl-token account-info <ACCOUNT> 看一眼就行。
提示:很多 Solana DeFi 的 UI 在"授权"按钮背后也是帮你做这个换算,把 UI 上显示的数量乘上 10^decimals 之后才往链上发 Approve 指令。所以别怀疑,CLI 就是这么设计的。
3. devnet 全流程实操:创建 Token、授权、验证
3.1 准备钱包和测试代币
空讲语法没意思,我直接带你跑一遍完整流程。以下操作全部在 devnet 上执行,步骤里我会标注每一步的作用。
先切网络、检查钱包:
bash复制solana config set --url https://api.devnet.solana.com
solana config get
solana airdrop 2
solana config get 会显示 RPC URL 和 Keypair Path。如果 Keypair Path 显示的不是你的开发钱包,用 solana config set --keypair <path> 切过去。airdrop 是为了准备手续费,devnet 上一般一次给 2 个 SOL,不够就再打几次。
然后创建测试代币和一个存放它的 Token Account:
bash复制spl-token create-token
spl-token create-account <MINT_ADDRESS>
spl-token mint <MINT_ADDRESS> 1000
create-token 会输出一个 mint 地址,记下来。create-account 默认创建的是与你钱包关联的 Associated Token Account(ATA),也就是系统自动推导出来的那个账户,后面 approve 的 TOKEN_ADDRESS 就是这个 ATA 的地址。mint 1000 相当于往自己账户里印 1000 个代币。
3.2 创建 delegate 账户并执行 approve
现在需要一个"被委托方"。我习惯用 solana-keygen new -o delegate.json 生成一个独立 keypair 来扮演第三方。你也可以直接用任意一个公钥,比如协议 program ID,但为了后面验证转账,建议还是生成一个你能控制的 keypair。
然后执行授权:
bash复制spl-token approve <TOKEN_ACCOUNT> 200 <DELEGATE_PUBKEY>
如果一切正常,终端会输出交易签名。如果报错,优先看是不是 AMOUNT 单位没算对、TOKEN_ACCOUNT 是不是写成 mint 地址了。devnet 上网络比较稳定,一般不用重试太多。
3.3 验证:通过 account-info 检查 delegated_amount
授权完成不代表就完事了,我习惯每次都验证一下链上状态。查看 Token Account 信息:
bash复制spl-token account-info <TOKEN_ACCOUNT>
输出里会有一行 Delegate: <DELEGATE_PUBKEY> 和 Delegated amount: 200。这一行直接对应链上结构体的 delegate 和 delegated_amount 字段,能看到就说明 approve 真的生效了。
这里补充一个经验:如果你在写自动化脚本,别只依赖 CLI 输出,建议用 RPC 直接拉账户数据解析。getAccountInfo 拿到账户数据后,用 @solana/spl-token 的 AccountLayout.decode 解码,就能程序化读取 delegate 和 delegatedAmount,方便做状态监控和告警。
3.4 真正让 delegate 转一笔试试
验证授权是否真的能用,最直接的办法是让 delegate 去实际转走一笔。CLI 里需要切换签名者,但多人签名和 authority 的传递逻辑绕来绕去容易出错,我更推荐用脚本验证。下面这段用 @solana/web3.js 和 @solana/spl-token,delegate 用自己的私钥签名,以 delegate 的地址作为 authority 来发起转账:
javascript复制import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js';
import { createTransferCheckedInstruction, getAssociatedTokenAddress } from '@solana/spl-token';
const delegate = Keypair.fromSecretKey(new Uint8Array([/* delegate.json 的私钥字节 */]));
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const mint = new PublicKey('MINT_ADDRESS');
const sourceAccount = new PublicKey('OWNER_TOKEN_ACCOUNT'); // owner 的 ATA
const recipient = new PublicKey('RECIPIENT_WALLET');
