dApp Docs/前端dApp接入指南
Development reference. Not independently verified for production.

MSG Chain 前端 dApp 接入指南

数据来源:MSG Chain 代码库核实

主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。

基于 React 18 + TypeScript 5 + CosmJS 的完整去中心化应用开发手册
适用 Chain ID: msg-chain-1 | 适用网络:本地开发 http://localhost:26657


目录

  1. 概述
  2. 项目初始化
  3. Keplr 钱包连接
  4. CosmJS 客户端封装
  5. 合约查询操作
  6. 合约执行操作
  7. Agent API 客户端
  8. React 组件库
  9. Hooks 封装
  10. 事件监听
  11. 实用工具函数
  12. 完整示例项目
  13. 附录:TypeScript 类型定义

一、概述

1.1 前端架构总览

MSG Chain 的前端 dApp 采用类似 Uniswap 的前端架构,即 React 组件层 + 链交互抽象层 + CosmJS 客户端层。与以太坊 dApp 使用 ethers.js + MetaMask 的模式类似,MSG Chain 使用 CosmJS + Keplr 钱包的组合。

┌──────────────────────────────────────────────────────┐
│                   React 组件层                         │
│  WalletConnectButton │ AgentCard │ AgentChat │ DIDList │
│  AccountBalance      │ PaymentSession │ Constitution  │
├──────────────────────────────────────────────────────┤
│                   React Hooks 层                       │
│  useWallet │ useAgent │ useDID │ usePayment │ useAgentExecute │
│  useCosmWasmClient  │ useSigningClient │ useEvents    │
├──────────────────────────────────────────────────────┤
│                   服务层                                │
│  CosmWasmClient (只读) │ SigningCosmWasmClient (签名) │
│  AgentAPIClient (REST) │ WebSocket 事件订阅            │
├──────────────────────────────────────────────────────┤
│           MSG Chain 网络层                             │
│  RPC (localhost:26657) │ REST (localhost:1317)        │
│  Agent API (localhost:8080) │ gRPC (localhost:9090)   │
└──────────────────────────────────────────────────────┘

1.2 技术栈选择

技术 版本 用途 类比以太坊生态
React 18.x UI 框架 React
TypeScript 5.x 类型安全 TypeScript
CosmJS Stargate ^0.32.0 链交互客户端 ethers.js v6
CosmJS CosmWasm ^0.32.0 合约交互客户端 ethers.js Contract
CosmJS Proto-Signing ^0.32.0 交易签名 wallet.signMessage
CosmJS Encoding ^0.32.0 地址编码 ethers.utils
CosmJS Math ^0.32.0 大数计算 BN.js
Keplr latest 钱包 MetaMask
Tailwind CSS 3.x 样式 Tailwind CSS
Vite 5.x 构建工具 Vite

1.3 链配置参数

export const CHAIN_CONFIG = {
  chainId: "msg-chain-1",
  chainNumberId: 1,
  bech32Prefix: "msg",
  coinType: 118,
  decimals: 18,
  gasPriceSteps: { low: 1000000000, average: 1000000000, high: 1000000000 },
  denom: "umsg",
  blockTime: 5000,
  rpc: "http://localhost:26657",
  rest: "http://localhost:1317",
  agentApi: "http://localhost:8080",
} as const;

1.4 核心合约

合约 Canonical Key 核心职责
aidid_did_registry_v1 aidid_did_registry_v1 AI 去中心化身份
ai_agent_constitution_v1 ai_agent_constitution_v1 Agent 宪章与策略引擎
agent_registry_v1 动态解析 Agent 注册与发现
agent_payment_v1 动态解析 AIPAY 支付全生命周期
micropayment_session_v1 动态解析 微支付按秒计费

二、项目初始化

2.1 使用 Vite 创建项目

npm create vite@latest msg-chain-dapp -- --template react-ts
cd msg-chain-dapp
npm install @cosmjs/cosmwasm-stargate@^0.32.0 @cosmjs/stargate@^0.32.0 @cosmjs/proto-signing@^0.32.0 @cosmjs/encoding@^0.32.0 @cosmjs/math@^0.32.0
npm install react-router-dom @tanstack/react-query
npm install -D tailwindcss@3 postcss autoprefixer @types/node @keplr-wallet/types
npx tailwindcss init -p

2.2 tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        msg: { 50: "#eff6ff", 100: "#dbeafe", 200: "#bfdbfe", 500: "#3b82f6", 600: "#2563eb", 700: "#1d4ed8", 900: "#1e3a5f" },
      },
    },
  },
  plugins: [],
};

2.3 项目目录结构

src/
├── main.tsx
├── App.tsx
├── index.css
├── vite-env.d.ts
├── config/
│   ├── chain.ts
│   ├── contracts.ts
│   └── keplr.ts
├── types/
│   ├── chain.ts
│   ├── contracts.ts
│   ├── agent.ts
│   └── global.d.ts
├── lib/
│   ├── cosmwasm-client.ts
│   ├── signing-client.ts
│   ├── agent-api.ts
│   ├── agent-error.ts
│   ├── error-handler.ts
│   ├── events.ts
│   ├── utils.ts
│   ├── execute.ts
│   ├── queries/
│   │   ├── did.ts
│   │   ├── agent.ts
│   │   ├── constitution.ts
│   │   └── payment.ts
│   └── executes/
│       ├── agent.ts
│       ├── did.ts
│       ├── constitution.ts
│       └── payment.ts
├── hooks/
│   ├── useWallet.ts
│   ├── useCosmWasmClient.ts
│   ├── useSigningClient.ts
│   ├── useAgent.ts
│   ├── useDID.ts
│   ├── usePayment.ts
│   ├── useAgentExecute.ts
│   ├── useFormActions.ts
│   └── useEvents.ts
├── components/
│   ├── ui.tsx
│   ├── ErrorBoundary.tsx
│   ├── WalletConnectButton.tsx
│   ├── AccountBalance.tsx
│   ├── AgentCard.tsx
│   ├── DIDList.tsx
│   ├── PaymentSession.tsx
│   ├── AgentChat.tsx
│   └── ConstitutionViewer.tsx
├── pages/
│   ├── Home.tsx
│   ├── Agents.tsx
│   ├── DIDPage.tsx
│   ├── PaymentPage.tsx
│   └── ConstitutionPage.tsx
└── providers/
    ├── WalletProvider.tsx
    └── QueryProvider.tsx

三、Keplr 钱包连接

3.1 Keplr 链配置(MUST USE EXACT VALUES)

// src/config/keplr.ts
export const getKeplrChainConfig = () => ({
  chainId: "msg-chain-1",
  chainName: "MSG Chain",
  rpc: "http://localhost:26657",
  rest: "http://localhost:1317",
  bip44: { coinType: 118 },
  bech32Config: {
    bech32PrefixAccAddr: "msg",
    bech32PrefixAccPub: "msgpub",
    bech32PrefixValAddr: "msgvaloper",
    bech32PrefixValPub: "msgvaloperpub",
    bech32PrefixConsAddr: "msgvalcons",
    bech32PrefixConsPub: "msgvalconspub",
  },
  currencies: [{ coinDenom: "MSG", coinMinimalDenom: "umsg", coinDecimals: 18 }],
  feeCurrencies: [{ coinDenom: "MSG", coinMinimalDenom: "umsg", coinDecimals: 18, gasPriceStep: { low: 1000000000, average: 1000000000, high: 1000000000 } }],
  stakeCurrency: { coinDenom: "MSG", coinMinimalDenom: "umsg", coinDecimals: 18 },
  gasPriceStep: { low: 1000000000, average: 1000000000, high: 1000000000 },
});

3.2 全局类型声明

// src/types/global.d.ts
import { Window as KeplrWindow } from "@keplr-wallet/types";
declare global {
  interface Window extends KeplrWindow {}
}
export {};

3.3 WalletProvider 上下文提供者

