高级 Day 10 - 最终篇 70 分钟

综合项目与最佳实践

构建完整的 Mini Wallet CLI 工具,并学习生产环境最佳实践。恭喜完成 10 天学习之旅!

学习目标
  • 构建完整的 CLI 钱包工具
  • 实现余额查询功能
  • 实现转账发送功能
  • 理解安全性最佳实践
  • 掌握配置管理技巧
  • 了解性能优化方法

第一部分:Mini Wallet CLI 项目

将前 9 天学到的知识综合运用,构建一个功能完整的命令行钱包工具。

1. 项目结构设计
目录结构
ae-wallet-cli/
├── main.go           # 入口文件,命令行解析
├── wallet/
│   ├── wallet.go     # 钱包核心逻辑
│   └── keystore.go   # Keystore 管理
├── cmd/
│   ├── balance.go    # 余额查询命令
│   ├── send.go       # 转账命令
│   └── info.go       # 账户信息命令
├── config/
│   └── config.go     # 配置管理
├── go.mod
└── go.sum
功能规划
命令功能参数
balance查询余额
send发送转账-to, -amount
info显示账户信息
create创建新钱包-password
import导入私钥-key, -password
2. 基础版本实现
main.go - 入口文件
package main

import (
    "flag"
    "fmt"
    "log"
    "math/big"
    "os"

    "github.com/aeternity/aepp-sdk-go/v9/account"
    "github.com/aeternity/aepp-sdk-go/v9/config"
    "github.com/aeternity/aepp-sdk-go/v9/naet"
    "github.com/aeternity/aepp-sdk-go/v9/transactions"
)

const (
    DefaultNodeURL   = "https://testnet.aeternity.io"
    DefaultNetworkID = "ae_uat"
    WalletFile       = "wallet.json"
)

func main() {
    // 定义命令行参数
    action := flag.String("action", "balance", "Action: balance|send|info|create")
    recipient := flag.String("to", "", "Recipient address (for send)")
    amountStr := flag.String("amount", "0", "Amount in aettos (for send)")
    password := flag.String("password", "", "Wallet password")
    nodeURL := flag.String("node", DefaultNodeURL, "Node URL")
    flag.Parse()

    // 初始化节点连接
    node := naet.NewNode(*nodeURL, false)

    // 执行对应操作
    switch *action {
    case "balance":
        cmdBalance(node, *password)
    case "send":
        cmdSend(node, *password, *recipient, *amountStr)
    case "info":
        cmdInfo(node, *password)
    case "create":
        cmdCreate(*password)
    default:
        fmt.Println("Unknown action. Use: balance|send|info|create")
        os.Exit(1)
    }
}

func loadWallet(password string) *account.Account {
    if password == "" {
        log.Fatal("Password required. Use -password flag")
    }
    
    acc, err := account.LoadFromKeyStoreFile(WalletFile, password)
    if err != nil {
        log.Fatalf("Failed to load wallet: %v", err)
    }
    return acc
}

func cmdBalance(node *naet.Node, password string) {
    acc := loadWallet(password)
    
    accInfo, err := node.GetAccount(acc.Address)
    if err != nil {
        log.Fatalf("Failed to get account: %v", err)
    }
    
    // 转换为 AE 单位
    balance := new(big.Float).SetInt(accInfo.Balance)
    balance.Quo(balance, big.NewFloat(1e18))
    
    fmt.Printf("Address: %s\n", acc.Address)
    fmt.Printf("Balance: %s AE\n", balance.Text('f', 6))
    fmt.Printf("Nonce:   %d\n", accInfo.Nonce)
}

func cmdSend(node *naet.Node, password, recipient, amountStr string) {
    // 验证参数
    if recipient == "" {
        log.Fatal("Recipient address required. Use -to flag")
    }
    if len(recipient) < 3 || recipient[:3] != "ak_" {
        log.Fatal("Invalid recipient address format. Must start with 'ak_'")
    }
    
    acc := loadWallet(password)
    
    // 解析金额
    amount := new(big.Int)
    if _, ok := amount.SetString(amountStr, 10); !ok {
        log.Fatal("Invalid amount format")
    }
    
    fmt.Printf("Sending %s aettos to %s...\n", amountStr, recipient)
    
    // 创建交易
    ttlnoncer := transactions.NewTTLNoncer(node)
    tx, err := transactions.NewSpendTx(
        acc.Address, 
        recipient, 
        amount, 
        []byte("Sent via AE Wallet CLI"), 
        ttlnoncer,
    )
    if err != nil {
        log.Fatalf("Failed to create transaction: %v", err)
    }
    
    // 签名
    signedTx, txHash, _, err := transactions.SignHashTx(
        acc, tx, DefaultNetworkID,
    )
    if err != nil {
        log.Fatalf("Failed to sign transaction: %v", err)
    }
    
    // 广播
    txStr, _ := transactions.SerializeTx(signedTx)
    err = node.PostTransaction(txStr, txHash)
    if err != nil {
        log.Fatalf("Failed to broadcast: %v", err)
    }
    
    fmt.Printf("✓ Transaction sent!\n")
    fmt.Printf("  Hash: %s\n", txHash)
    fmt.Printf("  Explorer: https://testnet.aescan.io/transactions/%s\n", txHash)
}

