Derick
1239 words
6 minutes
Solana 指令与 Program 调用

一、“智能合约”和“Program”的本质区别#

在以太坊中,智能合约(Smart Contract)是部署在链上的字节码,

每个用户可以调用它的函数。

在 Solana 中,Program 才是“智能合约”的概念。

但它的运行方式有两点不同:

对比项EthereumSolana
执行环境EVM 虚拟机BPF 虚拟机
存储方式每个合约自带 Storage数据存储在独立 Account 中
调用方式调用函数名+参数传入指令(Instruction)+账户上下文
状态读写由合约自身维护由 Program 操作外部账户数据

换句话说,Solana 的 Program 是无状态的

它需要外部账户作为“参数容器”来读写数据。


二、理解 Solana 的 Instruction(指令)#

Instruction 是链上执行的最小单元。

它告诉验证节点:

  1. 调用哪个 Program(ProgramID)
  2. 操作哪些账户(AccountMeta 列表)
  3. 传入什么数据(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 Program11111111111111111111111111111111账户创建、转账
Token Program (SPL Token)TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA管理 SPL Token(类似 ERC-20)
Associated Token ProgramATokenGPvotbQhHiTgJ7TGJ3TbxvucD8oCJeykT6tXk生成 Token 账户
Memo ProgramMemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr写链上备注信息
Stake ProgramStake11111111111111111111111111111111111111委托质押管理
Compute Budget ProgramComputeBudget111111111111111111111111111111调整交易执行的资源预算

在实际开发中,我们经常需要和 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
常见 ProgramSystem、Token、Memo、Stake
Program 调用构造 → 序列化 → 签名 → 发送
账户元信息IsSigner / IsWritable 控制并发安全
调试方式gRPC 监听链上 Program 日志
Solana 指令与 Program 调用
https://blog.ithuo.net/posts/solana-program-invocation/
Author
Derick
Published at
2024-12-17