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. 概述
1.1 文档目标
本文档为 AI 编码代理(AI Coding Agent)提供 MSG Chain dApp 前端启动器与示例的完整参考。所有内容均基于 MSG Chain Whitepaper 的真实公开文件,包括:
examples/index.json— dApp 启动器索引recipes/dapp_minimal.json— 最小 dApp 配方(含 6 步工作流)developer_capability_matrix.json— 开发者能力矩阵(16 个能力面)chain_config/network_presets.json— 链网络配置预设developer_entry.json— 开发者入口与推荐引导顺序module_exports/keplr.json— Keplr 钱包模块导出module_exports/rpc.json— RPC 模块导出api_specs/rpc_methods.json— RPC 方法摘要examples/dapp_starters/keplr_contract_console/manifest.json— 首个 dApp starter 清单
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
- best_for: ["dApp 前端适配器样板", "钱包连接初始化", "钱包注入生成"]
- blocking_gaps: ["前端配对层仍缺 CDN/模板市场", "前端 starter 对 AI 的作用受限于前端工具链的快速迭代"]
- production_supported: false
1.4 wallet_frontend
状态: guarded_integration + production_supported: false
- machine_safe_for_codegen: true
- write_path_ready: true
- schema_available: false
- boundaries: ["当前是兼容路径与配置底座,不应表述成官方钱包生态已全量上线。"]
1.5 chain_config_pack
状态: starter_ready + production_supported: true
- best_for: ["前端链配置生成", "Keplr suggestChain", "CosmJS/钱包接入初始化"]
- blocking_gaps: ["缺少从 live/public 节点自动探测并签名确认的网络元数据机制"]
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 行为:
- 从 developer_entry.json 读取推荐引导顺序
- 识别 keplr.html 中的钱包集成边界
- 识别 rpc.html 中的查询和广播端点
- 识别 explorer.html 中的回执追踪能力
- 记录所有 boundary_clauses
Step 2: 绑定链配置与钱包路径
action: bind chain config and wallet path
purpose: 根据 chainId=msg-chain-1、Keplr/CosmJS 路径生成前端链配置。
AI 行为:
- 从 chain_config/network_presets.json 读取链配置
- 生成 Keplr suggestChain 配置对象
- 配置 CosmJS 客户端
- 设置 bech32 地址前缀 (msg)
Step 3: 生成查询与写入适配器
action: generate query and write adapters
purpose: 封装 RPC/REST/contract query、签名广播、receipt 回放与错误处理。
AI 行为:
- 使用 api_specs/rpc_methods.json 中的方法定义
- 生成 Tendermint RPC 查询适配器
- 生成 REST API 查询适配器
- 生成合约 execute 适配器
- 封装 broadcast_tx_commit 广播路径
- 实现回执解析与错误处理
Step 4: 生成前端页面与 Hooks
action: generate frontend pages and hooks
purpose: 输出状态读取、交易提交流程、事件/receipt 展示与失败态。
AI 行为:
- 生成 React components / hooks
- 实现钱包连接状态管理
- 实现余额查询 UI
- 实现交易提交表单
- 实现回执展示组件
- 实现错误与加载状态处理
- 移动端适配
Step 5: 使用 Explorer/证据循环验证
action: validate with explorer/evidence loop
purpose: 使用 tx hash、receipt、contract source/query 与日志检索校验结果。
AI 行为:
- 使用 tx hash 通过 RPC 查询交易回执
- 使用 receipt 确认交易执行状态
- 查询合约状态验证写入效果
- 使用 Explorer REST 端点检索区块和日志
- 比较预期结果与实际链上状态
Step 6: 人工发布门禁
action: human release gate
purpose: 确认生产域名、环境变量、密钥、监控与上线窗口。
AI 行为:
- 生成环境变量清单
- 生成发布检查清单
- 标记所有需要人工确认的项
- 输出部署说明
- AI 不能自动执行最终发布
3.6 必需的人工输入
配方明确要求以下人工输入:
- 产品流程与页面需求
- 目标合约地址或 canonical key
- 钱包接入策略
- 部署环境、域名与运维发布权限
3.7 成功产物
配方的成功产物包括:
- 前端源码目录
- 钱包接入配置
- 查询与签名适配器
- 交易回执与错误处理 UI
- 上线环境变量与发布说明
3.8 硬边界
配方定义了不可逾越的边界:
- 当前 starter 与链配置包能显著降低起步成本,但仍不能替代正式稳定 SDK。
- Explorer/索引面可辅助调试,但不应替代正式生产运维监控。
- 没有真实部署权限、域名控制权与生产配置,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 前端原型和适配器代码,但生产部署需要:
- 人工审查生成的代码
- 补充设计系统和 UI 打磨
- 添加生产监控和告警
- 配置正式域名、证书和 CDN
- 进行安全审计和测试
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": ["当前是兼容路径与配置底座,不应表述成官方钱包生态已全量上线。"]
}
可以主张:
- Keplr 钱包接入配置存在(chain info, suggestChain, getOfflineSigner)
- DApp 可以按文档连接 Keplr 和 MetaMask
- CosmJS 查询和签名路径可用
- RPC/REST/gRPC 节点接口可用
不能主张:
- "官方钱包生态已经全部上线并完成广泛生产验证"
- "所有外部钱包都已完成端到端主网级接入验收"
- "钱包集成已完全运营化"
6.3 链配置边界
来自 chain_config/network_presets.json 的 boundary 字段:
链配置是接入基线,不等于 live/public 端点已经被正式接受为生产网络。
生产接入前仍需复核 endpoint 可用性、域名控制、证书、SLO 与发布权限。
6.4 RPC 能力边界
来自 rpc_methods.json 的边界声明:
{
"boundaries": [
"当前是白皮书级概要,不包含完整参数与错误码 schema。"
]
}
- 查询路径(Tendermint RPC + REST)已实现,可安全用于 codegen
- 合约部署与写路径(contract_surface)处于 partial 状态
- Agent 受保护写路径需要 API Key 和人工审批
- Stub 写路径(defi, bridge, registry)返回 stub 标记,写入未实际执行
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:
- 当前 starter 与链配置包能显著降低起步成本,但仍不能替代正式稳定 SDK。
- Explorer/索引面可辅助调试,但不应替代正式生产运维监控。
- 没有真实部署权限、域名控制权与生产配置,AI 不能独立完成最终发布。
6.8 开发者入口边界
来自 developer_entry.json 的 current_boundaries:
- 当前开发协议层可显著提升 AI coding 的可执行性,但仍不能诚实承诺"只靠入口即可 100% 自动完成任何产品上线"。
- 当前已补 Quick Start、source-backed 合约消费索引、正式 API/Schema 契约索引与 fail-closed sandbox 策略,但仍不等于 signed public SDK、public sandbox 或 not independently verified for production 交付。
- 涉及私钥、部署权限、生产域名、资金操作、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 公开数据生成:
- 白皮书基础 URL:
https://msgchain.org/whitepaper/ - 能力矩阵:
developer_capability_matrix.json - 配方:
recipes/dapp_minimal.json - 示例索引:
examples/index.json - 链配置:
chain_config/network_presets.json - 开发者入口:
developer_entry.json
所有代码示例按原样提供,仅供参考和学习使用。生产使用前需人工审查和测试。
文档结束 — 共生成约 1500 行
