高级
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. 进阶学习资源
课程总结
10 天学习回顾
初级阶段
- Day 1: 环境与节点
- Day 2: 账户与密钥
- Day 3: 链上数据
中级阶段
- Day 4: Spend 交易
- Day 5: AENS 域名
- Day 6: 预言机
高级阶段
- Day 7-8: 智能合约
- Day 9: 高级交易
- Day 10: 综合项目
你已掌握的能力
节点连接
账户管理
Keystore 操作
链上数据查询
转账交易
AENS 域名
预言机系统
智能合约部署
合约调用
错误处理
生产最佳实践
本页目录