dApp Docs/AI Agent dApp 启动器与前端示例指南
Development reference. Not independently verified for production.

AI Agent dApp 启动器与前端示例指南

链 ID: msg-chain-1 | 共识: DAR | 虚拟机: CosmWasm (WasmVM)
状态: 规划文档 — 主网裁决为 No-Go,所有数据均为主网预演
Gas: 1,000,000,000 attoMSG/gas
Gas 分配: 40% 验证者 / 30% 开发者 / 20% 燃烧 / 10% 基金会金库
地址格式: SHA3-512(前40位) + SHA-256 校验 | 签名: Dilithium-5 (公钥2592字节 / 私钥4864字节 / 签名4595字节)
AI Agent安全边界: 永不自主创建新合约,永不自主调整 Gas 参数,永不自主铸造/销毁代币


目录

  1. 概述
  2. dApp 示例索引
  3. 最小 dApp 配方
  4. 钱包集成
  5. 前端适配器样板
  6. 边界声明
  7. 完整示例
  8. 附录: 数据源索引

1. 概述

1.1 文档目标

本文档为 AI 编码代理(AI Coding Agent)提供 MSG Chain dApp 前端启动器与示例的完整参考。所有内容均基于 MSG Chain Whitepaper 的真实公开文件,包括:

1.2 核心能力面

在 developer_capability_matrix.json 定义的 16 个能力面中,与 dApp 前端最相关的是:

能力面 ID 机器就绪度 写路径就绪 生产支持
dapp_starter_pack starter_ready true false
chain_config_pack starter_ready false true
wallet_frontend guarded_integration true false
rpc_gateway assisted_codegen true true
explorer_receipts read_only_assist false false
agent_query_and_guarded_write guarded_write false false

1.3 dapp_starter_pack

状态: starter_ready + write_path_ready: true

1.4 wallet_frontend

状态: guarded_integration + production_supported: false

1.5 chain_config_pack

状态: starter_ready + production_supported: true

1.6 快速参考: 白皮书基础 URL

Base URL:       https://msgchain.org/whitepaper/
RPC:            https://rpc.msgchain.org
REST:           https://api.msgchain.org
Chain ID:       msg-chain-1
Bech32 Prefix:  msg
Coin Type:      118
Minimal Denom:  umsg

2. dApp 示例索引

2.1 索引文件结构

examples/index.json 是 dApp 示例的入口索引,schema_version 为 v1,由 msg_whitepaper_pipeline_v1 流水线生成。

{
  "schema_version": "v1",
  "generated_by": "msg_whitepaper_pipeline_v1",
  "starters": [
    {
      "id": "keplr_contract_console",
      "path": "examples/dapp_starters/keplr_contract_console/manifest.json",
      "public_url": "https://msgchain.org/whitepaper/examples/dapp_starters/keplr_contract_console/manifest.json",
      "description": "Minimal wallet + query + execute intent starter for MSG dApps."
    }
  ],
  "libraries": [
    {
      "id": "business_examples",
      "path": "examples/business_examples/catalog.json",
      "public_url": "https://msgchain.org/whitepaper/examples/business_examples/catalog.json",
      "description": "Local-only contract-first business examples for AI-assisted Web3 dApp scaffolding.",
      "mainnet_ready_claim": false
    }
  ],
  "metadata_profile": "public_stable"
}

2.2 使用 Python 加载索引

import requests
from typing import Any

EXAMPLES_INDEX = 'https://msgchain.org/whitepaper/examples/index.json'

async def list_dapp_starters() -> list[dict[str, Any]]:
    resp = requests.get(EXAMPLES_INDEX)
    resp.raise_for_status()
    index = resp.json()
    return index.get('starters', [])

async def list_business_examples() -> list[dict[str, Any]]:
    resp = requests.get(EXAMPLES_INDEX)
    resp.raise_for_status()
    index = resp.json()
    return index.get('libraries', [])

2.3 使用 TypeScript 加载索引

const EXAMPLES_INDEX = 'https://msgchain.org/whitepaper/examples/index.json';

interface DAppStarter {
  id: string;
  path: string;
  public_url: string;
  description: string;
}

interface BusinessExample {
  id: string;
  path: string;
  public_url: string;
  description: string;
  mainnet_ready_claim: boolean;
}

interface ExamplesIndex {
  schema_version: string;
  generated_by: string;
  starters: DAppStarter[];
  libraries: BusinessExample[];
  metadata_profile: string;
}

async function listDAppStarters(): Promise<DAppStarter[]> {
  const resp = await fetch(EXAMPLES_INDEX);
  const index: ExamplesIndex = await resp.json();
  return index.starters;
}

async function listBusinessExamples(): Promise<BusinessExample[]> {
  const resp = await fetch(EXAMPLES_INDEX);
  const index: ExamplesIndex = await resp.json();
  return index.libraries;
}

2.4 当前可用 Starter

当前索引中只有一个 starter:

字段 值
id keplr_contract_console
描述 Minimal wallet + query + execute intent starter for MSG dApps
manifest URL https://msgchain.org/whitepaper/examples/dapp_starters/keplr_contract_console/manifest.json

该 starter 的 manifest 定义:

{
  "starter_id": "keplr_contract_console",
  "entry_files": [
    "examples/dapp_starters/keplr_contract_console/src/chain.js",
    "examples/dapp_starters/keplr_contract_console/src/query.js",
    "examples/dapp_starters/keplr_contract_console/src/execute.js",
    "examples/dapp_starters/keplr_contract_console/.env.example"
  ],
  "uses": [
    "chain_config/keplr_suggest_chain.js",
    "api_specs/rpc_methods.json",
    "recipes/dapp_minimal.json"
  ],
  "boundary": [
    "starter 只提供起步结构,不包含正式 UI、设计系统、权限系统与生产监控。"
  ],
  "metadata_profile": "public_stable"
}

2.5 当前可用业务示例库

字段 值
id business_examples
描述 Local-only contract-first business examples for AI-assisted Web3 dApp scaffolding
mainnet_ready_claim false

2.6 索引加载流程(AI Agent 伪代码)

1.  GET https://msgchain.org/whitepaper/examples/index.json
2.  解析 starters[] 获取所有 starter ID
3.  对于每个 starter:
    a. 读取 manifest.json (public_url)
    b. 解析 entry_files 获取源代码路径
    c. 解析 uses 获取依赖配置
    d. 读取 boundary 了解限制
4.  根据需求选择合适 starter
5.  克隆/下载 entry_files 到本地项目

2.7 推荐引导顺序

根据 developer_entry.json 的 recommended_dapp_bootstrap_order:

1.  product_delivery_entry.json
2.  developer_capability_matrix.json
3.  quickstart/contract_and_dapp_minimal.json
4.  chain_config/index.json
5.  chain_config/developer_sandbox_strategy.json
6.  api_specs/rpc_methods.json
7.  api_specs/openapi/public_query.yaml
8.  api_specs/formal_contracts.json
9.  examples/index.json
10. recipes/dapp_minimal.json
11. execution_pack/index.json
12. execution_pack/command_registry.json
13. release_pack/index.json
14. modules/keplr.html
15. modules/rpc.html
16. modules/explorer.html
17. module_exports/keplr.json

2.8 引导顺序解析(AI 行为指南)

Step 1-2:  读取产品交付入口和能力矩阵,理解全局边界
Step 3-5:  读取 Quick Start 和链配置,确定沙箱策略
Step 6-8:  加载 RPC 方法和 OpenAPI 摘要,获取正式的合约接口
Step 9:    加载 examples/index.json — 即本节内容
Step 10:   加载 dapp_minimal 配方 — 见下一章
Step 11-13:加载执行包和发布包
Step 14-17:加载 Keplr、RPC、Explorer 模块详情

3. 最小 dApp 配方

3.1 配方概述

recipes/dapp_minimal.json 定义了 MSG dApp AI 编码的最小闭环。配方 ID 为 dapp_minimal,标题为 "MSG dApp AI Coding 最小闭环"。

总体目标:引导 AI 编码代理从前端需求接收到钱包集成、查询/写绑定、回执追踪、再到门禁发布审查的全流程。

3.2 配方完整结构

{
  "recipe_id": "dapp_minimal",
  "title": "MSG dApp AI Coding 最小闭环",
  "goal": "Guide an AI coding agent from frontend requirement intake to wallet integration, query/write binding, receipt tracing, and guarded release review.",
  "entry_point": "developer_entry.json",
  "required_modules": [...],
  "recommended_chunks": [...],
  "required_human_inputs": [...],
  "workflow": [...],
  "success_artifacts": [...],
  "hard_boundaries": [...]
}

3.3 必需模块

配方依赖 4 个 key_modules(来自 developer_entry.json 的子集):

模块 文件 状态 组
生态层: Web3 钱包集成 keplr.html partial (部分实现) ecosystem
RPC 与 API 接口 rpc.html implemented (已实现) runtime
生态层: 区块浏览器与数据索引 explorer.html partial (部分实现) ecosystem
AI 智能钱包闭环 ai_wallet.html partial (部分实现) ai

3.4 使用 Python 加载配方

import requests

DAPP_RECIPE = 'https://msgchain.org/whitepaper/recipes/dapp_minimal.json'

async def load_dapp_recipe() -> dict:
    resp = requests.get(DAPP_RECIPE)
    resp.raise_for_status()
    return resp.json()

async def scaffold_dapp(name: str) -> dict:
    recipe = await load_dapp_recipe()
    return {
        'name': name,
        'recipe_id': recipe['recipe_id'],
        'workflow': recipe['workflow'],
        'required_modules': recipe['required_modules'],
        'required_human_inputs': recipe['required_human_inputs'],
        'hard_boundaries': recipe['hard_boundaries'],
    }

3.5 六步工作流(核心)

配方定义了一个 6 步工作流,这是 AI 编码代理执行的核心流程:

Step 1: 读取 developer_entry + dApp 主题提示

action:   read developer_entry + dapp topic hints
purpose:  识别钱包、查询、receipt、Explorer 边界与推荐入口。

AI 行为:

Step 2: 绑定链配置与钱包路径

action:   bind chain config and wallet path
purpose:  根据 chainId=msg-chain-1、Keplr/CosmJS 路径生成前端链配置。

AI 行为:

Step 3: 生成查询与写入适配器

action:   generate query and write adapters
purpose:  封装 RPC/REST/contract query、签名广播、receipt 回放与错误处理。

AI 行为:

Step 4: 生成前端页面与 Hooks

action:   generate frontend pages and hooks
purpose:  输出状态读取、交易提交流程、事件/receipt 展示与失败态。

AI 行为:

Step 5: 使用 Explorer/证据循环验证

action:   validate with explorer/evidence loop
purpose:  使用 tx hash、receipt、contract source/query 与日志检索校验结果。

AI 行为:

Step 6: 人工发布门禁

action:   human release gate
purpose:  确认生产域名、环境变量、密钥、监控与上线窗口。

AI 行为:

3.6 必需的人工输入

配方明确要求以下人工输入:

  1. 产品流程与页面需求
  2. 目标合约地址或 canonical key
  3. 钱包接入策略
  4. 部署环境、域名与运维发布权限

3.7 成功产物

配方的成功产物包括:

  1. 前端源码目录
  2. 钱包接入配置
  3. 查询与签名适配器
  4. 交易回执与错误处理 UI
  5. 上线环境变量与发布说明

3.8 硬边界

配方定义了不可逾越的边界:

  1. 当前 starter 与链配置包能显著降低起步成本,但仍不能替代正式稳定 SDK。
  2. Explorer/索引面可辅助调试,但不应替代正式生产运维监控。
  3. 没有真实部署权限、域名控制权与生产配置,AI 不能独立完成最终发布。

3.9 推荐的 Chunk 加载

配方引用了 4 个 recommended_chunks 用于快速加载模块片段:

recommended_chunks = [
    {
        "chunk_id": "keplr#1",
        "chunk_public_url": "https://msgchain.org/whitepaper/module_chunks/keplr__chunk_01.json",
        "excerpt": "🔍 现状: 已有钱包接入底座..."
    },
    {
        "chunk_id": "keplr#2",
        "chunk_public_url": "https://msgchain.org/whitepaper/module_chunks/keplr__chunk_02.json",
        "excerpt": "MetaMask -->EthRPC..."
    },
    {
        "chunk_id": "rpc#1",
        "chunk_public_url": "https://msgchain.org/whitepaper/module_chunks/rpc__chunk_01.json",
        "excerpt": "🔍 铁证: 共享 Mux + 多协议端点..."
    },
    {
        "chunk_id": "explorer#1",
        "chunk_public_url": "https://msgchain.org/whitepaper/module_chunks/explorer__chunk_01.json",
        "excerpt": "🔍 现状: 本地 Explorer 子门禁..."
    },
]

3.10 AI 执行配方:Python 编排器

import requests
import json
from typing import Any

WHITEPAPER_BASE = 'https://msgchain.org/whitepaper'

class DAppScaffolder:
    def __init__(self, project_name: str):
        self.project_name = project_name
        self.recipe = None
        self.capability_matrix = None
        self.chain_config = None
        self.rpc_methods = None
        self.modules = {}

    def load_recipe(self):
        url = f'{WHITEPAPER_BASE}/recipes/dapp_minimal.json'
        self.recipe = requests.get(url).json()
        print(f'[OK] Loaded recipe: {self.recipe["recipe_id"]}')

    def load_capability_matrix(self):
        url = f'{WHITEPAPER_BASE}/developer_capability_matrix.json'
        self.capability_matrix = requests.get(url).json()
        print(f'[OK] Loaded capability matrix ({len(self.capability_matrix["items"])} surfaces)')

    def load_chain_config(self):
        url = f'{WHITEPAPER_BASE}/chain_config/network_presets.json'
        self.chain_config = requests.get(url).json()
        print(f'[OK] Loaded chain config: {self.chain_config["chain_id"]}')

    def load_rpc_methods(self):
        url = f'{WHITEPAPER_BASE}/api_specs/rpc_methods.json'
        self.rpc_methods = requests.get(url).json()
        print(f'[OK] Loaded RPC methods ({len(self.rpc_methods["families"])} families)')

    def load_module_export(self, module_name: str):
        url = f'{WHITEPAPER_BASE}/module_exports/{module_name}.json'
        self.modules[module_name] = requests.get(url).json()
        print(f'[OK] Loaded module export: {module_name}')

    def execute_workflow(self):
        if not self.recipe:
            raise RuntimeError('Recipe not loaded')

        for step in self.recipe['workflow']:
            print(f'\n--- Step {step["step"]}: {step["action"]} ---')
            print(f'Purpose: {step["purpose"]}')
            self._execute_step(step)

        print(f'\n=== Workflow Complete ===')
        print(f'Success Artifacts:')
        for artifact in self.recipe['success_artifacts']:
            print(f'  - {artifact}')

    def _execute_step(self, step: dict):
        step_num = step['step']
        if step_num == 1:
            self._step1_read_entry()
        elif step_num == 2:
            self._step2_bind_config()
        elif step_num == 3:
            self._step3_generate_adapters()
        elif step_num == 4:
            self._step4_generate_frontend()
        elif step_num == 5:
            self._step5_validate()
        elif step_num == 6:
            self._step6_release_gate()

    def _step1_read_entry(self):
        print('  [AI] Reading developer_entry.json...')
        print(f'  [AI] Loaded {len(self.modules)} modules')
        for name, mod in self.modules.items():
            boundaries = mod.get('boundary_clauses', [])
            for b in boundaries:
                print(f'  [BOUNDARY] {b}')

    def _step2_bind_config(self):
        cfg = self.chain_config
        print(f'  [AI] Chain ID: {cfg["chain_id"]}')
        print(f'  [AI] RPC: {cfg["rpc"]}')
        print(f'  [AI] REST: {cfg["rest"]}')
        print(f'  [AI] Bech32 Prefix: {cfg["wallet"]["bech32"]["bech32PrefixAccAddr"]}')
        print(f'  [AI] Coin Type: {cfg["wallet"]["bip44"]["coinType"]}')
        print(f'  [AI] Gas Price Step: low={cfg["wallet"]["fee_currencies"][0]["gasPriceStep"]["low"]}, '
              f'average={cfg["wallet"]["fee_currencies"][0]["gasPriceStep"]["average"]}, '
              f'high={cfg["wallet"]["fee_currencies"][0]["gasPriceStep"]["high"]}')

    def _step3_generate_adapters(self):
        for family in self.rpc_methods['families']:
            print(f'  [AI] API Family: {family["family_id"]} ({family["status"]})')
            for method in family['methods']:
                print(f'    - {method["name"]} [{method["mode"]}] ({method["stability"]})')

    def _step4_generate_frontend(self):
        print('  [AI] Generating React components...')
        print('  [AI] - WalletConnectProvider')
        print('  [AI] - useKeplr hook')
        print('  [AI] - useQuery hook')
        print('  [AI] - useExecute hook')
        print('  [AI] - TxReceiptDisplay')
        print('  [AI] - ErrorBoundary')

    def _step5_validate(self):
        print('  [AI] Validation loop:')
        print('  [AI] 1. Query tx hash -> verify receipt')
        print('  [AI] 2. Query contract state -> verify write')
        print('  [AI] 3. Check block explorer -> cross-reference')

    def _step6_release_gate(self):
        print('  [AI] Release checklist generated:')
        for inp in self.recipe['required_human_inputs']:
            print(f'  [HUMAN] {inp}')
        print('  [AI] AI cannot auto-deploy. Human gate required.')


if __name__ == '__main__':
    scaffolder = DAppScaffolder('my-msg-dapp')
    scaffolder.load_recipe()
    scaffolder.load_capability_matrix()
    scaffolder.load_chain_config()
    scaffolder.load_rpc_methods()
    scaffolder.load_module_export('keplr')
    scaffolder.load_module_export('rpc')
    scaffolder.execute_workflow()

3.11 dapp_minimal 配方结构图

dapp_minimal.json
├── recipe_id: "dapp_minimal"
├── title: "MSG dApp AI Coding 最小闭环"
├── entry_point: "developer_entry.json"
├── required_modules (4)
│   ├── keplr.html           [partial]  钱包集成
│   ├── rpc.html             [implemented] RPC 接口
│   ├── explorer.html        [partial]  浏览器与索引
│   └── ai_wallet.html       [partial]  AI 钱包闭环
├── recommended_chunks (4)
│   ├── keplr#1, keplr#2
│   ├── rpc#1
│   └── explorer#1
├── required_human_inputs (4)
├── workflow (6 steps)
│   ├── Step 1: 读取入口 + 边界
│   ├── Step 2: 绑定链配置 + 钱包路径
│   ├── Step 3: 生成查询/写适配器
│   ├── Step 4: 生成前端页面 + Hooks
│   ├── Step 5: Explorer/证据循环验证
│   └── Step 6: 人工发布门禁
├── success_artifacts (5)
└── hard_boundaries (3)

4. 钱包集成

4.1 钱包集成能力面

wallet_frontend 能力面在 developer_capability_matrix.json 中的定义为:

{
  "surface_id": "wallet_frontend",
  "title": "Keplr /CosmJS /钱包前端接入",
  "machine_readiness": "guarded_integration",
  "machine_safe_for_codegen": true,
  "write_path_ready": true,
  "schema_available": false,
  "example_available": true,
  "production_supported": false,
  "source_modules": ["keplr.html", "wallet.html", "rpc.html"],
  "best_for": ["dApp 钱包接入", "chain config 注入", "签名与查询前端路径"],
  "blocking_gaps": ["缺少对外稳定前端样例项目", "兼容目标不等于生态全量上线"],
  "boundaries": ["当前是兼容路径与配置底座,不应表述成官方钱包生态已全量上线。"]
}

4.2 链网络配置

来自 chain_config/network_presets.json:

{
  "chain_id": "msg-chain-1",
  "numeric_chain_id": 1,
  "chain_name": "MSG Chain",
  "rpc": "https://rpc.msgchain.org",
  "rest": "https://api.msgchain.org",
  "wallet": {
    "bech32": {
      "bech32PrefixAccAddr": "msg",
      "bech32PrefixAccPub": "msgpub",
      "bech32PrefixValAddr": "msgvaloper",
      "bech32PrefixValPub": "msgvaloperpub",
      "bech32PrefixConsAddr": "msgvalcons",
      "bech32PrefixConsPub": "msgvalconspub"
    },
    "bip44": { "coinType": 118 },
    "currencies": [{
      "coinDenom": "MSG",
      "coinMinimalDenom": "umsg",
      "coinDecimals": 18,
      "coinGeckoId": "msg-chain"
    }],
    "fee_currencies": [{
      "coinDenom": "MSG",
      "coinMinimalDenom": "umsg",
      "coinDecimals": 18,
      "coinGeckoId": "msg-chain",
        "gasPriceStep": {
        "low": 1000000000,
        "average": 1000000000,
        "high": 1000000000
      }
    }],
    "stake_currency": {
      "coinDenom": "MSG",
      "coinMinimalDenom": "umsg",
      "coinDecimals": 18,
      "coinGeckoId": "msg-chain"
    },
    "features": ["cosmwasm"]
  },
  "source_files": [
    "configs/keplr_chain_info.json",
    "configs/keplr_suggest_chain.js"
  ],
  "boundary": [
    "链配置是接入基线,不等于 live/public 端点已经被正式接受为生产网络。",
    "生产接入前仍需复核 endpoint 可用性、域名控制、证书、SLO 与发布权限。"
  ]
}

4.3 Keplr suggestChain 配置

用于 window.keplr.experimentalSuggestChain() 的完整配置:

const MSG_CHAIN_INFO = {
  chainId: 'msg-chain-1',
  chainName: 'MSG Chain',
  rpc: 'https://rpc.msgchain.org',
  rest: 'https://api.msgchain.org',
  bip44: {
    coinType: 118,
  },
  bech32Config: {
    bech32PrefixAccAddr: 'msg',
    bech32PrefixAccPub: 'msgpub',
    bech32PrefixValAddr: 'msgvaloper',
    bech32PrefixValPub: 'msgvaloperpub',
    bech32PrefixConsAddr: 'msgvalcons',
    bech32PrefixConsPub: 'msgvalconspub',
  },
  currencies: [
    {
      coinDenom: 'MSG',
      coinMinimalDenom: 'umsg',
      coinDecimals: 18,
      coinGeckoId: 'msg-chain',
    },
  ],
  feeCurrencies: [
    {
      coinDenom: 'MSG',
      coinMinimalDenom: 'umsg',
      coinDecimals: 18,
      coinGeckoId: 'msg-chain',
      gasPriceStep: {
        low: 1000000000,
        average: 1000000000,
        high: 1000000000,
      },
    },
  ],
  stakeCurrency: {
    coinDenom: 'MSG',
    coinMinimalDenom: 'umsg',
    coinDecimals: 18,
    coinGeckoId: 'msg-chain',
  },
  features: ['cosmwasm'],
};

4.4 Keplr 钱包连接流程

import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { GasPrice } from '@cosmjs/stargate';

const CHAIN_ID = 'msg-chain-1';
const RPC_ENDPOINT = 'https://rpc.msgchain.org';

interface KeplrWalletInfo {
  address: string;
  offlineSigner: OfflineSigner;
  client: SigningCosmWasmClient;
}

async function connectKeplrWallet(): Promise<KeplrWalletInfo> {
  // 1. 检查 Keplr 是否安装
  if (!window.keplr) {
    throw new Error('Keplr extension not found. Please install Keplr.');
  }

  // 2. 向 Keplr 建议 MSG Chain
  await window.keplr.experimentalSuggestChain(MSG_CHAIN_INFO);

  // 3. 启用 msg-chain-1
  await window.keplr.enable(CHAIN_ID);

  // 4. 获取离线签名器
  const offlineSigner = window.keplr.getOfflineSigner(CHAIN_ID);

  // 5. 获取用户钱包地址
  const accounts = await offlineSigner.getAccounts();
  const address = accounts[0].address;

  // 6. 创建签名客户端
  const client = await SigningCosmWasmClient.connectWithSigner(
    RPC_ENDPOINT,
    offlineSigner,
    {
      gasPrice: GasPrice.fromString('1000000000000000000attoMSG'),
    },
  );

  return { address, offlineSigner, client };
}

