MSG Chain Go SDK 开发指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目录
1. 概述
1.1 为什么选择 Go SDK
MSG Chain 的底层核心由 Go 语言构建——Cosmos SDK 生态本身以 Go 为第一公民,节点二进制 (genesis_node_linux, quantum_node_linux) 均通过 Go 编译。对于需要高性能、低延迟、强类型保障的链上交互场景,Go SDK 是比 cosmjs (TypeScript) 更优的选择:
| 维度 | Go SDK | cosmjs (TS) |
|---|---|---|
| 类型安全 | 编译期强类型 | 运行时弱类型 |
| 性能 | 原生编译,无 VM 开销 | Node.js / 浏览器受限 |
| 并行 | goroutine 原生并发 | 单线程 + Worker |
| 依赖体积 | 单二进制 | node_modules 膨胀 |
| 量子安全签名 | 原生 Dilithium-5 绑定 | 无 WASM 支持 |
| 与节点集成 | 可直接嵌入 RPC / gRPC | 仅 HTTP 桥接 |
1.2 MSG Chain Go 代码库架构
MSG Chain 的 Go 代码库以标准 Cosmos SDK 项目布局组织:
msg-chain/
├── pkg/
│ ├── quantum/ # 量子安全层
│ │ ├── node.go # 节点启动逻辑
│ │ ├── agent_api.go # Agent API 路由注册
│ │ ├── tendermint_rpc.go # Tendermint RPC 层
│ │ └── transaction_executor.go # 交易执行器
│ ├── wasm/ # CosmWasm 集成
│ │ ├── real_vm.go # 虚拟机运行时
│ │ └── core_contracts_e2e_test.go # 端到端测试
│ └── indexer/ # 事件索引
│ └── event_listener.go
├── cmd/
│ ├── genesis_node/ # 创世节点入口
│ └── quantum_node/ # 量子节点入口
├── Makefile # make deps/lint/test/build-linux/ci-contracts
└── go.mod
构建系统通过 Makefile 驱动:
| 命令 | 作用 |
|---|---|
make deps |
拉取 Go module 缓存 |
make lint |
gofmt, go vet, golangci-lint |
make test |
Go 包测试 |
make test-quantum |
pkg/quantum 后量子密码学测试 |
make build-linux |
编译 bin/genesis_node_linux + bin/quantum_node_linux |
make ci-contracts |
合约 wasm 编译 + cargo test |
1.3 链参数速查
| 参数 | 值 |
|---|---|
| Chain ID | msg-chain-1 |
| Bech32 前缀 | msg |
| CoinType | 118 |
| MSG 小数位 | 18 |
| Gas 价格 | 1,000,000,000 attoMSG/gas |
| 出块时间 | 5s |
| 共识机制 | Round-Robin + DAR |
| 签名算法 | Dilithium-5 (后量子) |
| 底层存储 | BadgerDB |
2. 安装与依赖
2.1 初始化项目
mkdir msg-sdk-example && cd msg-sdk-example
go mod init github.com/your-org/msg-sdk-example
2.2 安装 SDK
go get github.com/msg-chain/go-sdk@v0.2.0
SDK 自动拉取以下依赖:github.com/cosmos/cosmos-sdk v0.47.x、github.com/CosmWasm/wasmd v0.45.x、github.com/cometbft/cometbft v0.37.x、github.com/cosmos/ibc-go/v7 v7.0.0。
整理依赖并验证:
go mod tidy
go build ./...
2.3 完整 go.mod 文件
module github.com/your-org/msg-sdk-example
go 1.21
require (
github.com/msg-chain/go-sdk v0.2.0
github.com/cosmos/cosmos-sdk v0.47.10
github.com/CosmWasm/wasmd v0.45.0
github.com/cometbft/cometbft v0.37.5
github.com/cosmos/gogoproto v1.4.11
github.com/cosmos/cosmos-proto v1.0.0-beta.4
github.com/cosmos/ibc-go/v7 v7.4.0
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/cosmos/cosmos-crypto v0.2.0
github.com/cosmos/go-bip39 v1.0.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/tyler-smith/go-bip39 v1.1.0
github.com/gorilla/websocket v1.5.1
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.21.0
golang.org/x/time v0.5.0
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
)
require (
github.com/dgraph-io/badger/v4 v4.2.0 // indirect
github.com/cloudflare/circl v1.3.8 // indirect, Dilithium support
)
2.4 Makefile 集成
SDK 项目自身使用标准 Makefile 模式:
DEPS := go mod tidy && go mod download
LINT := gofmt -l . && go vet ./... && golangci-lint run
TEST := go test -count=1 -v ./...
BUILD := go build -ldflags="-w -s" -o bin/ ./cmd/...
.PHONY: deps lint test build
deps:
$(DEPS)
lint:
$(LINT)
test:
$(TEST)
test-quantum:
go test -count=1 -tags=quantum ./pkg/quantum/...
build:
$(BUILD)
build-linux:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-o bin/genesis_node_linux ./cmd/genesis_node
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-o bin/quantum_node_linux ./cmd/quantum_node
ci-contracts:
cd contracts && cargo build --release --target wasm32-unknown-unknown && cargo test
2.5 go generate 模式
//go:generate go run github.com/msg-chain/go-sdk/cmd/genclient -pkg msgchain -o ./client
//go:generate protoc --go_out=. --go_opt=paths=source_relative \
// -I proto/ proto/msgchain/*.proto
//go:generate mockgen -source=client/interfaces.go -destination=client/mocks.go -package=client
package msgchain
3. 核心客户端
3.1 MSGChainClient 结构体
MSGChainClient 是 SDK 的顶级入口,封装了与 MSG Chain 节点交互的所有能力。类似 cosmos-sdk 的 client.Context,但更注重易用性。
package msgchain
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
sdk "github.com/cosmos/cosmos-sdk/types"
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
"github.com/cosmos/cosmos-sdk/types/tx/signing"
"github.com/gorilla/websocket"
"github.com/pkg/errors"
"github.com/msg-chain/go-sdk/pkg/quantum"
"golang.org/x/time/rate"
"google.golang.org/grpc"
)
// ChainConfig 链配置参数
type ChainConfig struct {
ChainID string // 链ID,默认 "msg-chain-1"
Bech32Prefix string // Bech32 地址前缀,默认 "msg"
CoinType uint32 // SLIP-44 CoinType,默认 118
GasPrices string // Gas 价格,默认 "1000000000attoMSG"
GasAdjustment float64 // Gas 调整系数,默认 1.3
GasLow string // 低价 Gas,默认 "1000000000attoMSG"
GasAvg string // 均价 Gas,默认 "1000000000attoMSG"
GasHigh string // 高价 Gas,默认 "1000000000attoMSG"
BlockTime time.Duration // 出块时间,默认 5s
MsgDecimals uint8 // MSG 小数位,默认 18
}
// DefaultChainConfig 返回默认链配置
func DefaultChainConfig() ChainConfig {
return ChainConfig{
ChainID: "msg-chain-1",
Bech32Prefix: "msg",
CoinType: 118,
GasPrices: "1000000000attoMSG",
GasAdjustment: 1.3,
GasLow: "1000000000attoMSG",
GasAvg: "1000000000attoMSG",
GasHigh: "1000000000attoMSG",
BlockTime: 5 * time.Second,
MsgDecimals: 18,
}
}
// ClientConn 连接配置
type ClientConn struct {
RPCEndpoint string // Tendermint RPC 地址
RESTEndpoint string // REST API 地址
GRPCEndpoint string // gRPC 地址
Timeout time.Duration
}
// MSGChainClient 核心客户端
type MSGChainClient struct {
config ChainConfig
conn ClientConn
grpcConn *grpc.ClientConn
httpClient *http.Client
bank *BankClient
staking *StakingClient
auth *AuthClient
wasm *WasmClient
gov *GovernanceClient
agentAPI *AgentAPIClient
event *EventClient
ctx context.Context
cancel context.CancelFunc
mu sync.RWMutex
chainID string
limiter *rate.Limiter
keybase keyring.Keyring
}
// NewMSGChainClient 创建客户端实例
func NewMSGChainClient(config ChainConfig, conn ClientConn) (*MSGChainClient, error) {
ctx, cancel := context.WithTimeout(context.Background(), conn.Timeout)
if config == (ChainConfig{}) {
config = DefaultChainConfig()
}
sdkConfig := sdk.GetConfig()
sdkConfig.SetBech32PrefixForAccount(
config.Bech32Prefix,
fmt.Sprintf("%spub", config.Bech32Prefix),
)
sdkConfig.SetCoinType(config.CoinType)
gRPCConn, err := grpc.DialContext(ctx, conn.GRPCEndpoint,
grpc.WithInsecure(), grpc.WithBlock(),
)
if err != nil {
cancel()
return nil, fmt.Errorf("gRPC 连接失败: %w", err)
}
c := &MSGChainClient{
config: config,
conn: conn,
grpcConn: gRPCConn,
httpClient: &http.Client{Timeout: conn.Timeout},
ctx: ctx,
cancel: cancel,
chainID: config.ChainID,
limiter: rate.NewLimiter(rate.Limit(100), 200),
}
c.bank = NewBankClient(c)
c.staking = NewStakingClient(c)
c.auth = NewAuthClient(c)
c.wasm = NewWasmClient(c)
c.gov = NewGovernanceClient(c)
c.agentAPI = NewAgentAPIClient(c)
c.event = NewEventClient(c)
return c, nil
}
// Close 关闭客户端
func (c *MSGChainClient) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
c.cancel()
if c.grpcConn != nil {
return c.grpcConn.Close()
}
return nil
}
// Bank 返回银行模块客户端
func (c *MSGChainClient) Bank() *BankClient { return c.bank }
// Staking 返回质押模块客户端
func (c *MSGChainClient) Staking() *StakingClient { return c.staking }
// Wasm 返回 CosmWasm 客户端
func (c *MSGChainClient) Wasm() *WasmClient { return c.wasm }
// Governance 返回治理模块客户端
func (c *MSGChainClient) Governance() *GovernanceClient { return c.gov }
// AgentAPI 返回 Agent API 客户端
func (c *MSGChainClient) AgentAPI() *AgentAPIClient { return c.agentAPI }
// Event 返回事件监听客户端
func (c *MSGChainClient) Event() *EventClient { return c.event }
3.2 基础查询方法
// GetNodeInfo 获取节点信息
func (c *MSGChainClient) GetNodeInfo(ctx context.Context) (*NodeInfo, error) {
path := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/node_info", c.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := c.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var info NodeInfo
if err := json.NewDecoder(res.Body).Decode(&info); err != nil {
return nil, NewSDKError(ErrTxDecode, "解析节点信息失败", err)
}
return &info, nil
}
// GetLatestBlock 获取最新区块
func (c *MSGChainClient) GetLatestBlock(ctx context.Context) (*Block, error) {
path := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/latest",
c.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := c.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var block Block
if err := json.NewDecoder(res.Body).Decode(&block); err != nil {
return nil, err
}
return &block, nil
}
// GetBlockByHeight 根据高度查询区块
func (c *MSGChainClient) GetBlockByHeight(ctx context.Context, height int64) (*Block, error) {
path := fmt.Sprintf("%s/cosmos/base/tendermint/v1beta1/blocks/%d",
c.conn.RESTEndpoint, height)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := c.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var block Block
if err := json.NewDecoder(res.Body).Decode(&block); err != nil {
return nil, err
}
return &block, nil
}
// GetTxByHash 根据交易哈希查询交易
func (c *MSGChainClient) GetTxByHash(ctx context.Context, hash string) (*Transaction, error) {
path := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", c.conn.RESTEndpoint, hash)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := c.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var tx Transaction
if err := json.NewDecoder(res.Body).Decode(&tx); err != nil {
return nil, err
}
return &tx, nil
}
3.3 错误码与限流
// ErrorCode MSG Chain 错误码
type ErrorCode uint32
const (
ErrOK ErrorCode = 0
ErrTxDecode ErrorCode = 1
ErrInvalidSequence ErrorCode = 2
ErrUnauthorized ErrorCode = 3
ErrInsufficientFunds ErrorCode = 4
ErrUnknownRequest ErrorCode = 5
ErrInvalidAddress ErrorCode = 6
ErrInvalidCoins ErrorCode = 7
ErrOutOfGas ErrorCode = 8
ErrMemoTooLarge ErrorCode = 9
ErrInsufficientFee ErrorCode = 10
ErrTooManySignatures ErrorCode = 11
ErrGasOverflow ErrorCode = 12
ErrNoSignatures ErrorCode = 13
ErrQuantumSignature ErrorCode = 1001 // Dilithium 签名失败
ErrAgentAPIRateLimit ErrorCode = 1002 // Agent API 限流
ErrMPCFailed ErrorCode = 1003 // MPC 签名失败
ErrStubNotImplemented ErrorCode = 1004 // Stub 端点未实现
ErrDARConsensus ErrorCode = 1005 // DAR 共识错误
ErrBadgerDB ErrorCode = 1006 // BadgerDB 存储错误
)
// SDKError SDK 错误封装
type SDKError struct {
Code ErrorCode
Message string
Cause error
}
func (e *SDKError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[MSG:%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[MSG:%d] %s", e.Code, e.Message)
}
func (e *SDKError) Unwrap() error { return e.Cause }
// NewSDKError 创建 SDK 错误
func NewSDKError(code ErrorCode, msg string, cause error) *SDKError {
return &SDKError{Code: code, Message: msg, Cause: cause}
}
// IsErr 判断错误码
func IsErr(err error, code ErrorCode) bool {
var sdkErr *SDKError
if errors.As(err, &sdkErr) {
return sdkErr.Code == code
}
return false
}
// IsInsufficientFee 检查 Gas 费用不足
func IsInsufficientFee(err error) bool { return IsErr(err, ErrInsufficientFee) }
// IsOutOfGas 检查 Gas 耗尽
func IsOutOfGas(err error) bool { return IsErr(err, ErrOutOfGas) }
// WithRateLimit 设置自定义限流(默认 100 req/s,burst 200)
func (c *MSGChainClient) WithRateLimit(rps, burst int) *MSGChainClient {
c.limiter = rate.NewLimiter(rate.Limit(rps), burst)
return c
}
// doRequest 带限流的 HTTP 请求
func (c *MSGChainClient) doRequest(req *http.Request) (*http.Response, error) {
if err := c.limiter.Wait(c.ctx); err != nil {
return nil, NewSDKError(ErrAgentAPIRateLimit, "请求限流", err)
}
res, err := c.httpClient.Do(req)
if err != nil {
return nil, NewSDKError(ErrUnknownRequest, "HTTP 请求失败", err)
}
return res, nil
}
3.4 数据类型
// NodeInfo 节点信息
type NodeInfo struct {
ProtocolVersion struct {
P2P uint64 `json:"p2p"`
Block uint64 `json:"block"`
App uint64 `json:"app"`
} `json:"protocol_version"`
NodeID string `json:"node_id"`
ListenAddr string `json:"listen_addr"`
Network string `json:"network"`
Version string `json:"version"`
Moniker string `json:"moniker"`
}
// Block 区块数据
type Block struct {
BlockID struct {
Hash string `json:"hash"`
PartSetHeader struct {
Total int `json:"total"`
Hash string `json:"hash"`
} `json:"part_set_header"`
} `json:"block_id"`
Block struct {
Header struct {
Version struct {
Block uint64 `json:"block"`
} `json:"version"`
ChainID string `json:"chain_id"`
Height string `json:"height"`
Time time.Time `json:"time"`
LastBlockID struct {
Hash string `json:"hash"`
PartSetHeader struct {
Total int `json:"total"`
Hash string `json:"hash"`
} `json:"part_set_header"`
} `json:"last_block_id"`
LastCommitHash string `json:"last_commit_hash"`
DataHash string `json:"data_hash"`
ValidatorsHash string `json:"validators_hash"`
ProposerAddress string `json:"proposer_address"`
} `json:"header"`
Data struct {
Txs []string `json:"txs"`
} `json:"data"`
} `json:"block"`
}
// Transaction 交易数据
type Transaction struct {
TxResponse struct {
Height string `json:"height"`
TxHash string `json:"txhash"`
Code uint32 `json:"code"`
RawLog string `json:"raw_log"`
GasWanted string `json:"gas_wanted"`
GasUsed string `json:"gas_used"`
Logs []struct {
Events []struct {
Type string `json:"type"`
Attributes []struct {
Key string `json:"key"`
Value string `json:"value"`
} `json:"attributes"`
} `json:"events"`
} `json:"logs"`
Tx struct {
Type string `json:"@type"`
Msgs []sdk.Msg `json:"messages"`
Fee sdktx.Fee `json:"fee"`
Signatures []signing.SignatureV2 `json:"signatures"`
Memo string `json:"memo"`
} `json:"tx"`
} `json:"tx_response"`
}
// Coin 代币类型
type Coin struct {
Denom string `json:"denom"`
Amount string `json:"amount"`
}
func (c Coin) String() string {
return fmt.Sprintf("%s%s", c.Amount, c.Denom)
}
// Coins 代币集合
type Coins []Coin
// Account 账户信息
type Account struct {
Address string `json:"address"`
AccountNumber uint64 `json:"account_number"`
Sequence uint64 `json:"sequence"`
}
4. 账户与钱包管理
4.1 密钥生成(BIP39 + Dilithium-5)
package msgchain
import (
"crypto/rand"
"encoding/hex"
"fmt"
"path/filepath"
"os"
"github.com/cosmos/go-bip39"
"github.com/cosmos/cosmos-sdk/crypto/hd"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
)
// KeyType 密钥类型
type KeyType int
const (
KeyTypeSecp256k1 KeyType = iota // 标准 ECDSA
KeyTypeDilithium5 // 后量子 Dilithium-5
KeyTypeED25519 // Ed25519
)
// GenerateMnemonic 生成 BIP39 助记词
func GenerateMnemonic(bitSize int) (string, error) {
if bitSize != 128 && bitSize != 192 && bitSize != 256 {
return "", fmt.Errorf("熵长度必须为 128/192/256,当前: %d", bitSize)
}
entropy, err := bip39.NewEntropy(bitSize)
if err != nil {
return "", fmt.Errorf("生成熵失败: %w", err)
}
mnemonic, err := bip39.NewMnemonic(entropy)
if err != nil {
return "", fmt.Errorf("生成助记词失败: %w", err)
}
return mnemonic, nil
}
// GenerateDefaultMnemonic 生成 256 bit 助记词
func GenerateDefaultMnemonic() (string, error) {
return GenerateMnemonic(256)
}
// KeyConfig 密钥配置
type KeyConfig struct {
Name string // 密钥名称
Password string // 密码
Mnemonic string // 助记词(为空则生成)
KeyType KeyType // 密钥类型
CoinType uint32 // SLIP-44 类型
Account uint32 // BIP44 账户索引
Index uint32 // BIP44 地址索引
}
// CreateKey 创建密钥
func CreateKey(cfg KeyConfig) (cryptotypes.PrivKey, sdk.AccAddress, error) {
var privKey cryptotypes.PrivKey
var err error
if cfg.Mnemonic == "" {
cfg.Mnemonic, err = GenerateDefaultMnemonic()
if err != nil {
return nil, nil, err
}
}
switch cfg.KeyType {
case KeyTypeDilithium5:
privKey, err = deriveDilithiumKey(cfg.Mnemonic, cfg.CoinType, cfg.Account, cfg.Index)
case KeyTypeSecp256k1:
privKey, err = deriveSecp256k1Key(cfg.Mnemonic, cfg.CoinType, cfg.Account, cfg.Index)
case KeyTypeED25519:
privKey, err = deriveEd25519Key(cfg.Mnemonic, cfg.CoinType, cfg.Account, cfg.Index)
default:
return nil, nil, fmt.Errorf("不支持的密钥类型: %v", cfg.KeyType)
}
if err != nil {
return nil, nil, err
}
addr := sdk.AccAddress(privKey.PubKey().Address())
return privKey, addr, nil
}
// deriveDilithiumKey 通过 BIP39 种子派生 Dilithium-5 密钥
func deriveDilithiumKey(mnemonic string, coinType, account, index uint32) (cryptotypes.PrivKey, error) {
seed := bip39.NewSeed(mnemonic, "")
path := hd.NewFundraiserParams(account, coinType, index)
privKey, err := quantum.DeriveKey(seed, path.String())
if err != nil {
return nil, fmt.Errorf("Dilithium-5 密钥派生失败: %w", err)
}
return privKey, nil
}
// deriveSecp256k1Key 标准 secp256k1 密钥派生
func deriveSecp256k1Key(mnemonic string, coinType, account, index uint32) (cryptotypes.PrivKey, error) {
seed := bip39.NewSeed(mnemonic, "")
master, ch := hd.ComputeMastersFromSeed(seed)
privKeyBytes, err := hd.DerivePrivateKeyForPath(master, ch,
fmt.Sprintf("m/44'/%d'/%d'/0/%d", coinType, account, index))
if err != nil {
return nil, fmt.Errorf("secp256k1 密钥派生失败: %w", err)
}
return &secp256k1.PrivKey{Key: privKeyBytes}, nil
}
// deriveEd25519Key Ed25519 密钥派生
func deriveEd25519Key(mnemonic string, coinType, account, index uint32) (cryptotypes.PrivKey, error) {
seed := bip39.NewSeed(mnemonic, "")
master, ch := hd.ComputeMastersFromSeed(seed)
privKeyBytes, err := hd.DerivePrivateKeyForPath(master, ch,
fmt.Sprintf("m/44'/%d'/%d'/0/%d", coinType, account, index))
if err != nil {
return nil, fmt.Errorf("Ed25519 密钥派生失败: %w", err)
}
return &ed25519.PrivKey{Key: privKeyBytes}, nil
}
// Bech32Address 从公钥生成 Bech32 地址
func Bech32Address(pubKey cryptotypes.PubKey, prefix string) (string, error) {
addr := sdk.AccAddress(pubKey.Address())
return sdk.Bech32ifyAddressBytes(prefix, addr)
}
// ParseAddress 解析 Bech32 地址
func ParseAddress(addrStr string) (sdk.AccAddress, error) {
addr, err := sdk.AccAddressFromBech32(addrStr)
if err != nil {
return nil, NewSDKError(ErrInvalidAddress, fmt.Sprintf("无效地址: %s", addrStr), err)
}
return addr, nil
}
// SignWithDilithium 使用 Dilithium-5 签名
func SignWithDilithium(privKey cryptotypes.PrivKey, msg []byte) ([]byte, error) {
sig, err := privKey.Sign(msg)
if err != nil {
return nil, NewSDKError(ErrQuantumSignature, "Dilithium-5 签名失败", err)
}
return sig, nil
}
4.2 钱包管理器
// WalletManager 钱包管理器——类似 cosmos-sdk 的 keyring
type WalletManager struct {
keybase keyring.Keyring
config ChainConfig
}
// NewWalletManager 创建钱包管理器
func NewWalletManager(config ChainConfig) (*WalletManager, error) {
k, err := keyring.New(
"msg-chain",
keyring.BackendFile,
filepath.Join(os.Getenv("HOME"), ".msg-chain", "keyring"),
os.Stdin,
hd.CreateHDPath(config.CoinType, 0, 0).String(),
)
if err != nil {
return nil, fmt.Errorf("创建 keyring 失败: %w", err)
}
return &WalletManager{keybase: k, config: config}, nil
}
// NewKey 创建新密钥
func (wm *WalletManager) NewKey(name, password string) (string, error) {
mnemonic, err := GenerateDefaultMnemonic()
if err != nil {
return "", err
}
_, addr, err := CreateKey(KeyConfig{
Name: name,
Password: password,
Mnemonic: mnemonic,
KeyType: KeyTypeDilithium5,
CoinType: wm.config.CoinType,
})
if err != nil {
return "", err
}
return Bech32Address(addr, wm.config.Bech32Prefix)
}
// RecoverKey 通过助记词恢复密钥
func (wm *WalletManager) RecoverKey(name, password, mnemonic string) (string, error) {
_, addr, err := CreateKey(KeyConfig{
Name: name,
Password: password,
Mnemonic: mnemonic,
KeyType: KeyTypeDilithium5,
CoinType: wm.config.CoinType,
})
if err != nil {
return "", err
}
return Bech32Address(addr, wm.config.Bech32Prefix)
}
// ListKeys 列出所有密钥
func (wm *WalletManager) ListKeys() ([]WalletKey, error) {
keys, err := wm.keybase.List()
if err != nil {
return nil, err
}
result := make([]WalletKey, 0, len(keys))
for _, k := range keys {
addr, _ := k.GetAddress()
result = append(result, WalletKey{
Name: k.Name,
Address: addr.String(),
})
}
return result, nil
}
// WalletKey 钱包密钥摘要
type WalletKey struct {
Name string `json:"name"`
Address string `json:"address"`
}
5. 银行模块
5.1 BankClient 结构体
// BankClient 银行模块客户端——类似 gaia CLI 的 bank 查询
type BankClient struct {
client *MSGChainClient
}
// NewBankClient 创建银行客户端
func NewBankClient(c *MSGChainClient) *BankClient {
return &BankClient{client: c}
}
// BalanceResponse 余额查询响应
type BalanceResponse struct {
Balance Coin `json:"balance"`
}
// AllBalancesResponse 所有余额查询响应
type AllBalancesResponse struct {
Balances Coins `json:"balances"`
Pagination *PaginationResponse `json:"pagination,omitempty"`
}
// SupplyResponse 供应量查询响应
type SupplyResponse struct {
Supply Coins `json:"supply"`
Pagination *PaginationResponse `json:"pagination,omitempty"`
}
// PaginationResponse 分页响应
type PaginationResponse struct {
NextKey string `json:"next_key"`
Total string `json:"total"`
}
5.2 余额查询与转账
// GetBalance 查询单个代币余额
func (bc *BankClient) GetBalance(ctx context.Context, address, denom string) (*Coin, error) {
path := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s/by_denom?denom=%s",
bc.client.conn.RESTEndpoint, address, denom)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := bc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp BalanceResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp.Balance, nil
}
// GetAllBalances 查询所有余额
func (bc *BankClient) GetAllBalances(ctx context.Context, address string) (Coins, error) {
path := fmt.Sprintf("%s/cosmos/bank/v1beta1/balances/%s",
bc.client.conn.RESTEndpoint, address)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := bc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp AllBalancesResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.Balances, nil
}
// GetMSG 快捷查询 MSG 余额
func (bc *BankClient) GetMSG(ctx context.Context, address string) (*Coin, error) {
return bc.GetBalance(ctx, address, "umsg")
}
// GetTotalSupply 查询总供应量
func (bc *BankClient) GetTotalSupply(ctx context.Context) (Coins, error) {
path := fmt.Sprintf("%s/cosmos/bank/v1beta1/supply",
bc.client.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := bc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp SupplyResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.Supply, nil
}
// SendMsg 转账消息
type SendMsg struct {
FromAddress string `json:"from_address"`
ToAddress string `json:"to_address"`
Amount Coins `json:"amount"`
}
// Send 发送转账交易
func (bc *BankClient) Send(ctx context.Context, fromAddr, toAddr string,
amount Coins, privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &banktypes.MsgSend{
FromAddress: fromAddr,
ToAddress: toAddr,
Amount: amount,
}
return bc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// BatchSend 批量转账
func (bc *BankClient) BatchSend(ctx context.Context, fromAddr string,
transfers []Transfer, privKey cryptotypes.PrivKey) (*Transaction, error) {
msgs := make([]sdk.Msg, len(transfers))
for i, t := range transfers {
msgs[i] = &banktypes.MsgSend{
FromAddress: fromAddr,
ToAddress: t.ToAddress,
Amount: t.Amount,
}
}
return bc.client.BuildAndBroadcastTx(ctx, msgs, privKey)
}
// Transfer 转账结构
type Transfer struct {
ToAddress string
Amount Coins
Memo string
}
5.3 BankClient 接口
type BankClientInterface interface {
GetBalance(ctx context.Context, address, denom string) (*Coin, error)
GetAllBalances(ctx context.Context, address string) (Coins, error)
GetMSG(ctx context.Context, address string) (*Coin, error)
GetTotalSupply(ctx context.Context) (Coins, error)
Send(ctx context.Context, fromAddr, toAddr string, amount Coins, privKey cryptotypes.PrivKey) (*Transaction, error)
BatchSend(ctx context.Context, fromAddr string, transfers []Transfer, privKey cryptotypes.PrivKey) (*Transaction, error)
}
6. 质押模块
6.1 StakingClient 与验证者查询
// StakingClient 质押模块客户端
type StakingClient struct {
client *MSGChainClient
}
func NewStakingClient(c *MSGChainClient) *StakingClient {
return &StakingClient{client: c}
}
// Validator 验证者
type Validator struct {
OperatorAddress string `json:"operator_address"`
Jailed bool `json:"jailed"`
Status string `json:"status"`
Tokens string `json:"tokens"`
DelegatorShares string `json:"delegator_shares"`
Description struct {
Moniker string `json:"moniker"`
Website string `json:"website"`
Details string `json:"details"`
} `json:"description"`
Commission struct {
CommissionRates struct {
Rate string `json:"rate"`
MaxRate string `json:"max_rate"`
MaxChangeRate string `json:"max_change_rate"`
} `json:"commission_rates"`
} `json:"commission"`
MinSelfDelegation string `json:"min_self_delegation"`
}
// GetValidators 获取验证者列表
func (sc *StakingClient) GetValidators(ctx context.Context, status string) ([]Validator, error) {
path := fmt.Sprintf("%s/cosmos/staking/v1beta1/validators?status=%s",
sc.client.conn.RESTEndpoint, status)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := sc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Validators []Validator `json:"validators"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.Validators, nil
}
// GetActiveValidators 获取活跃验证者
func (sc *StakingClient) GetActiveValidators(ctx context.Context) ([]Validator, error) {
return sc.GetValidators(ctx, "BOND_STATUS_BONDED")
}
6.2 委托与收益
// Delegation 委托信息
type Delegation struct {
DelegatorAddress string `json:"delegator_address"`
ValidatorAddress string `json:"validator_address"`
Shares string `json:"shares"`
Balance Coin `json:"balance"`
}
// GetDelegations 查询委托列表
func (sc *StakingClient) GetDelegations(ctx context.Context, delegatorAddr string) ([]Delegation, error) {
path := fmt.Sprintf("%s/cosmos/staking/v1beta1/delegations/%s",
sc.client.conn.RESTEndpoint, delegatorAddr)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := sc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
DelegationResponses []Delegation `json:"delegation_responses"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.DelegationResponses, nil
}
// RewardsResponse 收益查询响应
type RewardsResponse struct {
Rewards []struct {
ValidatorAddress string `json:"validator_address"`
Reward Coins `json:"reward"`
} `json:"rewards"`
Total Coins `json:"total"`
}
// GetRewards 查询委托收益
func (sc *StakingClient) GetRewards(ctx context.Context, delegatorAddr string) (*RewardsResponse, error) {
path := fmt.Sprintf("%s/cosmos/distribution/v1beta1/delegators/%s/rewards",
sc.client.conn.RESTEndpoint, delegatorAddr)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := sc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp RewardsResponse
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp, nil
}
// Delegate 委托代币
func (sc *StakingClient) Delegate(ctx context.Context, delegatorAddr, validatorAddr string,
amount Coin, privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &staketypes.MsgDelegate{
DelegatorAddress: delegatorAddr,
ValidatorAddress: validatorAddr,
Amount: amount,
}
return sc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// Unbond 解委托
func (sc *StakingClient) Unbond(ctx context.Context, delegatorAddr, validatorAddr string,
amount Coin, privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &staketypes.MsgUndelegate{
DelegatorAddress: delegatorAddr,
ValidatorAddress: validatorAddr,
Amount: amount,
}
return sc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// Redelegate 转委托
func (sc *StakingClient) Redelegate(ctx context.Context, delegatorAddr, srcValidator, dstValidator string,
amount Coin, privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &staketypes.MsgBeginRedelegate{
DelegatorAddress: delegatorAddr,
ValidatorSrcAddress: srcValidator,
ValidatorDstAddress: dstValidator,
Amount: amount,
}
return sc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
7. 合约查询
7.1 WasmClient 结构体
CosmWasm 合约查询通过 REST 或 gRPC 进行。参考 pkg/wasm/real_vm.go 和 pkg/wasm/core_contracts_e2e_test.go 的实现模式。
// WasmClient CosmWasm 合约查询客户端
type WasmClient struct {
client *MSGChainClient
}
func NewWasmClient(c *MSGChainClient) *WasmClient {
return &WasmClient{client: c}
}
// CodeInfo 代码信息
type CodeInfo struct {
CodeID string `json:"code_id"`
Creator string `json:"creator"`
DataHash string `json:"data_hash"`
InstantiatePermission struct {
Permission string `json:"permission"`
Addresses []string `json:"addresses"`
} `json:"instantiate_permission"`
}
// ContractInfo 合约信息
type ContractInfo struct {
Address string `json:"address"`
Creator string `json:"creator"`
Admin string `json:"admin"`
CodeID string `json:"code_id"`
Label string `json:"label"`
}
7.2 智能合约状态查询
// SmartContractState 查询合约智能状态
func (wc *WasmClient) SmartContractState(ctx context.Context,
contractAddr string, queryMsg interface{}) ([]byte, error) {
queryJSON, err := json.Marshal(queryMsg)
if err != nil {
return nil, fmt.Errorf("序列化查询消息失败: %w", err)
}
encodedQuery := base64.StdEncoding.EncodeToString(queryJSON)
path := fmt.Sprintf("%s/cosmwasm/wasm/v1/contract/%s/smart?query=%s",
wc.client.conn.RESTEndpoint, contractAddr,
url.QueryEscape(encodedQuery))
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := wc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Data struct {
Raw string `json:"raw"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(resp.Data.Raw)
}
// SmartQuery 带类型反序列化的智能查询
func (wc *WasmClient) SmartQuery(ctx context.Context, contractAddr string,
queryMsg interface{}, result interface{}) error {
data, err := wc.SmartContractState(ctx, contractAddr, queryMsg)
if err != nil {
return err
}
return json.Unmarshal(data, result)
}
// RawContractState 查询合约原始状态
func (wc *WasmClient) RawContractState(ctx context.Context,
contractAddr string, key []byte) ([]byte, error) {
encodedKey := hex.EncodeToString(key)
path := fmt.Sprintf("%s/cosmwasm/wasm/v1/contract/%s/raw?key=%s",
wc.client.conn.RESTEndpoint, contractAddr, encodedKey)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := wc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Data struct {
Raw string `json:"raw"`
} `json:"data"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return base64.StdEncoding.DecodeString(resp.Data.Raw)
}
7.3 代码与合约信息查询
// GetCodeInfo 获取代码信息
func (wc *WasmClient) GetCodeInfo(ctx context.Context, codeID uint64) (*CodeInfo, error) {
path := fmt.Sprintf("%s/cosmwasm/wasm/v1/code/%d",
wc.client.conn.RESTEndpoint, codeID)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := wc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
CodeInfo CodeInfo `json:"code_info"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp.CodeInfo, nil
}
// GetContractInfo 获取合约信息
func (wc *WasmClient) GetContractInfo(ctx context.Context, contractAddr string) (*ContractInfo, error) {
path := fmt.Sprintf("%s/cosmwasm/wasm/v1/contract/%s",
wc.client.conn.RESTEndpoint, contractAddr)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := wc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
ContractInfo ContractInfo `json:"contract_info"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp.ContractInfo, nil
}
8. 合约执行
8.1 交易构建与签名
参考 pkg/quantum/transaction_executor.go 中的实现模式。
// BuildAndBroadcastTx 构建、签名并广播交易
func (c *MSGChainClient) BuildAndBroadcastTx(ctx context.Context,
msgs []sdk.Msg, privKey cryptotypes.PrivKey) (*Transaction, error) {
// 构建交易
txBuilder := c.createTxBuilder()
if err := txBuilder.SetMsgs(msgs...); err != nil {
return nil, NewSDKError(ErrTxDecode, "设置消息失败", err)
}
txBuilder.SetMemo("")
txBuilder.SetFeeAmount(sdk.NewCoins(sdk.NewCoin("umsg", sdk.NewInt(5000))))
txBuilder.SetGasLimit(200000)
// 签名
signMode := signing.SignMode_SIGN_MODE_DIRECT
signBytes := authtx.StdSignBytes(c.chainID, 0, 0,
txBuilder.GetTx().GetTimeoutHeight(), txBuilder.GetTx().GetFee(),
txBuilder.GetTx().GetMsgs(), txBuilder.GetTx().GetMemo())
sig, err := privKey.Sign(signBytes)
if err != nil {
return nil, NewSDKError(ErrQuantumSignature, "签名失败", err)
}
txBuilder.SetSignatures(signing.SignatureV2{
PubKey: privKey.PubKey(),
Data: &signing.SingleSignatureData{
SignMode: signMode,
Signature: sig,
},
Sequence: 0,
})
txBytes, err := c.encodingConfig.TxConfig.TxEncoder()(txBuilder.GetTx())
if err != nil {
return nil, err
}
return c.BroadcastTx(ctx, txBytes)
}
func (c *MSGChainClient) createTxBuilder() client.TxBuilder {
return c.encodingConfig.TxConfig.NewTxBuilder()
}
8.2 广播交易
// BroadcastMode 广播模式
type BroadcastMode string
const (
BroadcastAsync BroadcastMode = "BROADCAST_MODE_ASYNC" // 异步
BroadcastSync BroadcastMode = "BROADCAST_MODE_SYNC" // 同步,等待 CheckTx
BroadcastCommit BroadcastMode = "BROADCAST_MODE_BLOCK" // 阻塞直到上链
)
// BroadcastTx 广播已签名交易
func (c *MSGChainClient) BroadcastTx(ctx context.Context,
txBytes []byte) (*Transaction, error) {
return c.BroadcastTxWithMode(ctx, txBytes, BroadcastSync)
}
// BroadcastTxWithMode 以指定模式广播
func (c *MSGChainClient) BroadcastTxWithMode(ctx context.Context,
txBytes []byte, mode BroadcastMode) (*Transaction, error) {
reqBody := struct {
TxBytes string `json:"tx_bytes"`
Mode BroadcastMode `json:"mode"`
}{
TxBytes: base64.StdEncoding.EncodeToString(txBytes),
Mode: mode,
}
body, _ := json.Marshal(reqBody)
path := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs", c.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
res, err := c.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var tx Transaction
if err := json.NewDecoder(res.Body).Decode(&tx); err != nil {
return nil, err
}
if tx.TxResponse.Code != 0 {
return &tx, NewSDKError(ErrorCode(tx.TxResponse.Code),
tx.TxResponse.RawLog, nil)
}
return &tx, nil
}
8.3 Gas 估算
// SimulateTx 模拟交易(估算 Gas)
func (c *MSGChainClient) SimulateTx(ctx context.Context, msgs []sdk.Msg,
privKey cryptotypes.PrivKey) (uint64, error) {
txBuilder := c.createTxBuilder()
if err := txBuilder.SetMsgs(msgs...); err != nil {
return 0, err
}
txBytes, _ := c.encodingConfig.TxConfig.TxEncoder()(txBuilder.GetTx())
reqBody := struct {
TxBytes string `json:"tx_bytes"`
}{
TxBytes: base64.StdEncoding.EncodeToString(txBytes),
}
body, _ := json.Marshal(reqBody)
path := fmt.Sprintf("%s/cosmos/tx/v1beta1/simulate", c.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
res, err := c.doRequest(req)
if err != nil {
return 0, err
}
defer res.Body.Close()
var simResp struct {
GasInfo struct {
GasWanted uint64 `json:"gas_wanted"`
GasUsed uint64 `json:"gas_used"`
} `json:"gas_info"`
}
if err := json.NewDecoder(res.Body).Decode(&simResp); err != nil {
return 0, err
}
return uint64(float64(simResp.GasInfo.GasUsed) * c.config.GasAdjustment), nil
}
8.4 合约执行
// ExecuteContract 执行合约
func (wc *WasmClient) ExecuteContract(ctx context.Context, contractAddr, sender string,
execMsg interface{}, funds Coins, privKey cryptotypes.PrivKey) (*Transaction, error) {
msgBytes, _ := json.Marshal(execMsg)
msg := &wasmtypes.MsgExecuteContract{
Sender: sender,
Contract: contractAddr,
Msg: msgBytes,
Funds: funds,
}
return wc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// InstantiateContract 实例化合约
func (wc *WasmClient) InstantiateContract(ctx context.Context, codeID uint64,
creator, admin string, initMsg interface{}, label string,
funds Coins, privKey cryptotypes.PrivKey) (*Transaction, error) {
msgBytes, _ := json.Marshal(initMsg)
msg := &wasmtypes.MsgInstantiateContract{
Sender: creator,
Admin: admin,
CodeID: codeID,
Label: label,
Msg: msgBytes,
Funds: funds,
}
return wc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// MigrateContract 迁移合约
func (wc *WasmClient) MigrateContract(ctx context.Context, contractAddr, admin string,
newCodeID uint64, migrateMsg interface{}, privKey cryptotypes.PrivKey) (*Transaction, error) {
msgBytes, _ := json.Marshal(migrateMsg)
msg := &wasmtypes.MsgMigrateContract{
Sender: admin,
Contract: contractAddr,
CodeID: newCodeID,
Msg: msgBytes,
}
return wc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
8.5 交易收据解析
// TxReceipt 交易收据
type TxReceipt struct {
Hash string `json:"hash"`
Height string `json:"height"`
GasWanted string `json:"gas_wanted"`
GasUsed string `json:"gas_used"`
Code uint32 `json:"code"`
Log string `json:"log"`
Events []Event `json:"events"`
}
// Event 链上事件
type Event struct {
Type string `json:"type"`
Attributes map[string]string `json:"attributes"`
}
// ParseTxResponse 解析交易响应中的事件
func ParseTxResponse(tx *Transaction) (*TxReceipt, error) {
receipt := &TxReceipt{
Hash: tx.TxResponse.TxHash,
Height: tx.TxResponse.Height,
GasWanted: tx.TxResponse.GasWanted,
GasUsed: tx.TxResponse.GasUsed,
Code: tx.TxResponse.Code,
Log: tx.TxResponse.RawLog,
}
for _, log := range tx.TxResponse.Logs {
for _, event := range log.Events {
evt := Event{
Type: event.Type,
Attributes: make(map[string]string),
}
for _, attr := range event.Attributes {
evt.Attributes[attr.Key] = attr.Value
}
receipt.Events = append(receipt.Events, evt)
}
}
return receipt, nil
}
// FindEvents 根据类型查找事件
func (r *TxReceipt) FindEvents(eventType string) []Event {
var result []Event
for _, e := range r.Events {
if e.Type == eventType {
result = append(result, e)
}
}
return result
}
9. Agent API 客户端
Agent API 是 MSG Chain 的特有功能,参考 pkg/quantum/agent_api.go 中的路由注册实现。API 分为三类路由:公共查询(wrapPublic)、受保护操作(wrapProtected)和 Stub 端点。
9.1 AgentAPIClient 结构体
// AgentAPIClient Agent API 客户端
type AgentAPIClient struct {
client *MSGChainClient
baseURL string
apiKey string
stubMode bool
}
func NewAgentAPIClient(c *MSGChainClient) *AgentAPIClient {
return &AgentAPIClient{
client: c,
baseURL: c.conn.RESTEndpoint,
}
}
// WithAPIKey 设置 API 密钥(受保护路由需要)
func (a *AgentAPIClient) WithAPIKey(key string) *AgentAPIClient {
a.apiKey = key
return a
}
// WithStubMode 启用 Stub 模式(测试用)
func (a *AgentAPIClient) WithStubMode(enabled bool) *AgentAPIClient {
a.stubMode = enabled
return a
}
9.2 公共查询路由
Agent API 公共路由通过 wrapPublic 中间件注册,无需认证:
GET /agent/v1/query/account/{address}
GET /agent/v1/query/balance/{address}
GET /agent/v1/query/balances
GET /agent/v1/query/tx/{hash}
GET /agent/v1/query/block/{height}
GET /agent/v1/query/blocks/range
GET /agent/v1/events/history
GET /agent/v1/events/subscribe (WebSocket 升级)
GET /agent/v1/oracle/price
// QueryAccount 查询账户信息
func (a *AgentAPIClient) QueryAccount(ctx context.Context,
address string) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/account/%s", a.baseURL, address)
return a.publicGet(ctx, path)
}
// QueryBalance 查询余额
func (a *AgentAPIClient) QueryBalance(ctx context.Context,
address string) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/balance/%s", a.baseURL, address)
return a.publicGet(ctx, path)
}
// QueryBalances 查询所有余额
func (a *AgentAPIClient) QueryBalances(ctx context.Context) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/balances", a.baseURL)
return a.publicGet(ctx, path)
}
// QueryTx 查询交易
func (a *AgentAPIClient) QueryTx(ctx context.Context,
hash string) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/tx/%s", a.baseURL, hash)
return a.publicGet(ctx, path)
}
// QueryBlock 查询区块
func (a *AgentAPIClient) QueryBlock(ctx context.Context,
height int64) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/block/%d", a.baseURL, height)
return a.publicGet(ctx, path)
}
// QueryBlocksRange 查询区块范围
func (a *AgentAPIClient) QueryBlocksRange(ctx context.Context,
from, to int64) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/query/blocks/range?from=%d&to=%d",
a.baseURL, from, to)
return a.publicGet(ctx, path)
}
// QueryEventsHistory 查询事件历史
func (a *AgentAPIClient) QueryEventsHistory(ctx context.Context) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/events/history", a.baseURL)
return a.publicGet(ctx, path)
}
// QueryOraclePrice 查询预言机价格
func (a *AgentAPIClient) QueryOraclePrice(ctx context.Context) (map[string]interface{}, error) {
path := fmt.Sprintf("%s/agent/v1/oracle/price", a.baseURL)
return a.publicGet(ctx, path)
}
// publicGet 公共 GET 请求
func (a *AgentAPIClient) publicGet(ctx context.Context,
path string) (map[string]interface{}, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
if a.stubMode {
req.Header.Set("X-MSG-Stub", "true")
}
res, err := a.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
9.3 受保护路由
受保护路由通过 wrapProtected 中间件注册,需要 Authorization: Bearer <apiKey> 头:
POST /agent/v1/wallet/
POST /agent/v1/mpc/sign
POST /agent/v1/payment/session
// WalletCreate 创建钱包
func (a *AgentAPIClient) WalletCreate(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.protectedPost(ctx, "/agent/v1/wallet/", params)
}
// MPCSignRequest MPC 签名请求
type MPCSignRequest struct {
SessionID string `json:"session_id"`
Message string `json:"message"`
KeyShareID string `json:"key_share_id"`
Threshold int `json:"threshold"`
Participants []string `json:"participants"`
}
// MPCSign MPC 多方签名
func (a *AgentAPIClient) MPCSign(ctx context.Context,
params MPCSignRequest) (map[string]interface{}, error) {
return a.protectedPost(ctx, "/agent/v1/mpc/sign", params)
}
// PaymentSessionRequest 支付会话请求
type PaymentSessionRequest struct {
From string `json:"from"`
To string `json:"to"`
Amount Coin `json:"amount"`
Description string `json:"description,omitempty"`
WebhookURL string `json:"webhook_url,omitempty"`
}
// PaymentSession 创建支付会话
func (a *AgentAPIClient) PaymentSession(ctx context.Context,
params PaymentSessionRequest) (map[string]interface{}, error) {
return a.protectedPost(ctx, "/agent/v1/payment/session", params)
}
// protectedPost 受保护 POST 请求
func (a *AgentAPIClient) protectedPost(ctx context.Context,
path string, body interface{}) (map[string]interface{}, error) {
bodyBytes, _ := json.Marshal(body)
req, _ := http.NewRequestWithContext(ctx, "POST", path,
bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
if a.apiKey != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.apiKey))
}
if a.stubMode {
req.Header.Set("X-MSG-Stub", "true")
}
res, err := a.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, NewSDKError(ErrUnauthorized, "Agent API 请求被拒绝", nil)
}
var result map[string]interface{}
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
9.4 WebSocket 事件订阅
// SubscribeEvents 通过 WebSocket 订阅链上事件
func (a *AgentAPIClient) SubscribeEvents(ctx context.Context,
eventTypes []string) (<-chan Event, error) {
wsURL := strings.Replace(a.baseURL, "http", "ws", 1)
url := fmt.Sprintf("%s/agent/v1/events/subscribe", wsURL)
if len(eventTypes) > 0 {
typesJSON, _ := json.Marshal(eventTypes)
url = fmt.Sprintf("%s?types=%s", url, url.QueryEscape(string(typesJSON)))
}
dialer := websocket.DefaultDialer
conn, _, err := dialer.DialContext(ctx, url, nil)
if err != nil {
return nil, fmt.Errorf("WebSocket 连接失败: %w", err)
}
eventCh := make(chan Event, 100)
go func() {
defer close(eventCh)
defer conn.Close()
for {
select {
case <-ctx.Done():
return
default:
_, message, err := conn.ReadMessage()
if err != nil {
return
}
var event Event
if err := json.Unmarshal(message, &event); err != nil {
continue
}
eventCh <- event
}
}
}()
return eventCh, nil
}
9.5 Stub 端点
以下端点仅在 X-MSG-Stub=true 头时可用,用于开发和测试:
POST /agent/v1/defi/swap // X-MSG-Stub=true
POST /agent/v1/defi/liquidity // X-MSG-Stub=true
POST /agent/v1/defi/stake // X-MSG-Stub=true
POST /agent/v1/registry/register // X-MSG-Stub=true
POST /agent/v1/registry/discover // X-MSG-Stub=true
POST /agent/v1/registry/list // X-MSG-Stub=true
POST /agent/v1/bridge/transfer // X-MSG-Stub=true
// AgentRegistration AI Agent 注册信息
type AgentRegistration struct {
Name string `json:"name"`
Description string `json:"description"`
Endpoint string `json:"endpoint"`
Owner string `json:"owner"`
Fee Coin `json:"fee,omitempty"`
Tags []string `json:"tags,omitempty"`
Permissions []string `json:"permissions,omitempty"`
}
// DeFiSwap DeFi 交换 (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) DeFiSwap(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/defi/swap", params)
}
// DeFiLiquidity 添加流动性 (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) DeFiLiquidity(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/defi/liquidity", params)
}
// DeFiStake DeFi 质押 (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) DeFiStake(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/defi/stake", params)
}
// RegistryRegister 注册 AI Agent (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) RegistryRegister(ctx context.Context,
params AgentRegistration) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/registry/register", params)
}
// RegistryDiscover 发现 AI Agent (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) RegistryDiscover(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/registry/discover", params)
}
// RegistryList 列出所有 Agent (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) RegistryList(ctx context.Context) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/registry/list", nil)
}
// BridgeTransfer 跨链桥转账 (Stub)
// X-MSG-Stub=true
func (a *AgentAPIClient) BridgeTransfer(ctx context.Context,
params map[string]interface{}) (map[string]interface{}, error) {
return a.stubPost(ctx, "/agent/v1/bridge/transfer", params)
}
// stubPost Stub POST 请求
func (a *AgentAPIClient) stubPost(ctx context.Context,
route string, body interface{}) (map[string]interface{}, error) {
if !a.stubMode {
return nil, NewSDKError(ErrStubNotImplemented,
fmt.Sprintf("Stub 端点 %s 需要设置 X-MSG-Stub=true", route), nil)
}
return a.protectedPost(ctx, fmt.Sprintf("%s%s", a.baseURL, route), body)
}
10. 治理操作
10.1 GovernanceClient
// GovernanceClient 治理模块客户端
type GovernanceClient struct {
client *MSGChainClient
}
func NewGovernanceClient(c *MSGChainClient) *GovernanceClient {
return &GovernanceClient{client: c}
}
// Proposal 提案
type Proposal struct {
ProposalID string `json:"proposal_id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
FinalTallyResult struct {
Yes string `json:"yes"`
Abstain string `json:"abstain"`
No string `json:"no"`
NoWithVeto string `json:"no_with_veto"`
} `json:"final_tally_result"`
SubmitTime string `json:"submit_time"`
VotingStartTime string `json:"voting_start_time"`
VotingEndTime string `json:"voting_end_time"`
TotalDeposit Coins `json:"total_deposit"`
Proposer string `json:"proposer"`
}
// VoteOption 投票选项
type VoteOption string
const (
VoteYes VoteOption = "VOTE_OPTION_YES"
VoteNo VoteOption = "VOTE_OPTION_NO"
VoteNoWithVeto VoteOption = "VOTE_OPTION_NO_WITH_VETO"
VoteAbstain VoteOption = "VOTE_OPTION_ABSTAIN"
)
10.2 提案与投票
// GetProposals 获取提案列表
func (gc *GovernanceClient) GetProposals(ctx context.Context) ([]Proposal, error) {
path := fmt.Sprintf("%s/cosmos/gov/v1beta1/proposals",
gc.client.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := gc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Proposals []Proposal `json:"proposals"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.Proposals, nil
}
// GetProposal 获取单个提案
func (gc *GovernanceClient) GetProposal(ctx context.Context,
proposalID uint64) (*Proposal, error) {
path := fmt.Sprintf("%s/cosmos/gov/v1beta1/proposals/%d",
gc.client.conn.RESTEndpoint, proposalID)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := gc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Proposal Proposal `json:"proposal"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp.Proposal, nil
}
// Vote 投票
func (gc *GovernanceClient) Vote(ctx context.Context, voterAddr string,
proposalID uint64, option VoteOption,
privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &govtypes.MsgVote{
ProposalID: proposalID,
Voter: voterAddr,
Option: govtypes.VoteOptionFromString(string(option)),
}
return gc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// GetTally 获取计票结果
func (gc *GovernanceClient) GetTally(ctx context.Context,
proposalID uint64) (*ProposalFinalTally, error) {
path := fmt.Sprintf("%s/cosmos/gov/v1beta1/proposals/%d/tally",
gc.client.conn.RESTEndpoint, proposalID)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := gc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Tally ProposalFinalTally `json:"tally"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return &resp.Tally, nil
}
// ProposalFinalTally 最终计票
type ProposalFinalTally struct {
Yes string `json:"yes"`
Abstain string `json:"abstain"`
No string `json:"no"`
NoWithVeto string `json:"no_with_veto"`
}
// SubmitProposal 提交文本提案
func (gc *GovernanceClient) SubmitProposal(ctx context.Context,
proposerAddr, title, description string, initialDeposit Coins,
privKey cryptotypes.PrivKey) (*Transaction, error) {
content := &govtypes.TextProposal{
Title: title,
Description: description,
}
msg, err := govtypes.NewMsgSubmitProposal(content, initialDeposit, proposerAddr)
if err != nil {
return nil, err
}
return gc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// Deposit 提案存款
func (gc *GovernanceClient) Deposit(ctx context.Context,
depositorAddr string, proposalID uint64, amount Coins,
privKey cryptotypes.PrivKey) (*Transaction, error) {
msg := &govtypes.MsgDeposit{
ProposalID: proposalID,
Depositor: depositorAddr,
Amount: amount,
}
return gc.client.BuildAndBroadcastTx(ctx, []sdk.Msg{msg}, privKey)
}
// CommunityPool 查询社区池余额
func (gc *GovernanceClient) CommunityPool(ctx context.Context) (Coins, error) {
path := fmt.Sprintf("%s/cosmos/distribution/v1beta1/community_pool",
gc.client.conn.RESTEndpoint)
req, _ := http.NewRequestWithContext(ctx, "GET", path, nil)
res, err := gc.client.doRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var resp struct {
Pool struct {
Pool Coins `json:"pool"`
} `json:"pool"`
}
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
return nil, err
}
return resp.Pool.Pool, nil
}
11. 事件监听
11.1 EventClient
基于 WebSocket 的事件监听实现,参考 pkg/indexer/event_listener.go 和 pkg/quantum/tendermint_rpc.go。
// EventClient 事件监听客户端
type EventClient struct {
client *MSGChainClient
mu sync.Mutex
subscriptions map[string]context.CancelFunc
}
func NewEventClient(c *MSGChainClient) *EventClient {
return &EventClient{
client: c,
subscriptions: make(map[string]context.CancelFunc),
}
}
// EventSubscription 事件订阅
type EventSubscription struct {
ID string
Events <-chan Event
cancel context.CancelFunc
}
// SubscribeNewBlocks 订阅新区块
func (ec *EventClient) SubscribeNewBlocks(ctx context.Context) (*EventSubscription, error) {
return ec.subscribe(ctx, "tm.event='NewBlock'")
}
// SubscribeTxs 订阅交易
func (ec *EventClient) SubscribeTxs(ctx context.Context) (*EventSubscription, error) {
return ec.subscribe(ctx, "tm.event='Tx'")
}
// subscribe WebSocket 订阅
func (ec *EventClient) subscribe(ctx context.Context,
query string) (*EventSubscription, error) {
wsURL := strings.Replace(ec.client.conn.RPCEndpoint, "http", "ws", 1)
dialer := websocket.DefaultDialer
conn, _, err := dialer.DialContext(ctx, fmt.Sprintf("%s/websocket", wsURL), nil)
if err != nil {
return nil, fmt.Errorf("WebSocket 连接失败: %w", err)
}
subID := fmt.Sprintf("sub-%d", time.Now().UnixNano())
eventCh := make(chan Event, 200)
subCtx, cancel := context.WithCancel(ctx)
ec.mu.Lock()
ec.subscriptions[subID] = cancel
ec.mu.Unlock()
// 发送订阅请求
subscribeReq := map[string]interface{}{
"jsonrpc": "2.0",
"method": "subscribe",
"id": subID,
"params": map[string]interface{}{"query": query},
}
if err := conn.WriteJSON(subscribeReq); err != nil {
cancel()
conn.Close()
return nil, err
}
go func() {
defer func() {
conn.Close()
ec.mu.Lock()
delete(ec.subscriptions, subID)
ec.mu.Unlock()
}()
for {
select {
case <-subCtx.Done():
return
default:
_, msg, err := conn.ReadMessage()
if err != nil {
return
}
var wsMsg struct {
Result struct {
Data struct {
Type string `json:"type"`
Value json.RawMessage `json:"value"`
} `json:"data"`
Events map[string][]string `json:"events"`
} `json:"result"`
}
if err := json.Unmarshal(msg, &wsMsg); err != nil {
continue
}
event := Event{
Type: wsMsg.Result.Data.Type,
Attributes: make(map[string]string),
}
for key, values := range wsMsg.Result.Events {
if len(values) > 0 {
event.Attributes[key] = values[0]
}
}
select {
case eventCh <- event:
default:
}
}
}
}()
return &EventSubscription{
ID: subID,
Events: eventCh,
cancel: cancel,
}, nil
}
// Unsubscribe 取消订阅
func (ec *EventClient) Unsubscribe(sub *EventSubscription) {
if sub != nil && sub.cancel != nil {
sub.cancel()
}
}
12. 完整示例
12.1 cmd/balance-checker/main.go——查询余额
// cmd/balance-checker/main.go
// 查询多个 MSG Chain 地址的余额
// go run ./cmd/balance-checker <addr1> [addr2 ...]
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/msg-chain/go-sdk/msgchain"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintf(os.Stderr, "用法: %s <address1> [address2 ...]\n", os.Args[0])
os.Exit(1)
}
addresses := os.Args[1:]
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cl, err := msgchain.NewMSGChainClient(
msgchain.DefaultChainConfig(),
msgchain.ClientConn{
RPCEndpoint: "http://localhost:26657",
RESTEndpoint: "http://localhost:1317",
GRPCEndpoint: "localhost:9090",
Timeout: 10 * time.Second,
},
)
if err != nil {
fmt.Fprintf(os.Stderr, "创建客户端失败: %v\n", err)
os.Exit(1)
}
defer cl.Close()
fmt.Println("═══════════════════════════════════════")
fmt.Println(" MSG Chain 余额查询工具")
fmt.Println("═══════════════════════════════════════")
for _, addr := range addresses {
balances, err := cl.Bank().GetAllBalances(ctx, addr)
if err != nil {
fmt.Printf(" %s -> 错误: %v\n", addr, err)
continue
}
fmt.Printf(" %s\n", addr)
for _, c := range balances {
fmt.Printf(" %s\n", c.String())
}
}
}
go.mod:
module github.com/your-org/balance-checker
go 1.21
require github.com/msg-chain/go-sdk v0.2.0
12.2 cmd/agent-bot/main.go——AI Agent 监控机器人
// cmd/agent-bot/main.go
// AI Agent 监控链上事件并自动响应
// go run ./cmd/agent-bot --api-key <key>
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/msg-chain/go-sdk/msgchain"
)
var (
apiKey = flag.String("api-key", "", "Agent API 密钥")
rpcAddr = flag.String("rpc", "http://localhost:26657", "RPC 地址")
restAddr = flag.String("rest", "http://localhost:1317", "REST 地址")
grpcAddr = flag.String("grpc", "localhost:9090", "gRPC 地址")
)
func main() {
flag.Parse()
if *apiKey == "" {
log.Fatal("必须设置 --api-key")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cl, err := msgchain.NewMSGChainClient(
msgchain.DefaultChainConfig(),
msgchain.ClientConn{
RPCEndpoint: *rpcAddr,
RESTEndpoint: *restAddr,
GRPCEndpoint: *grpcAddr,
Timeout: 30 * time.Second,
},
)
if err != nil {
log.Fatalf("创建客户端失败: %v", err)
}
defer cl.Close()
agentAPI := cl.AgentAPI().WithAPIKey(*apiKey)
// 查询 Oracle 价格
price, err := agentAPI.QueryOraclePrice(ctx)
if err != nil {
log.Printf("Oracle 价格查询失败: %v", err)
} else {
log.Printf("Oracle 价格: %v", price)
}
// 订阅区块事件
sub, err := cl.Event().SubscribeNewBlocks(ctx)
if err != nil {
log.Fatalf("订阅失败: %v", err)
}
log.Println("已订阅新区块,等待事件...")
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
blockCount := 0
for {
select {
case <-sigCh:
log.Println("正在关闭...")
cl.Event().Unsubscribe(sub)
return
case <-sub.Events:
blockCount++
if blockCount%10 == 0 {
log.Printf("已处理 %d 个区块", blockCount)
}
case <-time.After(5 * time.Minute):
log.Println("执行定期任务...")
}
}
}
12.3 cmd/agent-registrar/main.go——注册 AI Agent
// cmd/agent-registrar/main.go
// 在 MSG Chain 上注册 AI Agent(需 Stub 模式)
// go run ./cmd/agent-registrar --stub --name "MyAgent" --endpoint "http://agent:8080" --owner msg1...
package main
import (
"context"
"flag"
"fmt"
"log"
"time"
"github.com/msg-chain/go-sdk/msgchain"
)
func main() {
var (
stub = flag.Bool("stub", false, "Stub 模式")
name = flag.String("name", "", "Agent 名称")
desc = flag.String("desc", "", "描述")
endpoint = flag.String("endpoint", "", "端点 URL")
owner = flag.String("owner", "", "所有者地址")
apiKey = flag.String("api-key", "", "API 密钥")
)
flag.Parse()
if *name == "" || *endpoint == "" || *owner == "" {
log.Fatal("必须设置 --name, --endpoint, --owner")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
cl, err := msgchain.NewMSGChainClient(
msgchain.DefaultChainConfig(),
msgchain.ClientConn{
RESTEndpoint: "http://localhost:1317",
Timeout: 10 * time.Second,
},
)
if err != nil {
log.Fatalf("创建客户端失败: %v", err)
}
defer cl.Close()
agentAPI := cl.AgentAPI().WithStubMode(*stub)
if *apiKey != "" {
agentAPI = agentAPI.WithAPIKey(*apiKey)
}
reg := msgchain.AgentRegistration{
Name: *name,
Description: *desc,
Endpoint: *endpoint,
Owner: *owner,
Tags: []string{"go-sdk", "ai-agent"},
Permissions: []string{"query:balance", "wallet:create"},
}
result, err := agentAPI.RegistryRegister(ctx, reg)
if err != nil {
if msgchain.IsErr(err, msgchain.ErrStubNotImplemented) {
log.Fatalf("需要启用 --stub 模式: %v", err)
}
log.Fatalf("注册失败: %v", err)
}
fmt.Printf("Agent [%s] 注册成功!响应: %v\n", *name, result)
}
12.4 msgchain_client_test.go——测试文件
// msgchain_client_test.go
package msgchain
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefaultChainConfig(t *testing.T) {
cfg := DefaultChainConfig()
assert.Equal(t, "msg-chain-1", cfg.ChainID)
assert.Equal(t, "msg", cfg.Bech32Prefix)
assert.Equal(t, uint32(118), cfg.CoinType)
assert.Equal(t, "1000000000attoMSG", cfg.GasPrices)
assert.Equal(t, 5*time.Second, cfg.BlockTime)
assert.Equal(t, uint8(18), cfg.MsgDecimals)
}
func TestGenerateMnemonic(t *testing.T) {
mnemonic, err := GenerateDefaultMnemonic()
require.NoError(t, err)
assert.NotEmpty(t, mnemonic)
words := strings.Split(mnemonic, " ")
assert.Equal(t, 24, len(words))
}
func TestGenerateMnemonicInvalidBitSize(t *testing.T) {
_, err := GenerateMnemonic(64)
assert.Error(t, err)
}
func TestParseAddress(t *testing.T) {
// MSG Chain 地址以 msg 开头
validAddr := "msg1qypqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqp8r9g"
addr, err := ParseAddress(validAddr)
require.NoError(t, err)
assert.NotNil(t, addr)
_, err = ParseAddress("cosmos1...")
assert.Error(t, err)
}
func TestCoinString(t *testing.T) {
coin := Coin{Denom: "umsg", Amount: "1000000"}
assert.Equal(t, "1000000umsg", coin.String())
}
func TestNewSDKError(t *testing.T) {
err := NewSDKError(ErrInsufficientFunds, "资金不足", nil)
assert.Error(t, err)
assert.True(t, IsErr(err, ErrInsufficientFunds))
var sdkErr *SDKError
assert.ErrorAs(t, err, &sdkErr)
assert.Equal(t, "资金不足", sdkErr.Message)
}
func TestIsInsufficientFee(t *testing.T) {
err := NewSDKError(ErrInsufficientFee, "费用不足", nil)
assert.True(t, IsInsufficientFee(err))
assert.False(t, IsOutOfGas(err))
}
func TestAgentRegistration(t *testing.T) {
reg := AgentRegistration{
Name: "test-agent",
Endpoint: "http://localhost:8080",
Owner: "msg1test...",
Tags: []string{"test"},
}
assert.Equal(t, "test-agent", reg.Name)
assert.Contains(t, reg.Tags, "test")
}
func TestBuildAndBroadcastTx_InvalidParams(t *testing.T) {
cfg := DefaultChainConfig()
conn := ClientConn{
RESTEndpoint: "http://localhost:1317",
Timeout: 5 * time.Second,
}
cl, err := NewMSGChainClient(cfg, conn)
if err != nil {
t.Skip("节点未运行,跳过")
}
defer cl.Close()
_, err = cl.Bank().GetMSG(context.Background(), "invalid")
assert.Error(t, err)
}
func TestBankClientInterface(t *testing.T) {
// 编译期接口检查
var _ BankClientInterface = (*BankClient)(nil)
}
// BenchmarkGenerateMnemonic 基准测试
func BenchmarkGenerateMnemonic(b *testing.B) {
for i := 0; i < b.N; i++ {
GenerateDefaultMnemonic()
}
}
12.5 完整 go.mod 模板
module github.com/your-org/msg-go-sdk-examples
go 1.21
require (
github.com/msg-chain/go-sdk v0.2.0
github.com/cosmos/cosmos-sdk v0.47.10
github.com/CosmWasm/wasmd v0.45.0
github.com/cometbft/cometbft v0.37.5
github.com/gorilla/websocket v1.5.1
github.com/stretchr/testify v1.9.0
golang.org/x/time v0.5.0
)
require (
github.com/99designs/keyring v1.2.2
github.com/CosmWasm/wasmvm v1.5.2
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/cosmos/cosmos-crypto v0.2.0
github.com/cosmos/go-bip39 v1.0.0
github.com/cosmos/gogoproto v1.4.11
github.com/cosmos/ibc-go/v7 v7.4.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0
github.com/dgraph-io/badger/v4 v4.2.0
github.com/gorilla/mux v1.8.1
github.com/pkg/errors v0.9.1
github.com/tyler-smith/go-bip39 v1.1.0
golang.org/x/crypto v0.21.0
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
google.golang.org/grpc v1.62.1
google.golang.org/protobuf v1.33.0
)
附录
A. 快速参考——模块方法速查
| 模块 | 方法 | 描述 |
|---|---|---|
MSGChainClient |
GetNodeInfo, GetLatestBlock, GetBlockByHeight, GetTxByHash |
基础查询 |
MSGChainClient |
SimulateTx, BroadcastTx, BuildAndBroadcastTx |
交易操作 |
BankClient |
GetBalance, GetAllBalances, GetMSG, GetTotalSupply |
余额与供应 |
BankClient |
Send, BatchSend |
转账 |
StakingClient |
GetValidators, GetActiveValidators, GetDelegations |
验证者与委托 |
StakingClient |
GetRewards, Delegate, Unbond, Redelegate |
收益与操作 |
WasmClient |
SmartContractState, SmartQuery, RawContractState |
合约查询 |
WasmClient |
GetCodeInfo, GetContractInfo, ExecuteContract |
合约信息与执行 |
WasmClient |
InstantiateContract, MigrateContract |
合约部署与迁移 |
GovernanceClient |
GetProposals, GetProposal, Vote, GetTally |
提案与投票 |
GovernanceClient |
SubmitProposal, Deposit, CommunityPool |
提交与财政库 |
AgentAPIClient |
QueryAccount, QueryBalance, QueryTx, QueryBlock |
Agent 查询 |
AgentAPIClient |
QueryOraclePrice, QueryEventsHistory |
Oracle 与事件 |
AgentAPIClient |
WalletCreate, MPCSign, PaymentSession |
受保护操作 |
AgentAPIClient |
SubscribeEvents |
WebSocket 订阅 |
AgentAPIClient |
DeFiSwap, DeFiLiquidity, DeFiStake (Stub) |
DeFi 操作 |
AgentAPIClient |
RegistryRegister, RegistryDiscover, RegistryList (Stub) |
Agent 注册 |
AgentAPIClient |
BridgeTransfer (Stub) |
跨链桥 |
EventClient |
SubscribeNewBlocks, SubscribeTxs |
链上事件监听 |
B. 常见错误排除
| 错误码 | 原因 | 排查方法 |
|---|---|---|
ErrInsufficientFee |
Gas 费用不足 | 检查 GasPrices 配置,MSG Chain gas 价格为 1,000,000,000 attoMSG/gas |
ErrOutOfGas |
Gas 耗尽 | 增大 GasAdjustment(默认 1.3)或调用 SimulateTx |
ErrQuantumSignature |
Dilithium-5 签名失败 | 确认私钥类型为 Dilithium-5,检查 pkg/quantum |
ErrAgentAPIRateLimit |
超过 100 req/s | 降低请求频率或调整 WithRateLimit |
ErrStubNotImplemented |
调用了 Stub 端点 | 设置 X-MSG-Stub=true 头或 WithStubMode(true) |
ErrUnauthorized |
Agent API 认证失败 | 检查 API Key 是否正确 |
C. 迁移指南:从 cosmjs 到 Go SDK
| cosmjs | Go SDK |
|---|---|
await client.getAccount() |
client.GetNodeInfo() |
await client.getBalance() |
client.Bank().GetBalance() |
await client.sendTokens() |
client.Bank().Send() |
await client.delegateTokens() |
client.Staking().Delegate() |
SigningStargateClient |
MSGChainClient.BuildAndBroadcastTx() |
CosmWasmClient.queryContractSmart() |
client.Wasm().SmartContractState() |
Secp256k1HdWallet |
WalletManager / CreateKey(KeyTypeSecp256k1) |
await client.broadcastTx() |
client.BroadcastTxWithMode() |
GasPrice.fromString() |
ChainConfig.GasPrices |
EventSubscription |
EventClient.SubscribeNewBlocks() |
calculateFee() |
client.SimulateTx() / EstimateGasFee() |
注意: 本文档中的代码示例基于
github.com/msg-chain/go-sdk v0.2.0。实际开发过程中,请始终参考pkg/quantum/、pkg/wasm/、pkg/indexer/目录下的真实源代码以获取最新实现细节。Agent API 路由定义请参考pkg/quantum/agent_api.go。交易执行逻辑请参考pkg/quantum/transaction_executor.go。Node 启动流程请参考pkg/quantum/node.go。CosmWasm 集成请参考pkg/wasm/real_vm.go和pkg/wasm/core_contracts_e2e_test.go。事件索引实现请参考pkg/indexer/event_listener.go。测试使用make test/make test-quantum运行,合约构建使用make ci-contracts。