func cmdInfo(node *naet.Node, password string) {
    acc := loadWallet(password)
    
    fmt.Println("=== Wallet Info ===")
    fmt.Printf("Address:    %s\n", acc.Address)
    fmt.Printf("Public Key: %s\n", acc.SigningKeyToHexString()[:64]+"...")
    fmt.Printf("Network:    %s\n", DefaultNetworkID)
    fmt.Printf("Node:       %s\n", DefaultNodeURL)
}

func cmdCreate(password string) {
    if password == "" {
        log.Fatal("Password required. Use -password flag")
    }
    
    // 生成新账户
    acc, err := account.New()
    if err != nil {
        log.Fatalf("Failed to create account: %v", err)
    }
    
    // 保存到 Keystore
    err = account.SaveToKeyStoreFile(acc, password, WalletFile)
    if err != nil {
        log.Fatalf("Failed to save wallet: %v", err)
    }
    
    fmt.Println("✓ New wallet created!")
    fmt.Printf("  Address:  %s\n", acc.Address)
    fmt.Printf("  Saved to: %s\n", WalletFile)
    fmt.Println("\n⚠️  IMPORTANT: Backup your wallet file and remember your password!")
}
3. 运行与测试
使用示例
# 1. 创建新钱包
go run main.go -action create -password mySecretPass123

# 输出:
# ✓ New wallet created!
#   Address:  ak_2LZx8JvH...
#   Saved to: wallet.json

# 2. 查询余额
go run main.go -action balance -password mySecretPass123

# 输出:
# Address: ak_2LZx8JvH...
# Balance: 0.000000 AE
# Nonce:   0

# 3. 发送转账(需要先从水龙头获取测试币)
go run main.go -action send \
    -password mySecretPass123 \
    -to ak_recipientAddress123... \
    -amount 1000000000000000000

# 输出:
# Sending 1000000000000000000 aettos to ak_recipient...
# ✓ Transaction sent!
#   Hash: th_2abc...
#   Explorer: https://testnet.aescan.io/transactions/th_2abc...

# 4. 查看账户信息
go run main.go -action info -password mySecretPass123
编译为可执行文件
# 编译
go build -o ae-wallet main.go

# Linux/Mac
./ae-wallet -action balance -password mypass

# Windows
ae-wallet.exe -action balance -password mypass

第二部分:生产环境最佳实践

构建安全、高效的生产级应用的关键指南。

4. 安全性最佳实践
密钥与凭证安全
规则说明实现方式
永远不要提交密钥 私钥、密码不能进入版本控制 添加到 .gitignore
使用环境变量 敏感配置从环境读取 os.Getenv()
验证地址格式 防止发送到无效地址 检查 ak_ 前缀
限制权限 Keystore 文件权限 600 os.Chmod()
安全代码示例
import (
    "os"
    "strings"
)

// 从环境变量读取敏感配置
func getConfig() *Config {
    return &Config{
        NodeURL:   getEnvOrDefault("AE_NODE_URL", "https://testnet.aeternity.io"),
        NetworkID: getEnvOrDefault("AE_NETWORK_ID", "ae_uat"),
        Password:  os.Getenv("AE_WALLET_PASSWORD"),  // 必须从环境获取
    }
}

func getEnvOrDefault(key, defaultVal string) string {
    if val := os.Getenv(key); val != "" {
        return val
    }
    return defaultVal
}

// 验证地址格式
func validateAddress(addr string) error {
    if len(addr) < 53 {
        return fmt.Errorf("address too short")
    }
    if !strings.HasPrefix(addr, "ak_") {
        return fmt.Errorf("invalid prefix, expected 'ak_'")
    }
    return nil
}

// 安全保存 Keystore
func saveKeystoreSecure(acc *account.Account, password, path string) error {
    err := account.SaveToKeyStoreFile(acc, password, path)
    if err != nil {
        return err
    }
    // 设置文件权限为仅所有者可读写
    return os.Chmod(path, 0600)
}

// .gitignore 内容
/*
wallet.json
*.keystore
.env
*/
5. 配置管理
网络配置检查
// 关键:NetworkID 必须与节点匹配!
// 使用主网 ID 签名的交易无法在测试网广播

