一、“智能合约”和“Program”的本质区别
在以太坊中,智能合约(Smart Contract)是部署在链上的字节码,
每个用户可以调用它的函数。
在 Solana 中,Program 才是“智能合约”的概念。
但它的运行方式有两点不同:
| 对比项 | Ethereum | Solana |
|---|---|---|
| 执行环境 | EVM 虚拟机 | BPF 虚拟机 |
| 存储方式 | 每个合约自带 Storage | 数据存储在独立 Account 中 |
| 调用方式 | 调用函数名+参数 | 传入指令(Instruction)+账户上下文 |
| 状态读写 | 由合约自身维护 | 由 Program 操作外部账户数据 |
换句话说,Solana 的 Program 是无状态的,
它需要外部账户作为“参数容器”来读写数据。
二、理解 Solana 的 Instruction(指令)
Instruction 是链上执行的最小单元。
它告诉验证节点:
- 调用哪个 Program(ProgramID)
- 操作哪些账户(AccountMeta 列表)
- 传入什么数据(Data,通常是序列化后的参数)
其结构如下:
type Instruction struct {
Program solana.PublicKey // 被调用的程序ID
Accounts []*AccountMeta // 参与执行的账户
Data []byte // 指令参数
}
示例:System Program 转账
System Program 是 Solana 内置的系统合约。
我们在第二讲用它完成了转账,其实就是发送一条 Transfer 指令。
instruction := system.NewTransferInstruction(
1_000_000_000, // 转账 1 SOL
sender.PublicKey(), // 来源账户
receiver.PublicKey(), // 接收账户
).Build()
这就是一条最简单的 Program 调用。
三、Solana 常见 Program 一览
| Program 名称 | Program ID | 作用 |
|---|---|---|
| System Program | 11111111111111111111111111111111 | 账户创建、转账 |
| Token Program (SPL Token) | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA | 管理 SPL Token(类似 ERC-20) |
| Associated Token Program | ATokenGPvotbQhHiTgJ7TGJ3TbxvucD8oCJeykT6tXk | 生成 Token 账户 |
| Memo Program | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr | 写链上备注信息 |
| Stake Program | Stake11111111111111111111111111111111111111 | 委托质押管理 |
| Compute Budget Program | ComputeBudget111111111111111111111111111111 | 调整交易执行的资源预算 |
在实际开发中,我们经常需要和 Token Program 或自定义 Program 进行交互。
四、与 Memo Program 对话:最简单的 Program 调用
Memo Program 是 Solana 上的“Hello World”合约。
它接收一段字符串,并将其记录在链上,供任何人查看。
我们用 Go 调用它:
package main
import (
"context"
"fmt"
"log"
"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/programs/memo"
"github.com/gagliardetto/solana-go/rpc"
)
func main() {
ctx := context.Background()
client := rpc.New(rpc.DevNet_RPC)
// 加载本地钱包
sender, err := solana.PrivateKeyFromSolanaKeygenFile("/Users/alan/.config/solana/devnet.json")
if err != nil {
log.Fatalf("加载钱包失败: %v", err)
}
// 构造 Memo 指令
instruction := memo.NewMemoInstruction(
[]byte("Hello Solana from Go!"),
).Build()
// 获取最新区块哈希
recent, err := client.GetRecentBlockhash(ctx, rpc.CommitmentFinalized)
if err != nil {
log.Fatalf("获取区块哈希失败: %v", err)
}
// 创建交易
tx, err := solana.NewTransaction(
[]solana.Instruction{instruction},
recent.Value.Blockhash,
sender.PublicKey(),
)
if err != nil {
log.Fatalf("创建交易失败: %v", err)
}
// 签名与发送
_, err = tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey {
if pub.Equals(sender.PublicKey()) {
return &sender
}
return nil
})
if err != nil {
log.Fatalf("签名失败: %v", err)
}
sig, err := client.SendTransaction(ctx, tx)
if err != nil {
log.Fatalf("发送失败: %v", err)
}
fmt.Println(" Memo 已上链,交易签名:", sig)
执行完后,在 Solana Explorer (Devnet) 中搜索签名哈希,
你能看到链上记录的那条 "Hello Solana from Go!" 备注。
五、账户元信息:AccountMeta
每条指令都需要声明哪些账户参与执行。
这是 Solana 并行架构的关键,因为只有“互不重叠的账户”才能同时执行。
AccountMeta 的定义如下:
| 字段 | 说明 |
|---|---|
| PublicKey | 账户公钥 |
| IsSigner | 是否需要签名授权 |
| IsWritable | 是否可以被修改 |
示例:
meta := solana.NewAccountMeta(sender.PublicKey(), true, true)
记住:如果一个账户在指令中被标记为“写入”,
它会被系统锁定,直到该交易执行结束。
六、自定义指令数据的序列化
大多数自定义 Program 都需要传入特定格式的参数。
Solana 的惯例是用 Borsh(Binary Object Representation Serializer for Hashing) 序列化数据。
solana-go 已内置 binary 包支持:
import "github.com/gagliardetto/binary"
type MyInstruction struct {
Action uint8
Value uint64
}
data, err := binary.Encode(struct {
Action uint8
Value uint64
}{1, 42})
然后将这段 data 作为 Instruction 的 Data 字段:
instruction := solana.NewInstruction(
myProgramID,
accountMetaList...,
).SetData(data)
七、组合多条指令执行(Batch Transaction)
在 Solana 中,一笔交易可以包含多条指令。
例如我们可以先写入一条 Memo,再转账:
instructions := []solana.Instruction{
memo.NewMemoInstruction([]byte("Before transfer")).Build(),
system.NewTransferInstruction(
1_000_000,
sender.PublicKey(),
receiver.PublicKey(),
).Build(),
}
然后像之前一样打包、签名、广播即可。
这让你能把多步操作打包成一次链上执行,大幅减少网络延迟与费用。
八、RPC 与 gRPC 调用的 Program 监听
在生产环境中,我们常常需要监听某个 Program 的事件。
Solana 提供了 gRPC 流式订阅能力,非常适合链上服务。
订阅指定 Program 的日志输出
grpcClient := jsonrpc.NewClient("https://api.devnet.solana.com:443")
stream, err := grpcClient.SubscribeLogs(ctx, "all", nil)
if err != nil {
log.Fatalf("订阅失败: %v", err)
}
for {
logs, err := stream.Recv()
if err != nil {
log.Fatalf("接收失败: %v", err)
}
fmt.Println("新的 Program 日志:", logs)
}
每当某个 Program 执行时,就会收到它的链上日志。
这是做链上事件追踪或构建索引服务的核心手段。
九、小结
| 模块 | 内容 |
|---|---|
| 指令模型 | Program + Accounts + Data |
| 常见 Program | System、Token、Memo、Stake |
| Program 调用 | 构造 → 序列化 → 签名 → 发送 |
| 账户元信息 | IsSigner / IsWritable 控制并发安全 |
| 调试方式 | gRPC 监听链上 Program 日志 |