// src/providers/WalletProvider.tsx
import React, { createContext, useContext, useCallback, useEffect, useMemo, useState } from "react";
import { SigningCosmWasmClient, CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { getKeplrChainConfig } from "../config/keplr";

export interface WalletState {
  address: string | null;
  isConnected: boolean;
  isInstalled: boolean;
  isConnecting: boolean;
  error: string | null;
  chainId: string;
}

export interface WalletContextType extends WalletState {
  connect: () => Promise<void>;
  disconnect: () => void;
  getSigningClient: () => Promise<SigningCosmWasmClient>;
  getQueryClient: () => Promise<CosmWasmClient>;
}

const WalletContext = createContext<WalletContextType | null>(null);
const initialState: WalletState = {
  address: null, isConnected: false, isInstalled: false,
  isConnecting: false, error: null, chainId: "msg-chain-1",
};

export function WalletProvider({ children }: { children: React.ReactNode }) {
  const [state, setState] = useState<WalletState>(initialState);
  const chainConfig = useMemo(() => getKeplrChainConfig(), []);

  useEffect(() => {
    setState((prev) => ({ ...prev, isInstalled: !!(typeof window !== "undefined" && window.keplr) }));
  }, []);

  const connect = useCallback(async () => {
    if (!window.keplr) {
      setState((prev) => ({ ...prev, error: "请安装 Keplr 钱包浏览器扩展", isConnecting: false }));
      return;
    }
    setState((prev) => ({ ...prev, isConnecting: true, error: null }));
    try {
      await window.keplr.experimentalSuggestChain(chainConfig);
      await window.keplr.enable(chainConfig.chainId);
      const offlineSigner = window.keplr.getOfflineSigner(chainConfig.chainId);
      const accounts = await offlineSigner.getAccounts();
      if (accounts.length === 0) throw new Error("Keplr 未返回账户信息");
      const address = accounts[0].address;
      setState({ address, isConnected: true, isInstalled: true, isConnecting: false, error: null, chainId: chainConfig.chainId });
    } catch (err) {
      const message = err instanceof Error ? err.message : "钱包连接失败";
      setState((prev) => ({ ...prev, isConnected: false, isConnecting: false, error: message.includes("rejected") ? "用户取消连接" : message }));
    }
  }, [chainConfig]);

  const disconnect = useCallback(() => {
    setState({ ...initialState, isInstalled: !!window.keplr });
  }, []);

  const getSigningClient = useCallback(async () => {
    if (!window.keplr || !state.address) throw new Error("钱包未连接");
    const offlineSigner = window.keplr.getOfflineSigner(chainConfig.chainId);
    return SigningCosmWasmClient.connectWithSigner("http://localhost:26657", offlineSigner, { gasPrice: GasPrice.fromString("1000000000attoMSG") });
  }, [state.address, chainConfig.chainId]);

  const getQueryClient = useCallback(async () => CosmWasmClient.connect("http://localhost:26657"), []);

  const contextValue = useMemo(() => ({ ...state, connect, disconnect, getSigningClient, getQueryClient }), [state, connect, disconnect, getSigningClient, getQueryClient]);

  return <WalletContext.Provider value={contextValue}>{children}</WalletContext.Provider>;
}

export function useWalletContext(): WalletContextType {
  const ctx = useContext(WalletContext);
  if (!ctx) throw new Error("useWalletContext must be used within a WalletProvider");
  return ctx;
}

3.4 useWallet React Hook

// src/hooks/useWallet.ts
import { useState, useCallback } from "react";
import { SigningCosmWasmClient, CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { getKeplrChainConfig } from "../config/keplr";

interface WalletState {
  address: string | null;
  isConnected: boolean;
  isInstalled: boolean;
  isConnecting: boolean;
  error: string | null;
  balance: string | null;
}

interface WalletActions {
  connect: () => Promise<void>;
  disconnect: () => void;
  getSigningClient: () => Promise<SigningCosmWasmClient>;
  getQueryClient: () => Promise<CosmWasmClient>;
  refreshBalance: () => Promise<void>;
}

const chainConfig = getKeplrChainConfig();

export function useWallet(): WalletState & WalletActions {
  const [state, setState] = useState<WalletState>({
    address: null, isConnected: false, isInstalled: !!(typeof window !== "undefined" && window.keplr),
    isConnecting: false, error: null, balance: null,
  });
  const [cachedSigningClient, setCachedSigningClient] = useState<SigningCosmWasmClient | null>(null);

  const connect = useCallback(async () => {
    if (!window.keplr) { setState((prev) => ({ ...prev, error: "请安装 Keplr 钱包" })); return; }
    setState((prev) => ({ ...prev, isConnecting: true, error: null }));
    try {
      await window.keplr.experimentalSuggestChain(chainConfig);
      await window.keplr.enable(chainConfig.chainId);
      const offlineSigner = window.keplr.getOfflineSigner(chainConfig.chainId);
      const accounts = await offlineSigner.getAccounts();
      const address = accounts[0].address;
      const signingClient = await SigningCosmWasmClient.connectWithSigner(chainConfig.rpc, offlineSigner, { gasPrice: GasPrice.fromString("1000000000attoMSG") });
      setCachedSigningClient(signingClient);
      const queryClient = await CosmWasmClient.connect(chainConfig.rpc);
      const balance = await queryClient.getBalance(address, "umsg");
      setState({ address, isConnected: true, isInstalled: true, isConnecting: false, error: null, balance: balance.amount });
    } catch (err) {
      const msg = err instanceof Error ? err.message : "未知错误";
      setState((prev) => ({ ...prev, isConnected: false, isConnecting: false, error: msg.includes("rejected") ? "用户取消连接" : msg }));
    }
  }, []);

  const disconnect = useCallback(() => {
    setCachedSigningClient(null);
    setState((prev) => ({ ...prev, address: null, isConnected: false, balance: null, error: null }));
  }, []);

  const getSigningClient = useCallback(async () => {
    if (cachedSigningClient) return cachedSigningClient;
    if (!window.keplr || !state.address) throw new Error("钱包未连接");
    const offlineSigner = window.keplr.getOfflineSigner(chainConfig.chainId);
    const client = await SigningCosmWasmClient.connectWithSigner(chainConfig.rpc, offlineSigner, { gasPrice: GasPrice.fromString("1000000000attoMSG") });
    setCachedSigningClient(client);
    return client;
  }, [cachedSigningClient, state.address]);

  const getQueryClient = useCallback(async () => CosmWasmClient.connect(chainConfig.rpc), []);
  const refreshBalance = useCallback(async () => {
    if (!state.address) return;
    try {
      const client = await CosmWasmClient.connect(chainConfig.rpc);
      const balance = await client.getBalance(state.address, "umsg");
      setState((prev) => ({ ...prev, balance: balance.amount }));
    } catch (err) { console.error("刷新余额失败:", err); }
  }, [state.address]);

  return { ...state, connect, disconnect, getSigningClient, getQueryClient, refreshBalance };
}

3.5 签名客户端工厂

// src/lib/signing-client.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, Coin, StdFee } from "@cosmjs/stargate";
import { OfflineSigner } from "@cosmjs/proto-signing";

const RPC_URL = "http://localhost:26657";
export const GAS_PRICE_STEPS = {
  low: GasPrice.fromString("1000000000attoMSG"),
  average: GasPrice.fromString("1000000000attoMSG"),
  high: GasPrice.fromString("1000000000attoMSG"),
};

export async function createSigningClient(signer: OfflineSigner, step: "low" | "average" | "high" = "average"): Promise<SigningCosmWasmClient> {
  return SigningCosmWasmClient.connectWithSigner(RPC_URL, signer, { gasPrice: GAS_PRICE_STEPS[step] });
}

export async function getKeplrSigner(): Promise<OfflineSigner> {
  if (!window.keplr) throw new Error("Keplr 钱包未安装");
  await window.keplr.enable("msg-chain-1");
  return window.keplr.getOfflineSigner("msg-chain-1");
}

export async function getKeplrAddress(): Promise<string> {
  const signer = await getKeplrSigner();
  const accounts = await signer.getAccounts();
  return accounts[0].address;
}

export async function estimateGas(client: SigningCosmWasmClient, sender: string, contractAddress: string, executeMsg: Record<string, unknown>, funds?: Coin[]): Promise<number> {
  try {
    const result = await client.simulate(sender, [{
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: { sender, contract: contractAddress, msg: Buffer.from(JSON.stringify(executeMsg)).toString("base64"), funds: funds || [] },
    }], undefined);
    return Math.ceil(result * 1.3);
  } catch { return 300_000; }
}

export function calculateFee(gasLimit: number, gasPrice: number = 1000000000): StdFee {
  const amount = Math.ceil(gasLimit * gasPrice);
  return { amount: [{ denom: "umsg", amount: String(amount) }], gas: String(gasLimit) };
}

四、CosmJS 客户端封装

4.1 只读客户端

// src/lib/cosmwasm-client.ts
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { Coin } from "@cosmjs/stargate";

const RPC_URL = "http://localhost:26657";
const REST_URL = "http://localhost:1317";
let queryClientInstance: CosmWasmClient | null = null;

export async function getQueryClient(): Promise<CosmWasmClient> {
  if (!queryClientInstance) queryClientInstance = await CosmWasmClient.connect(RPC_URL);
  return queryClientInstance;
}

export async function getChainInfo() {
  const client = await getQueryClient();
  return { chainId: await client.getChainId(), height: await client.getHeight() };
}

export async function queryBalance(address: string): Promise<Coin> {
  const client = await getQueryClient();
  return client.getBalance(address, "umsg");
}

export async function queryAllBalances(address: string): Promise<Coin[]> {
  const client = await getQueryClient();
  return client.getAllBalances(address);
}

export async function smartQuery<T>(contractAddress: string, queryMsg: Record<string, unknown>): Promise<T> {
  const client = await getQueryClient();
  try {
    return await client.queryContractSmart(contractAddress, queryMsg) as T;
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    throw new Error(`合约查询失败 [${contractAddress.slice(0, 12)}...]: ${msg}`);
  }
}

export async function rawQuery(contractAddress: string, key: Uint8Array): Promise<Uint8Array | null> {
  const client = await getQueryClient();
  return client.queryContractRaw(contractAddress, key);
}

export async function batchQuery<T extends Record<string, unknown>>(queries: { address: string; msg: Record<string, unknown> }[]): Promise<T[]> {
  const client = await getQueryClient();
  return Promise.all(queries.map((q) => client.queryContractSmart(q.address, q.msg) as Promise<T>));
}

4.2 合约地址解析

// src/config/contracts.ts
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";

const RPC_URL = "http://localhost:26657";

export const CONTRACT_NAMES = {
  GENESIS_REGISTRY: "genesis_registry",
  DID_REGISTRY: "aidid_did_registry_v1",
  CONSTITUTION: "ai_agent_constitution_v1",
  AGENT_REGISTRY: "agent_registry_v1",
  AGENT_PAYMENT: "agent_payment_v1",
  MICROPAYMENT: "micropayment_session_v1",
} as const;

export async function resolveContractAddresses(genesisRegistryAddress: string): Promise<Record<string, string>> {
  const client = await CosmWasmClient.connect(RPC_URL);
  const keys = Object.values(CONTRACT_NAMES);
  const addresses: Record<string, string> = {};
  for (const key of keys) {
    try {
      const result = await client.queryContractSmart(genesisRegistryAddress, { resolve_canonical: { key } });
      addresses[key] = result.address;
    } catch { console.warn(`无法解析: ${key}`); }
  }
  return addresses;
}

五、合约查询操作

5.1 DID 合约查询

// src/lib/queries/did.ts
import { smartQuery } from "../cosmwasm-client";

export interface VerificationMethod {
  id: string; controller: string; type_: string; public_key_multibase: string;
}
export interface Service {
  id: string; type_: string; service_endpoint: string;
}
export interface DidDocument {
  context: string[]; id: string; verification_method: VerificationMethod[];
  authentication: string[]; assertion_method?: string[]; key_agreement?: string[];
  capability_invocation?: string[]; capability_delegation?: string[];
  service?: Service[]; created?: string; updated?: string;
}
export interface DIDResolutionResponse {
  did: string; document: DidDocument; active: boolean;
  created_height: number; updated_height: number;
}

export async function resolveDID(registryAddress: string, did: string): Promise<DIDResolutionResponse> {
  return smartQuery<DIDResolutionResponse>(registryAddress, { resolve_did: { did } });
}

export async function checkDIDActive(registryAddress: string, did: string): Promise<{ active: boolean }> {
  return smartQuery(registryAddress, { check_did_active: { did } });
}

export async function listDIDsByController(registryAddress: string, controller: string): Promise<{ dids: DIDResolutionResponse[] }> {
  return smartQuery(registryAddress, { list_dids_by_controller: { controller } });
}

export async function getDIDMetadata(registryAddress: string, did: string): Promise<{ did: string; controller: string; created_height: number; updated_height: number; version: number }> {
  return smartQuery(registryAddress, { get_did_metadata: { did } });
}

5.2 Agent Registry 合约查询

// src/lib/queries/agent.ts
import { smartQuery } from "../cosmwasm-client";

export interface PriceModel { price_type?: string; price?: string; currency?: string; }
export interface AgentMetadata { natural_language?: string; prompt?: string; node_executes_llm?: boolean; consensus_llm_parse?: boolean; }
export type AgentStatus = "active" | "paused" | "suspended";
export interface AgentResponse {
  agent_id: string; name: string; owner: string; capabilities: string[]; endpoint: string;
  price_model: PriceModel | null; status: AgentStatus; registered_height: number;
  updated_at: number; metadata: AgentMetadata | null; tx_hash: string | null;
}

export async function getAgent(registryAddress: string, agentId: string): Promise<{ agent: AgentResponse }> {
  return smartQuery(registryAddress, { get_agent: { agent_id: agentId } });
}

export async function listAgents(registryAddress: string): Promise<{ agents: AgentResponse[]; count: number }> {
  return smartQuery(registryAddress, { list_agents: {} });
}

export async function getAgentsByOwner(registryAddress: string, owner: string): Promise<{ agents: AgentResponse[] }> {
  return smartQuery(registryAddress, { get_agents_by_owner: { owner } });
}

export async function discoverAgents(registryAddress: string, capability?: string, maxPrice?: number, limit: number = 20): Promise<{ agents: AgentResponse[]; count: number }> {
  return smartQuery(registryAddress, { discover_agents: { capability: capability || undefined, max_price: maxPrice, limit } });
}

5.3 Constitution 合约查询

// src/lib/queries/constitution.ts
import { smartQuery } from "../cosmwasm-client";

export type RiskTier = "low" | "medium" | "high";
export interface Rule {
  action: string; risk_tier: RiskTier; allowed: boolean;
  required_controls: string[]; max_amount?: string; budget_id?: string;
}
export interface ConstitutionResponse { constitution: string; version: string; constitution_hash: string; active: boolean; }
export interface CheckActionResponse {
  allowed: boolean; decision: "allowed" | "denied" | "requires_approval";
  reasons: string[]; required_controls: string[]; active_constitution_version: string; active_constitution_hash: string;
}

export async function getConstitution(constitutionAddress: string): Promise<ConstitutionResponse> {
  return smartQuery<ConstitutionResponse>(constitutionAddress, { get_constitution: {} });
}

export async function getRule(constitutionAddress: string, ruleId: string): Promise<{ rule: Rule }> {
  return smartQuery(constitutionAddress, { get_rule: { rule_id: ruleId } });
}

export async function listRules(constitutionAddress: string): Promise<{ rules: Rule[] }> {
  return smartQuery(constitutionAddress, { list_rules: {} });
}

export async function isActionAllowed(constitutionAddress: string, agentId: string, action: string, riskTier: RiskTier, amount?: string): Promise<CheckActionResponse> {
  return smartQuery<CheckActionResponse>(constitutionAddress, { is_action_allowed: { agent_id: agentId, action, risk_tier: riskTier, amount: amount || undefined } });
}

export async function getActiveStatus(constitutionAddress: string): Promise<{ active: boolean }> {
  return smartQuery(constitutionAddress, { get_active_status: {} });
}

5.4 Payment 合约查询

// src/lib/queries/payment.ts
import { smartQuery } from "../cosmwasm-client";

export type SessionStatus = "pending" | "active" | "completed" | "disputed" | "cancelled";
export interface MilestoneResponse { milestone_id: string; description: string; amount: string; due_by: number; completed: boolean; }
export interface SessionResponse {
  session_id: string; payer: string; payee: string; agent_id: string; amount: string;
  funded: string; released: string; status: SessionStatus; asset: string; terms_hash: string;
  created_at: number; expires_at: number; milestones: MilestoneResponse[];
}
export interface DisputeStatus { disputed: boolean; disputed_by: string | null; reason: string | null; resolved: boolean; resolution: string | null; }

export async function getSession(paymentAddress: string, sessionId: string): Promise<SessionResponse> {
  return smartQuery<SessionResponse>(paymentAddress, { get_session: { session_id: sessionId } });
}

export async function getAgentSessions(paymentAddress: string, agentId: string): Promise<{ sessions: SessionResponse[] }> {
  return smartQuery(paymentAddress, { get_agent_sessions: { agent_id: agentId } });
}

export async function listSessions(paymentAddress: string): Promise<{ sessions: SessionResponse[] }> {
  return smartQuery(paymentAddress, { list_sessions: {} });
}

export async function getPaymentBalance(paymentAddress: string, address: string, asset: string = "umsg"): Promise<{ balance: string }> {
  return smartQuery(paymentAddress, { get_balance: { address, asset } });
}

export async function getDisputeStatus(paymentAddress: string, paymentId: string): Promise<DisputeStatus> {
  return smartQuery<DisputeStatus>(paymentAddress, { get_dispute_status: { payment_id: paymentId } });
}

六、合约执行操作

6.1 通用执行函数

// src/lib/execute.ts
import { SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
import { Coin } from "@cosmjs/stargate";

export interface ExecuteOptions { gasLimit?: number; gasPriceStep?: "low" | "average" | "high"; memo?: string; funds?: Coin[]; }
export interface ParsedExecuteResult { transactionHash: string; gasUsed: number; height: number; rawLog: string; events: Record<string, unknown>[]; success: boolean; }

export async function execute(client: SigningCosmWasmClient, sender: string, contractAddress: string, executeMsg: Record<string, unknown>, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  try {
    const result: ExecuteResult = await client.execute(sender, contractAddress, executeMsg, "auto", options?.memo || "", options?.funds);
    return { transactionHash: result.transactionHash, gasUsed: result.gasUsed, height: result.height, rawLog: result.rawLog, events: JSON.parse(result.rawLog || "[]"), success: result.code === 0 };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error(`交易执行失败: ${message}`);
  }
}

export function parseEventAttribute(rawLog: string, eventType: string, attributeKey: string): string | null {
  try {
    const events = JSON.parse(rawLog);
    for (const event of events) {
      if (event.type === eventType) {
        const attr = event.attributes.find((a: { key: string; value: string }) => a.key === attributeKey);
        if (attr) return attr.value;
      }
    }
  } catch { /* ignore */ }
  return null;
}

export async function pollForTx(client: SigningCosmWasmClient, txHash: string, maxAttempts: number = 20, intervalMs: number = 1500): Promise<boolean> {
  for (let i = 0; i < maxAttempts; i++) {
    const tx = await client.getTx(txHash);
    if (tx) return tx.code === 0;
    await new Promise((resolve) => setTimeout(resolve, intervalMs));
  }
  throw new Error(`交易 ${txHash} 在 ${maxAttempts * intervalMs}ms 内未确认`);
}

6.2 Agent Registry 执行函数

// src/lib/executes/agent.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { execute, ExecuteOptions, ParsedExecuteResult } from "../execute";

export async function registerAgent(client: SigningCosmWasmClient, sender: string, registryAddress: string, params: {
  agent_id: string; name?: string; owner: string; capabilities: string[]; endpoint?: string;
  price_model?: { price_type?: string; price?: string; currency?: string };
  natural_language?: string; prompt?: string; node_executes_llm?: boolean; consensus_llm_parse?: boolean;
}, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { register_agent: params }, options);
}

export async function updateAgent(client: SigningCosmWasmClient, sender: string, registryAddress: string, params: {
  agent_id: string; name?: string; capabilities?: string[]; endpoint?: string;
  price_model?: { price_type?: string; price?: string; currency?: string };
}, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { update_agent: params }, options);
}

export async function deregisterAgent(client: SigningCosmWasmClient, sender: string, registryAddress: string, agentId: string, reason?: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { deregister_agent: { agent_id: agentId, reason } }, options);
}