var NetworkConfigs = map[string]struct {
    URL       string
    NetworkID string
}{
    "mainnet": {
        URL:       "https://mainnet.aeternity.io",
        NetworkID: "ae_mainnet",
    },
    "testnet": {
        URL:       "https://testnet.aeternity.io",
        NetworkID: "ae_uat",
    },
}

func initNetwork(network string) (*naet.Node, string, error) {
    cfg, ok := NetworkConfigs[network]
    if !ok {
        return nil, "", fmt.Errorf("unknown network: %s", network)
    }
    
    node := naet.NewNode(cfg.URL, false)
    
    // 验证连接并确认 NetworkID
    status, err := node.GetStatus()
    if err != nil {
        return nil, "", err
    }
    
    if status.NetworkID != cfg.NetworkID {
        return nil, "", fmt.Errorf(
            "network mismatch: expected %s, got %s",
            cfg.NetworkID, status.NetworkID,
        )
    }
    
    return node, cfg.NetworkID, nil
}
费用配置
type FeeConfig struct {
    DefaultGasPrice *big.Int
    MinGasPrice     *big.Int
    MaxGasPrice     *big.Int
    SpendGasLimit   *big.Int
    ContractGasLimit *big.Int
}

var DefaultFeeConfig = FeeConfig{
    DefaultGasPrice:  big.NewInt(1_000_000_000),      // 1 Gwei
    MinGasPrice:      big.NewInt(1_000_000_000),      // 最低
    MaxGasPrice:      big.NewInt(10_000_000_000),     // 10 Gwei
    SpendGasLimit:    big.NewInt(16_800),             // 普通转账
    ContractGasLimit: big.NewInt(200_000),            // 合约调用
}

// 根据网络拥堵调整 Gas Price
func adjustGasPrice(node *naet.Node, base *big.Int) *big.Int {
    // 简化示例:实际可检查 mempool 大小
    return base
}
6. 性能优化
复用连接实例
// ✅ 正确:复用 Node 实例
type App struct {
    node     *naet.Node
    compiler *naet.Compiler
}

func NewApp(nodeURL, compilerURL string) *App {
    return &App{
        node:     naet.NewNode(nodeURL, false),
        compiler: naet.NewCompiler(compilerURL, false),
    }
}

// 复用已有连接
func (a *App) GetBalance(addr string) (*big.Int, error) {
    acc, err := a.node.GetAccount(addr)
    if err != nil {
        return nil, err
    }
    return acc.Balance, nil
}

// ❌ 错误:每次都创建新连接
func getBalanceBad(addr string) (*big.Int, error) {
    node := naet.NewNode("...", false)  // 每次新建!
    acc, _ := node.GetAccount(addr)
    return acc.Balance, nil
}
缓存策略
import "sync"

// 缓存合约字节码(避免重复编译)
type ContractCache struct {
    mu       sync.RWMutex
    bytecode map[string]string  // sourceHash -> bytecode
}

func (c *ContractCache) GetOrCompile(
    compiler *naet.Compiler,
    source string,
) (string, error) {
    hash := sha256Hash(source)
    
    c.mu.RLock()
    if bc, ok := c.bytecode[hash]; ok {
        c.mu.RUnlock()
        return bc, nil
    }
    c.mu.RUnlock()
    
    // 编译
    c.mu.Lock()
    defer c.mu.Unlock()
    
    bc, err := compiler.CompileContract(source)
    if err != nil {
        return "", err
    }
    
    c.bytecode[hash] = bc
    return bc, nil
}

// 缓存区块高度(短时间内有效)
type HeightCache struct {
    height    uint64
    fetchedAt time.Time
    ttl       time.Duration
}

func (h *HeightCache) Get(node *naet.Node) (uint64, error) {
    if time.Since(h.fetchedAt) < h.ttl {
        return h.height, nil
    }
    
    height, err := node.GetHeight()
    if err != nil {
        return 0, err
    }
    
    h.height = height
    h.fetchedAt = time.Now()
    return height, nil
}
7. 进阶学习资源
推荐阅读
  • SDK 的 transactions/ 目录 - 底层实现
  • Sophia 语言规范
  • FATE VM 白皮书
  • Aeternity 共识机制
课程总结
10 天学习回顾
初级阶段
  • Day 1: 环境与节点
  • Day 2: 账户与密钥
  • Day 3: 链上数据
中级阶段
  • Day 4: Spend 交易
  • Day 5: AENS 域名
  • Day 6: 预言机
高级阶段
  • Day 7-8: 智能合约
  • Day 9: 高级交易
  • Day 10: 综合项目

你已掌握的能力
节点连接 账户管理 Keystore 操作 链上数据查询 转账交易 AENS 域名 预言机系统 智能合约部署 合约调用 错误处理 生产最佳实践

恭喜毕业!

你已完成 Go SDK 10 天学习课程
现在可以使用 Go 语言构建 Aeternity 区块链应用了!