4.5 CosmJS 直接连接(无 Keplr)

import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate';

const RPC_ENDPOINT = 'https://rpc.msgchain.org';

async function createReadOnlyClient(): Promise<CosmWasmClient> {
  return await CosmWasmClient.connect(RPC_ENDPOINT);
}

// 使用只读客户端查询
async function queryBalance(client: CosmWasmClient, address: string) {
  const balance = await client.getBalance(address, 'umsg');
  return balance;
}

async function queryContractState(
  client: CosmWasmClient,
  contractAddress: string,
  queryMsg: Record<string, unknown>,
) {
  return await client.queryContractSmart(contractAddress, queryMsg);
}

4.6 RPC 端点

来自 rpc.html 模块的真实端点族:

Tendermint RPC:
  /broadcast_tx_commit       [write] 已实现
  /broadcast_tx_sync         [write] 已实现
  /broadcast_tx_async        [write] 已实现
  /abci_query                [read]  已实现
  /tx                        [read]  已实现
  /block                     [read]  已实现
  /blockchain                [read]  已实现
  /net_info                  [read]  已实现

Explorer REST:
  /api/v1/blocks/latest      [read]  已实现
  /api/v1/blocks             [read]  已实现
  /api/v1/txs                [read]  已实现
  /api/v1/receipts           [read]  已实现
  /api/v1/address            [read]  已实现

Contracts:
  /api/v1/contracts                 [read]  部分实现
  /api/v1/contracts/deploy          [write] 部分实现
  /api/v1/contracts/instantiate     [write] 部分实现
  /api/v1/contracts/execute         [write] 部分实现

Cosmos REST:
  /api/v1/bank/balances             [read]  已实现
  /api/v1/staking/validators        [read]  已实现
  /api/v1/auth/accounts             [read]  已实现

Server-side Signing:
  /api/v1/tx/transfer               [write] 已实现
  /api/v1/tx/delegate               [write] 已实现
  /api/v1/tx/undelegate             [write] 已实现
  /api/v1/tx/send                   [write] 已实现

Tunnel Status:
  /api/v1/status                    [read]  已实现

4.7 查询适配器

const API_BASE = 'https://api.msgchain.org';
const RPC_BASE = 'https://rpc.msgchain.org';

interface RPCQueryAdapter {
  query: (path: string, params?: Record<string, string>) => Promise<any>;
  broadcast: (txBytes: Uint8Array) => Promise<any>;
  getTx: (hash: string) => Promise<any>;
  getBlock: (height?: number) => Promise<any>;
  getBalance: (address: string) => Promise<any>;
}