export async function setAgentStatus(client: SigningCosmWasmClient, sender: string, registryAddress: string, agentId: string, status: "active" | "paused" | "suspended", options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { set_status: { agent_id: agentId, status } }, options);
}

6.3 Payment 合约执行函数

// src/lib/executes/payment.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { execute, ExecuteOptions, ParsedExecuteResult } from "../execute";

export async function createPaymentSession(client: SigningCosmWasmClient, sender: string, paymentAddress: string, params: {
  session_id: string; payer: string; payee: string; agent_id: string; amount: string; asset?: string; terms_hash: string; expiry: number;
}, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, paymentAddress, { create_session: params }, options);
}

export async function fundSession(client: SigningCosmWasmClient, sender: string, paymentAddress: string, sessionId: string, amount: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, paymentAddress, { fund_session: { session_id: sessionId, amount } }, options);
}

export async function releasePayment(client: SigningCosmWasmClient, sender: string, paymentAddress: string, paymentId: string, releaseHash: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, paymentAddress, { release_payment: { payment_id: paymentId, release_hash: releaseHash } }, options);
}

export async function disputePayment(client: SigningCosmWasmClient, sender: string, paymentAddress: string, paymentId: string, reason: string, proofHash: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, paymentAddress, { dispute_payment: { payment_id: paymentId, reason, proof_hash: proofHash } }, options);
}

export async function closePaymentSession(client: SigningCosmWasmClient, sender: string, paymentAddress: string, sessionId: string, closeReceiptHash: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, paymentAddress, { close_session: { session_id: sessionId, close_receipt_hash: closeReceiptHash } }, options);
}

6.4 DID & Constitution 执行函数

// src/lib/executes/did.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { execute, ExecuteOptions, ParsedExecuteResult } from "../execute";
import { DidDocument, VerificationMethod, Service } from "../queries/did";

export async function createDID(client: SigningCosmWasmClient, sender: string, registryAddress: string, params: { did: string; document: DidDocument; verification_method_id: string; signature: string }, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { create_did: params }, options);
}

export async function updateDID(client: SigningCosmWasmClient, sender: string, registryAddress: string, did: string, document: DidDocument, signature: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { update_did: { did, document, signature } }, options);
}

export async function deactivateDID(client: SigningCosmWasmClient, sender: string, registryAddress: string, did: string, signature: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { deactivate_did: { did, signature } }, options);
}

export async function addVerificationMethod(client: SigningCosmWasmClient, sender: string, registryAddress: string, did: string, verificationMethod: VerificationMethod, signature: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { add_verification_method: { did, verification_method: verificationMethod, signature } }, options);
}

export async function removeVerificationMethod(client: SigningCosmWasmClient, sender: string, registryAddress: string, did: string, verificationMethodId: string, signature: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, registryAddress, { remove_verification_method: { did, verification_method_id: verificationMethodId, signature } }, options);
}

// src/lib/executes/constitution.ts
import { Rule } from "../queries/constitution";

export async function updateConstitution(client: SigningCosmWasmClient, sender: string, constitutionAddress: string, constitution: string, version: string, constitutionHash: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, constitutionAddress, { update_constitution: { constitution, version, constitution_hash: constitutionHash } }, options);
}

export async function addRule(client: SigningCosmWasmClient, sender: string, constitutionAddress: string, policyId: string, rule: Rule, ruleId: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, constitutionAddress, { add_rule: { policy_id: policyId, rule, rule_id: ruleId } }, options);
}

export async function removeRule(client: SigningCosmWasmClient, sender: string, constitutionAddress: string, ruleId: string, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, constitutionAddress, { remove_rule: { rule_id: ruleId } }, options);
}

export async function setConstitutionActive(client: SigningCosmWasmClient, sender: string, constitutionAddress: string, active: boolean, options?: ExecuteOptions): Promise<ParsedExecuteResult> {
  return execute(client, sender, constitutionAddress, { set_active: { active } }, options);
}

6.5 错误处理

// src/lib/error-handler.ts
export const ERROR_CODE_MAP: Record<number, string> = {
  0: "成功", 1: "Stub 端点成功响应", 2: "功能尚未实现", 3: "未授权 — 缺少或无效的 API Key",
  4: "余额不足", 5: "合约执行失败", 6: "查询超时或返回空结果", 7: "请求参数无效",
  8: "DAO 时间锁期间未结束", 9: "高价值交易门槛未满足", 10: "未找到指定 Agent",
  11: "Agent 已经注册", 12: "动作违反 Agent 宪章", 13: "支付会话已过期",
  14: "支付会话超限", 15: "Dilithium-5 签名验证失败", 16: "检测到重复 Nonce",
  17: "请求频率超限",
};

export function getChineseErrorMessage(code: number, fallback?: string): string {
  return ERROR_CODE_MAP[code] || fallback || `未知错误 (代码: ${code})`;
}

export function parseTxError(error: unknown): string {
  if (error instanceof Error) {
    const msg = error.message;
    if (msg.includes("insufficient funds")) return "余额不足";
    if (msg.includes("out of gas")) return "Gas 不足,请增加 Gas 限制";
    if (msg.includes("unauthorized")) return "未授权操作";
    if (msg.includes("contract not found")) return "合约地址不存在";
    if (msg.includes("not found")) return "未找到指定资源";
    if (msg.includes("timeout")) return "请求超时,请重试";
    return msg;
  }
  return String(error);
}

七、Agent API 客户端

7.1 类型定义

// src/types/agent.ts
export interface AgentQueryRequest { agent_id: string; query: string; context?: { session_id?: string; user_id?: string; }; }
export interface AgentQueryResponse { agent_id: string; response: string; metadata: { model: string; processing_time_ms: number; tokens_used: number; }; }
export interface AgentStatusResponse { agent_id: string; status: string; is_online: boolean; last_heartbeat: string; current_task: string; uptime_seconds: number; version: string; }
export interface AgentHistoryResponse { agent_id: string; history: Array<{ tx_hash: string; action: string; status: string; timestamp: string; details: Record<string, unknown>; }>; total: number; limit: number; offset: number; }
export interface AgentEvent { type: string; data: Record<string, unknown>; timestamp: number; }
export interface EventsReplayRequest { event_types: string[]; from_timestamp: number; to_timestamp: number; limit?: number; }
export interface EventsReplayResponse { events: AgentEvent[]; total: number; }
export interface OracleRequest { model: string; prompt: string; input_data: unknown; parameters?: { temperature?: number; max_tokens?: number; }; }
export interface OracleRequestResponse { request_id: string; status: string; estimated_completion_ms: number; }
export interface OracleResultResponse { request_id: string; status: string; result?: { analysis: string; confidence: number; recommendation: string; }; proof?: { model_id: string; inference_hash: string; signature: string; }; processing_time_ms?: number; progress?: number; }
export interface OracleVerifyRequest { request_id: string; proof: { model_id: string; inference_hash: string; signature: string; }; }
export interface OracleVerifyResponse { valid: boolean; verified_by: string; verified_at: string; }
export interface WalletCreateRequest { agent_id: string; owner: string; }
export interface WalletCreateResponse { agent_id: string; wallet_address: string; created_at: string; initial_balance: string; }
export interface WalletBalanceResponse { agent_id: string; wallet_address: string; balances: Array<{ denom: string; amount: string; }>; escrow_balance: string; }
export interface WalletTransferRequest { agent_id: string; to: string; amount: string; denom?: string; justification: string; idempotency_key: string; }
export interface WalletTransferResponse { status: string; approval_gate: string; next_step: string; tx_hash: string | null; }
export interface WalletTransactionsResponse { agent_id: string; transactions: Array<{ tx_hash: string; type: string; to: string; amount: string; denom: string; status: string; timestamp: string; height: number; }>; total: number; }
export interface MPCSignRequest { key_id: string; payload: string; participants: string[]; threshold: number; algorithm: string; }
export interface MPCSignResponse { session_id: string; status: string; participants_required: number; participants_ready: number; }
export interface MPCSubmitRequest { session_id: string; participant: string; partial_signature: string; }
export interface MPCSubmitResponse { session_id: string; status: string; participants_submitted: number; threshold: number; }
export interface MPCAggregateResponse { session_id: string; status: string; aggregated_signature: string; public_key: string; }
export interface PaymentSessionCreateRequest { agent_id: string; payer: string; payee: string; rate_per_sec: string; initial_balance: string; asset?: string; terms_hash?: string; }
export interface PaymentSessionCreateResponse { session_id: string; status: string; rate_per_sec: string; balance: string; created_at: string; }
export interface PaymentSessionCloseRequest { session_id: string; close_receipt_hash: string; }
export interface PaymentSessionCloseResponse { session_id: string; status: string; total_charged: string; remaining_balance: string; refund_address: string; }
export interface AgentApiError { error: { code: number; name: string; message: string; details?: string; }; request_id: string; }

7.2 Agent API 客户端类

// src/lib/agent-api.ts
import type {
  AgentQueryRequest, AgentQueryResponse, AgentStatusResponse, AgentHistoryResponse,
  EventsReplayRequest, EventsReplayResponse, OracleRequest, OracleRequestResponse,
  OracleResultResponse, OracleVerifyRequest, OracleVerifyResponse, WalletCreateRequest,
  WalletCreateResponse, WalletBalanceResponse, WalletTransferRequest, WalletTransferResponse,
  WalletTransactionsResponse, MPCSignRequest, MPCSignResponse, MPCSubmitRequest,
  MPCSubmitResponse, MPCAggregateResponse, PaymentSessionCreateRequest,
  PaymentSessionCreateResponse, PaymentSessionCloseRequest, PaymentSessionCloseResponse,
} from "../types/agent";

export interface AgentAPIConfig { baseUrl: string; apiKey?: string; }

export class AgentAPIClient {
  private config: AgentAPIConfig;
  constructor(config: AgentAPIConfig) { this.config = config; }

  private getHeaders(): HeadersInit {
    const headers: Record<string, string> = { "Content-Type": "application/json" };
    if (this.config.apiKey) headers["X-API-Key"] = this.config.apiKey;
    return headers;
  }

