MSG Chain 前端 dApp 接入指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
基于 React 18 + TypeScript 5 + CosmJS 的完整去中心化应用开发手册
适用 Chain ID:msg-chain-1| 适用网络:本地开发http://localhost:26657
目录
- 概述
- 项目初始化
- Keplr 钱包连接
- CosmJS 客户端封装
- 合约查询操作
- 合约执行操作
- Agent API 客户端
- React 组件库
- Hooks 封装
- 事件监听
- 实用工具函数
- 完整示例项目
- 附录: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 集成。主要要点:
- 钱包连接: 通过 Keplr + CosmJS 实现类似 MetaMask + ethers.js 的体验
- 合约查询: 使用
CosmWasmClient.queryContractSmart()类似 ScopeKit 的合约查询模式 - 合约执行: 使用
SigningCosmWasmClient.execute()进行交易签名和广播 - Agent API: 18 个 REST/WebSocket 端点的完整 TypeScript 客户端封装
- 事件监听: Tendermint WebSocket + Agent API WebSocket 双通道事件订阅
- React 集成: 完整的组件库 + Hooks 封装,覆盖加载/错误/空状态
所有代码基于 MSG Chain msg-chain-1 网络参数,可直接在本地开发环境 (http://localhost:26657) 运行。
