本节实现了一个 Solana SPL Token 程序,包括链上智能合约和客户端调用代码。主要功能包括:
- 创建新的 SPL Token
- 铸造代币到用户账户
1. 核心概念
1.1 什么是 SPL Token
SPL Token 是 Solana 上的标准代币协议,类似于以太坊的 ERC-20。它定义了在 Solana 区块链上创建和管理可替代代币的规范。
1.2 关键账户类型
| 账户类型 | 所有者 | 用途 | 大小 |
|---|---|---|---|
| Mint Account | SPL Token Program | 存储代币元数据(总供应量、小数位数、铸币权限等) | 82 字节 |
| Token Account | SPL Token Program | 存储用户持有的特定代币余额 | 165 字节 |
| Associated Token Account (ATA) | SPL Token Program | 与用户钱包关联的标准代币账户 | 165 字节 |
1.3 核心程序
- SPL Token Program:管理代币铸造、转账等操作的核心程序
- Associated Token Account Program:管理 ATA 的创建和查询
- System Program:Solana 系统程序,用于创建账户和转账 SOL
2. 链上程序实现
2.1 程序入口
1 | pub fn process( |
代码解析:
try_from_slice:使用 Borsh 反序列化指令数据match:根据指令类型路由到不同的处理函数- 支持两种指令:创建代币和铸造代币
2.2 创建代币流程
步骤一:账户准备
1 | let accounts_iter = &mut accounts.iter(); |
关键点:
next_account_info:按顺序获取账户信息- 账户顺序必须与客户端传入的顺序一致
- 每个账户都有特定的用途和权限要求
步骤二:创建 Mint 账户
1 | invoke( |
代码解析:
invoke:跨程序调用(CPI),调用系统程序创建账户minimum_balance(Mint::LEN):计算存储 Mint 数据所需的最低租金Mint::LEN:Mint 账户固定大小为 82 字节token_program.key:将账户所有权转给 SPL Token 程序
租金机制:
Solana 要求账户保持最低余额以避免被垃圾回收。82 字节 Mint 账户约需 0.00144 SOL(具体金额取决于网络)。这个余额类似于押金,关闭账户时可以完全收回。
步骤三:初始化 Mint
1 | let ix = initialize_mint( |
代码解析:
initialize_mint:构造初始化 Mint 的指令decimals:代币小数位数(如 USDC 使用 6,表示 1 USDC = 1,000,000 基础单位)None:不设置冻结权限(设置后可冻结用户账户)invoke_signed:使用签名调用(这里虽然传空数组,但保持接口一致性)
为什么分两步?
- 系统程序只负责创建账户并分配空间
- SPL Token 程序负责初始化 Mint 的具体数据结构
这种设计符合 Solana 的所有权模型:只有账户的所有者程序才能修改其数据。
2.3 铸造代币流程
步骤一:账户准备
1 | let accounts_iter = &mut accounts.iter(); |
步骤二:检查并创建 ATA
1 | if associated_token_account.lamports() == 0 { |
代码解析:
lamports() == 0:检查账户是否存在(未创建的账户余额为 0)- ATA 地址通过确定性算法生成:
derive([owner, token_program, mint]) - 首次铸造时自动创建,后续无需重复创建
ATA 的优势:
- 每个用户对每种代币只有一个标准地址
- 地址可预测,无需提前告知
- 简化钱包管理
步骤三:执行铸币
1 | invoke( |
代码解析:
mint_to:SPL Token 程序的铸币指令payer.key:必须是 Mint 的 mint_authorityamount:最小单位数量(需要考虑 decimals)
权限验证:
SPL Token 程序会验证 payer 是否为 mint_authority。只有拥有铸币权限的账户才能铸造新代币。
3. 客户端实现
3.1 指令定义
1 |
|
代码解析:
- 枚举类型定义两种指令
BorshSerialize/BorshDeserialize:实现 Borsh 序列化- 必须与链上程序的定义完全一致
3.2 创建代币
基础配置
1 | let rpc_client = RpcClient::new("http://127.0.0.1:8899".to_string()); |
代码解析:
RpcClient:连接到本地测试网(也可连接 devnet、mainnet)read_keypair_file:从文件加载钱包私钥Keypair::new():生成新的 Mint 账户密钥对
构建指令数据
1 | let instruction_data = borsh::to_vec(&TokenInstruction::CreateToken { decimals })?; |
序列化过程:
- 创建
CreateToken指令实例 - Borsh 序列化为字节数组
- 字节数组会被传递给链上程序
配置账户元数据
1 | let accounts = vec![ |
AccountMeta 详解:
| 方法 | 说明 | 用途 |
|---|---|---|
new(pubkey, is_signer) |
可写账户 | 需要修改数据或余额的账户 |
new_readonly(pubkey, is_signer) |
只读账户 | 只读取数据的账户 |
**重要提示:**账户顺序必须与链上程序 next_account_info 的顺序完全一致!
构建和发送交易
1 | let token_instruction = Instruction { |
代码解析:
get_latest_blockhash():获取最新区块哈希防止重放攻击- 双重签名:
payer支付手续费,mint_account授权创建 send_and_confirm_transaction:发送并等待确认
3.3 铸造代币
计算 ATA 地址
1 | let ata = get_associated_token_address( |
确定性推导:
- 基于 owner 和 mint 确定性计算
- 无需提前创建,地址可预测
- 公式:
findProgramAddress([owner, TOKEN_PROGRAM_ID, mint], ASSOCIATED_TOKEN_PROGRAM_ID)
配置账户元数据
1 | let accounts = vec![ |
权限说明:
- Mint 账户可写:需要更新 supply 字段
- ATA 可写:需要更新代币余额
- Payer 可写:需要支付 ATA 创建费用(如果尚未创建)
发送交易
1 | let tx = Transaction::new_signed_with_payer( |
4. 完整工作流程
1 | 客户端 Solana 网络 链上程序 |
5. 核心知识点
5.1 CPI(跨程序调用)
1 | // 普通调用 |
使用场景:
| 调用方式 | 使用场景 | 示例 |
|---|---|---|
invoke |
调用其他程序的公开功能 | 调用 System Program 创建账户 |
invoke_signed |
程序代表 PDA 签名 | PDA 作为铸币权限 |
5.2 账户所有权模型
| 账户类型 | 所有者 | 可修改数据 |
|---|---|---|
| Mint Account | SPL Token Program | SPL Token Program |
| Token Account | SPL Token Program | SPL Token Program |
| Wallet | System Program | System Program |
| PDA | 自定义程序 | 自定义程序 |
核心原则: 只有账户的所有者程序才能修改账户数据
5.3 交易签名规则
1 | &[payer, mint_account] |
签名者列表规则:
- 所有标记为
is_signer: true的账户都需要签名 - 手续费支付者(payer)必须签名
5.4 Decimals 设计原则
| 代币类型 | decimals | 示例 | 1 代币 = ? 基础单位 |
|---|---|---|---|
| 稳定币 | 6 | USDC, USDT | 1,000,000 |
| 治理代币 | 9 | SOL | 1,000,000,000 |
| NFT | 0 | 不可分割 | 1 |
6. 实战技巧
6.1 调试日志
1 | msg!("Creating mint account..."); |
查看日志:
1 | solana logs |
6.2 账户大小计算
1 | // Mint 账户:82 字节 |
6.3 权限管理最佳实践
1 | // 创建时设置 mint_authority |
6.4 错误处理
1 | let result = invoke(...)?; // ? 操作符传播错误 |
常见错误:
| 错误类型 | 原因 | 解决方案 |
|---|---|---|
AccountNotFound |
账户不存在 | 检查账户地址是否正确 |
InsufficientFunds |
余额不足 | 确保账户有足够的 SOL |
InvalidAccountData |
数据格式错误 | 检查序列化/反序列化逻辑 |
MissingRequiredSignature |
缺少必要签名 | 检查签名者列表 |
7. 常见问题
Q1: 为什么需要租金?
A: Solana 通过租金机制防止状态膨胀,账户需要保持最低余额。但这更像是押金,关闭账户时可以完全收回。
Q2: ATA 地址会冲突吗?
A: 不会冲突,ATA 通过 [owner, mint] 确定性生成,每个组合唯一。
Q3: 可以修改 decimals 吗?
A: 不可以,Mint 初始化后 decimals 不可更改。
Q4: 如何转移铸币权限?
A: 使用 set_authority 指令修改 mint_authority。
Q5: 代币可以销毁吗?
A: 可以,使用 burn 指令销毁持有的代币。
Q6: 为什么要分两步创建账户?
A: 遵循 Solana 的所有权模型。System Program 创建账户并分配空间,然后将所有权转给 Token Program,最后由 Token Program 初始化具体数据。
9. 总结
- 账户是核心:Mint Account 和 Token Account 是 SPL Token 的基础
- 所有权很重要:只有所有者程序可以修改账户数据
- CPI 是关键:通过跨程序调用实现功能组合
- ATA 简化管理:确定性地址生成简化代币账户管理
- 租金是押金:不是真正的费用,关闭账户时可以收回
- 程序和数据分离:遵循 Solana 的账户模型设计
Hooray!SPL Token 开发学习完成!!!