  private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
    const url = `${this.config.baseUrl}${path}`;
    const response = await fetch(url, { ...options, headers: { ...this.getHeaders(), ...(options.headers as Record<string, string>) } });
    const isStub = response.headers.get("X-MSG-Stub") === "true";
    if (!response.ok) {
      let errorBody: any = null;
      try { errorBody = await response.json(); } catch { /* ignore */ }
      const message = errorBody?.error?.message || `HTTP ${response.status}`;
      const error = new Error(message) as Error & { code?: number; isStub?: boolean; status?: number; };
      error.code = errorBody?.error?.code; error.isStub = isStub; error.status = response.status;
      throw error;
    }
    if (isStub) console.warn(`[AgentAPI/Stub] ${path} — 此端点尚未完全实现`);
    return response.json();
  }

  // Group 1: Agent Query
  async agentQuery(req: AgentQueryRequest): Promise<AgentQueryResponse> {
    return this.request<AgentQueryResponse>("/api/v1/agent/query", { method: "POST", body: JSON.stringify(req) });
  }
  async getAgentStatus(agentId: string): Promise<AgentStatusResponse> {
    return this.request<AgentStatusResponse>(`/api/v1/agent/${agentId}/status`);
  }
  async getAgentHistory(agentId: string, params?: { limit?: number; offset?: number; action?: string }): Promise<AgentHistoryResponse> {
    const sp = new URLSearchParams();
    if (params?.limit) sp.set("limit", String(params.limit));
    if (params?.offset) sp.set("offset", String(params.offset));
    if (params?.action) sp.set("action", params.action);
    const qs = sp.toString();
    return this.request<AgentHistoryResponse>(`/api/v1/agent/${agentId}/history${qs ? `?${qs}` : ""}`);
  }

  // Group 2: Agent Events (WebSocket)
  subscribeEvents(eventTypes: string[], filter?: { agent_id?: string }): WebSocket {
    const wsUrl = this.config.baseUrl.replace(/^http/, "ws");
    const ws = new WebSocket(`${wsUrl}/api/v1/agent/events/subscribe`);
    ws.onopen = () => { ws.send(JSON.stringify({ event_types: eventTypes, filter: filter || {} })); };
    return ws;
  }
  async unsubscribeEvents(sessionId: string, eventTypes: string[]): Promise<{ status: string; session_id: string }> {
    return this.request("/api/v1/agent/events/unsubscribe", { method: "POST", body: JSON.stringify({ session_id: sessionId, event_types: eventTypes }) }); // X-MSG-Stub=true
  }
  async replayEvents(req: EventsReplayRequest): Promise<EventsReplayResponse> {
    return this.request<EventsReplayResponse>("/api/v1/agent/events/replay", { method: "POST", body: JSON.stringify(req) });
  }

  // Group 3: AI Oracle
  async oracleRequest(req: OracleRequest): Promise<OracleRequestResponse> {
    return this.request<OracleRequestResponse>("/api/v1/agent/oracle/request", { method: "POST", body: JSON.stringify(req) });
  }
  async getOracleResult(requestId: string): Promise<OracleResultResponse> {
    return this.request<OracleResultResponse>(`/api/v1/agent/oracle/result/${requestId}`);
  }
  async verifyOracleProof(req: OracleVerifyRequest): Promise<OracleVerifyResponse> {
    return this.request<OracleVerifyResponse>("/api/v1/agent/oracle/verify", { method: "POST", body: JSON.stringify(req) });
  }

  // Group 4: Agent Wallet
  async createAgentWallet(req: WalletCreateRequest): Promise<WalletCreateResponse> {
    return this.request<WalletCreateResponse>("/api/v1/agent/wallet/create", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
  async getAgentWalletBalance(agentId: string): Promise<WalletBalanceResponse> {
    return this.request<WalletBalanceResponse>(`/api/v1/agent/wallet/${agentId}/balance`);
  }
  async agentWalletTransfer(req: WalletTransferRequest): Promise<WalletTransferResponse> {
    return this.request<WalletTransferResponse>("/api/v1/agent/wallet/transfer", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
  async getAgentWalletTransactions(agentId: string, params?: { limit?: number }): Promise<WalletTransactionsResponse> {
    const qs = params?.limit ? `?limit=${params.limit}` : "";
    return this.request<WalletTransactionsResponse>(`/api/v1/agent/wallet/${agentId}/transactions${qs}`);
  }

  // Group 5: MPC
  async mpcSign(req: MPCSignRequest): Promise<MPCSignResponse> {
    return this.request<MPCSignResponse>("/api/v1/agent/mpc/sign", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
  async mpcSubmitPartialSignature(req: MPCSubmitRequest): Promise<MPCSubmitResponse> {
    return this.request<MPCSubmitResponse>("/api/v1/agent/mpc/submit", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
  async mpcAggregateSignatures(sessionId: string): Promise<MPCAggregateResponse> {
    return this.request<MPCAggregateResponse>("/api/v1/agent/mpc/aggregate", { method: "POST", body: JSON.stringify({ session_id: sessionId }) }); // X-MSG-Stub=true
  }

  // Group 6: Agent Payment
  async createPaymentSession(req: PaymentSessionCreateRequest): Promise<PaymentSessionCreateResponse> {
    return this.request<PaymentSessionCreateResponse>("/api/v1/agent/payment/create-session", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
  async closePaymentSession(req: PaymentSessionCloseRequest): Promise<PaymentSessionCloseResponse> {
    return this.request<PaymentSessionCloseResponse>("/api/v1/agent/payment/close-session", { method: "POST", body: JSON.stringify(req) }); // X-MSG-Stub=true
  }
}

export const defaultAgentApi = new AgentAPIClient({ baseUrl: "http://localhost:8080" });

八、React 组件库

8.1 UI 基础组件

// src/components/ui.tsx
import React from "react";

export function LoadingSpinner({ text = "加载中..." }: { text?: string }) {
  return (
    <div className="flex items-center justify-center py-8">
      <div className="animate-spin h-8 w-8 border-4 border-blue-500 border-t-transparent rounded-full" />
      <span className="ml-3 text-gray-500">{text}</span>
    </div>
  );
}

export function EmptyState({ icon = "📭", title = "暂无数据", description, action }: { icon?: string; title?: string; description?: string; action?: React.ReactNode }) {
  return (
    <div className="text-center py-12">
      <div className="text-4xl mb-3">{icon}</div>
      <h3 className="text-lg font-medium text-gray-700 mb-1">{title}</h3>
      {description && <p className="text-gray-500 text-sm mb-4">{description}</p>}
      {action}
    </div>
  );
}

export function ErrorMessage({ message, onRetry }: { message: string; onRetry?: () => void }) {
  return (
    <div className="p-4 bg-red-50 border border-red-200 rounded-lg">
      <div className="flex items-start gap-3">
        <span className="text-red-500 text-lg">!</span>
        <div className="flex-1">
          <p className="text-red-700 text-sm">{message}</p>
          {onRetry && <button onClick={onRetry} className="mt-2 text-sm text-red-600 underline hover:text-red-800">重试</button>}
        </div>
      </div>
    </div>
  );
}

8.2 ErrorBoundary

// src/components/ErrorBoundary.tsx
import React, { Component, ErrorInfo, ReactNode } from "react";
interface Props { children: ReactNode; fallback?: ReactNode; }
interface State { hasError: boolean; error: Error | null; }
export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) { super(props); this.state = { hasError: false, error: null }; }
  static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; }
  componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("[ErrorBoundary]", error, errorInfo); }
  render() {
    if (this.state.hasError) {
      if (this.props.fallback) return this.props.fallback;
      return (
        <div className="p-6 bg-red-50 border border-red-200 rounded-lg">
          <h3 className="text-lg font-semibold text-red-800 mb-2">组件发生错误</h3>
          <p className="text-red-600 text-sm mb-3">{this.state.error?.message || "未知错误"}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })} className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm">重试</button>
        </div>
      );
    }
    return this.props.children;
  }
}

8.3 WalletConnectButton

// src/components/WalletConnectButton.tsx
import React from "react";
import { useWallet } from "../hooks/useWallet";

export function WalletConnectButton({ onConnect, onDisconnect }: { onConnect?: (addr: string) => void; onDisconnect?: () => void }) {
  const { address, isConnected, isInstalled, isConnecting, error, connect, disconnect } = useWallet();
  const handleConnect = async () => { await connect(); if (address && onConnect) onConnect(address); };
  const handleDisconnect = () => { disconnect(); if (onDisconnect) onDisconnect(); };

  if (!isInstalled) return (
    <div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
      <p className="text-yellow-800 text-sm">请安装 <a href="https://chrome.google.com/webstore/detail/keplr/dmkamcknogkgcdfhhbddcghachkejeap" target="_blank" rel="noopener noreferrer" className="underline font-medium">Keplr 钱包</a></p>
    </div>
  );

  if (error) return (
    <div className="space-y-2">
      <div className="p-2 bg-red-50 border border-red-200 rounded text-red-700 text-sm">{error}</div>
      <button onClick={handleConnect} className="w-full px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">重试连接</button>
    </div>
  );

  if (isConnected && address) return (
    <div className="flex items-center gap-3 p-3 bg-green-50 border border-green-200 rounded-lg">
      <div className="flex items-center gap-2">
        <span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
        <span className="text-green-800 text-sm font-medium">已连接</span>
      </div>
      <code className="text-xs bg-white px-2 py-1 rounded border font-mono flex-1 truncate">{address.slice(0, 8)}...{address.slice(-6)}</code>
      <button onClick={handleDisconnect} className="text-xs text-gray-500 hover:text-red-600 underline">断开</button>
    </div>
  );

  return (
    <button onClick={handleConnect} disabled={isConnecting} className="w-full flex items-center justify-center gap-2 px-6 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors">
      {isConnecting ? (
        <><span className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" />连接中...</>
      ) : "连接 Keplr 钱包"}
    </button>
  );
}

8.4 AccountBalance

// src/components/AccountBalance.tsx
import React, { useEffect, useState } from "react";
import { useWallet } from "../hooks/useWallet";
import { queryBalance } from "../lib/cosmwasm-client";
import { formatUmsg } from "../lib/utils";

export function AccountBalance({ address: propAddress, showRefresh = true }: { address?: string; showRefresh?: boolean }) {
  const { address: walletAddress } = useWallet();
  const addr = propAddress || walletAddress;
  const [balance, setBalance] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const fetchBalance = async () => {
    if (!addr) return; setLoading(true); setError(null);
    try { const coin = await queryBalance(addr); setBalance(coin.amount); }
    catch (err) { setError(err instanceof Error ? err.message : "查询余额失败"); }
    finally { setLoading(false); }
  };
  useEffect(() => { fetchBalance(); }, [addr]);
  if (!addr) return <div className="p-4 bg-gray-50 rounded-lg text-gray-500 text-sm">请先连接钱包</div>;
  if (error) return <div className="p-4 bg-red-50 rounded-lg"><p className="text-red-600 text-sm">{error}</p><button onClick={fetchBalance} className="mt-2 text-sm text-red-600 underline">重试</button></div>;
  return (
    <div className="p-4 bg-white border rounded-lg">
      <div className="flex items-center justify-between">
        <div>
          <p className="text-xs text-gray-500 mb-1">MSG 余额</p>
          {loading ? <div className="h-8 w-32 bg-gray-200 animate-pulse rounded" /> :
            <p className="text-2xl font-bold text-gray-900">{balance ? formatUmsg(balance) : "0"} <span className="text-sm font-normal text-gray-500">MSG</span></p>}
        </div>
        {showRefresh && <button onClick={fetchBalance} disabled={loading} className="p-2 text-gray-400 hover:text-blue-600 disabled:opacity-50"><svg className={`w-5 h-5 ${loading ? "animate-spin" : ""}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /></svg></button>}
      </div>
      <p className="text-xs text-gray-400 font-mono mt-2 truncate">{addr}</p>
    </div>
  );
}

8.5 AgentCard

// src/components/AgentCard.tsx
import React from "react";
import type { AgentResponse } from "../lib/queries/agent";

const STATUS_COLORS: Record<string, string> = { active: "bg-green-100 text-green-700 border-green-200", paused: "bg-yellow-100 text-yellow-700 border-yellow-200", suspended: "bg-red-100 text-red-700 border-red-200" };
const STATUS_LABELS: Record<string, string> = { active: "活跃", paused: "已暂停", suspended: "已停用" };

export function AgentCard({ agent, onSelect, showActions = true }: { agent: AgentResponse; onSelect?: (id: string) => void; showActions?: boolean }) {
  return (
    <div className="border rounded-lg p-4 hover:shadow-md transition-shadow bg-white">
      <div className="flex items-start justify-between mb-3">
        <div className="flex-1 min-w-0">
          <h3 className="font-semibold text-gray-900 truncate">{agent.name || agent.agent_id}</h3>
          <p className="text-xs text-gray-500 font-mono mt-0.5 truncate">ID: {agent.agent_id}</p>
        </div>
        <span className={`ml-2 px-2 py-0.5 text-xs rounded-full border ${STATUS_COLORS[agent.status]}`}>{STATUS_LABELS[agent.status] || agent.status}</span>
      </div>
      <p className="text-xs text-gray-500 font-mono mb-3 truncate">创建者: {agent.owner.slice(0, 10)}...{agent.owner.slice(-4)}</p>
      <div className="flex flex-wrap gap-1 mb-3">
        {agent.capabilities.slice(0, 4).map((cap) => (<span key={cap} className="bg-blue-50 text-blue-700 text-xs px-2 py-0.5 rounded">{cap}</span>))}
        {agent.capabilities.length > 4 && <span className="text-xs text-gray-400">+{agent.capabilities.length - 4}</span>}
      </div>
      {agent.price_model && <div className="flex items-center gap-2 text-sm text-gray-600 mb-3"><span className="font-medium">{agent.price_model.price || "0"}</span><span className="text-xs text-gray-400">{agent.price_model.currency || "umsg"}</span>{agent.price_model.price_type && <span className="text-xs text-gray-400">({agent.price_model.price_type})</span>}</div>}
      <div className="text-xs text-gray-400">注册高度: #{agent.registered_height}</div>
      {showActions && onSelect && <button onClick={() => onSelect(agent.agent_id)} className="mt-3 w-full px-3 py-1.5 text-sm bg-blue-50 text-blue-700 rounded hover:bg-blue-100">查看详情</button>}
    </div>
  );
}

export function AgentList({ agents, loading, error, onSelect, emptyMessage = "暂无 Agent" }: { agents: AgentResponse[]; loading: boolean; error?: string | null; onSelect?: (id: string) => void; emptyMessage?: string }) {
  if (loading) return <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">{Array.from({ length: 3 }).map((_, i) => (<div key={i} className="border rounded-lg p-4 animate-pulse"><div className="h-5 w-3/4 bg-gray-200 rounded mb-3" /><div className="h-4 w-1/2 bg-gray-200 rounded mb-3" /><div className="flex gap-1 mb-3"><div className="h-5 w-16 bg-gray-200 rounded" /><div className="h-5 w-16 bg-gray-200 rounded" /></div><div className="h-4 w-1/3 bg-gray-200 rounded" /></div>))}</div>;
  if (error) return <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{error}</div>;
  if (agents.length === 0) return <div className="text-center py-12 text-gray-500">{emptyMessage}</div>;
  return <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">{agents.map((a) => <AgentCard key={a.agent_id} agent={a} onSelect={onSelect} />)}</div>;
}

8.6 DIDList

// src/components/DIDList.tsx
import React, { useState } from "react";
import type { DIDResolutionResponse } from "../lib/queries/did";

export function DIDList({ dids, loading, onResolve, onDeactivate }: { dids: DIDResolutionResponse[]; loading: boolean; onResolve: (did: string) => void; onDeactivate?: (did: string) => void }) {
  const [expandedDid, setExpandedDid] = useState<string | null>(null);
  if (loading) return <div className="space-y-3">{Array.from({ length: 3 }).map((_, i) => (<div key={i} className="border rounded-lg p-4 animate-pulse"><div className="h-5 w-2/3 bg-gray-200 rounded mb-2" /><div className="h-4 w-1/2 bg-gray-200 rounded" /></div>))}</div>;
  if (dids.length === 0) return <div className="text-center py-12"><p className="text-gray-500">暂无 DID</p><p className="text-gray-400 text-sm mt-1">创建 AI Agent 的链上身份标识</p></div>;
  return <div className="space-y-3">{dids.map((dr) => (
    <div key={dr.did} className="border rounded-lg overflow-hidden">
      <div className="p-4 bg-white cursor-pointer hover:bg-gray-50" onClick={() => setExpandedDid(expandedDid === dr.did ? null : dr.did)}>
        <div className="flex items-center justify-between">
          <div className="flex-1 min-w-0"><p className="font-mono text-sm text-blue-700 truncate">{dr.did}</p><div className="flex items-center gap-3 mt-1 text-xs text-gray-500"><span>创建高度: #{dr.created_height}</span><span>更新高度: #{dr.updated_height}</span></div></div>
          <span className={`ml-2 px-2 py-0.5 text-xs rounded-full ${dr.active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-500"}`}>{dr.active ? "活跃" : "已停用"}</span>
        </div>
      </div>
      {expandedDid === dr.did && <div className="px-4 pb-4 bg-gray-50 border-t"><div className="mt-3 space-y-2">
        <h4 className="text-xs font-semibold text-gray-600 uppercase">验证方法 ({dr.document.verification_method.length})</h4>
        {dr.document.verification_method.map((vm) => (<div key={vm.id} className="text-xs font-mono bg-white p-2 rounded border"><p>ID: {vm.id}</p><p>Type: {vm.type_}</p><p className="truncate">Key: {vm.public_key_multibase.slice(0, 40)}...</p></div>))}
        {dr.document.service && dr.document.service.length > 0 && (<><h4 className="text-xs font-semibold text-gray-600 uppercase mt-3">服务端点</h4>{dr.document.service.map((svc) => (<div key={svc.id} className="text-xs bg-white p-2 rounded border"><p>ID: {svc.id}</p><p>Type: {svc.type_}</p><p className="truncate">Endpoint: {svc.service_endpoint}</p></div>))}</>)}
        <div className="flex gap-2 mt-3">
          <button onClick={(e) => { e.stopPropagation(); onResolve(dr.did); }} className="px-3 py-1 text-xs bg-blue-50 text-blue-700 rounded hover:bg-blue-100">刷新</button>
          {dr.active && onDeactivate && <button onClick={(e) => { e.stopPropagation(); onDeactivate(dr.did); }} className="px-3 py-1 text-xs bg-red-50 text-red-700 rounded hover:bg-red-100">停用</button>}
        </div>
      </div></div>}
    </div>
  ))}</div>;
}

8.7 PaymentSession

// src/components/PaymentSession.tsx
import React, { useState } from "react";
import type { SessionResponse, SessionStatus } from "../lib/queries/payment";
import { formatUmsg } from "../lib/utils";

const STATUS_LABELS: Record<SessionStatus, string> = { pending: "待处理", active: "进行中", completed: "已完成", disputed: "争议中", cancelled: "已取消" };
const STATUS_COLORS: Record<SessionStatus, string> = { pending: "bg-gray-100 text-gray-600", active: "bg-green-100 text-green-700", completed: "bg-blue-100 text-blue-700", disputed: "bg-red-100 text-red-700", cancelled: "bg-gray-100 text-gray-500" };

export function PaymentSession({ session, onFund, onClose, onDispute }: { session: SessionResponse; onFund?: (id: string, amount: string) => void; onClose?: (id: string) => void; onDispute?: (id: string, reason: string) => void }) {
  const [fundAmount, setFundAmount] = useState("");
  const [disputeReason, setDisputeReason] = useState("");
  const [showDispute, setShowDispute] = useState(false);
  return (
    <div className="border rounded-lg p-4 bg-white">
      <div className="flex items-center justify-between mb-3">
        <h3 className="font-mono text-sm font-semibold text-gray-900">{session.session_id.slice(0, 12)}...</h3>
        <span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_COLORS[session.status]}`}>{STATUS_LABELS[session.status]}</span>
      </div>
      <div className="grid grid-cols-2 gap-3 text-sm mb-3">
        <div><span className="text-gray-500 text-xs">金额</span><p className="font-medium">{formatUmsg(session.amount)} MSG</p></div>
        <div><span className="text-gray-500 text-xs">已释放</span><p className="font-medium">{formatUmsg(session.released)} MSG</p></div>
        <div><span className="text-gray-500 text-xs">已存入</span><p className="font-medium">{formatUmsg(session.funded)} MSG</p></div>
        <div><span className="text-gray-500 text-xs">状态</span><p className="font-medium">{STATUS_LABELS[session.status]}</p></div>
      </div>
      <div className="text-xs text-gray-400 space-y-0.5 font-mono mb-3">
        <p>付款方: {session.payer.slice(0, 10)}...{session.payer.slice(-4)}</p>
        <p>收款方: {session.payee.slice(0, 10)}...{session.payee.slice(-4)}</p>
        <p>Agent ID: {session.agent_id}</p>
      </div>
      {session.status === "active" && (
        <div className="space-y-2">
          {onFund && <div className="flex gap-2"><input type="text" placeholder="充值金额 (umsg)" value={fundAmount} onChange={(e) => setFundAmount(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border rounded" /><button onClick={() => { if (fundAmount) { onFund(session.session_id, fundAmount); setFundAmount(""); } }} className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded hover:bg-blue-700">充值</button></div>}
          <div className="flex gap-2">
            {onClose && <button onClick={() => onClose(session.session_id)} className="flex-1 px-3 py-1.5 text-sm bg-gray-100 text-gray-700 rounded hover:bg-gray-200">关闭</button>}
            {onDispute && <button onClick={() => setShowDispute(!showDispute)} className="flex-1 px-3 py-1.5 text-sm bg-red-50 text-red-700 rounded hover:bg-red-100">争议</button>}
          </div>
          {showDispute && onDispute && <div className="flex gap-2"><input type="text" placeholder="争议原因" value={disputeReason} onChange={(e) => setDisputeReason(e.target.value)} className="flex-1 px-3 py-1.5 text-sm border rounded" /><button onClick={() => { if (disputeReason) { onDispute(session.session_id, disputeReason); setDisputeReason(""); setShowDispute(false); } }} className="px-3 py-1.5 text-sm bg-red-600 text-white rounded hover:bg-red-700">提交</button></div>}
        </div>
      )}
    </div>
  );
}

8.8 AgentChat

// src/components/AgentChat.tsx
import React, { useState, useRef, useEffect } from "react";
import { defaultAgentApi } from "../lib/agent-api";
import type { AgentQueryResponse } from "../types/agent";

interface Message { role: "user" | "agent"; content: string; timestamp: Date; metadata?: { model?: string; processing_time_ms?: number; tokens_used?: number; }; }

export function AgentChat({ agentId, agentName }: { agentId: string; agentName?: string }) {
  const [messages, setMessages] = useState<Message[]>([{ role: "agent", content: `你好!我是 ${agentName || agentId},有什么可以帮助你的?`, timestamp: new Date() }]);
  const [input, setInput] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]);

  const handleSend = async () => {
    if (!input.trim() || loading) return;
    const userMessage: Message = { role: "user", content: input.trim(), timestamp: new Date() };
    setMessages((prev) => [...prev, userMessage]); setInput(""); setLoading(true); setError(null);
    try {
      const response: AgentQueryResponse = await defaultAgentApi.agentQuery({ agent_id: agentId, query: userMessage.content });
      setMessages((prev) => [...prev, { role: "agent", content: response.response, timestamp: new Date(), metadata: response.metadata }]);
    } catch (err) {
      const msg = err instanceof Error ? err.message : "查询失败";
      setError(msg);
      setMessages((prev) => [...prev, { role: "agent", content: `抱歉,发生了错误: ${msg}`, timestamp: new Date() }]);
    } finally { setLoading(false); }
  };

  const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } };

  return (
    <div className="flex flex-col h-[500px] border rounded-lg bg-white">
      <div className="px-4 py-3 border-b bg-gray-50 rounded-t-lg">
        <h3 className="font-semibold text-gray-900">{agentName || agentId}</h3>
        <p className="text-xs text-gray-500">Agent ID: {agentId}</p>
      </div>
      <div className="flex-1 overflow-y-auto p-4 space-y-3">
        {messages.map((msg, i) => (
          <div key={i} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
            <div className={`max-w-[80%] rounded-lg px-3 py-2 ${msg.role === "user" ? "bg-blue-600 text-white" : "bg-gray-100 text-gray-900"}`}>
              <p className="text-sm whitespace-pre-wrap">{msg.content}</p>
              {msg.metadata && <div className="mt-1 flex gap-2 text-xs opacity-70">{msg.metadata.model && <span>{msg.metadata.model}</span>}{msg.metadata.processing_time_ms && <span>{msg.metadata.processing_time_ms}ms</span>}{msg.metadata.tokens_used && <span>{msg.metadata.tokens_used} tokens</span>}</div>}
            </div>
          </div>
        ))}
        {loading && <div className="flex justify-start"><div className="bg-gray-100 rounded-lg px-3 py-2"><div className="flex gap-1"><span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" /><span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-100" /><span className="w-2 h-2 bg-gray-400 rounded-full animate-bounce delay-200" /></div></div></div>}
        <div ref={messagesEndRef} />
      </div>
      {error && <div className="px-4 py-2 bg-red-50 border-t border-red-200"><p className="text-xs text-red-600">{error}</p></div>}
      <div className="p-3 border-t">
        <div className="flex gap-2">
          <input type="text" placeholder="输入你的问题..." value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} disabled={loading} className="flex-1 px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" />
          <button onClick={handleSend} disabled={loading || !input.trim()} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">发送</button>
        </div>
      </div>
    </div>
  );
}

8.9 ConstitutionViewer

// src/components/ConstitutionViewer.tsx
import React from "react";
import type { ConstitutionResponse, Rule } from "../lib/queries/constitution";

export function ConstitutionViewer({ constitution, rules, loading, error }: { constitution: ConstitutionResponse | null; rules: Rule[]; loading: boolean; error?: string | null }) {
  if (loading) return <div className="space-y-4 animate-pulse"><div className="h-6 w-48 bg-gray-200 rounded" /><div className="h-4 w-32 bg-gray-200 rounded" /><div className="space-y-2 mt-4">{Array.from({ length: 4 }).map((_, i) => (<div key={i} className="h-16 bg-gray-200 rounded" />))}</div></div>;
  if (error) return <div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">{error}</div>;
  if (!constitution) return <div className="text-center py-12 text-gray-500">未加载宪章数据</div>;
  return (
    <div className="space-y-4">
      <div className="bg-white border rounded-lg p-4">
        <div className="flex items-center justify-between mb-2">
          <h3 className="font-semibold text-gray-900">AI Agent 宪章</h3>
          <span className={`px-2 py-0.5 text-xs rounded-full ${constitution.active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-500"}`}>{constitution.active ? "生效中" : "已停用"}</span>
        </div>
        <p className="text-xs text-gray-500 font-mono mb-2">版本: {constitution.version}</p>
        <p className="text-xs text-gray-400 font-mono truncate">哈希: {constitution.constitution_hash}</p>
        {constitution.constitution && <div className="mt-3 p-3 bg-gray-50 rounded text-sm text-gray-700 whitespace-pre-wrap max-h-48 overflow-y-auto">{constitution.constitution}</div>}
      </div>
      <div><h4 className="text-sm font-semibold text-gray-700 mb-2">策略规则 ({rules.length})</h4>
        <div className="space-y-2">{rules.map((rule, i) => (
          <div key={i} className="bg-white border rounded-lg p-3 flex items-start justify-between">
            <div className="flex-1 min-w-0">
              <div className="flex items-center gap-2 mb-1">
                <span className="font-mono text-sm font-medium text-gray-900">{rule.action}</span>
                <span className={`text-xs px-1.5 py-0.5 rounded ${rule.allowed ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"}`}>{rule.allowed ? "允许" : "禁止"}</span>
                <span className="text-xs text-gray-400">风险: {rule.risk_tier}</span>
              </div>
              {rule.required_controls.length > 0 && <div className="flex flex-wrap gap-1 mt-1">{rule.required_controls.map((ctrl) => (<span key={ctrl} className="text-xs bg-yellow-50 text-yellow-700 px-1.5 py-0.5 rounded">{ctrl}</span>))}</div>}
              {rule.max_amount && <p className="text-xs text-gray-500 mt-1">最大金额: {rule.max_amount} umsg</p>}
            </div>
          </div>
        ))}</div>
      </div>
    </div>
  );
}

九、Hooks 封装

9.1 useCosmWasmClient

// src/hooks/useCosmWasmClient.ts
import { useState, useEffect, useCallback } from "react";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";

export function useCosmWasmClient() {
  const [client, setClient] = useState<CosmWasmClient | null>(null);
  const [error, setError] = useState<string | null>(null);
  const connect = useCallback(async () => {
    try { setError(null); setClient(await CosmWasmClient.connect("http://localhost:26657")); }
    catch (err) { setError(err instanceof Error ? err.message : "连接 RPC 失败"); setClient(null); }
  }, []);
  useEffect(() => { connect(); return () => { client?.disconnect(); }; }, []);
  return { client, isReady: !!client, error, reconnect: connect };
}

9.2 useSigningClient

// src/hooks/useSigningClient.ts
import { useState, useCallback } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { OfflineSigner } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";

export function useSigningClient() {
  const [client, setClient] = useState<SigningCosmWasmClient | null>(null);
  const [error, setError] = useState<string | null>(null);
  const init = useCallback(async (signer: OfflineSigner) => {
    try {
      setError(null);
      setClient(await SigningCosmWasmClient.connectWithSigner("http://localhost:26657", signer, { gasPrice: GasPrice.fromString("1000000000attoMSG") }));
    } catch (err) { setError(err instanceof Error ? err.message : "创建签名客户端失败"); }
  }, []);
  const reset = useCallback(() => { client?.disconnect(); setClient(null); setError(null); }, [client]);
  return { client, isReady: !!client, error, init, reset };
}

9.3 useAgent

// src/hooks/useAgent.ts
import { useState, useEffect, useCallback } from "react";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type { AgentResponse } from "../lib/queries/agent";

const RPC_URL = "http://localhost:26657";

export function useAgent(registryAddress: string, agentId: string | null) {
  const [agent, setAgent] = useState<AgentResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const fetchAgent = useCallback(async () => {
    if (!registryAddress || !agentId) { setAgent(null); return; }
    setLoading(true); setError(null);
    try {
      const client = await CosmWasmClient.connect(RPC_URL);
      const result = await client.queryContractSmart(registryAddress, { get_agent: { agent_id: agentId } });
      setAgent(result.agent);
    } catch (err) { setError(err instanceof Error ? err.message : "查询 Agent 失败"); setAgent(null); }
    finally { setLoading(false); }
  }, [registryAddress, agentId]);
  useEffect(() => { fetchAgent(); }, [fetchAgent]);
  return { agent, loading, error, refetch: fetchAgent };
}

export function useAgentsByOwner(registryAddress: string, owner: string | null) {
  const [agents, setAgents] = useState<AgentResponse[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const fetchAgents = useCallback(async () => {
    if (!registryAddress || !owner) return;
    setLoading(true); setError(null);
    try {
      const client = await CosmWasmClient.connect(RPC_URL);
      const result = await client.queryContractSmart(registryAddress, { get_agents_by_owner: { owner } });
      setAgents(result.agents || []);
    } catch (err) { setError(err instanceof Error ? err.message : "查询 Agent 列表失败"); }
    finally { setLoading(false); }
  }, [registryAddress, owner]);
  useEffect(() => { fetchAgents(); }, [fetchAgents]);
  return { agents, loading, error, refetch: fetchAgents };
}

9.4 useDID

// src/hooks/useDID.ts
import { useState, useEffect, useCallback } from "react";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type { DIDResolutionResponse } from "../lib/queries/did";

const RPC_URL = "http://localhost:26657";

export function useDID(registryAddress: string, did: string | null) {
  const [didDocument, setDidDocument] = useState<DIDResolutionResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const resolveDID = useCallback(async () => {
    if (!registryAddress || !did) { setDidDocument(null); return; }
    setLoading(true); setError(null);
    try {
      const client = await CosmWasmClient.connect(RPC_URL);
      const result = await client.queryContractSmart(registryAddress, { resolve_did: { did } });
      setDidDocument(result);
    } catch (err) { setError(err instanceof Error ? err.message : "解析 DID 失败"); setDidDocument(null); }
    finally { setLoading(false); }
  }, [registryAddress, did]);
  useEffect(() => { resolveDID(); }, [resolveDID]);
  return { didDocument, loading, error, refetch: resolveDID };
}

export function useDIDsByController(registryAddress: string, controller: string | null) {
  const [dids, setDids] = useState<DIDResolutionResponse[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const fetchDIDs = useCallback(async () => {
    if (!registryAddress || !controller) return;
    setLoading(true); setError(null);
    try {
      const client = await CosmWasmClient.connect(RPC_URL);
      const result = await client.queryContractSmart(registryAddress, { list_dids_by_controller: { controller } });
      setDids(result.dids || []);
    } catch (err) { setError(err instanceof Error ? err.message : "查询 DID 列表失败"); }
    finally { setLoading(false); }
  }, [registryAddress, controller]);
  useEffect(() => { fetchDIDs(); }, [fetchDIDs]);
  return { dids, loading, error, refetch: fetchDIDs };
}

9.5 usePayment

// src/hooks/usePayment.ts
import { useState, useEffect, useCallback } from "react";
import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import type { SessionResponse } from "../lib/queries/payment";

const RPC_URL = "http://localhost:26657";

export function usePayment(paymentAddress: string, agentId: string | null) {
  const [sessions, setSessions] = useState<SessionResponse[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const fetchSessions = useCallback(async () => {
    if (!paymentAddress || !agentId) return;
    setLoading(true); setError(null);
    try {
      const client = await CosmWasmClient.connect(RPC_URL);
      const result = await client.queryContractSmart(paymentAddress, { get_agent_sessions: { agent_id: agentId } });
      setSessions(result.sessions || []);
    } catch (err) { setError(err instanceof Error ? err.message : "查询支付会话失败"); }
    finally { setLoading(false); }
  }, [paymentAddress, agentId]);
  useEffect(() => { fetchSessions(); }, [fetchSessions]);
  return { sessions, loading, error, refetch: fetchSessions };
}

9.6 useAgentExecute

// src/hooks/useAgentExecute.ts
import { useState, useCallback } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";

export function useAgentExecute(client: SigningCosmWasmClient | null, sender: string) {
  const [txHash, setTxHash] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const execute = useCallback(async (contractAddress: string, executeMsg: Record<string, unknown>): Promise<string> => {
    if (!client) throw new Error("签名客户端未初始化");
    setLoading(true); setError(null); setTxHash(null);
    try {
      const result = await client.execute(sender, contractAddress, executeMsg, "auto");
      setTxHash(result.transactionHash);
      return result.transactionHash;
    } catch (err) {
      const msg = err instanceof Error ? err.message : "交易执行失败";
      setError(msg); throw err;
    } finally { setLoading(false); }
  }, [client, sender]);
  const reset = useCallback(() => { setTxHash(null); setError(null); setLoading(false); }, []);
  return { execute, txHash, loading, error, reset };
}

9.7 useEvents

// src/hooks/useEvents.ts
import { useState, useEffect, useCallback, useRef } from "react";
import { ChainEventSubscriber, AgentEventSubscriber, ChainEvent, AgentWsEvent } from "../lib/events";

export function useChainEvents(rpcUrl?: string) {
  const [latestBlock, setLatestBlock] = useState<ChainEvent | null>(null);
  const [latestTx, setLatestTx] = useState<ChainEvent | null>(null);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);
  useEffect(() => {
    const subscriber = new ChainEventSubscriber(rpcUrl);
    subscriber.on("connected", () => setIsConnected(true));
    subscriber.on("disconnected", () => setIsConnected(false));
    subscriber.on("error", (err: Error) => setError(err.message || "WebSocket 错误"));
    subscriber.on("NewBlock", (event: ChainEvent) => setLatestBlock(event));
    subscriber.on("Tx", (event: ChainEvent) => setLatestTx(event));
    subscriber.connect();
    const blockSubId = subscriber.subscribeNewBlocks();
    const txSubId = subscriber.subscribeTx();
    return () => { subscriber.unsubscribe(blockSubId); subscriber.unsubscribe(txSubId); subscriber.disconnect(); };
  }, [rpcUrl]);
  return { latestBlock, latestTx, isConnected, error };
}

export function useAgentEvents(config: { baseUrl: string; eventTypes: string[]; filter?: Record<string, string> }) {
  const [events, setEvents] = useState<AgentWsEvent[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const subscriberRef = useRef<AgentEventSubscriber | null>(null);
  const connect = useCallback(() => {
    const subscriber = new AgentEventSubscriber(config);
    subscriberRef.current = subscriber;
    subscriber.on("connected", () => setIsConnected(true));
    subscriber.on("disconnected", () => setIsConnected(false));
    subscriber.on("error", (err: unknown) => setError(err instanceof Error ? err.message : "Agent 事件错误"));
    subscriber.on("event", (event: AgentWsEvent) => setEvents((prev) => [event, ...prev].slice(0, 100)));
    subscriber.connect();
  }, [config]);
  const disconnect = useCallback(() => { subscriberRef.current?.disconnect(); subscriberRef.current = null; setIsConnected(false); }, []);
  useEffect(() => () => subscriberRef.current?.disconnect(), []);
  return { events, isConnected, error, connect, disconnect };
}

十、事件监听

10.1 WebSocket 事件管理

// src/lib/events.ts
import { EventEmitter } from "events";

export interface ChainEvent { type: "NewBlock" | "Tx" | "ValidatorSetUpdates" | "NewBlockHeader"; data: Record<string, unknown>; height?: number; }
export interface AgentWsEvent { type: string; data: Record<string, unknown>; timestamp: number; }

export class ChainEventSubscriber extends EventEmitter {
  private ws: WebSocket | null = null;
  private url: string;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 3000;
  private subscriptions: Map<string, string> = new Map();

  constructor(rpcUrl: string = "http://localhost:26657") {
    super();
    this.url = rpcUrl.replace(/^http/, "ws") + "/websocket";
  }

  connect(): void {
    try {
      this.ws = new WebSocket(this.url);
      this.ws.onopen = () => { this.reconnectAttempts = 0; this.emit("connected"); this.subscriptions.forEach((query, id) => this.sendSubscribe(id, query)); };
      this.ws.onmessage = (event: MessageEvent) => {
        try {
          const msg = JSON.parse(event.data);
          if (msg.result?.data?.value) {
            const value = msg.result.data.value;
            if (value.block) {
              const height = parseInt(value.block.header.height);
              this.emit("NewBlock", { type: "NewBlock", data: { height, txCount: value.block.data.txs?.length || 0, time: value.block.header.time }, height } as ChainEvent);
            }
            if (value.TxResult) this.emit("Tx", { type: "Tx", data: value.TxResult } as ChainEvent);
          }
        } catch { /* ignore */ }
      };
      this.ws.onclose = () => { this.emit("disconnected"); this.scheduleReconnect(); };
      this.ws.onerror = () => this.emit("error", new Error("WebSocket 连接错误"));
    } catch (err) { this.emit("error", err); this.scheduleReconnect(); }
  }

  private sendSubscribe(id: string, query: string): void {
    if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify({ jsonrpc: "2.0", method: "subscribe", id, params: { query } }));
  }

  subscribeNewBlocks(): string { const id = "nb-" + Date.now(); this.subscriptions.set(id, "tm.event='NewBlock'"); this.sendSubscribe(id, "tm.event='NewBlock'"); return id; }
  subscribeTx(): string { const id = "tx-" + Date.now(); this.subscriptions.set(id, "tm.event='Tx'"); this.sendSubscribe(id, "tm.event='Tx'"); return id; }
  subscribeValidatorUpdates(): string { const id = "vu-" + Date.now(); this.subscriptions.set(id, "tm.event='ValidatorSetUpdates'"); this.sendSubscribe(id, "tm.event='ValidatorSetUpdates'"); return id; }

  unsubscribe(id: string): void {
    const query = this.subscriptions.get(id);
    if (this.ws?.readyState === WebSocket.OPEN && query) this.ws.send(JSON.stringify({ jsonrpc: "2.0", method: "unsubscribe", id, params: { query } }));
    this.subscriptions.delete(id);
  }

  private scheduleReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.emit("max_reconnect_exceeded"); return; }
    this.reconnectAttempts++;
    setTimeout(() => this.connect(), this.reconnectDelay * Math.min(this.reconnectAttempts, 5));
  }

  disconnect(): void { this.subscriptions.clear(); this.ws?.close(); this.ws = null; }
}

export class AgentEventSubscriber extends EventEmitter {
  private ws: WebSocket | null = null;
  private baseUrl: string;
  private eventTypes: string[];
  private filter: Record<string, string>;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;
  private reconnectDelay = 3000;

  constructor(config: { baseUrl: string; eventTypes: string[]; filter?: Record<string, string> }) {
    super();
    this.baseUrl = config.baseUrl;
    this.eventTypes = config.eventTypes;
    this.filter = config.filter || {};
  }

  connect(): void {
    const wsUrl = this.baseUrl.replace(/^http/, "ws");
    try {
      this.ws = new WebSocket(`${wsUrl}/api/v1/agent/events/subscribe`);
      this.ws.onopen = () => { this.reconnectAttempts = 0; this.ws!.send(JSON.stringify({ event_types: this.eventTypes, filter: this.filter })); this.emit("connected"); };
      this.ws.onmessage = (event: MessageEvent) => { try { const msg: AgentWsEvent = JSON.parse(event.data); this.emit(msg.type, msg); this.emit("event", msg); } catch { /* ignore */ } };
      this.ws.onclose = () => { this.emit("disconnected"); this.scheduleReconnect(); };
      this.ws.onerror = (err) => this.emit("error", err);
    } catch (err) { this.emit("error", err); this.scheduleReconnect(); }
  }

  private scheduleReconnect(): void {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) { this.emit("max_reconnect_exceeded"); return; }
    this.reconnectAttempts++;
    setTimeout(() => this.connect(), this.reconnectDelay * Math.min(this.reconnectAttempts, 5));
  }

  disconnect(): void { this.ws?.close(); this.ws = null; }
}

十一、实用工具函数

// src/lib/utils.ts
import { Decimal } from "@cosmjs/math";
import { fromBech32, toBech32 } from "@cosmjs/encoding";

const MSG_DECIMALS = 18;

export function formatUmsg(amount: string | number | undefined): string {
  if (!amount) return "0";
  try { return Decimal.fromAtomics(amount.toString(), MSG_DECIMALS).toString(); }
  catch { return String(amount); }
}

export function toUmsg(amount: string): string {
  try { return Decimal.fromUserInput(amount, MSG_DECIMALS).atomics; }
  catch { return amount; }
}

export function shortenAddress(address: string, prefixLen: number = 8, suffixLen: number = 6): string {
  if (address.length <= prefixLen + suffixLen) return address;
  return `${address.slice(0, prefixLen)}...${address.slice(-suffixLen)}`;
}

export function isValidMsgAddress(address: string): boolean {
  try { const decoded = fromBech32(address); return decoded.prefix === "msg" && decoded.data.length === 20; }
  catch { return false; }
}

export function isValidDID(did: string): boolean {
  return /^did:msg:[a-zA-Z0-9._-]+:[a-zA-Z0-9._-]+$/.test(did);
}

export function isValidAgentId(agentId: string): boolean {
  return /^[a-zA-Z0-9_-]{3,64}$/.test(agentId);
}

export function convertAddressPrefix(address: string, newPrefix: string): string {
  return toBech32(newPrefix, fromBech32(address).data);
}

export function addressToHex(address: string): string {
  return Buffer.from(fromBech32(address).data).toString("hex");
}

export function copyToClipboard(text: string): Promise<void> {
  return navigator.clipboard.writeText(text);
}

// 计算 Gas 费用
export function estimateMsgFee(gasLimit: number, gasPrice: number = 1000000000): string {
  return String(Math.ceil(gasLimit * gasPrice));
}

// 时间工具
export function formatBlockTime(timestamp: string | number): string {
  const date = new Date(typeof timestamp === "number" ? timestamp * 1000 : timestamp);
  return date.toLocaleString("zh-CN");
}

export function timeAgo(timestamp: number): string {
  const seconds = Math.floor((Date.now() - timestamp * 1000) / 1000);
  if (seconds < 60) return `${seconds}秒前`;
  if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟前`;
  if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时前`;
  return `${Math.floor(seconds / 86400)}天前`;
}

十二、完整示例项目

12.1 App.tsx 根组件

// src/App.tsx
import React from "react";
import { BrowserRouter, Routes, Route, Link, Navigate } from "react-router-dom";
import { WalletProvider } from "./providers/WalletProvider";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { Home } from "./pages/Home";
import { Agents } from "./pages/Agents";
import { DIDPage } from "./pages/DIDPage";
import { PaymentPage } from "./pages/PaymentPage";
import { ConstitutionPage } from "./pages/ConstitutionPage";

function Navbar() {
  return (
    <nav className="bg-white border-b shadow-sm">
      <div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
        <div className="flex items-center gap-6">
          <Link to="/" className="text-xl font-bold text-msg-900">MSG Chain</Link>
          <div className="hidden md:flex gap-4">
            <Link to="/agents" className="text-sm text-gray-600 hover:text-msg-600">Agent 管理</Link>
            <Link to="/dids" className="text-sm text-gray-600 hover:text-msg-600">DID 管理</Link>
            <Link to="/payments" className="text-sm text-gray-600 hover:text-msg-600">支付管理</Link>
            <Link to="/constitution" className="text-sm text-gray-600 hover:text-msg-600">宪章查看</Link>
          </div>
        </div>
      </div>
    </nav>
  );
}

export default function App() {
  return (
    <ErrorBoundary>
      <WalletProvider>
        <BrowserRouter>
          <div className="min-h-screen bg-gray-50">
            <Navbar />
            <main className="max-w-7xl mx-auto px-4 py-6">
              <ErrorBoundary>
                <Routes>
                  <Route path="/" element={<Home />} />
                  <Route path="/agents" element={<Agents />} />
                  <Route path="/dids" element={<DIDPage />} />
                  <Route path="/payments" element={<PaymentPage />} />
                  <Route path="/constitution" element={<ConstitutionPage />} />
                  <Route path="*" element={<Navigate to="/" replace />} />
                </Routes>
              </ErrorBoundary>
            </main>
          </div>
        </BrowserRouter>
      </WalletProvider>
    </ErrorBoundary>
  );
}

12.2 首页 Home

// src/pages/Home.tsx
import React, { useEffect, useState } from "react";
import { useWallet } from "../hooks/useWallet";
import { WalletConnectButton } from "../components/WalletConnectButton";
import { AccountBalance } from "../components/AccountBalance";
import { AgentCard, AgentList } from "../components/AgentCard";
import { useAgentsByOwner } from "../hooks/useAgent";
import { formatUmsg, shortenAddress } from "../lib/utils";
import { getChainInfo } from "../lib/cosmwasm-client";

const REGISTRY_ADDRESS = "msg1...agentRegistryAddress";
const PAYMENT_ADDRESS = "msg1...agentPaymentAddress";

export function Home() {
  const { address, isConnected } = useWallet();
  const { agents, loading: agentsLoading } = useAgentsByOwner(REGISTRY_ADDRESS, address);
  const [chainInfo, setChainInfo] = useState<{ chainId: string; height: number } | null>(null);

  useEffect(() => {
    getChainInfo().then(setChainInfo).catch(console.error);
  }, []);

  return (
    <div className="space-y-6">
      <div className="bg-gradient-to-r from-msg-600 to-msg-900 rounded-xl p-8 text-white">
        <h1 className="text-3xl font-bold mb-2">AI Agent 管理平台</h1>
        <p className="text-blue-100 mb-4">基于 MSG Chain 的去中心化 AI Agent 管理面板</p>
        {chainInfo && (
          <div className="flex gap-4 text-sm text-blue-200">
            <span>Chain: {chainInfo.chainId}</span>
            <span>区块高度: #{chainInfo.height}</span>
          </div>
        )}
      </div>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        <div className="md:col-span-1 space-y-4">
          <WalletConnectButton />
          {isConnected && address && <AccountBalance />}
        </div>
        <div className="md:col-span-2">
          {isConnected ? (
            <div>
              <h2 className="text-lg font-semibold text-gray-900 mb-4">我的 Agent</h2>
              <AgentList agents={agents} loading={agentsLoading} emptyMessage="还没有注册 Agent,前往 Agent 管理页面注册" />
            </div>
          ) : (
            <div className="bg-white border rounded-xl p-8 text-center">
              <div className="text-5xl mb-4">🤖</div>
              <h2 className="text-xl font-semibold text-gray-700 mb-2">连接钱包开始使用</h2>
              <p className="text-gray-500">连接 Keplr 钱包后,您可以管理 AI Agent、创建 DID、处理支付</p>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

12.3 Agent 管理页面

// src/pages/Agents.tsx
import React, { useState, useCallback } from "react";
import { useWallet } from "../hooks/useWallet";
import { WalletConnectButton } from "../components/WalletConnectButton";
import { AgentList } from "../components/AgentCard";
import { AgentChat } from "../components/AgentChat";
import { useAgentsByOwner } from "../hooks/useAgent";
import { useAgentExecute } from "../hooks/useAgentExecute";
import { useFormActions } from "../hooks/useFormActions";
import { LoadingSpinner, ErrorMessage } from "../components/ui";
import { ErrorBoundary } from "../components/ErrorBoundary";

const REGISTRY_ADDRESS = "msg1...agentRegistryAddress";

interface AgentFormValues {
  agent_id: string;
  name: string;
  owner: string;
  capabilities: string;
  endpoint: string;
  price: string;
}

export function Agents() {
  const { address, isConnected, getSigningClient } = useWallet();
  const { agents, loading, error: listError, refetch } = useAgentsByOwner(REGISTRY_ADDRESS, address);
  const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
  const [showRegister, setShowRegister] = useState(false);
  const [txResult, setTxResult] = useState<string | null>(null);

  const { values, errors, isSubmitting, submitError, setValue, handleSubmit, reset } = useFormActions<AgentFormValues>({
    agent_id: "", name: "", owner: address || "", capabilities: "", endpoint: "", price: "",
  });

  const doRegister = useCallback(async (formValues: AgentFormValues) => {
    const client = await getSigningClient();
    if (!address) throw new Error("钱包未连接");
    const result = await client.execute(address, REGISTRY_ADDRESS, {
      register_agent: {
        agent_id: formValues.agent_id,
        name: formValues.name || undefined,
        owner: formValues.owner,
        capabilities: formValues.capabilities.split(",").map((s: string) => s.trim()).filter(Boolean),
        endpoint: formValues.endpoint || undefined,
        price_model: formValues.price ? { price: formValues.price, currency: "umsg", price_type: "per_task" } : undefined,
      },
    }, "auto");
    setTxResult(result.transactionHash);
    setShowRegister(false);
    reset();
    refetch();
  }, [address, getSigningClient, refetch, reset]);

  return (
    <ErrorBoundary>
      <div className="space-y-6">
        <div className="flex items-center justify-between">
          <div>
            <h1 className="text-2xl font-bold text-gray-900">Agent 管理</h1>
            <p className="text-gray-500 text-sm mt-1">注册、管理和与 AI Agent 交互</p>
          </div>
          <div className="flex gap-3">
            {isConnected && <button onClick={() => setShowRegister(!showRegister)} className="px-4 py-2 bg-msg-600 text-white rounded-lg hover:bg-msg-700 text-sm">{showRegister ? "取消" : "注册 Agent"}</button>}
          </div>
        </div>

        {!isConnected && <WalletConnectButton />}

        {submitError && <ErrorMessage message={submitError} />}
        {txResult && <div className="p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm">注册成功! 交易哈希: {txResult}</div>}

        {showRegister && (
          <div className="bg-white border rounded-xl p-6">
            <h2 className="text-lg font-semibold mb-4">注册新 Agent</h2>
            <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">Agent ID *</label>
                <input type="text" value={values.agent_id} onChange={(e) => setValue("agent_id", e.target.value)} className="w-full px-3 py-2 border rounded-lg text-sm" placeholder="my-agent-001" />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">名称</label>
                <input type="text" value={values.name} onChange={(e) => setValue("name", e.target.value)} className="w-full px-3 py-2 border rounded-lg text-sm" placeholder="Agent 名称" />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">能力 (逗号分隔) *</label>
                <input type="text" value={values.capabilities} onChange={(e) => setValue("capabilities", e.target.value)} className="w-full px-3 py-2 border rounded-lg text-sm" placeholder="text-generation,code-review" />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">服务端点</label>
                <input type="text" value={values.endpoint} onChange={(e) => setValue("endpoint", e.target.value)} className="w-full px-3 py-2 border rounded-lg text-sm" placeholder="https://..." />
              </div>
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">价格 (umsg)</label>
                <input type="text" value={values.price} onChange={(e) => setValue("price", e.target.value)} className="w-full px-3 py-2 border rounded-lg text-sm" placeholder="50000" />
              </div>
            </div>
            <div className="mt-4 flex gap-2">
              <button onClick={handleSubmit(doRegister)} disabled={isSubmitting} className="px-6 py-2 bg-msg-600 text-white rounded-lg hover:bg-msg-700 disabled:opacity-50">
                {isSubmitting ? "提交中..." : "注册"}
              </button>
              <button onClick={() => { reset(); setShowRegister(false); }} className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200">取消</button>
            </div>
          </div>
        )}

        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          <div>
            <h2 className="text-lg font-semibold text-gray-900 mb-4">我的 Agent</h2>
            {listError ? <ErrorMessage message={listError} onRetry={refetch} /> :
              <AgentList agents={agents} loading={loading} onSelect={(id) => setSelectedAgentId(id)} />}
          </div>
          <div>
            <h2 className="text-lg font-semibold text-gray-900 mb-4">Agent 对话</h2>
            {selectedAgentId ? <AgentChat agentId={selectedAgentId} /> :
              <div className="border rounded-lg p-8 text-center text-gray-500 bg-white">
                选择一个 Agent 开始对话
              </div>}
          </div>
        </div>
      </div>
    </ErrorBoundary>
  );
}

12.4 DID 管理页面

// src/pages/DIDPage.tsx
import React, { useState, useCallback } from "react";
import { useWallet } from "../hooks/useWallet";
import { WalletConnectButton } from "../components/WalletConnectButton";
import { DIDList } from "../components/DIDList";
import { useDIDsByController } from "../hooks/useDID";
import { useAgentExecute } from "../hooks/useAgentExecute";
import { ErrorMessage } from "../components/ui";
import { ErrorBoundary } from "../components/ErrorBoundary";

const REGISTRY_ADDRESS = "msg1...didRegistryAddress";

export function DIDPage() {
  const { address, isConnected, getSigningClient } = useWallet();
  const { dids, loading, error, refetch } = useDIDsByController(REGISTRY_ADDRESS, address);
  const [txResult, setTxResult] = useState<string | null>(null);

  const handleDeactivate = useCallback(async (did: string) => {
    const client = await getSigningClient();
    if (!address) return;
    try {
      const result = await client.execute(address, REGISTRY_ADDRESS, {
        deactivate_did: { did, signature: "0x" },
      }, "auto");
      setTxResult(`DID 已停用: ${result.transactionHash}`);
      refetch();
    } catch (err) { setTxResult(`错误: ${err instanceof Error ? err.message : "操作失败"}`); }
  }, [address, getSigningClient, refetch]);

  return (
    <ErrorBoundary>
      <div className="space-y-6">
        <div><h1 className="text-2xl font-bold text-gray-900">DID 管理</h1><p className="text-gray-500 text-sm mt-1">AI 去中心化身份管理</p></div>
        {!isConnected && <WalletConnectButton />}
        {txResult && <div className="p-3 bg-blue-50 border border-blue-200 rounded text-sm">{txResult}</div>}
        {error ? <ErrorMessage message={error} onRetry={refetch} /> :
          <DIDList dids={dids} loading={loading} onResolve={(did) => refetch()} onDeactivate={handleDeactivate} />}
      </div>
    </ErrorBoundary>
  );
}

12.5 Payment 支付页面

// src/pages/PaymentPage.tsx
import React, { useState, useCallback } from "react";
import { useWallet } from "../hooks/useWallet";
import { WalletConnectButton } from "../components/WalletConnectButton";
import { PaymentSession } from "../components/PaymentSession";
import { usePayment } from "../hooks/usePayment";
import { ErrorMessage, LoadingSpinner } from "../components/ui";
import { ErrorBoundary } from "../components/ErrorBoundary";

const PAYMENT_ADDRESS = "msg1...agentPaymentAddress";

export function PaymentPage() {
  const { address, isConnected, getSigningClient } = useWallet();
  const [agentId, setAgentId] = useState("");
  const { sessions, loading, error, refetch } = usePayment(PAYMENT_ADDRESS, agentId || null);
  const [txResult, setTxResult] = useState<string | null>(null);

  const handleFund = useCallback(async (sessionId: string, amount: string) => {
    const client = await getSigningClient();
    if (!address) return;
    try {
      const result = await client.execute(address, PAYMENT_ADDRESS, { fund_session: { session_id: sessionId, amount } }, "auto");
      setTxResult(`充值成功: ${result.transactionHash}`);
      refetch();
    } catch (err) { setTxResult(`错误: ${err instanceof Error ? err.message : "充值失败"}`); }
  }, [address, getSigningClient, refetch]);

  const handleClose = useCallback(async (sessionId: string) => {
    const client = await getSigningClient();
    if (!address) return;
    try {
      const result = await client.execute(address, PAYMENT_ADDRESS, { close_session: { session_id: sessionId, close_receipt_hash: "0x" } }, "auto");
      setTxResult(`会话已关闭: ${result.transactionHash}`);
      refetch();
    } catch (err) { setTxResult(`错误: ${err instanceof Error ? err.message : "关闭失败"}`); }
  }, [address, getSigningClient, refetch]);

  return (
    <ErrorBoundary>
      <div className="space-y-6">
        <div><h1 className="text-2xl font-bold text-gray-900">支付管理</h1><p className="text-gray-500 text-sm mt-1">管理 AI Agent 支付会话</p></div>
        {!isConnected && <WalletConnectButton />}
        {txResult && <div className="p-3 bg-blue-50 border border-blue-200 rounded text-sm">{txResult}</div>}
        <div className="flex gap-2 max-w-md">
          <input type="text" placeholder="输入 Agent ID 查询支付会话" value={agentId} onChange={(e) => setAgentId(e.target.value)} className="flex-1 px-3 py-2 border rounded-lg text-sm" />
          <button onClick={refetch} className="px-4 py-2 bg-msg-600 text-white rounded-lg text-sm hover:bg-msg-700">查询</button>
        </div>
        {error ? <ErrorMessage message={error} onRetry={refetch} /> :
          loading ? <LoadingSpinner text="加载支付会话..." /> :
            !sessions.length ? <div className="text-center py-12 text-gray-500">暂无支付会话</div> :
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                {sessions.map((s) => <PaymentSession key={s.session_id} session={s} onFund={handleFund} onClose={handleClose} />)}
              </div>}
      </div>
    </ErrorBoundary>
  );
}

12.6 宪章查看页面

// src/pages/ConstitutionPage.tsx
import React, { useState, useCallback } from "react";
import { useWallet } from "../hooks/useWallet";
import { ConstitutionViewer } from "../components/ConstitutionViewer";
import { isActionAllowed } from "../lib/queries/constitution";
import { ErrorMessage } from "../components/ui";
import { ErrorBoundary } from "../components/ErrorBoundary";

const CONSTITUTION_ADDRESS = "msg1...constitutionAddress";

export function ConstitutionPage() {
  const [constitution, setConstitution] = useState<any>(null);
  const [rules, setRules] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [agentId, setAgentId] = useState("");
  const [action, setAction] = useState("");
  const [checkResult, setCheckResult] = useState<any>(null);

  const loadConstitution = useCallback(async () => {
    setLoading(true); setError(null);
    try {
      const { CosmWasmClient } = await import("@cosmjs/cosmwasm-stargate");
      const client = await CosmWasmClient.connect("http://localhost:26657");
      const [constResult, rulesResult] = await Promise.all([
        client.queryContractSmart(CONSTITUTION_ADDRESS, { get_constitution: {} }),
        client.queryContractSmart(CONSTITUTION_ADDRESS, { list_rules: {} }),
      ]);
      setConstitution(constResult);
      setRules(rulesResult.rules || []);
    } catch (err) { setError(err instanceof Error ? err.message : "加载宪章失败"); }
    finally { setLoading(false); }
  }, []);

  const handleCheckAction = useCallback(async () => {
    if (!agentId || !action) return;
    try {
      const result = await isActionAllowed(CONSTITUTION_ADDRESS, agentId, action, "low");
      setCheckResult(result);
    } catch (err) { setError(err instanceof Error ? err.message : "检查失败"); }
  }, [agentId, action]);

  return (
    <ErrorBoundary>
      <div className="space-y-6">
        <div className="flex items-center justify-between">
          <div><h1 className="text-2xl font-bold text-gray-900">宪章查看</h1><p className="text-gray-500 text-sm mt-1">AI Agent 行为规则与策略引擎</p></div>
          <button onClick={loadConstitution} disabled={loading} className="px-4 py-2 bg-msg-600 text-white rounded-lg text-sm hover:bg-msg-700">加载宪章</button>
        </div>

        {error && <ErrorMessage message={error} />}

        <div className="bg-white border rounded-xl p-6">
          <h2 className="text-lg font-semibold mb-4">动作检查</h2>
          <div className="flex gap-2">
            <input type="text" placeholder="Agent ID" value={agentId} onChange={(e) => setAgentId(e.target.value)} className="flex-1 px-3 py-2 border rounded-lg text-sm" />
            <input type="text" placeholder="动作 (如 transfer)" value={action} onChange={(e) => setAction(e.target.value)} className="flex-1 px-3 py-2 border rounded-lg text-sm" />
            <button onClick={handleCheckAction} className="px-4 py-2 bg-gray-100 text-gray-700 rounded-lg text-sm hover:bg-gray-200 whitespace-nowrap">检查</button>
          </div>
          {checkResult && (
            <div className={`mt-3 p-3 rounded-lg text-sm ${checkResult.allowed ? "bg-green-50 text-green-700" : "bg-red-50 text-red-700"}`}>
              <p>决策: {checkResult.decision}</p>
              {checkResult.reasons?.length > 0 && <p>原因: {checkResult.reasons.join(", ")}</p>}
              {checkResult.required_controls?.length > 0 && <p>需要审批: {checkResult.required_controls.join(", ")}</p>}
            </div>
          )}
        </div>

        <ConstitutionViewer constitution={constitution} rules={rules} loading={loading} error={error} />
      </div>
    </ErrorBoundary>
  );
}

十三、附录:TypeScript 类型定义

13.1 链配置类型

// src/types/chain.ts
export interface ChainConfig {
  chainId: string;
  chainNumberId: number;
  bech32Prefix: string;
  coinType: number;
  decimals: number;
  gasPriceSteps: { low: number; average: number; high: number };
  denom: string;
  blockTime: number;
  rpc: string;
  rest: string;
  agentApi: string;
}

export interface KeplrChainConfig {
  chainId: string;
  chainName: string;
  rpc: string;
  rest: string;
  bip44: { coinType: number };
  bech32Config: {
    bech32PrefixAccAddr: string;
    bech32PrefixAccPub: string;
    bech32PrefixValAddr: string;
    bech32PrefixValPub: string;
    bech32PrefixConsAddr: string;
    bech32PrefixConsPub: string;
  };
  currencies: Array<{ coinDenom: string; coinMinimalDenom: string; coinDecimals: number }>;
  feeCurrencies: Array<{
    coinDenom: string; coinMinimalDenom: string; coinDecimals: number;
    gasPriceStep: { low: number; average: number; high: number };
  }>;
  stakeCurrency: { coinDenom: string; coinMinimalDenom: string; coinDecimals: number };
  gasPriceStep: { low: number; average: number; high: number };
}

13.2 合约类型

// src/types/contracts.ts
export interface ResolveCanonicalResponse {
  address: string;
  canonical_key: string;
  exists: boolean;
}

export interface ContractInfo {
  address: string;
  code_id: number;
  creator: string;
  admin: string;
  label: string;
}

export interface CodeInfo {
  code_id: number;
  creator: string;
  data_hash: string;
}

13.3 全局事件类型

// src/types/events.ts
export type TendermintEventType = "NewBlock" | "NewBlockHeader" | "Tx" | "ValidatorSetUpdates";

export type AgentEventType =
  | "Transfer" | "ContractExecute" | "BlockProduced"
  | "ValidatorChange" | "AgentAction" | "PaymentSettled";

export interface EventSubscription {
  eventTypes: AgentEventType[];
  filter?: Record<string, string>;
}

export interface TransactionEvent {
  height: number;
  txHash: string;
  success: boolean;
  gasUsed: number;
  gasWanted: number;
  rawLog: string;
}

总结

本文档完整覆盖了 MSG Chain 前端 dApp 开发的各个方面,从钱包连接到合约交互,从事件监听到 Agent API 集成。主要要点:

  1. 钱包连接: 通过 Keplr + CosmJS 实现类似 MetaMask + ethers.js 的体验
  2. 合约查询: 使用 CosmWasmClient.queryContractSmart() 类似 ScopeKit 的合约查询模式
  3. 合约执行: 使用 SigningCosmWasmClient.execute() 进行交易签名和广播
  4. Agent API: 18 个 REST/WebSocket 端点的完整 TypeScript 客户端封装
  5. 事件监听: Tendermint WebSocket + Agent API WebSocket 双通道事件订阅
  6. React 集成: 完整的组件库 + Hooks 封装,覆盖加载/错误/空状态

所有代码基于 MSG Chain msg-chain-1 网络参数,可直接在本地开发环境 (http://localhost:26657) 运行。