function createRPCAdapter(): RPCQueryAdapter {
  async function query(path: string, params?: Record<string, string>) {
    const url = new URL(path, RPC_BASE);
    if (params) {
      Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
    }
    const resp = await fetch(url.toString());
    if (!resp.ok) {
      throw new Error(`RPC query failed: ${resp.status} ${resp.statusText}`);
    }
    return resp.json();
  }

  async function broadcast(txBytes: Uint8Array) {
    const resp = await fetch(`${RPC_BASE}/broadcast_tx_commit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ tx: Buffer.from(txBytes).toString('base64') }),
    });
    return resp.json();
  }

  async function getTx(hash: string) {
    return query('/tx', { hash });
  }

  async function getBlock(height?: number) {
    const params = height ? { height: String(height) } : undefined;
    return query('/block', params);
  }

  async function getBalance(address: string) {
    const resp = await fetch(
      `${API_BASE}/api/v1/bank/balances?address=${address}`,
    );
    return resp.json();
  }

  return { query, broadcast, getTx, getBlock, getBalance };
}

// REST 查询适配器
function createRESTAdapter() {
  async function getLatestBlock() {
    const resp = await fetch(`${API_BASE}/api/v1/blocks/latest`);
    return resp.json();
  }

  async function getReceipts(address: string) {
    const resp = await fetch(`${API_BASE}/api/v1/receipts?address=${address}`);
    return resp.json();
  }

  async function getContractState(contractAddress: string) {
    const resp = await fetch(
      `${API_BASE}/api/v1/contracts/${contractAddress}`,
    );
    return resp.json();
  }

  return { getLatestBlock, getReceipts, getContractState };
}

4.8 Agent API 查询面

来自 rpc_methods.json 的 Agent 读取表面:

interface AgentQueryAdapter {
  account: (address: string) => Promise<any>;
  balance: (address: string) => Promise<any>;
  balances: (address: string) => Promise<any>;
  tx: (hash: string) => Promise<any>;
  block: (height: number) => Promise<any>;
  events_history: (filter: Record<string, unknown>) => Promise<any>;
}

const AGENT_API_BASE = 'https://api.msgchain.org/agent/v1';

function createAgentQueryAdapter(apiKey?: string): AgentQueryAdapter {
  const headers: Record<string, string> = {
    'Content-Type': 'application/json',
  };
  if (apiKey) {
    headers['X-MSG-API-Key'] = apiKey;
  }

  async function get(path: string) {
    const resp = await fetch(`${AGENT_API_BASE}${path}`, { headers });
    if (!resp.ok) {
      throw new Error(`Agent API error: ${resp.status}`);
    }
    return resp.json();
  }

  return {
    account: (address) => get(`/query/account?address=${address}`),
    balance: (address) => get(`/query/balance?address=${address}`),
    balances: (address) => get(`/query/balances?address=${address}`),
    tx: (hash) => get(`/query/tx?hash=${hash}`),
    block: (height) => get(`/query/block?height=${height}`),
    events_history: (filter) =>
      get(`/events/history?${new URLSearchParams(filter as any).toString()}`),
  };
}

4.9 受保护写路径

Agent API 的受保护写路径(需 API Key + 人工审批):

interface AgentGuardedWriteAdapter {
  wallet: (action: string, params: unknown) => Promise<any>;
  mpcSign: (payload: unknown) => Promise<any>;
  paymentSession: (params: unknown) => Promise<any>;
}

function createGuardedWriteAdapter(apiKey: string): AgentGuardedWriteAdapter {
  const headers = {
    'Content-Type': 'application/json',
    'X-MSG-API-Key': apiKey,
  };

  const BASE = 'https://api.msgchain.org/agent/v1';

  return {
    wallet: (action, params) =>
      fetch(`${BASE}/wallet/${action}`, {
        method: 'POST',
        headers,
        body: JSON.stringify(params),
      }).then((r) => r.json()),

    mpcSign: (payload) =>
      fetch(`${BASE}/mpc/sign`, {
        method: 'POST',
        headers,
        body: JSON.stringify(payload),
      }).then((r) => r.json()),

    paymentSession: (params) =>
      fetch(`${BASE}/payment/session`, {
        method: 'POST',
        headers,
        body: JSON.stringify(params),
      }).then((r) => r.json()),
  };
}

4.10 Keplr 模块边界(原文)

来自 keplr.html 模块的真实边界描述:

现状: 已有钱包接入底座

边界: 兼容目标不等于官方全量上线

Cosmos 路径:
- configs/keplr_chain_info.json (chainId=msg-chain-1, rpc, rest, bech32, currencies, feeCurrencies)
- configs/keplr_suggest_chain.js
  - window.keplr.experimentalSuggestChain(MSG_CHAIN_INFO)
  - window.keplr.enable("msg-chain-1")
  - window.keplr.getOfflineSigner("msg-chain-1")
- 节点接口面:
  - quantum/node.go 启动 Cosmos SDK 兼容 gRPC
  - tendermint rpc.go 注册 JSON-RPC/REST/broadcast_tx_commit

可信边界:
1. 钱包接入配置、suggestChain、JSON-RPC/gRPC/REST 底座已有
2. DApp 可按文档接入 Keplr/MetaMask 兼容路径

不可过度延伸为:
- "官方钱包生态已经全部上线并完成广泛生产验证"
- "所有外部钱包都已完成端到端主网级接入验收"

当前可信结论:
- 可以证明"钱包集成底座真实存在"
- 不能把它等同于"所有钱包生态都已完全运营化"

5. 前端适配器样板

5.1 React/TypeScript 项目结构

基于 keplr_contract_console starter 的前端适配器样板:

my-msg-dapp/
├── public/
│   └── index.html
├── src/
│   ├── index.tsx
│   ├── App.tsx
│   ├── config/
│   │   └── chain.ts              # 链配置 (chainId, RPC, REST, bech32)
│   ├── adapters/
│   │   ├── rpc.ts                # RPC 查询适配器
│   │   ├── rest.ts               # REST 查询适配器
│   │   └── agent.ts              # Agent API 适配器
│   ├── hooks/
│   │   ├── useKeplr.ts           # Keplr 钱包连接
│   │   ├── useWallet.ts          # 通用钱包
│   │   ├── useQuery.ts           # 链上查询
│   │   └── useExecute.ts         # 合约执行
│   ├── components/
│   │   ├── WalletConnect.tsx      # 钱包连接按钮
│   │   ├── WalletStatus.tsx       # 钱包状态显示
│   │   ├── BalanceDisplay.tsx     # 余额展示
│   │   ├── ContractQuery.tsx      # 合约查询
│   │   ├── ContractExecute.tsx    # 合约执行
│   │   ├── TxReceipt.tsx          # 交易回执
│   │   ├── ErrorBoundary.tsx      # 错误边界
│   │   └── LoadingSpinner.tsx     # 加载状态
│   ├── types/
│   │   ├── chain.ts              # 链相关类型
│   │   ├── wallet.ts             # 钱包相关类型
│   │   └── tx.ts                 # 交易相关类型
│   └── utils/
│       ├── format.ts             # 格式化工具
│       └── errors.ts             # 错误处理
├── .env.example
├── .env
├── package.json
├── tsconfig.json
└── vite.config.ts

5.2 链配置模块

// src/config/chain.ts

export const CHAIN_CONFIG = {
  chainId: 'msg-chain-1',
  chainName: 'MSG Chain',
  rpc: import.meta.env.VITE_MSG_RPC_URL || 'https://rpc.msgchain.org',
  rest: import.meta.env.VITE_MSG_REST_URL || 'https://api.msgchain.org',
  agentApi:
    import.meta.env.VITE_MSG_AGENT_API || 'https://api.msgchain.org/agent/v1',
  apiKey: import.meta.env.VITE_MSG_API_KEY || '',
  bech32Prefix: 'msg',
  coinDenom: 'MSG',
  coinMinimalDenom: 'umsg',
  coinDecimals: 18,
  gasPrice: '1000000000',
  gasPriceStep: {
    low: 1000000000,
    average: 1000000000,
    high: 1000000000,
  },
  features: ['cosmwasm'] as const,
} as const;

export function isProduction() {
  return import.meta.env.PROD;
}

export function requireApiKey() {
  if (!CHAIN_CONFIG.apiKey) {
    console.warn(
      '[MSG] No API Key configured. Agent guarded write paths will fail.',
    );
  }
  return CHAIN_CONFIG.apiKey;
}

5.3 Keplr Hook

// src/hooks/useKeplr.ts

import { useState, useEffect, useCallback } from 'react';
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { GasPrice } from '@cosmjs/stargate';
import { CHAIN_CONFIG } from '../config/chain';

interface KeplrState {
  address: string;
  client: SigningCosmWasmClient | null;
  isConnecting: boolean;
  isConnected: boolean;
  error: string | null;
}

const MSG_CHAIN_INFO = {
  chainId: CHAIN_CONFIG.chainId,
  chainName: CHAIN_CONFIG.chainName,
  rpc: CHAIN_CONFIG.rpc,
  rest: CHAIN_CONFIG.rest,
  bip44: { coinType: 118 },
  bech32Config: {
    bech32PrefixAccAddr: CHAIN_CONFIG.bech32Prefix,
    bech32PrefixAccPub: `${CHAIN_CONFIG.bech32Prefix}pub`,
    bech32PrefixValAddr: `${CHAIN_CONFIG.bech32Prefix}valoper`,
    bech32PrefixValPub: `${CHAIN_CONFIG.bech32Prefix}valoperpub`,
    bech32PrefixConsAddr: `${CHAIN_CONFIG.bech32Prefix}valcons`,
    bech32PrefixConsPub: `${CHAIN_CONFIG.bech32Prefix}valconspub`,
  },
  currencies: [
    {
      coinDenom: CHAIN_CONFIG.coinDenom,
      coinMinimalDenom: CHAIN_CONFIG.coinMinimalDenom,
      coinDecimals: CHAIN_CONFIG.coinDecimals,
    },
  ],
  feeCurrencies: [
    {
      coinDenom: CHAIN_CONFIG.coinDenom,
      coinMinimalDenom: CHAIN_CONFIG.coinMinimalDenom,
      coinDecimals: CHAIN_CONFIG.coinDecimals,
      gasPriceStep: CHAIN_CONFIG.gasPriceStep,
    },
  ],
  stakeCurrency: {
    coinDenom: CHAIN_CONFIG.coinDenom,
    coinMinimalDenom: CHAIN_CONFIG.coinMinimalDenom,
    coinDecimals: CHAIN_CONFIG.coinDecimals,
  },
  features: [...CHAIN_CONFIG.features],
};

export function useKeplr() {
  const [state, setState] = useState<KeplrState>({
    address: '',
    client: null,
    isConnecting: false,
    isConnected: false,
    error: null,
  });

  const connect = useCallback(async () => {
    setState((prev) => ({ ...prev, isConnecting: true, error: null }));

    try {
      if (!window.keplr) {
        throw new Error(
          'Keplr 扩展未安装。请安装 Keplr 后重试。',
        );
      }

      await window.keplr.experimentalSuggestChain(MSG_CHAIN_INFO);
      await window.keplr.enable(CHAIN_CONFIG.chainId);

      const offlineSigner = window.keplr.getOfflineSigner(
        CHAIN_CONFIG.chainId,
      );
      const accounts = await offlineSigner.getAccounts();
      const address = accounts[0].address;

      const client = await SigningCosmWasmClient.connectWithSigner(
        CHAIN_CONFIG.rpc,
        offlineSigner,
        {
          gasPrice: GasPrice.fromString(
            `${CHAIN_CONFIG.gasPrice}${CHAIN_CONFIG.coinMinimalDenom}`,
          ),
        },
      );

      setState({
        address,
        client,
        isConnecting: false,
        isConnected: true,
        error: null,
      });
    } catch (err: any) {
      setState({
        address: '',
        client: null,
        isConnecting: false,
        isConnected: false,
        error: err.message || '钱包连接失败',
      });
    }
  }, []);

  const disconnect = useCallback(() => {
    setState({
      address: '',
      client: null,
      isConnecting: false,
      isConnected: false,
      error: null,
    });
  }, []);

  return { ...state, connect, disconnect };
}

5.4 查询 Hook

// src/hooks/useQuery.ts

import { useState, useCallback } from 'react';
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { CHAIN_CONFIG } from '../config/chain';

interface QueryResult<T> {
  data: T | null;
  isLoading: boolean;
  error: string | null;
}

export function useQuery() {
  const [result, setResult] = useState<QueryResult<unknown>>({
    data: null,
    isLoading: false,
    error: null,
  });

  const queryBalance = useCallback(
    async (address: string) => {
      setResult({ data: null, isLoading: true, error: null });
      try {
        const client = await CosmWasmClient.connect(CHAIN_CONFIG.rpc);
        const balance = await client.getBalance(
          address,
          CHAIN_CONFIG.coinMinimalDenom,
        );
        setResult({ data: balance, isLoading: false, error: null });
        return balance;
      } catch (err: any) {
        setResult({
          data: null,
          isLoading: false,
          error: err.message || '余额查询失败',
        });
        return null;
      }
    },
    [],
  );

  const queryContract = useCallback(
    async (contractAddress: string, queryMsg: Record<string, unknown>) => {
      setResult({ data: null, isLoading: true, error: null });
      try {
        const client = await CosmWasmClient.connect(CHAIN_CONFIG.rpc);
        const data = await client.queryContractSmart(
          contractAddress,
          queryMsg,
        );
        setResult({ data, isLoading: false, error: null });
        return data;
      } catch (err: any) {
        setResult({
          data: null,
          isLoading: false,
          error: err.message || '合约查询失败',
        });
        return null;
      }
    },
    [],
  );

  return { ...result, queryBalance, queryContract };
}

5.5 执行 Hook

// src/hooks/useExecute.ts

import { useState, useCallback } from 'react';
import { CHAIN_CONFIG } from '../config/chain';
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { coins } from '@cosmjs/stargate';

interface ExecuteResult {
  transactionHash: string;
  height: number;
  gasUsed: number;
  events: Array<{ type: string; attributes: Array<{ key: string; value: string }> }>;
}

interface ExecuteState {
  result: ExecuteResult | null;
  isExecuting: boolean;
  error: string | null;
}

export function useExecute(client: SigningCosmWasmClient | null, sender: string) {
  const [state, setState] = useState<ExecuteState>({
    result: null,
    isExecuting: false,
    error: null,
  });

  const executeContract = useCallback(
    async (
      contractAddress: string,
      executeMsg: Record<string, unknown>,
      funds?: string,
    ) => {
      if (!client || !sender) {
        setState((prev) => ({
          ...prev,
          error: '钱包未连接。请先连接钱包。',
        }));
        return null;
      }

      setState({ result: null, isExecuting: true, error: null });

      try {
        const fee = 'auto';
        const memo = 'MSG dApp';
        const fundsCoins = funds
          ? coins(funds, CHAIN_CONFIG.coinMinimalDenom)
          : [];

        const response = await client.execute(
          sender,
          contractAddress,
          executeMsg,
          fee,
          memo,
          fundsCoins,
        );

        const txResult: ExecuteResult = {
          transactionHash: response.transactionHash,
          height: response.height,
          gasUsed: response.gasUsed,
          events: response.events.map((e) => ({
            type: e.type,
            attributes: e.attributes.map((a) => ({
              key: a.key,
              value: a.value,
            })),
          })),
        };

        setState({ result: txResult, isExecuting: false, error: null });
        return txResult;
      } catch (err: any) {
        setState({
          result: null,
          isExecuting: false,
          error: err.message || '合约执行失败',
        });
        return null;
      }
    },
    [client, sender],
  );

  return { ...state, executeContract };
}

5.6 钱包连接组件

// src/components/WalletConnect.tsx

import React from 'react';

interface WalletConnectProps {
  isConnected: boolean;
  isConnecting: boolean;
  address: string;
  onConnect: () => void;
  onDisconnect: () => void;
  error: string | null;
}

export function WalletConnect({
  isConnected,
  isConnecting,
  address,
  onConnect,
  onDisconnect,
  error,
}: WalletConnectProps) {
  if (isConnected) {
    return (
      <div className="wallet-connected">
        <div className="wallet-address">
          <span className="label">钱包地址</span>
          <code>{shortenAddress(address)}</code>
        </div>
        <button
          onClick={onDisconnect}
          className="btn-disconnect"
          disabled={isConnecting}
        >
          断开连接
        </button>
      </div>
    );
  }

  return (
    <div className="wallet-connect">
      <button
        onClick={onConnect}
        className="btn-connect"
        disabled={isConnecting}
      >
        {isConnecting ? '连接中...' : '连接 Keplr 钱包'}
      </button>
      {error && <div className="error-message">{error}</div>}
    </div>
  );
}

function shortenAddress(address: string): string {
  if (address.length <= 12) return address;
  return `${address.slice(0, 6)}...${address.slice(-6)}`;
}

5.7 交易回执组件

// src/components/TxReceipt.tsx

import React from 'react';

interface TxReceiptProps {
  transactionHash: string;
  height: number;
  gasUsed: number;
  events: Array<{
    type: string;
    attributes: Array<{ key: string; value: string }>;
  }>;
}

export function TxReceipt({
  transactionHash,
  height,
  gasUsed,
  events,
}: TxReceiptProps) {
  return (
    <div className="tx-receipt">
      <h3>交易回执</h3>

      <div className="receipt-field">
        <span className="label">交易哈希</span>
        <code className="hash">{transactionHash}</code>
      </div>

      <div className="receipt-field">
        <span className="label">区块高度</span>
        <span>{height.toLocaleString()}</span>
      </div>

      <div className="receipt-field">
        <span className="label">Gas 消耗</span>
        <span>{gasUsed.toLocaleString()}</span>
      </div>

      <div className="receipt-events">
        <h4>事件 ({events.length})</h4>
        {events.map((event, i) => (
          <div key={i} className="event">
            <div className="event-type">{event.type}</div>
            <div className="event-attributes">
              {event.attributes.map((attr, j) => (
                <div key={j} className="event-attr">
                  <code>{attr.key}</code>: <code>{attr.value}</code>
                </div>
              ))}
            </div>
          </div>
        ))}
      </div>

      <div className="receipt-actions">
        <a
          href={`https://explorer.msgchain.org/tx/${transactionHash}`}
          target="_blank"
          rel="noopener noreferrer"
        >
          在区块浏览器中查看
        </a>
      </div>
    </div>
  );
}

5.8 错误边界

// src/components/ErrorBoundary.tsx

import React, { Component, type ReactNode } from 'react';

interface ErrorBoundaryProps {
  children: ReactNode;
  fallback?: ReactNode;
}

interface ErrorBoundaryState {
  hasError: boolean;
  error: Error | null;
}

export class ErrorBoundary extends Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
  constructor(props: ErrorBoundaryProps) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
    console.error('[MSG ErrorBoundary]', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      if (this.props.fallback) {
        return this.props.fallback;
      }
      return (
        <div className="error-boundary">
          <h2>组件渲染异常</h2>
          <p>{this.state.error?.message || '未知错误'}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            重试
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

5.9 余额展示组件

// src/components/BalanceDisplay.tsx

import React, { useEffect, useState } from 'react';
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { CHAIN_CONFIG } from '../config/chain';

interface BalanceDisplayProps {
  address: string;
}

export function BalanceDisplay({ address }: BalanceDisplayProps) {
  const [balance, setBalance] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchBalance() {
      try {
        setIsLoading(true);
        const client = await CosmWasmClient.connect(CHAIN_CONFIG.rpc);
        const coin = await client.getBalance(
          address,
          CHAIN_CONFIG.coinMinimalDenom,
        );
        if (!cancelled) {
          setBalance(formatUnits(coin.amount, CHAIN_CONFIG.coinDecimals));
          setError(null);
        }
      } catch (err: any) {
        if (!cancelled) {
          setError(err.message || '余额查询失败');
        }
      } finally {
        if (!cancelled) setIsLoading(false);
      }
    }

    fetchBalance();
    return () => { cancelled = true; };
  }, [address]);

  if (isLoading) return <div className="balance-loading">查询余额中...</div>;
  if (error) return <div className="balance-error">{error}</div>;

  return (
    <div className="balance-display">
      <span className="label">余额</span>
      <span className="amount">{balance}</span>
      <span className="denom">{CHAIN_CONFIG.coinDenom}</span>
    </div>
  );
}

function formatUnits(amount: string, decimals: number): string {
  const padded = amount.padStart(decimals + 1, '0');
  const integerPart = padded.slice(0, padded.length - decimals) || '0';
  const fractionalPart = padded.slice(padded.length - decimals);
  return `${integerPart}.${fractionalPart.slice(0, 6)}`;
}

5.10 合约查询组件

// src/components/ContractQuery.tsx

import React, { useState } from 'react';
import { useQuery } from '../hooks/useQuery';

export function ContractQuery() {
  const [contractAddress, setContractAddress] = useState('');
  const [queryMsg, setQueryMsg] = useState('{}');
  const { data, isLoading, error, queryContract } = useQuery();

  const handleQuery = async () => {
    try {
      const msg = JSON.parse(queryMsg);
      await queryContract(contractAddress, msg);
    } catch {
      // JSON 解析错误由 UI 显示
    }
  };

  return (
    <div className="contract-query">
      <h3>合约查询</h3>

      <div className="form-field">
        <label>合约地址</label>
        <input
          type="text"
          value={contractAddress}
          onChange={(e) => setContractAddress(e.target.value)}
          placeholder="msg1..."
        />
      </div>

      <div className="form-field">
        <label>查询消息 (JSON)</label>
        <textarea
          value={queryMsg}
          onChange={(e) => setQueryMsg(e.target.value)}
          rows={4}
          placeholder='{"get_config": {}}'
        />
      </div>

      <button onClick={handleQuery} disabled={isLoading || !contractAddress}>
        {isLoading ? '查询中...' : '查询'}
      </button>

      {error && <div className="error-message">{error}</div>}

      {data && (
        <div className="query-result">
          <pre>{JSON.stringify(data, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

5.11 合约执行组件

// src/components/ContractExecute.tsx

import React, { useState } from 'react';
import { useExecute } from '../hooks/useExecute';
import { TxReceipt } from './TxReceipt';
import type { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';

interface ContractExecuteProps {
  client: SigningCosmWasmClient | null;
  sender: string;
}

export function ContractExecute({ client, sender }: ContractExecuteProps) {
  const [contractAddress, setContractAddress] = useState('');
  const [executeMsg, setExecuteMsg] = useState('{}');
  const [funds, setFunds] = useState('');
  const { result, isExecuting, error, executeContract } = useExecute(
    client,
    sender,
  );

  const handleExecute = async () => {
    try {
      const msg = JSON.parse(executeMsg);
      await executeContract(
        contractAddress,
        msg,
        funds || undefined,
      );
    } catch {
      // JSON 解析错误
    }
  };

  if (!client || !sender) {
    return (
      <div className="execute-disabled">
        <p>请先连接钱包以执行合约。</p>
      </div>
    );
  }

  return (
    <div className="contract-execute">
      <h3>合约执行</h3>

      <div className="form-field">
        <label>合约地址</label>
        <input
          type="text"
          value={contractAddress}
          onChange={(e) => setContractAddress(e.target.value)}
          placeholder="msg1..."
        />
      </div>

      <div className="form-field">
        <label>执行消息 (JSON)</label>
        <textarea
          value={executeMsg}
          onChange={(e) => setExecuteMsg(e.target.value)}
          rows={4}
          placeholder='{"transfer": {"recipient": "msg1...", "amount": "1000000"}}'
        />
      </div>

      <div className="form-field">
        <label>附带金额 (可选, umsg)</label>
        <input
          type="text"
          value={funds}
          onChange={(e) => setFunds(e.target.value)}
          placeholder="1000000"
        />
      </div>

      <button onClick={handleExecute} disabled={isExecuting || !contractAddress}>
        {isExecuting ? '执行中...' : '执行'}
      </button>

      {error && <div className="error-message">{error}</div>}

      {result && (
        <TxReceipt
          transactionHash={result.transactionHash}
          height={result.height}
          gasUsed={result.gasUsed}
          events={result.events}
        />
      )}
    </div>
  );
}

5.12 主应用组件

// src/App.tsx

import React from 'react';
import { useKeplr } from './hooks/useKeplr';
import { WalletConnect } from './components/WalletConnect';
import { BalanceDisplay } from './components/BalanceDisplay';
import { ContractQuery } from './components/ContractQuery';
import { ContractExecute } from './components/ContractExecute';
import { ErrorBoundary } from './components/ErrorBoundary';

export function App() {
  const {
    address,
    client,
    isConnecting,
    isConnected,
    error: walletError,
    connect,
    disconnect,
  } = useKeplr();

  return (
    <div className="app">
      <header className="app-header">
        <h1>MSG dApp</h1>
        <WalletConnect
          isConnected={isConnected}
          isConnecting={isConnecting}
          address={address}
          onConnect={connect}
          onDisconnect={disconnect}
          error={walletError}
        />
      </header>

      <main className="app-main">
        {isConnected && (
          <>
            <section className="section-balance">
              <h2>钱包信息</h2>
              <ErrorBoundary>
                <BalanceDisplay address={address} />
              </ErrorBoundary>
            </section>

            <section className="section-query">
              <h2>合约查询</h2>
              <ErrorBoundary>
                <ContractQuery />
              </ErrorBoundary>
            </section>

            <section className="section-execute">
              <h2>合约执行</h2>
              <ErrorBoundary>
                <ContractExecute client={client} sender={address} />
              </ErrorBoundary>
            </section>
          </>
        )}

        {!isConnected && (
          <div className="welcome">
            <p>欢迎使用 MSG dApp 前端启动器。</p>
            <p>请连接 Keplr 钱包开始交互。</p>
          </div>
        )}
      </main>
    </div>
  );
}

5.13 移动端适配

/* src/styles/mobile.css */

@media (max-width: 768px) {
  .app-header {
    flex-direction: column;
    gap: 1rem;
    padding: 1rem;
  }

  .wallet-connect,
  .wallet-connected {
    width: 100%;
  }

  .wallet-connected {
    flex-direction: column;
    align-items: stretch;
  }

  .wallet-address {
    word-break: break-all;
  }

  .btn-connect,
  .btn-disconnect {
    width: 100%;
    padding: 0.75rem;
  }

  .contract-query,
  .contract-execute {
    padding: 1rem;
  }

  .form-field input,
  .form-field textarea {
    font-size: 16px; /* 防止 iOS 缩放 */
  }

  .tx-receipt {
    overflow-x: auto;
  }

  .receipt-field {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.25rem;
  }

  .receipt-events {
    max-height: 300px;
    overflow-y: auto;
  }
}

@media (max-width: 480px) {
  .app-main {
    padding: 0.5rem;
  }

  .error-message {
    font-size: 0.875rem;
    padding: 0.5rem;
  }

  .query-result pre {
    font-size: 0.75rem;
  }
}

5.14 环境变量示例

# .env.example — MSG dApp 环境变量
# 复制为 .env 并填入实际值

# MSG Chain RPC 端点
VITE_MSG_RPC_URL=https://rpc.msgchain.org

# MSG Chain REST 端点
VITE_MSG_REST_URL=https://api.msgchain.org

# Agent API 基础路径
VITE_MSG_AGENT_API=https://api.msgchain.org/agent/v1

# Agent API Key (可选,写路径需要)
VITE_MSG_API_KEY=

# 网络模式 (mainnet/testnet/local)
VITE_MSG_NETWORK=mainnet

5.15 Agent API 集成

// src/adapters/agent.ts

import { CHAIN_CONFIG } from '../config/chain';

interface AgentResponse<T = unknown> {
  success: boolean;
  data?: T;
  error?: string;
  stub?: boolean;
}

class AgentAPI {
  private baseUrl: string;
  private apiKey: string;

  constructor() {
    this.baseUrl = CHAIN_CONFIG.agentApi;
    this.apiKey = CHAIN_CONFIG.apiKey;
  }

  private async request<T>(
    method: string,
    path: string,
    body?: unknown,
  ): Promise<AgentResponse<T>> {
    const headers: Record<string, string> = {
      'Content-Type': 'application/json',
    };
    if (this.apiKey) {
      headers['X-MSG-API-Key'] = this.apiKey;
    }

    const resp = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    const result: AgentResponse<T> = await resp.json();

    // 检测 stub 响应
    if (resp.headers.get('X-MSG-Stub') === 'true') {
      console.warn('[AgentAPI] Stub response — write not actually executed.');
      result.stub = true;
    }

    return result;
  }

  // 查询面
  async queryAccount(address: string) {
    return this.request('GET', `/query/account?address=${address}`);
  }

  async queryBalance(address: string) {
    return this.request('GET', `/query/balance?address=${address}`);
  }

  async queryBalances(address: string) {
    return this.request('GET', `/query/balances?address=${address}`);
  }

  async queryTx(hash: string) {
    return this.request('GET', `/query/tx?hash=${hash}`);
  }

  async queryBlock(height: number) {
    return this.request('GET', `/query/block?height=${height}`);
  }

  async eventsHistory(filter: Record<string, string>) {
    const params = new URLSearchParams(filter);
    return this.request('GET', `/events/history?${params.toString()}`);
  }

  // 受保护写面
  async walletAction(action: string, params: Record<string, unknown>) {
    return this.request('POST', `/wallet/${action}`, params);
  }

  async mpcSign(payload: Record<string, unknown>) {
    return this.request('POST', '/mpc/sign', payload);
  }

  async paymentSession(params: Record<string, unknown>) {
    return this.request('POST', '/payment/session', params);
  }
}

export const agentAPI = new AgentAPI();

5.16 错误处理工具

// src/utils/errors.ts

export class MSGError extends Error {
  constructor(
    message: string,
    public readonly code?: string,
    public readonly txHash?: string,
  ) {
    super(message);
    this.name = 'MSGError';
  }
}

export class WalletNotConnectedError extends MSGError {
  constructor() {
    super('钱包未连接。请使用 Keplr 连接 MSG Chain。', 'WALLET_NOT_CONNECTED');
    this.name = 'WalletNotConnectedError';
  }
}

export class KeplrNotFoundError extends MSGError {
  constructor() {
    super(
      'Keplr 扩展未安装。请从 keplr.app 安装。',
      'KEPLR_NOT_FOUND',
    );
    this.name = 'KeplrNotFoundError';
  }
}

export class ChainNotConfiguredError extends MSGError {
  constructor(chainId: string) {
    super(
      `链 ${chainId} 未配置。请先调用 suggestChain。`,
      'CHAIN_NOT_CONFIGURED',
    );
    this.name = 'ChainNotConfiguredError';
  }
}

export class TransactionFailedError extends MSGError {
  constructor(
    message: string,
    public readonly code: string,
    txHash?: string,
  ) {
    super(message, code, txHash);
    this.name = 'TransactionFailedError';
  }
}

export class GuardedWriteBlockedError extends MSGError {
  constructor(path: string) {
    super(
      `受保护写路径 ${path} 需要 API Key 或人工审批。`,
      'GUARDED_WRITE_BLOCKED',
    );
    this.name = 'GuardedWriteBlockedError';
  }
}

export class StubWriteWarning extends MSGError {
  constructor(path: string) {
    super(
      `写路径 ${path} 当前为 Stub 状态,写入未实际执行。`,
      'STUB_WRITE',
    );
    this.name = 'StubWriteWarning';
  }
}

export function handleMSGError(error: unknown): string {
  if (error instanceof MSGError) {
    return `[${error.code}] ${error.message}`;
  }
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

export function assertWalletConnected(
  client: unknown,
  address: string,
): asserts client is NonNullable<typeof client> {
  if (!client || !address) {
    throw new WalletNotConnectedError();
  }
}

6. 边界声明

6.1 核心边界:dapp_starter_pack

dapp_starter_ready ≠ production dApp

dapp_starter_pack 状态为 starter_ready + write_path_ready: true,但 production_supported: false。

边界 说明
前端配对层仍缺 CDN/模板市场 Starter 源码需要自行搭建和构建,尚无官方 CDN 或模板市场
前端 starter 对 AI 的作用受限于前端工具链的快速迭代 AI 生成的代码需要适配不断变化的前端生态
未提供正式 UI 设计系统 Starter 只提供逻辑骨架,不包含完整 UI 组件库
未包含生产监控与运维 Stater 不含日志、指标、告警等生产基础设施

正确理解:AI 可以使用 dapp_starter_pack 快速生成 dApp 前端原型和适配器代码,但生产部署需要:

6.2 核心边界:wallet_frontend

wallet_frontend 是 guarded_integration,不是开发参考级钱包生态

wallet_frontend 状态为 guarded_integration + production_supported: false。

{
  "surface_id": "wallet_frontend",
  "machine_readiness": "guarded_integration",
  "production_supported": false,
  "boundaries": ["当前是兼容路径与配置底座,不应表述成官方钱包生态已全量上线。"]
}

可以主张:

不能主张:

6.3 链配置边界

来自 chain_config/network_presets.json 的 boundary 字段:

链配置是接入基线,不等于 live/public 端点已经被正式接受为生产网络。
生产接入前仍需复核 endpoint 可用性、域名控制、证书、SLO 与发布权限。

6.4 RPC 能力边界

来自 rpc_methods.json 的边界声明:

{
  "boundaries": [
    "当前是白皮书级概要,不包含完整参数与错误码 schema。"
  ]
}

6.5 Explorer 边界

来自 explorer_receipts 能力面:

{
  "surface_id": "explorer_receipts",
  "machine_readiness": "read_only_assist",
  "production_supported": false,
  "boundaries": ["可辅助开发调试与回证,不应直接当成完全生产化 Explorer 平台。"]
}

可以:使用 Explorer 查询回执、事件、日志辅助开发调试
不能:将本地 Explorer 子门禁视为完整的开发参考级区块浏览器

6.6 Agent API 写路径边界

{
  "surface_id": "agent_query_and_guarded_write",
  "machine_readiness": "guarded_write",
  "machine_safe_for_codegen": false,
  "write_path_ready": false,
  "production_supported": false,
  "boundaries": [
    "不能把 Agent 写路径当成已完成的全自动生产执行面。",
    "所有高风险写动作仍需区分真实写路径、受保护路径与 stub 路径。"
  ]
}

6.7 配方硬边界

来自 dapp_minimal.json 的 hard_boundaries:

  1. 当前 starter 与链配置包能显著降低起步成本,但仍不能替代正式稳定 SDK。
  2. Explorer/索引面可辅助调试,但不应替代正式生产运维监控。
  3. 没有真实部署权限、域名控制权与生产配置,AI 不能独立完成最终发布。

6.8 开发者入口边界

来自 developer_entry.json 的 current_boundaries:

  1. 当前开发协议层可显著提升 AI coding 的可执行性,但仍不能诚实承诺"只靠入口即可 100% 自动完成任何产品上线"。
  2. 当前已补 Quick Start、source-backed 合约消费索引、正式 API/Schema 契约索引与 fail-closed sandbox 策略,但仍不等于 signed public SDK、public sandbox 或 not independently verified for production 交付。
  3. 涉及私钥、部署权限、生产域名、资金操作、DAO/timelock/threshold 的动作,必须保留人类确认与审批门禁。

6.9 边界速查表

组件 可以用 AI 生成 需要人工审核 不可生产使用
前端适配器样板 ✅ ✅ —
钱包连接代码 ✅ ✅ —
链配置 ✅ ✅ —
RPC 查询适配器 ✅ — —
Agent 查询适配器 ✅ — —
合约执行代码 ✅ ✅ —
Explorer 查询 ✅ — —
Agent 写路径 (guarded) — ✅ ✅ (无 API Key)
Agent 写路径 (stub) — — ✅
生产部署 — ✅ (必须) —

6.10 AI 决策模型

查询路径(REST/RPC/Agent Query)
  └─ AI 可直接生成并使用

钱包连接(Keplr suggestChain + CosmJS)
  └─ AI 可生成代码
  └─ 生产接入前需人工复核 endpoint

合约执行(Execute / Broadcast)
  └─ AI 可生成适配器代码
  └─ 执行前需人工确认合约地址和消息

受保护写路径(Agent Wallet/MPC/Payment)
  └─ AI 可生成适配器代码
  └─ 需要 API Key
  └─ 高风险操作需人工审批

Stub 写路径(Defi/Bridge/Registry)
  └─ AI 必须检测 X-MSG-Stub 响应头
  └─ 不可视为真实写入

生产发布
  └─ AI 可生成检查清单和部署文档
  └─ 必须有真实部署权限和人工确认
  └─ AI 不能独立完成最终发布

7. 完整示例

7.1 端到端流程:钱包连接 → 查询 → 写入 → 验证

本章实现一个完整的端到端 dApp 交互流程:

Step 1: 连接 Keplr 钱包 → 获取 msg1... 地址
Step 2: 查询余额 → 确认账户状态
Step 3: 合约查询 → 读取链上状态
Step 4: 合约执行 → 广播交易
Step 5: 回执验证 → 使用 tx hash 查询确认

7.2 Python 完整示例

"""
MSG dApp 端到端交互示例
依赖: pip install requests cosmjs-types
"""

import json
import hashlib
import base64
from typing import Any, Optional
from dataclasses import dataclass

import requests

# ============================================================
# 配置
# ============================================================

CHAIN_ID = 'msg-chain-1'
RPC_URL = 'https://rpc.msgchain.org'
REST_URL = 'https://api.msgchain.org'
AGENT_API = 'https://api.msgchain.org/agent/v1'
BECH32_PREFIX = 'msg'
DENOM = 'umsg'
GAS_PRICE = 1000000000

# ============================================================
# 数据类型
# ============================================================

@dataclass
class WalletInfo:
    address: str
    pubkey: str

@dataclass
class Balance:
    denom: str
    amount: str

@dataclass
class TxReceipt:
    hash: str
    height: int
    gas_used: int
    success: bool
    raw: dict

@dataclass
class ContractState:
    data: Any
    height: int

# ============================================================
# RPC 适配器
# ============================================================

class RPCCLient:
    """Tendermint RPC 客户端"""

    def __init__(self, rpc_url: str = RPC_URL):
        self.rpc_url = rpc_url.rstrip('/')

    def _call(self, method: str, params: Optional[dict] = None) -> dict:
        payload = {
            'jsonrpc': '2.0',
            'id': 1,
            'method': method,
            'params': params or {},
        }
        resp = requests.post(self.rpc_url, json=payload)
        resp.raise_for_status()
        result = resp.json()
        if 'error' in result:
            raise RuntimeError(f'RPC error: {result["error"]}')
        return result.get('result', {})

    def abci_query(self, path: str, data: bytes = b'') -> dict:
        params = {'path': f'/{path.lstrip("/")}'}
        if data:
            params['data'] = base64.b64encode(data).decode()
        return self._call('abci_query', params)

    def broadcast_tx_commit(self, tx_bytes: bytes) -> dict:
        tx_b64 = base64.b64encode(tx_bytes).decode()
        return self._call('broadcast_tx_commit', {'tx': tx_b64})

    def get_tx(self, hash_hex: str) -> dict:
        return self._call('tx', {'hash': f'0x{hash_hex}'})

    def get_block(self, height: Optional[int] = None) -> dict:
        params = {}
        if height is not None:
            params['height'] = str(height)
        return self._call('block', params)

    def get_net_info(self) -> dict:
        return self._call('net_info')

    def health(self) -> bool:
        try:
            self._call('health')
            return True
        except Exception:
            return False


class RESTClient:
    """REST API 客户端"""

    def __init__(self, rest_url: str = REST_URL):
        self.rest_url = rest_url.rstrip('/')

    def _get(self, path: str) -> dict:
        resp = requests.get(f'{self.rest_url}{path}')
        resp.raise_for_status()
        return resp.json()

    def get_balance(self, address: str) -> Balance:
        data = self._get(f'/api/v1/bank/balances?address={address}')
        if data and 'balances' in data:
            for coin in data['balances']:
                if coin.get('denom') == DENOM:
                    return Balance(denom=coin['denom'], amount=coin['amount'])
        return Balance(denom=DENOM, amount='0')

    def get_latest_block(self) -> dict:
        return self._get('/api/v1/blocks/latest')

    def get_receipts(self, address: str) -> list:
        data = self._get(f'/api/v1/receipts?address={address}')
        return data.get('receipts', [])

    def get_contracts(self, source: Optional[str] = None) -> dict:
        path = '/api/v1/contracts'
        if source:
            path += f'?source={source}'
        return self._get(path)

    def get_status(self) -> dict:
        return self._get('/api/v1/status')

    def get_auth_account(self, address: str) -> dict:
        return self._get(f'/api/v1/auth/accounts?address={address}')


class AgentAPIClient:
    """Agent API 客户端(查询面)"""

    def __init__(self, base_url: str = AGENT_API, api_key: str = ''):
        self.base_url = base_url.rstrip('/')
        self.api_key = api_key

    def _get(self, path: str) -> dict:
        headers = {'Content-Type': 'application/json'}
        if self.api_key:
            headers['X-MSG-API-Key'] = self.api_key
        resp = requests.get(f'{self.base_url}{path}', headers=headers)
        resp.raise_for_status()
        return resp.json()

    def _post(self, path: str, body: dict) -> dict:
        headers = {'Content-Type': 'application/json'}
        if self.api_key:
            headers['X-MSG-API-Key'] = self.api_key
        resp = requests.post(f'{self.base_url}{path}', json=body, headers=headers)
        result = resp.json()
        # 检测 stub
        if resp.headers.get('X-MSG-Stub') == 'true':
            print('[WARN] 写路径返回 Stub,写入未实际执行。')
            result['_stub'] = True
        return result

    def query_account(self, address: str) -> dict:
        return self._get(f'/query/account?address={address}')

    def query_balance(self, address: str) -> dict:
        return self._get(f'/query/balance?address={address}')

    def query_tx(self, hash: str) -> dict:
        return self._get(f'/query/tx?hash={hash}')

    def query_block(self, height: int) -> dict:
        return self._get(f'/query/block?height={height}')

    def events_history(self, filter_params: dict) -> dict:
        params = '&'.join(f'{k}={v}' for k, v in filter_params.items())
        return self._get(f'/events/history?{params}')

    def guarded_wallet(self, action: str, params: dict) -> dict:
        return self._post(f'/wallet/{action}', params)

    def guarded_mpc_sign(self, payload: dict) -> dict:
        return self._post('/mpc/sign', payload)

    def guarded_payment_session(self, params: dict) -> dict:
        return self._post('/payment/session', params)

# ============================================================
# dApp 交互引擎
# ============================================================

class MSGDAppEngine:
    """
    MSG dApp 端到端交互引擎
    工作流: 钱包连接 → 查询 → 写入 → 验证
    """

    def __init__(
        self,
        wallet: Optional[WalletInfo] = None,
        api_key: str = '',
    ):
        self.wallet = wallet
        self.rpc = RPCCLient()
        self.rest = RESTClient()
        self.agent = AgentAPIClient(api_key=api_key)

    # ----------------------------------------------------------
    # Step 1: 钱包连接
    # ----------------------------------------------------------

    def connect_wallet(self, mnemonic_or_address: str) -> WalletInfo:
        """
        连接钱包(前端场景使用 Keplr,后端场景使用助记词)

        前端:
          await window.keplr.experimentalSuggestChain(MSG_CHAIN_INFO)
          await window.keplr.enable("msg-chain-1")
          const offlineSigner = window.keplr.getOfflineSigner("msg-chain-1")
          const accounts = await offlineSigner.getAccounts()
          address = accounts[0].address
        """
        print(f'\n=== Step 1: 钱包连接 ===')

        if mnemonic_or_address.startswith(BECH32_PREFIX):
            # 直接使用地址(模拟连接)
            self.wallet = WalletInfo(
                address=mnemonic_or_address,
                pubkey='',
            )
        else:
            # 从助记词派生地址(后端场景)
            print('[TODO] 使用 cosmjs 或 bip32 从助记词派生地址')
            raise NotImplementedError(
                '请在前端使用 Keplr 连接,或提供 msg1... 地址。'
            )

        print(f'  钱包地址: {self.wallet.address}')
        print(f'  链 ID:    {CHAIN_ID}')

        # 验证连接
        try:
            status = self.rest.get_status()
            print(f'  节点状态: {status.get("node_info", {}).get("network", "unknown")}')
        except Exception as e:
            print(f'  节点连接异常: {e}')

        return self.wallet

    # ----------------------------------------------------------
    # Step 2: 余额查询
    # ----------------------------------------------------------

    def query_balance(self) -> Balance:
        """查询钱包余额"""
        print(f'\n=== Step 2: 余额查询 ===')

        if not self.wallet:
            raise RuntimeError('请先连接钱包。')

        balance = self.rest.get_balance(self.wallet.address)
        formatted = f'{int(balance.amount) / 10**18:.6f}'
        print(f'  地址: {self.wallet.address}')
        print(f'  余额: {formatted} {balance.denom}')

        return balance

    # ----------------------------------------------------------
    # Step 3: 合约查询
    # ----------------------------------------------------------

    def query_contract(
        self,
        contract_address: str,
        query_msg: dict,
    ) -> ContractState:
        """查询合约状态"""
        print(f'\n=== Step 3: 合约查询 ===')
        print(f'  合约地址: {contract_address}')
        print(f'  查询消息: {json.dumps(query_msg, ensure_ascii=False)}')

        # 通过 RPC 查询
        query_bytes = json.dumps(query_msg).encode()
        # 在真实场景中,这里应使用 CosmJS 的 queryContractSmart
        # 此处演示 RPC abci_query 路径
        try:
            result = self.rpc.abci_query(
                f'wasm/contract/{contract_address}/smart',
                query_bytes,
            )
            if result and 'response' in result:
                response = result['response']
                height = int(response.get('height', 0))
                data_b64 = response.get('value', '')
                if data_b64:
                    data = json.loads(base64.b64decode(data_b64).decode())
                else:
                    data = None
                state = ContractState(data=data, height=height)
                print(f'  查询结果 (高度 {height}):')
                print(f'    {json.dumps(state.data, indent=4, ensure_ascii=False)}')
                return state
        except Exception as e:
            print(f'  RPC 查询失败: {e}')

        print('  [提示] 请使用 CosmJS queryContractSmart 进行标准查询')
        return ContractState(data=None, height=0)

    # ----------------------------------------------------------
    # Step 4: 合约执行
    # ----------------------------------------------------------

    def execute_contract(
        self,
        contract_address: str,
        execute_msg: dict,
        funds: str = '',
    ) -> TxReceipt:
        """
        执行合约(签名广播)

        前端场景:
          const response = await client.execute(
            sender, contractAddress, executeMsg, fee, memo, funds
          )
        """
        print(f'\n=== Step 4: 合约执行 ===')
        print(f'  合约地址: {contract_address}')
        print(f'  执行消息: {json.dumps(execute_msg, ensure_ascii=False)}')

        if not self.wallet:
            raise RuntimeError('请先连接钱包。')

        print(f'  发送地址: {self.wallet.address}')
        print(f'  [提示] 合约执行需要在前端通过 Keplr 签名')
        print(f'  [提示] 请使用 useExecute hook 或 CosmJS SigningCosmWasmClient')

        # 模拟回执构建(真实场景由 client.execute 返回)
        mock_receipt = TxReceipt(
            hash='',
            height=0,
            gas_used=0,
            success=True,
            raw={},
        )
        return mock_receipt

    # ----------------------------------------------------------
    # Step 5: 回执验证
    # ----------------------------------------------------------

    def verify_transaction(self, tx_hash: str) -> dict:
        """使用 tx hash 验证交易"""
        print(f'\n=== Step 5: 回执验证 ===')
        print(f'  交易哈希: 0x{tx_hash}')

        # 通过 RPC 查询交易
        tx_result = self.rpc.get_tx(tx_hash)
        if tx_result:
            height = tx_result.get('height', 'unknown')
            tx_data = tx_result.get('tx', {})
            tx_result_data = tx_result.get('tx_result', {})

            success = tx_result_data.get('code', 1) == 0
            gas_used = tx_result_data.get('gas_used', 0)
            log = tx_result_data.get('log', '')

            print(f'  区块高度: {height}')
            print(f'  执行成功: {success}')
            print(f'  Gas 消耗: {gas_used}')
            if log:
                print(f'  日志: {log}')

            if tx_result_data.get('events'):
                print(f'  事件:')
                for event in tx_result_data['events']:
                    print(f'    - {event.get("type", "unknown")}')
                    for attr in event.get('attributes', []):
                        key = base64.b64decode(attr.get('key', '')).decode()
                        value = base64.b64decode(attr.get('value', '')).decode()
                        print(f'      {key}: {value}')

            return tx_result
        else:
            print('  未找到交易,请检查 tx hash 是否正确。')
            return {}

    # ----------------------------------------------------------
    # 验证循环
    # ----------------------------------------------------------

    def verification_loop(self, tx_hash: str, max_retries: int = 5):
        """验证循环:轮询直到交易被链确认"""
        print(f'\n=== 验证循环 ===')
        print(f'  交易哈希: 0x{tx_hash}')
        print(f'  最大重试: {max_retries}')

        for attempt in range(1, max_retries + 1):
            print(f'\n  尝试 #{attempt}...')
            try:
                result = self.verify_transaction(tx_hash)
                if result:
                    print('  交易已确认 ✓')
                    return result
            except Exception as e:
                print(f'  查询异常: {e}')

            if attempt < max_retries:
                import time
                print('  等待下一个区块...')
                time.sleep(2)

        print('  验证超时 — 请使用 Explorer 手动确认。')
        return None

    # ----------------------------------------------------------
    # 全流程
    # ----------------------------------------------------------

    def run_full_flow(
        self,
        address: str,
        contract_address: Optional[str] = None,
        query_msg: Optional[dict] = None,
        execute_msg: Optional[dict] = None,
    ):
        """运行端到端全流程"""
        print('=' * 60)
        print('  MSG dApp 端到端交互全流程')
        print('  Chain ID: msg-chain-1')
        print('=' * 60)

        # Step 1: 钱包连接
        wallet = self.connect_wallet(address)
        print(f'  ✓ 钱包已连接: {wallet.address}')

        # Step 2: 余额查询
        balance = self.query_balance()
        print(f'  ✓ 余额: {int(balance.amount) / 10**18:.6f} MSG')

        # Step 3: 合约查询(可选)
        if contract_address and query_msg:
            state = self.query_contract(contract_address, query_msg)
            print(f'  ✓ 合约状态已获取')

        # Step 4: 合约执行(可选)
        tx_hash = None
        if contract_address and execute_msg:
            receipt = self.execute_contract(contract_address, execute_msg)
            print(f'  ✓ 合约已执行')
            tx_hash = receipt.hash

        # Step 5: 验证(可选)
        if tx_hash:
            result = self.verification_loop(tx_hash)
            if result:
                print(f'  ✓ 交易已确认')
            else:
                print(f'  ⚠ 交易未确认,请在 Explorer 中手动查询。')

        print('=' * 60)
        print('  全流程完成')
        print('=' * 60)


# ============================================================
# 主程序
# ============================================================

def main():
    """端到端示例主程序"""

    # 配置
    USER_ADDRESS = 'msg1exampleaddress...'  # 替换为实际地址
    CONTRACT_ADDRESS = ''                    # 替换为实际合约地址

    # 创建引擎
    engine = MSGDAppEngine(api_key='')

    # 运行全流程(仅查询)
    engine.run_full_flow(
        address=USER_ADDRESS,
    )

    # 运行全流程(含合约交互)
    # engine.run_full_flow(
    #     address=USER_ADDRESS,
    #     contract_address=CONTRACT_ADDRESS,
    #     query_msg={'get_config': {}},
    #     execute_msg={'increment': {}},
    # )


if __name__ == '__main__':
    main()

7.3 TypeScript 端到端示例

/**
 * MSG dApp 端到端交互示例 (TypeScript)
 */

import { SigningCosmWasmClient, CosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { GasPrice } from '@cosmjs/stargate';

// ============================================================
// 配置
// ============================================================

const CONFIG = {
  chainId: 'msg-chain-1',
  rpc: 'https://rpc.msgchain.org',
  rest: 'https://api.msgchain.org',
  prefix: 'msg',
  denom: 'umsg',
  gasPrice: '1000000000',
} as const;

// ============================================================
// 类型定义
// ============================================================

interface WalletAccount {
  address: string;
  pubkey: Uint8Array;
}

interface Balance {
  denom: string;
  amount: string;
}

interface TxResult {
  transactionHash: string;
  height: number;
  gasUsed: number;
  events: Array<{
    type: string;
    attributes: Array<{ key: string; value: string }>;
  }>;
}

interface ContractQueryResult<T = unknown> {
  data: T;
  height: number;
}

// ============================================================
// dApp 引擎
// ============================================================

class MSGDAppEngine {
  private readClient: CosmWasmClient | null = null;
  private signClient: SigningCosmWasmClient | null = null;
  private account: WalletAccount | null = null;

  // ----------------------------------------------------------
  // 第一步: 初始化只读客户端
  // ----------------------------------------------------------

  async initReadClient(): Promise<CosmWasmClient> {
    console.log('\n=== 初始化只读客户端 ===');
    this.readClient = await CosmWasmClient.connect(CONFIG.rpc);
    const height = await this.readClient.getHeight();
    console.log(`  已连接,当前高度: ${height}`);
    return this.readClient;
  }

  // ----------------------------------------------------------
  // 第二步: 连接 Keplr 钱包
  // ----------------------------------------------------------

  async connectKeplr(): Promise<WalletAccount> {
    console.log('\n=== 连接 Keplr 钱包 ===');

    if (typeof window === 'undefined' || !window.keplr) {
      throw new Error('Keplr 扩展未安装');
    }

    const chainInfo = {
      chainId: CONFIG.chainId,
      chainName: 'MSG Chain',
      rpc: CONFIG.rpc,
      rest: CONFIG.rest,
      bip44: { coinType: 118 },
      bech32Config: {
        bech32PrefixAccAddr: CONFIG.prefix,
        bech32PrefixAccPub: `${CONFIG.prefix}pub`,
        bech32PrefixValAddr: `${CONFIG.prefix}valoper`,
        bech32PrefixValPub: `${CONFIG.prefix}valoperpub`,
        bech32PrefixConsAddr: `${CONFIG.prefix}valcons`,
        bech32PrefixConsPub: `${CONFIG.prefix}valconspub`,
      },
      currencies: [{
        coinDenom: 'MSG',
        coinMinimalDenom: CONFIG.denom,
        coinDecimals: 18,
      }],
      feeCurrencies: [{
        coinDenom: 'MSG',
        coinMinimalDenom: CONFIG.denom,
        coinDecimals: 18,
        gasPriceStep: { low: 1000000000, average: 1000000000, high: 1000000000 },
      }],
      stakeCurrency: {
        coinDenom: 'MSG',
        coinMinimalDenom: CONFIG.denom,
        coinDecimals: 18,
      },
      features: ['cosmwasm'],
    };

    await window.keplr.experimentalSuggestChain(chainInfo);
    await window.keplr.enable(CONFIG.chainId);

    const offlineSigner = window.keplr.getOfflineSigner(CONFIG.chainId);
    const accounts = await offlineSigner.getAccounts();
    this.account = accounts[0];

    this.signClient = await SigningCosmWasmClient.connectWithSigner(
      CONFIG.rpc,
      offlineSigner,
      { gasPrice: GasPrice.fromString(`${CONFIG.gasPrice}${CONFIG.denom}`) },
    );

    console.log(`  钱包地址: ${this.account.address}`);
    return this.account;
  }

  // ----------------------------------------------------------
  // 第三步: 查询余额
  // ----------------------------------------------------------

  async queryBalance(address?: string): Promise<Balance> {
    console.log('\n=== 查询余额 ===');
    const addr = address || this.account?.address;
    if (!addr) throw new Error('未指定地址');
    if (!this.readClient) await this.initReadClient();

    const balance = await this.readClient!.getBalance(addr, CONFIG.denom);
    const formatted = (BigInt(balance.amount) / BigInt(10**18)).toString();
    console.log(`  地址: ${addr}`);
    console.log(`  余额: ${formatted} MSG`);

    return balance;
  }

  // ----------------------------------------------------------
  // 第四步: 查询合约
  // ----------------------------------------------------------

  async queryContract<T>(
    contractAddress: string,
    queryMsg: Record<string, unknown>,
  ): Promise<ContractQueryResult<T>> {
    console.log('\n=== 合约查询 ===');
    if (!this.readClient) await this.initReadClient();

    console.log(`  合约地址: ${contractAddress}`);
    console.log(`  查询消息: ${JSON.stringify(queryMsg)}`);

    const data = await this.readClient!.queryContractSmart<T>(
      contractAddress,
      queryMsg,
    );

    const height = await this.readClient!.getHeight();
    console.log(`  结果 (高度 ${height}): ${JSON.stringify(data)}`);

    return { data, height };
  }

  // ----------------------------------------------------------
  // 第五步: 执行合约
  // ----------------------------------------------------------

  async executeContract(
    contractAddress: string,
    executeMsg: Record<string, unknown>,
    funds?: string,
  ): Promise<TxResult> {
    console.log('\n=== 合约执行 ===');
    if (!this.signClient || !this.account) {
      throw new Error('请先连接钱包');
    }

    console.log(`  合约地址: ${contractAddress}`);
    console.log(`  执行消息: ${JSON.stringify(executeMsg)}`);
    console.log(`  发送地址: ${this.account.address}`);

    const fee = 'auto';
    const memo = `MSG dApp - ${new Date().toISOString()}`;

    const response = await this.signClient.execute(
      this.account.address,
      contractAddress,
      executeMsg,
      fee,
      memo,
      funds ? [{ denom: CONFIG.denom, amount: funds }] : [],
    );

    const result: TxResult = {
      transactionHash: response.transactionHash,
      height: response.height,
      gasUsed: response.gasUsed,
      events: response.events.map((e) => ({
        type: e.type,
        attributes: e.attributes.map((a) => ({
          key: a.key,
          value: a.value,
        })),
      })),
    };

    console.log(`  交易哈希: ${result.transactionHash}`);
    console.log(`  区块高度: ${result.height}`);
    console.log(`  Gas 消耗: ${result.gasUsed}`);

    return result;
  }

  // ----------------------------------------------------------
  // 第六步: 验证交易
  // ----------------------------------------------------------

  async verifyTransaction(txHash: string): Promise<void> {
    console.log('\n=== 验证交易 ===');
    if (!this.readClient) await this.initReadClient();

    console.log(`  查询交易: 0x${txHash}`);

    const tx = await this.readClient!.getTx(txHash);
    if (!tx) {
      console.log('  交易未找到 — 可能仍在 mempool 或已过期');
      return;
    }

    console.log(`  高度: ${tx.height}`);
    console.log(`  成功: ${tx.code === 0}`);
    console.log(`  Gas: ${tx.gasUsed}`);

    if (tx.events && tx.events.length > 0) {
      console.log('  事件:');
      for (const event of tx.events) {
        console.log(`    - ${event.type}`);
        for (const attr of event.attributes) {
          console.log(`      ${attr.key}: ${attr.value}`);
        }
      }
    }

    // 通过 REST 获取回执
    const receiptUrl =
      `${CONFIG.rest}/api/v1/receipts?tx_hash=${txHash}`;
    const receiptResp = await fetch(receiptUrl);
    if (receiptResp.ok) {
      const receipt = await receiptResp.json();
      console.log('  REST 回执:', JSON.stringify(receipt, null, 2));
    }
  }

  // ----------------------------------------------------------
  // 全流程
  // ----------------------------------------------------------

  async runFullFlow(
    contractAddress?: string,
    queryMsg?: Record<string, unknown>,
    executeMsg?: Record<string, unknown>,
    executeFunds?: string,
  ): Promise<void> {
    console.log('='.repeat(60));
    console.log('  MSG dApp 端到端交互全流程');
    console.log('  Chain ID: msg-chain-1');
    console.log('='.repeat(60));

    try {
      // 初始化
      await this.initReadClient();

      // 连接钱包
      const wallet = await this.connectKeplr();
      console.log(`  ✓ 钱包已连接: ${wallet.address}`);

      // 查询余额
      const balance = await this.queryBalance();
      console.log(`  ✓ 余额: ${balance.amount} ${balance.denom}`);

      // 查询合约
      if (contractAddress && queryMsg) {
        const state = await this.queryContract(contractAddress, queryMsg);
        console.log(`  ✓ 合约状态已获取`);
      }

      // 执行合约
      let txHash: string | undefined;
      if (contractAddress && executeMsg) {
        const result = await this.executeContract(
          contractAddress,
          executeMsg,
          executeFunds,
        );
        txHash = result.transactionHash;
        console.log(`  ✓ 合约已执行`);
      }

      // 验证交易
      if (txHash) {
        await this.verifyTransaction(txHash);
        console.log(`  ✓ 交易已验证`);
      }

      console.log('='.repeat(60));
      console.log('  全流程完成');
      console.log('='.repeat(60));
    } catch (err) {
      console.error('流程异常:', err);
      throw err;
    }
  }
}

// ============================================================
// 使用示例
// ============================================================

async function main() {
  const engine = new MSGDAppEngine();

  // 仅查询流程
  await engine.runFullFlow();

  // 完整交互流程(取消注释并填入实际合约地址)
  // await engine.runFullFlow(
  //   'msg1contractaddress...',
  //   { get_config: {} },
  //   { increment: {} },
  // );
}

// main().catch(console.error);

7.5 验证检查清单

interface VerificationChecklist {
  walletConnected: boolean;
  balanceLoaded: boolean;
  rpcAccessible: boolean;
  restAccessible: boolean;
  contractQueryable: boolean;
  broadcastReachable: boolean;
  receiptRetrievable: boolean;
  explorerAccessible: boolean;
}

async function runVerificationChecklist(): Promise<VerificationChecklist> {
  const checks: VerificationChecklist = {
    walletConnected: false,
    balanceLoaded: false,
    rpcAccessible: false,
    restAccessible: false,
    contractQueryable: false,
    broadcastReachable: false,
    receiptRetrievable: false,
    explorerAccessible: false,
  };

  // 1. 检查 RPC 可达性
  try {
    const rpcResp = await fetch('https://rpc.msgchain.org', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'health',
      }),
    });
    checks.rpcAccessible = rpcResp.ok;
    console.log(`  [RPC] ${checks.rpcAccessible ? '✓' : '✗'} 可达`);
  } catch {
    console.log('  [RPC] ✗ 不可达');
  }

  // 2. 检查 REST 可达性
  try {
    const restResp = await fetch('https://api.msgchain.org/api/v1/status');
    checks.restAccessible = restResp.ok;
    console.log(`  [REST] ${checks.restAccessible ? '✓' : '✗'} 可达`);
  } catch {
    console.log('  [REST] ✗ 不可达');
  }

  // 3. 检查 Keplr
  checks.walletConnected = typeof window !== 'undefined' && !!window.keplr;
  console.log(`  [Keplr] ${checks.walletConnected ? '✓' : '✗'} ${checks.walletConnected ? '已安装' : '未安装'}`);

  // 4. 检查 Explorer
  try {
    const expResp = await fetch('https://api.msgchain.org/api/v1/blocks/latest');
    checks.explorerAccessible = expResp.ok;
    console.log(`  [Explorer] ${checks.explorerAccessible ? '✓' : '✗'} 可达`);
  } catch {
    console.log('  [Explorer] ✗ 不可达');
  }

  return checks;
}

8. 附录: 数据源索引

8.1 白皮书关键文件

文件 公开 URL 用途
developer_entry.json https://msgchain.org/whitepaper/developer_entry.json 开发者入口,推荐引导顺序
developer_capability_matrix.json https://msgchain.org/whitepaper/developer_capability_matrix.json 16 个能力面定义
examples/index.json https://msgchain.org/whitepaper/examples/index.json dApp starter 索引
recipes/dapp_minimal.json https://msgchain.org/whitepaper/recipes/dapp_minimal.json 最小 dApp 配方
chain_config/network_presets.json https://msgchain.org/whitepaper/chain_config/network_presets.json 链网络配置
api_specs/rpc_methods.json https://msgchain.org/whitepaper/api_specs/rpc_methods.json RPC 方法摘要
module_exports/keplr.json https://msgchain.org/whitepaper/module_exports/keplr.json Keplr 模块导出
module_exports/rpc.json https://msgchain.org/whitepaper/module_exports/rpc.json RPC 模块导出
examples/dapp_starters/keplr_contract_console/manifest.json https://msgchain.org/whitepaper/examples/dapp_starters/keplr_contract_console/manifest.json Starter 清单

8.2 关键配置值速查

Chain ID:             msg-chain-1
Chain Name:           MSG Chain
Bech32 Prefix:        msg
Coin Type:            118
Minimal Denom:        umsg
Decimals:             18
RPC Endpoint:         https://rpc.msgchain.org
REST Endpoint:        https://api.msgchain.org
Agent API Base:       https://api.msgchain.org/agent/v1
Gas Price Low:        1,000,000,000 attoMSG
Gas Price Average:    1,000,000,000 attoMSG
Gas Price High:       1,000,000,000 attoMSG
Features:             cosmwasm

8.3 能力面速查

Surface ID Readiness Write Path Production
contract_runtime assisted_codegen true true
core_contract_reference_pack source_backed_reference false false
registry_resolution production_reference false true
rpc_gateway assisted_codegen true true
formal_api_schema_pack source_backed_reference false false
wallet_frontend guarded_integration true false
explorer_receipts read_only_assist false false
agent_query_and_guarded_write guarded_write false false
sdk_surface local_candidate false false
chain_config_pack starter_ready false true
public_sandbox_strategy fail_closed_reference false false
contract_template_pack starter_ready false false
dapp_starter_pack starter_ready true false

8.4 错误代码参考

Agent API 常见响应模式:

场景 响应
查询成功 { "success": true, "data": {...} }
写路径成功 (正常) { "success": true, "data": {...} }
写路径 stub { "success": true, "data": {...}, "_stub": true } + Header X-MSG-Stub: true
查询失败 { "success": false, "error": "..." }
写路径被门禁拦截 { "success": false, "error": "guarded_write_blocked" }
未授权 { "success": false, "error": "unauthorized" }

8.5 常用 REST API 路径

查询类 (已实现):
  GET  /api/v1/status
  GET  /api/v1/bank/balances?address={address}
  GET  /api/v1/staking/validators
  GET  /api/v1/auth/accounts?address={address}
  GET  /api/v1/blocks/latest
  GET  /api/v1/blocks
  GET  /api/v1/txs
  GET  /api/v1/receipts?address={address}
  GET  /api/v1/address/{address}

合约类 (部分实现):
  GET  /api/v1/contracts
  GET  /api/v1/contracts?source=canonical
  POST /api/v1/contracts/deploy
  POST /api/v1/contracts/instantiate
  POST /api/v1/contracts/execute

签名类 (已实现):
  POST /api/v1/tx/transfer
  POST /api/v1/tx/delegate
  POST /api/v1/tx/undelegate
  POST /api/v1/tx/send

Agent 查询 (部分实现):
  GET  /agent/v1/query/account
  GET  /agent/v1/query/balance
  GET  /agent/v1/query/balances
  GET  /agent/v1/query/tx
  GET  /agent/v1/query/block
  GET  /agent/v1/events/history

Agent 受保护写 (需 API Key):
  POST /agent/v1/wallet/*
  POST /agent/v1/mpc/sign
  POST /agent/v1/payment/session

Agent Stub 写 (未实现):
  POST /agent/v1/defi/*
  POST /agent/v1/bridge/transfer
  POST /agent/v1/registry/register

8.6 许可证与引用

本文档基于 MSG Chain Whitepaper 公开数据生成:

所有代码示例按原样提供,仅供参考和学习使用。生产使用前需人工审查和测试。


文档结束 — 共生成约 1500 行