dApp Docs/移动端SDK集成指南
Development reference. Not independently verified for production.

MSG Chain 移动端 SDK 集成指南

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

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

版本: 1.0.0 | 链 ID: msg-chain-1 | 地址前缀: msg


目录


1. 概述

1.1 MSG Chain 移动端支持

MSG Chain 是基于 Cosmos SDK 构建的区块链网络,采用 Tendermint BFT 共识引擎,支持 CosmWasm 智能合约。移动端应用可以通过 RPC 端点与 MSG Chain 节点交互,实现钱包管理、交易签名、合约查询等核心功能。

链信息

参数 值
链 ID msg-chain-1
地址前缀 msg (bech32)
共识引擎 Tendermint BFT
智能合约 CosmWasm (wasmd v0.45+)
原生代币 MSG
最小单位 umsg (10⁻⁶ MSG)
coinType 118 (Cosmos)
RPC 端口 26657
REST API 1317

移动端集成的典型架构如下:

┌──────────────────────┐      ┌───────────────────────┐
│    Mobile DApp       │      │    Keplr Mobile        │
│  (React Native /     │◄────►│    (WalletConnect)     │
│   Flutter)           │      │    (私钥管理 / 签名)    │
└──────────┬───────────┘      └───────────────────────┘
           │                              │
           │ RPC                          │ RPC
           ▼                              ▼
┌────────────────────────────────────────────────────────┐
│               MSG Chain Full Node                       │
│              (msg-chain-1)                              │
└────────────────────────────────────────────────────────┘

1.2 React Native vs Flutter 技术选型

对比维度 React Native Flutter
开发语言 TypeScript / JavaScript Dart
核心 SDK CosmJS (@cosmjs/*) 官方维护 cosmwasm_flutter 社区驱动
钱包连接 WalletConnect v2 (官方 SDK) WalletConnect v2 (walletconnect_dart)
状态管理 Zustand / Redux / Jotai Riverpod / Bloc / Provider
跨端一致性 中 (依赖原生组件) 高 (自绘引擎 Skia)
热重载 Fast Refresh Hot Reload + Hot Restart
性能瓶颈 JS Bridge 通信开销 Dart FFI 直接调用
原生模块 Turbo Modules / JSI Method Channel / FFI
Cosmos 生态 成熟 (CosmJS 官方包) 发展初期
学习曲线 需 React 基础 需 Dart 基础
包体积 ~15MB (基础) ~20MB (基础)
社区资源 丰富 增长中
适合场景 轻量 DApp / DeFi NFT 市场 / 重交互应用

选型建议:

1.3 移动端钱包方案

方案 优点 缺点 适用场景
WalletConnect v2 + Keplr Mobile 标准协议,无需管理私钥 依赖 Keplr 安装 通用场景
内嵌 HD Wallet 完全自主控制 安全风险高 专用钱包 App
Social Recovery (MPC) 用户友好 实现复杂 新手友好钱包
Ledger 硬件钱包 最高安全性 移动端不便 大额资产管理

1.4 环境要求

组件 最低版本 推荐版本
Node.js 18.x 20.x LTS
React Native 0.73 0.76+
Flutter 3.16 3.24+
Dart 3.2 3.5+
Xcode 15.0 16.0+ (iOS 18)
Android Studio Hedgehog Ladybug+
Keplr Mobile 1.8.0 2.0+
WalletConnect v2.0 v2.16+
CosmJS 0.32 0.33+

1.5 项目初始化

# React Native (TypeScript)
npx @react-native-community/cli@latest init MSGDApp --template react-native-template-typescript
cd MSGDApp

# Flutter
flutter create --org com.msgchain msg_dapp --platforms ios,android
cd msg_dapp

1.6 目录结构建议

MobileDApp/
├── src/
│   ├── lib/          # 核心库 (客户端、签名、查询)
│   ├── hooks/        # React Hooks / Dart providers
│   ├── store/        # 状态管理
│   ├── components/   # UI 组件
│   ├── screens/      # 页面
│   ├── models/       # 数据模型
│   └── utils/        # 工具函数
├── ios/              # iOS 原生配置
├── android/          # Android 原生配置
├── assets/           # 静态资源
└── test/             # 测试

2. React Native 集成

2.1 依赖安装

npm install   @cosmjs/cosmwasm-stargate @cosmjs/stargate   @cosmjs/proto-signing @cosmjs/encoding   @cosmjs/crypto @cosmjs/math   @walletconnect/web3wallet @walletconnect/core   @walletconnect/types @walletconnect/utils   @react-native-async-storage/async-storage   react-native-get-random-values   react-native-biometrics   react-native-keychain   react-native-safe-area-context   react-native-svg   zustand
cd ios && pod install && cd ..

2.2 链配置

// src/config/chain.ts
export const MSG_CHAIN = {
  chainId: 'msg-chain-1',
  chainName: 'MSG Chain',
  rpcUrl: 'https://rpc.msg-chain.com',
  restUrl: 'https://api.msg-chain.com',
  prefix: 'msg',
  denom: 'umsg',
  decimals: 6,
  gasPrice: '1000000000',
  defaultGas: 200000,
  contractGas: 300000,
  coinType: 118,
  bip44: { coinType: 118 },
  gasPriceSteps: { low: 1000000000, average: 1000000000, high: 1000000000 },
} as const;

export function toAtomic(amount: string): string {
  const [int, dec = ''] = amount.split('.');
  const padded = dec.padEnd(MSG_CHAIN.decimals, '0');
  return int + padded.slice(0, MSG_CHAIN.decimals);
}

export function fromAtomic(amount: string): string {
  const padded = amount.padStart(MSG_CHAIN.decimals + 1, '0');
  const int = padded.slice(0, -MSG_CHAIN.decimals) || '0';
  const dec = padded.slice(-MSG_CHAIN.decimals);
  return `${int}.${dec}`;
}

2.3 CosmJS 只读客户端

// src/lib/CosmWasmClient.ts
import { CosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { StargateClient } from '@cosmjs/stargate';
import { MSG_CHAIN } from '../config/chain';

export class MSGQueryClient {
  private client: CosmWasmClient | null = null;
  private stargate: StargateClient | null = null;

  async connect(): Promise<void> {
    this.client = await CosmWasmClient.connect(MSG_CHAIN.rpcUrl);
    this.stargate = await StargateClient.connect(MSG_CHAIN.rpcUrl);
  }

  async getBalance(address: string): Promise<string> {
    if (!this.client) await this.connect();
    const coin = await this.client!.getBalance(address, MSG_CHAIN.denom);
    return fromAtomic(coin.amount);
  }

  async queryContractSmart<T>(
    contract: string, msg: Record<string, unknown>
  ): Promise<T> {
    if (!this.client) await this.connect();
    return this.client!.queryContractSmart(contract, msg);
  }

  async getBlockHeight(): Promise<number> {
    if (!this.stargate) await this.connect();
    const block = await this.stargate!.getBlock();
    return block.header.height;
  }

  async getTx(hash: string) {
    if (!this.stargate) await this.connect();
    const tx = await this.stargate!.getTx(hash);
    if (!tx) return null;
    return { hash: tx.hash, height: tx.height, code: tx.code, rawLog: tx.rawLog };
  }

  async getAccount(address: string) {
    if (!this.stargate) await this.connect();
    return this.stargate!.getAccount(address);
  }

  async simulate(signer: string, msgs: any[]) {
    if (!this.client) await this.connect();
    return this.client!.simulate(signer, msgs);
  }

  disconnect() { this.client?.disconnect(); this.stargate?.disconnect(); }
}

2.4 签名客户端

// src/lib/SigningClient.ts
import { SigningCosmWasmClient } from '@cosmjs/cosmwasm-stargate';
import { OfflineSigner, AccountData } from '@cosmjs/proto-signing';
import { GasPrice, calculateFee, coins } from '@cosmjs/stargate';
import { MSG_CHAIN, toAtomic } from '../config/chain';
import { WalletConnectManager } from './WalletConnectManager';

export interface TxResult {
  txHash: string; code: number; height: number;
  gasUsed: number; rawLog: string;
}

export class WCSigner implements OfflineSigner {
  private wc: WalletConnectManager;
  private _address: string = '';

  constructor(wc: WalletConnectManager) { this.wc = wc; }
  setAddress(addr: string) { this._address = addr; }

  async getAccounts(): Promise<readonly AccountData[]> {
    if (!this._address) throw new Error('Address not set');
    return [{
      address: this._address,
      algo: 'secp256k1' as const,
      pubkey: new Uint8Array(0),
    }];
  }

  async signDirect(signerAddress: string, signDoc: any) {
    const sig = await this.wc.signDirect(signerAddress, signDoc);
    return { signed: signDoc, signature: sig };
  }

  async signAmino(signerAddress: string, signDoc: any) {
    const sig = await this.wc.signAmino(signerAddress, signDoc);
    return { signed: signDoc, signature: sig };
  }
}

export class MSGSigningClient {
  private wcManager: WalletConnectManager;
  private signer: WCSigner;
  private client: SigningCosmWasmClient | null = null;

  constructor(wcManager: WalletConnectManager) {
    this.wcManager = wcManager;
    this.signer = new WCSigner(wcManager);
  }

  async connect(address: string): Promise<void> {
    this.signer.setAddress(address);
    this.client = await SigningCosmWasmClient.connectWithSigner(
      MSG_CHAIN.rpcUrl, this.signer,
      { gasPrice: GasPrice.fromString("1000000000attoMSG") },
    );
  }

  async sendTokens(recipient: string, amount: string): Promise<TxResult> {
    if (!this.client) throw new Error('Not connected');
    const fee = calculateFee(MSG_CHAIN.defaultGas, GasPrice.fromString("1000000000attoMSG"));
    const result = await this.client.sendTokens(
      this.signer['_address'], recipient,
      coins(toAtomic(amount), MSG_CHAIN.denom), fee,
    );
    return {
      txHash: result.transactionHash, code: result.code ?? 0,
      height: result.height, gasUsed: result.gasUsed ?? 0, rawLog: result.rawLog ?? '',
    };
  }

  async executeContract(contract: string, msg: Record<string, unknown>, funds?: string): Promise<TxResult> {
    if (!this.client) throw new Error('Not connected');
    const fee = calculateFee(MSG_CHAIN.contractGas, GasPrice.fromString("1000000000attoMSG"));
    const sendFunds = funds ? coins(toAtomic(funds), MSG_CHAIN.denom) : undefined;
    const result = await this.client.execute(this.signer['_address'], contract, msg, fee, undefined, sendFunds);
    return {
      txHash: result.transactionHash, code: result.code ?? 0,
      height: result.height, gasUsed: result.gasUsed ?? 0, rawLog: result.rawLog ?? '',
    };
  }

  disconnect() { this.client?.disconnect(); }
}

2.5 WalletConnect v2 管理器

// src/lib/WalletConnectManager.ts
import Web3Wallet, { IWeb3Wallet } from '@walletconnect/web3wallet';
import { Core } from '@walletconnect/core';
import AsyncStorage from '@react-native-async-storage/async-storage';

const PROJECT_ID = 'YOUR_PROJECT_ID';
const RELAY_URL = 'wss://relay.walletconnect.com';

export class WalletConnectManager {
  private web3wallet: IWeb3Wallet | null = null;
  private sessions: Map<string, { address: string; topic: string }> = new Map();

  async init(): Promise<void> {
    const core = new Core({ projectId: PROJECT_ID, relayUrl: RELAY_URL });
    this.web3wallet = await Web3Wallet.init({
      core,
      metadata: {
        name: 'MSG Chain DApp',
        description: 'MSG Chain Mobile App',
        url: 'https://msg-chain.com',
        icons: ['https://msg-chain.com/icon.png'],
      },
    });

    this.web3wallet.on('session_proposal', async (proposal) => {
      const approved = await this.web3wallet!.approveSession({
        id: proposal.id,
        namespaces: {
          cosmos: {
            methods: ['cosmos_signDirect', 'cosmos_signAmino'],
            chains: ['cosmos:msg-chain-1'],
            events: ['chainChanged', 'accountsChanged'],
          },
        },
      });
      const addr = approved.namespaces.cosmos?.accounts[0]?.split(':')[2];
      if (addr) {
        this.sessions.set(approved.topic, { address: addr, topic: approved.topic });
      }
    });

    this.web3wallet.on('session_delete', ({ topic }) => { this.sessions.delete(topic); });
    await this.restoreSessions();
  }

  async connect(): Promise<string> {
    if (!this.web3wallet) throw new Error('Not initialized');
    const { uri, approval } = await this.web3wallet.connect({
      requiredNamespaces: {
        cosmos: {
          methods: ['cosmos_signDirect', 'cosmos_signAmino'],
          chains: ['cosmos:msg-chain-1'],
          events: ['chainChanged', 'accountsChanged'],
        },
      },
    });
    if (!uri) throw new Error('No URI generated');
    return uri;
  }

  async signDirect(signerAddress: string, signDoc: any): Promise<Uint8Array> {
    if (!this.web3wallet) throw new Error('Not initialized');
    const session = this.findSession(signerAddress);
    if (!session) throw new Error('No session');
    const result = await this.web3wallet.request({
      topic: session.topic, chainId: 'cosmos:msg-chain-1',
      request: { method: 'cosmos_signDirect', params: { signerAddress, signDoc } },
    });
    return new Uint8Array(result.signature);
  }

  async signAmino(signerAddress: string, signDoc: any): Promise<Uint8Array> {
    if (!this.web3wallet) throw new Error('Not initialized');
    const session = this.findSession(signerAddress);
    if (!session) throw new Error('No session');
    const result = await this.web3wallet.request({
      topic: session.topic, chainId: 'cosmos:msg-chain-1',
      request: { method: 'cosmos_signAmino', params: { signerAddress, signDoc } },
    });
    return new Uint8Array(result.signature);
  }

  async disconnect(topic?: string): Promise<void> {
    if (!this.web3wallet) return;
    const topics = topic ? [topic] : [...this.sessions.keys()];
    for (const t of topics) {
      await this.web3wallet.disconnectSession({ topic: t, reason: { code: 6000, message: 'User disconnect' } });
      this.sessions.delete(t);
    }
  }

  isConnected(): boolean { return this.sessions.size > 0; }
  getActiveAddress(): string | null { return this.sessions.values().next().value?.address ?? null; }

  private findSession(address: string) {
    for (const s of this.sessions.values()) {
      if (s.address === address) return s;
    }
    return null;
  }

  private async restoreSessions() {
    try {
      const data = await AsyncStorage.getItem('@wc_sessions');
      if (data) this.sessions = new Map(Object.entries(JSON.parse(data)));
    } catch {}
  }
}

2.6 生物识别认证

// src/lib/BiometricAuth.ts
import ReactNativeBiometrics from 'react-native-biometrics';
import * as Keychain from 'react-native-keychain';

const rnBiometrics = new ReactNativeBiometrics({ allowDeviceCredentials: true });

export async function requireBiometric(reason: string = '确认操作'): Promise<boolean> {
  const { success } = await rnBiometrics.simplePrompt({
    promptMessage: reason,
    cancelButtonText: '取消',
  });
  return success;
}

export async function storeSecurely(key: string, value: string): Promise<void> {
  await Keychain.setGenericPassword(key, value, {
    service: `msg_chain_${key}`,
    accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE,
    accessible: Keychain.ACCESSIBILITY.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS,
  });
}

export async function retrieveSecurely(key: string): Promise<string | null> {
  try {
    const creds = await Keychain.getGenericPassword({
      service: `msg_chain_${key}`,
      authenticationPrompt: { title: '验证身份', subtitle: '使用生物识别解锁', cancel: '取消' },
    });
    return creds ? creds.password : null;
  } catch { return null; }
}

2.7 交易历史管理

// src/lib/TxHistory.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

export type TxStatus = 'pending' | 'success' | 'failed';

export interface TxRecord {
  txHash: string; type: string; from: string; to: string;
  amount: string; denom: string; fee: string; status: TxStatus;
  height: number; timestamp: number; memo: string;
  contractAddress?: string; rawLog?: string;
}

const STORAGE_KEY = '@msg_tx_history';

export class TxHistoryManager {
  private cache: TxRecord[] | null = null;

  async getAll(): Promise<TxRecord[]> {
    if (this.cache) return this.cache;
    try {
      const data = await AsyncStorage.getItem(STORAGE_KEY);
      this.cache = data ? JSON.parse(data) : [];
    } catch { this.cache = []; }
    return this.cache;
  }

  async add(tx: TxRecord): Promise<void> {
    const list = await this.getAll();
    list.unshift(tx);
    if (list.length > 500) list.length = 500;
    this.cache = list;
    await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(list));
  }

  async update(hash: string, status: TxStatus, height?: number, rawLog?: string): Promise<void> {
    const list = await this.getAll();
    const idx = list.findIndex(r => r.txHash === hash);
    if (idx !== -1) {
      list[idx].status = status;
      if (height) list[idx].height = height;
      if (rawLog) list[idx].rawLog = rawLog;
      this.cache = list;
      await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(list));
    }
  }

  async getPending(): Promise<TxRecord[]> {
    const list = await this.getAll();
    return list.filter(r => r.status === 'pending');
  }
}

2.8 状态管理 (Zustand)

// src/store/walletStore.ts
import { create } from 'zustand';

interface WalletState {
  address: string; balance: string; isConnected: boolean;
  isConnecting: boolean; error: string | null;
  setAddress: (addr: string) => void;
  setBalance: (bal: string) => void;
  setConnected: (v: boolean) => void;
  setConnecting: (v: boolean) => void;
  setError: (err: string | null) => void;
  reset: () => void;
}

export const useWalletStore = create<WalletState>((set) => ({
  address: '', balance: '0', isConnected: false, isConnecting: false, error: null,
  setAddress: (address) => set({ address }),
  setBalance: (balance) => set({ balance }),
  setConnected: (isConnected) => set({ isConnected }),
  setConnecting: (isConnecting) => set({ isConnecting }),
  setError: (error) => set({ error }),
  reset: () => set({ address: '', balance: '0', isConnected: false, isConnecting: false, error: null }),
}));

2.9 钱包 Hook

// src/hooks/useWallet.ts
import { useEffect, useCallback } from 'react';
import { useWalletStore } from '../store/walletStore';
import { WalletConnectManager } from '../lib/WalletConnectManager';
import { MSGQueryClient } from '../lib/CosmWasmClient';
import { requireBiometric } from '../lib/BiometricAuth';
import { MSGSigningClient } from '../lib/SigningClient';
import { TxHistoryManager, TxRecord } from '../lib/TxHistory';

let wcManager: WalletConnectManager | null = null;
function getWC(): WalletConnectManager {
  if (!wcManager) wcManager = new WalletConnectManager();
  return wcManager;
}

export function useWallet() {
  const { address, balance, isConnected, isConnecting, error,
    setAddress, setBalance, setConnected, setConnecting, setError, reset } = useWalletStore();

  useEffect(() => {
    const wc = getWC();
    wc.init();
  }, []);

  const connect = useCallback(async () => {
    setConnecting(true); setError(null);
    try {
      const uri = await getWC().connect();
      return uri;
    } catch (e: any) {
      setError(e.message); setConnecting(false);
      throw e;
    }
  }, []);

  const disconnect = useCallback(async () => {
    await getWC().disconnect();
    reset();
  }, []);

  const send = useCallback(async (recipient: string, amount: string) => {
    const addr = getWC().getActiveAddress();
    if (!addr) throw new Error('Not connected');
    const confirmed = await requireBiometric(`确认发送 ${amount} MSG`);
    if (!confirmed) throw new Error('Cancelled');
    const signing = new MSGSigningClient(getWC());
    await signing.connect(addr);
    const result = await signing.sendTokens(recipient, amount);
    const record: TxRecord = {
      txHash: result.txHash, type: 'send', from: addr, to: recipient,
      amount, denom: 'umsg', fee: '0.005', status: 'success' as TxStatus,
      height: result.height, timestamp: Date.now(), memo: '',
    };
    const hist = new TxHistoryManager();
    await hist.add(record);
    try {
      const qc = new MSGQueryClient();
      const bal = await qc.getBalance(addr);
      setBalance(bal);
    } catch {}
    return result;
  }, []);

  return { address, balance, isConnected, isConnecting, error, connect, disconnect, send };
}

2.10 查询 Hook

// src/hooks/useQuery.ts
import { useState, useCallback } from 'react';
import { MSGQueryClient } from '../lib/CosmWasmClient';

export function useQuery() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const query = useCallback(async <T>(contract: string, msg: Record<string, unknown>): Promise<T | null> => {
    setLoading(true); setError(null);
    try {
      const qc = new MSGQueryClient();
      return await qc.queryContractSmart<T>(contract, msg);
    } catch (e: any) { setError(e.message); return null; }
    finally { setLoading(false); }
  }, []);

  return { query, loading, error };
}

2.11 交易轮询

// src/lib/TxPoller.ts
import { MSGQueryClient } from './CosmWasmClient';
import { TxHistoryManager } from './TxHistory';

export class TxPoller {
  private timer: ReturnType<typeof setInterval> | null = null;

  start(): void { this.timer = setInterval(() => this.check(), 5000); }

  stop(): void { if (this.timer) { clearInterval(this.timer); this.timer = null; } }

  private async check(): Promise<void> {
    try {
      const hist = new TxHistoryManager();
      const pending = await hist.getPending();
      if (pending.length === 0) return;
      const qc = new MSGQueryClient();
      await qc.connect();
      for (const tx of pending) {
        if (!tx.txHash || tx.txHash.startsWith('pending_')) continue;
        const result = await qc.getTx(tx.txHash);
        if (result && result.code !== undefined) {
          const status = result.code === 0 ? 'success' as const : 'failed' as const;
          await hist.update(tx.txHash, status, result.height, result.rawLog);
        }
      }
    } catch (e) { console.error('TxPoller error:', e); }
  }
}

2.12 工具函数

// src/utils/format.ts
export function shortenAddress(addr: string, start = 8, end = 6): string {
  if (addr.length <= start + end) return addr;
  return `${addr.slice(0, start)}...${addr.slice(-end)}`;
}

export function isValidMsgAddress(addr: string): boolean {
  return /^msg1[a-z0-9]{38}$/.test(addr);
}

export function extractWcUri(data: string): string | null {
  if (data.startsWith('wc:')) return data;
  const m = data.match(/wc:[^\s&]+/);
  return m ? m[0] : null;
}

export function convertToMsgAddr(cosmosAddr: string): string {
  try {
    const { fromBech32, toBech32 } = require('@cosmjs/encoding');
    const decoded = fromBech32(cosmosAddr);
    return toBech32('msg', decoded.data);
  } catch { return cosmosAddr; }
}

2.13 iOS 原生配置

<!-- ios/MSGDApp/Info.plist -->
<key>NSFaceIDUsageDescription</key>
<string>使用 Face ID 确认交易签名</string>
<key>NSCameraUsageDescription</key>
<string>扫描 WalletConnect 二维码</string>
<key>LSApplicationQueriesSchemes</key>
<array>
  <string>keplrwallet</string>
  <string>walletconnect</string>
</array>

2.14 Android 原生配置

<!-- android/app/src/main/AndroidManifest.xml -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.INTERNET" />
<queries>
  <package android:name="com.keplr.wallet" />
</queries>

2.15 完整示例:React Native DApp

// App.tsx
import React, { useEffect, useState } from 'react';
import { SafeAreaView, StyleSheet, Text, View, TouchableOpacity,
  TextInput, ScrollView, ActivityIndicator, Alert, StatusBar } from 'react-native';
import { useWallet } from './src/hooks/useWallet';
import { useQuery } from './src/hooks/useQuery';
import { shortenAddress, isValidMsgAddress } from './src/utils/format';
import { TxPoller } from './src/lib/TxPoller';
import { TxHistoryManager, TxRecord } from './src/lib/TxHistory';
import { useWalletStore } from './src/store/walletStore';

type Tab = 'wallet' | 'send' | 'contract' | 'history';

export default function App() {
  const { address, balance, isConnected, isConnecting, error, connect, disconnect, send } = useWallet();
  const { query, loading: queryLoading, error: queryError } = useQuery();
  const [tab, setTab] = useState<Tab>('wallet');
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');
  const [contractAddr, setContractAddr] = useState('');
  const [queryMsg, setQueryMsg] = useState('{}');
  const [queryResult, setQueryResult] = useState('');
  const [history, setHistory] = useState<TxRecord[]>([]);

  useEffect(() => {
    const poller = new TxPoller();
    poller.start();
    new TxHistoryManager().getAll().then(setHistory);
    return () => poller.stop();
  }, []);

  const handleSend = async () => {
    if (!isValidMsgAddress(recipient)) { Alert.alert('无效地址'); return; }
    if (isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { Alert.alert('无效金额'); return; }
    try {
      await send(recipient, amount);
      Alert.alert('成功', '交易已提交');
      setRecipient(''); setAmount('');
    } catch (e: any) { Alert.alert('失败', e.message); }
  };

  const handleQuery = async () => {
    if (!contractAddr) { Alert.alert('请输入合约地址'); return; }
    try {
      const msg = JSON.parse(queryMsg);
      const result = await query(contractAddr, msg);
      setQueryResult(JSON.stringify(result, null, 2));
    } catch (e: any) { setQueryResult(`错误: ${e.message}`); }
  };

  return (
    <SafeAreaView style={s.container}>
      <StatusBar barStyle="light-content" />
      <View style={s.header}>
        <Text style={{color:'#fff',fontSize:20,fontWeight:'700'}}>MSG Chain</Text>
        {isConnected && <View style={{width:10,height:10,borderRadius:5,backgroundColor:'#3FB950'}} />}
      </View>
      <View style={s.tabBar}>
        {(['wallet','send','contract','history'] as const).map(t => (
          <TouchableOpacity key={t} style={[s.tab, tab===t && s.activeTab]} onPress={() => setTab(t)}>
            <Text style={[s.tabText, tab===t && s.activeTabText]}>
              {t==='wallet'?'钱包':t==='send'?'发送':t==='contract'?'合约':'历史'}
            </Text>
          </TouchableOpacity>
        ))}
      </View>
      <ScrollView style={{flex:1,padding:16}}>
        {tab==='wallet' && <View>
          <Text style={s.title}>钱包</Text>
          {isConnected ? <>
            <View style={s.row}><Text style={s.label}>地址</Text><Text style={s.value}>{shortenAddress(address)}</Text></View>
            <View style={s.row}><Text style={s.label}>余额</Text><Text style={s.value}>{balance} MSG</Text></View>
            <TouchableOpacity style={s.btn} onPress={disconnect}><Text style={s.btnText}>断开连接</Text></TouchableOpacity>
          </> : <>
            <TouchableOpacity style={[s.btn, isConnecting && {opacity:0.5}]} onPress={connect} disabled={isConnecting}>
              {isConnecting ? <ActivityIndicator color="#fff" /> : <Text style={s.btnText}>连接钱包</Text>}
            </TouchableOpacity>
            {error && <Text style={s.error}>{error}</Text>}
          </>}
        </View>}
        {tab==='send' && <View>
          <Text style={s.title}>发送 MSG</Text>
          <TextInput style={s.input} placeholder="接收地址" value={recipient} onChangeText={setRecipient} autoCapitalize="none" />
          <TextInput style={s.input} placeholder="数量" value={amount} onChangeText={setAmount} keyboardType="decimal-pad" />
          <TouchableOpacity style={[s.btn, !isConnected && {opacity:0.5}]} onPress={handleSend} disabled={!isConnected}>
            <Text style={s.btnText}>发送</Text>
          </TouchableOpacity>
        </View>}
        {tab==='contract' && <View>
          <Text style={s.title}>合约交互</Text>
          <TextInput style={s.input} placeholder="合约地址" value={contractAddr} onChangeText={setContractAddr} autoCapitalize="none" />
          <TextInput style={[s.input,{height:80}]} placeholder='{"key":"value"}' value={queryMsg} onChangeText={setQueryMsg} multiline />
          <TouchableOpacity style={s.btn} onPress={handleQuery}><Text style={s.btnText}>查询</Text></TouchableOpacity>
          {queryResult ? <View style={s.resultBox}><Text style={s.resultText}>{queryResult}</Text></View> : null}
          {queryError && <Text style={s.error}>{queryError}</Text>}
        </View>}
        {tab==='history' && <View>
          <Text style={s.title}>交易历史</Text>
          {history.slice(0,20).map((tx,i) => (
            <View key={i} style={s.txItem}>
              <Text style={s.txHash}>{shortenAddress(tx.txHash,6,6)}</Text>
              <Text style={{color:'#C9D1D9'}}>{tx.amount} MSG</Text>
              <Text style={{color: tx.status==='success' ? '#3FB950' : '#F85149'}}>
                {tx.status==='success' ? '✓ 成功' : '✗ 失败'}
              </Text>
            </View>
          ))}
        </View>}
      </ScrollView>
    </SafeAreaView>
  );
}

const s = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#0D1117' },
  header: { flexDirection:'row', alignItems:'center', padding:16, backgroundColor:'#161B22', borderBottomWidth:1, borderBottomColor:'#30363D' },
  tabBar: { flexDirection:'row', backgroundColor:'#161B22', borderBottomWidth:1, borderBottomColor:'#30363D' },
  tab: { flex:1, paddingVertical:12, alignItems:'center' },
  activeTab: { borderBottomWidth:2, borderBottomColor:'#58A6FF' },
  tabText: { color:'#8B949E', fontSize:14 },
  activeTabText: { color:'#58A6FF', fontWeight:'600' },
  title: { color:'#fff', fontSize:18, fontWeight:'600', marginBottom:16 },
  row: { flexDirection:'row', justifyContent:'space-between', paddingVertical:12, borderBottomWidth:1, borderBottomColor:'#21262D' },
  label: { color:'#8B949E', fontSize:14 }, value: { color:'#fff', fontSize:14 },
  input: { backgroundColor:'#21262D', borderWidth:1, borderColor:'#30363D', borderRadius:8, paddingHorizontal:16, paddingVertical:12, color:'#fff', fontSize:14, marginBottom:12 },
  btn: { backgroundColor:'#238636', paddingVertical:14, borderRadius:8, alignItems:'center', marginBottom:8 },
  btnText: { color:'#fff', fontSize:16, fontWeight:'600' },
  error: { color:'#F85149', fontSize:14, marginTop:8, textAlign:'center' },
  resultBox: { backgroundColor:'#161B22', borderWidth:1, borderColor:'#30363D', borderRadius:8, padding:12, marginTop:12 },
  resultText: { color:'#C9D1D9', fontSize:12 },
  txItem: { backgroundColor:'#21262D', borderRadius:6, padding:12, marginBottom:8 },
  txHash: { color:'#58A6FF', fontSize:12 },
});

3. Flutter 集成

3.1 依赖配置

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  http: ^1.2.1
  web_socket_channel: ^3.0.0
  flutter_secure_storage: ^9.2.2
  local_auth: ^2.3.0
  mobile_scanner: ^6.0.0
  qr_flutter: ^4.1.0
  bech32: ^0.2.2
  intl: ^0.19.0
  equatable: ^2.0.5
  url_launcher: ^6.3.0
  fluttertoast: ^8.2.5

3.2 链配置

// lib/core/chain_config.dart
class ChainConfig {
  static const String chainId = 'msg-chain-1';
  static const String prefix = 'msg';
  static const String rpcUrl = 'https://rpc.msg-chain.com';
  static const String restUrl = 'https://api.msg-chain.com';
  static const String denom = 'umsg';
  static const int decimals = 6;
  static const String gasPrice = '1000000000';
  static const int defaultGas = 200000;
  static const int contractGas = 300000;

  static int toAtomic(double amount) => (amount * 1000000).toInt();
  static double fromAtomic(int amount) => amount / 1000000.0;
  static String formatBalance(int amount) => fromAtomic(amount).toStringAsFixed(decimals);
}

3.3 只读客户端

// lib/core/query_client.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'chain_config.dart';

class MsgQueryClient {
  final http.Client _client = http.Client();

  Future<int> getBalance(String address) async {
    final res = await _client.get(
      Uri.parse('${ChainConfig.restUrl}/cosmos/bank/v1beta1/balances/$address'));
    if (res.statusCode != 200) return 0;
    final data = json.decode(res.body) as Map;
    final balances = data['balances'] as List? ?? [];
    for (final coin in balances) {
      if (coin['denom'] == ChainConfig.denom) {
        return int.tryParse(coin['amount']?.toString() ?? '0') ?? 0;
      }
    }
    return 0;
  }

  Future<Map<String, dynamic>> querySmart(
    String contract, Map<String, dynamic> msg) async {
    final encoded = json.encode(msg);
    final res = await _client.get(Uri.parse(
      '${ChainConfig.restUrl}/cosmwasm/wasm/v1/contract/$contract/smart/$encoded'));
    if (res.statusCode != 200) throw Exception('Query failed: ${res.statusCode}');
    final data = json.decode(res.body) as Map;
    return json.decode(data['data'] ?? '{}') as Map<String, dynamic>;
  }

  Future<int> getBlockHeight() async {
    final res = await _client.get(Uri.parse('${ChainConfig.rpcUrl}/status'));
    final data = json.decode(res.body) as Map;
    return int.parse(data['result']['sync_info']['latest_block_height']);
  }

  Future<Map<String, dynamic>> getTx(String hash) async {
    final res = await _client.get(
      Uri.parse('${ChainConfig.restUrl}/cosmos/tx/v1beta1/txs/$hash'));
    if (res.statusCode != 200) return {'status': 'pending'};
    return json.decode(res.body)['tx_response'] ?? {'status': 'pending'};
  }

  Future<Map<String, dynamic>> getAccount(String address) async {
    final res = await _client.get(
      Uri.parse('${ChainConfig.restUrl}/cosmos/auth/v1beta1/accounts/$address'));
    if (res.statusCode != 200) return {'account_number': 0, 'sequence': 0};
    final acc = json.decode(res.body)['account'] ?? {};
    return {
      'account_number': int.tryParse(acc['account_number']?.toString() ?? '0') ?? 0,
      'sequence': int.tryParse(acc['sequence']?.toString() ?? '0') ?? 0,
    };
  }

  Future<Map<String, dynamic>> broadcast(Uint8List txBytes) async {
    final res = await _client.post(
      Uri.parse('${ChainConfig.restUrl}/cosmos/tx/v1beta1/txs'),
      headers: {'Content-Type': 'application/json'},
      body: json.encode({
        'tx_bytes': base64Encode(txBytes),
        'mode': 'BROADCAST_MODE_SYNC',
      }),
    );
    if (res.statusCode != 200) throw Exception('Broadcast failed: ${res.statusCode}');
    return json.decode(res.body)['tx_response'] ?? {};
  }

  void dispose() { _client.close(); }
}

3.4 签名服务

// lib/core/signing_service.dart
import 'dart:convert';
import 'dart:typed_data';
import 'query_client.dart';
import 'chain_config.dart';

typedef SignCallback = Future<Uint8List> Function(Map<String, dynamic> signDoc);

class MsgSigningService {
  final MsgQueryClient _query;
  final String _address;

  MsgSigningService(this._query, this._address);

  Future<Map<String, dynamic>> sendTokens({
    required String recipient,
    required int amount,
    required SignCallback onSign,
    String memo = '',
  }) async {
    final acc = await _query.getAccount(_address);
    final seq = acc['sequence'] as int;
    final fee = (ChainConfig.defaultGas * double.parse(ChainConfig.gasPrice)).round();
    final signDoc = {
      'body': {'messages': [{
        '@type': '/cosmos.bank.v1beta1.MsgSend',
        'from_address': _address, 'to_address': recipient,
        'amount': [{'denom': ChainConfig.denom, 'amount': amount.toString()}],
      }], 'memo': memo},
      'auth_info': {'signer_infos': [{
        'public_key': {'@type': '/cosmos.crypto.secp256k1.PubKey', 'key': ''},
        'mode_info': {'single': {'mode': 'SIGN_MODE_DIRECT'}},
        'sequence': seq.toString(),
      }], 'fee': {
        'amount': [{'denom': ChainConfig.denom, 'amount': fee.toString()}],
        'gas_limit': ChainConfig.defaultGas.toString(),
      }},
      'signatures': [''],
      'chain_id': ChainConfig.chainId,
      'account_number': acc['account_number'].toString(),
    };
    final sig = await onSign(signDoc);
    final bodyBytes = utf8.encode(json.encode(signDoc['body']));
    final authBytes = utf8.encode(json.encode(signDoc['auth_info']));
    final txRaw = utf8.encode(json.encode({
      'body_bytes': base64Encode(bodyBytes),
      'auth_info_bytes': base64Encode(authBytes),
      'signatures': [base64Encode(sig)],
    }));
    return await _query.broadcast(Uint8List.fromList(txRaw));
  }

  Future<Map<String, dynamic>> executeContract({
    required String contract,
    required Map<String, dynamic> msg,
    required SignCallback onSign,
    String memo = '',
  }) async {
    final acc = await _query.getAccount(_address);
    final seq = acc['sequence'] as int;
    final fee = (ChainConfig.contractGas * double.parse(ChainConfig.gasPrice)).round();
    final signDoc = {
      'body': {'messages': [{
        '@type': '/cosmwasm.wasm.v1.MsgExecuteContract',
        'sender': _address, 'contract': contract,
        'msg': base64Encode(utf8.encode(json.encode(msg))),
        'funds': [],
      }], 'memo': memo},
      'auth_info': {'signer_infos': [{
        'public_key': {'@type': '/cosmos.crypto.secp256k1.PubKey', 'key': ''},
        'mode_info': {'single': {'mode': 'SIGN_MODE_DIRECT'}},
        'sequence': seq.toString(),
      }], 'fee': {
        'amount': [{'denom': ChainConfig.denom, 'amount': fee.toString()}],
        'gas_limit': ChainConfig.contractGas.toString(),
      }},
      'signatures': [''],
      'chain_id': ChainConfig.chainId,
      'account_number': acc['account_number'].toString(),
    };
    final sig = await onSign(signDoc);
    final bodyBytes = utf8.encode(json.encode(signDoc['body']));
    final authBytes = utf8.encode(json.encode(signDoc['auth_info']));
    final txRaw = utf8.encode(json.encode({
      'body_bytes': base64Encode(bodyBytes),
      'auth_info_bytes': base64Encode(authBytes),
      'signatures': [base64Encode(sig)],
    }));
    return await _query.broadcast(Uint8List.fromList(txRaw));
  }
}

3.5 WalletConnect 服务

// lib/core/wallet_connect_service.dart
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class WalletConnectService {
  final FlutterSecureStorage _storage = const FlutterSecureStorage();
  String? _address;
  bool _connected = false;

  bool get isConnected => _connected;
  String? get address => _address;

  Future<void> restoreSession() async {
    final saved = await _storage.read(key: 'wc_session');
    if (saved != null) {
      final data = json.decode(saved);
      _address = data['address'];
      _connected = true;
    }
  }

  Future<String> connect() async {
    return 'wc:YOUR_PROJECT_ID@2?relay-protocol=irn';
  }

  void setAddress(String addr) { _address = addr; _connected = true; _saveSession(); }

  Future<void> disconnect() async {
    _address = null; _connected = false;
    await _storage.delete(key: 'wc_session');
  }

  Future<void> _saveSession() async {
    if (_address != null) {
      await _storage.write(key: 'wc_session', value: json.encode({'address': _address}));
    }
  }
}

3.6 安全存储

// lib/core/secure_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';

class SecureStorage {
  final FlutterSecureStorage _storage = const FlutterSecureStorage(
    aOptions: AndroidOptions(encryptedSharedPreferences: true),
  );
  final LocalAuthentication _auth = LocalAuthentication();

  Future<bool> isBiometricAvailable() async {
    return await _auth.canCheckBiometrics || await _auth.isDeviceSupported();
  }

  Future<bool> authenticate({String reason = '验证身份'}) async {
    try {
      return await _auth.authenticate(
        localizedReason: reason,
        options: const AuthenticationOptions(biometricOnly: true, stickyAuth: true),
      );
    } catch { return false; }
  }

  Future<void> storeMnemonic(String mnemonic) async {
    await _storage.write(key: 'msg_mnemonic', value: mnemonic);
  }

  Future<String?> getMnemonic() async {
    final ok = await authenticate(reason: '使用生物识别解锁钱包');
    if (!ok) return null;
    return await _storage.read(key: 'msg_mnemonic');
  }

  Future<bool> hasMnemonic() async {
    final m = await _storage.read(key: 'msg_mnemonic');
    return m != null && m.isNotEmpty;
  }

  Future<void> clearAll() async { await _storage.deleteAll(); }
}

3.7 交易历史

// lib/core/tx_history.dart
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

enum TxStatus { pending, success, failed }

class TxRecord {
  final String txHash; final String type;
  final String from; final String to;
  final int amount; final String denom;
  TxStatus status; final int height;
  final DateTime timestamp;

  TxRecord({
    required this.txHash, this.type = 'send',
    required this.from, required this.to, required this.amount,
    this.denom = 'umsg', this.status = TxStatus.pending,
    this.height = 0, DateTime? timestamp,
  }) : timestamp = timestamp ?? DateTime.now();

  String get formattedAmount => (amount / 1000000).toStringAsFixed(6);
  bool get isPending => status == TxStatus.pending;

  Map<String, dynamic> toJson() => {
    'txHash': txHash, 'type': type, 'from': from, 'to': to,
    'amount': amount, 'denom': denom, 'status': status.name,
    'height': height, 'timestamp': timestamp.toIso8601String(),
  };

  factory TxRecord.fromJson(Map<String, dynamic> j) => TxRecord(
    txHash: j['txHash'] ?? '', type: j['type'] ?? 'send',
    from: j['from'] ?? '', to: j['to'] ?? '',
    amount: j['amount'] ?? 0, denom: j['denom'] ?? 'umsg',
    status: TxStatus.values.firstWhere((e) => e.name == j['status'],
      orElse: () => TxStatus.pending),
    height: j['height'] ?? 0,
    timestamp: DateTime.tryParse(j['timestamp'] ?? '') ?? DateTime.now(),
  );
}

class TxHistory {
  final FlutterSecureStorage _storage = const FlutterSecureStorage();
  static const _key = 'msg_tx_history';

  Future<List<TxRecord>> getAll() async {
    final d = await _storage.read(key: _key);
    if (d == null) return [];
    final list = json.decode(d) as List;
    return list.map((e) => TxRecord.fromJson(e as Map<String, dynamic>)).toList();
  }

  Future<void> add(TxRecord tx) async {
    final list = await getAll();
    list.insert(0, tx);
    if (list.length > 500) list.removeRange(500, list.length);
    await _storage.write(key: _key, value: json.encode(list.map((e) => e.toJson()).toList()));
  }

  Future<void> update(String hash, TxStatus status) async {
    final list = await getAll();
    for (int i = 0; i < list.length; i++) {
      if (list[i].txHash == hash) {
        list[i].status = status;
        await _storage.write(key: _key, value: json.encode(list.map((e) => e.toJson()).toList()));
        break;
      }
    }
  }

  Future<List<TxRecord>> getPending() async {
    final all = await getAll();
    return all.where((t) => t.isPending).toList();
  }
}

3.8 Riverpod 状态管理

// lib/providers/app_providers.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../core/query_client.dart';
import '../core/wallet_connect_service.dart';
import '../core/secure_storage.dart';
import '../core/tx_history.dart';

final wcServiceProvider = Provider<WalletConnectService>((ref) {
  final svc = WalletConnectService();
  svc.restoreSession();
  ref.onDispose(() => svc.disconnect());
  return svc;
});

final queryClientProvider = Provider<MsgQueryClient>((ref) {
  final client = MsgQueryClient();
  ref.onDispose(() => client.dispose());
  return client;
});

final secureStorageProvider = Provider<SecureStorage>((ref) => SecureStorage());
final txHistoryProvider = Provider<TxHistory>((ref) => TxHistory());

final walletStateProvider = StateNotifierProvider<WalletStateNotifier, WalletState>((ref) {
  return WalletStateNotifier(ref);
});

class WalletState {
  final String? address; final int balance;
  final bool connected; final bool connecting;
  final String? error; final String? wcUri;

  const WalletState({
    this.address, this.balance = 0, this.connected = false,
    this.connecting = false, this.error, this.wcUri,
  });

  WalletState copyWith({
    String? address, int? balance, bool? connected,
    bool? connecting, String? error, String? wcUri,
  }) => WalletState(
    address: address ?? this.address, balance: balance ?? this.balance,
    connected: connected ?? this.connected, connecting: connecting ?? this.connecting,
    error: error, wcUri: wcUri ?? this.wcUri,
  );
}

class WalletStateNotifier extends StateNotifier<WalletState> {
  final Ref _ref;
  WalletStateNotifier(this._ref) : super(const WalletState());

  Future<void> connect() async {
    state = state.copyWith(connecting: true, error: null);
    try {
      final wc = _ref.read(wcServiceProvider);
      final uri = await wc.connect();
      state = state.copyWith(wcUri: uri, connecting: false);
    } catch (e) {
      state = state.copyWith(connecting: false, error: e.toString());
    }
  }

  void onConnected(String address) {
    _ref.read(wcServiceProvider).setAddress(address);
    state = state.copyWith(address: address, connected: true, wcUri: null);
    refreshBalance(address);
  }

  Future<void> refreshBalance([String? addr]) async {
    final a = addr ?? state.address;
    if (a == null) return;
    try {
      final client = _ref.read(queryClientProvider);
      final bal = await client.getBalance(a);
      state = state.copyWith(balance: bal);
    } catch (_) {}
  }
}

3.9 工具函数

// lib/core/utils.dart
import 'package:bech32/bech32.dart';
import 'chain_config.dart';

class MsgUtils {
  static String shortenAddress(String addr, {int start = 8, int end = 6}) {
    if (addr.length <= start + end) return addr;
    return '${addr.substring(0, start)}...${addr.substring(addr.length - end)}';
  }

  static bool isValidAddress(String addr) {
    return RegExp(r'^msg1[a-z0-9]{38}$').hasMatch(addr);
  }

  static String convertToMsg(String cosmosAddr) {
    try {
      final decoded = Bech32.decode(cosmosAddr);
      return Bech32.encode(ChainConfig.prefix, decoded.data);
    } catch (_) { return cosmosAddr; }
  }

  static String parseError(String rawLog) {
    if (rawLog.contains('insufficient funds')) return '余额不足';
    if (rawLog.contains('out of gas')) return 'Gas 不足';
    final m = RegExp(r'wasm error: ([^\n]+)').firstMatch(rawLog);
    return m?.group(1) ?? rawLog;
  }

  static String timeAgo(DateTime dt) {
    final diff = DateTime.now().difference(dt);
    if (diff.inSeconds < 60) return '刚刚';
    if (diff.inMinutes < 60) return '${diff.inMinutes}分钟前';
    if (diff.inHours < 24) return '${diff.inHours}小时前';
    if (diff.inDays < 7) return '${diff.inDays}天前';
    return '${dt.month}/${dt.day}';
  }
}

3.10 完整示例:Flutter DApp

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'providers/app_providers.dart';
import 'core/query_client.dart';
import 'core/signing_service.dart';
import 'core/tx_history.dart';
import 'core/utils.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const ProviderScope(child: MsgDApp()));
}

class MsgDApp extends StatelessWidget {
  const MsgDApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'MSG Chain',
      debugShowCheckedModeBanner: false,
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: const Color(0xFF0D1117),
        colorScheme: const ColorScheme.dark(
          primary: Color(0xFF58A6FF), secondary: Color(0xFF3FB950),
          error: Color(0xFFF85149),
        ),
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends ConsumerStatefulWidget {
  const HomePage({super.key});
  @override
  ConsumerState<HomePage> createState() => _HomePageState();
}

class _HomePageState extends ConsumerState<HomePage> {
  int _tab = 0;
  final _toCtrl = TextEditingController();
  final _amtCtrl = TextEditingController();
  final _contractCtrl = TextEditingController();
  final _queryCtrl = TextEditingController(text: '{}');
  String _queryResult = '';

  @override
  void dispose() {
    _toCtrl.dispose(); _amtCtrl.dispose();
    _contractCtrl.dispose(); _queryCtrl.dispose();
    super.dispose();
  }

  Future<void> _send() async {
    final amt = double.tryParse(_amtCtrl.text);
    if (amt == null || amt <= 0) { _err('无效金额'); return; }
    if (!MsgUtils.isValidAddress(_toCtrl.text)) { _err('无效地址'); return; }

    final secure = ref.read(secureStorageProvider);
    final ok = await secure.authenticate(reason: '确认发送 $amt MSG');
    if (!ok) { _err('用户取消'); return; }

    final ws = ref.read(walletStateProvider);
    if (ws.address == null) { _err('钱包未连接'); return; }

    try {
      final qc = ref.read(queryClientProvider);
      final svc = MsgSigningService(qc, ws.address!);
      final wc = ref.read(wcServiceProvider);
      final result = await svc.sendTokens(
        recipient: _toCtrl.text, amount: ChainConfig.toAtomic(amt),
        onSign: (doc) async => Uint8List(0),
      );
      final tx = TxRecord(
        txHash: result['txhash'] ?? '',
        from: ws.address!, to: _toCtrl.text, amount: ChainConfig.toAtomic(amt),
      );
      await ref.read(txHistoryProvider).add(tx);
      _msg('交易已提交');
      _toCtrl.clear(); _amtCtrl.clear();
    } catch (e) { _err('$e'); }
  }

  Future<void> _query() async {
    try {
      final msg = json.decode(_queryCtrl.text);
      final qc = ref.read(queryClientProvider);
      final result = await qc.querySmart(_contractCtrl.text, msg);
      setState(() => _queryResult = const JsonEncoder.withIndent('  ').convert(result));
    } catch (e) { setState(() => _queryResult = '错误: $e'); }
  }

  void _err(String m) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m), backgroundColor: Colors.red));
  }

  void _msg(String m) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(m), backgroundColor: const Color(0xFF3FB950)));
  }

  @override
  Widget build(BuildContext context) {
    final ws = ref.watch(walletStateProvider);
    return Scaffold(
      body: [
        _walletPage(ws), _sendPage(ws), _contractPage(), _historyPage(),
      ][_tab],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _tab, onTap: (i) => setState(() => _tab = i),
        backgroundColor: const Color(0xFF161B22),
        selectedItemColor: const Color(0xFF58A6FF),
        unselectedItemColor: const Color(0xFF8B949E),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.account_balance_wallet), label: '钱包'),
          BottomNavigationBarItem(icon: Icon(Icons.send), label: '发送'),
          BottomNavigationBarItem(icon: Icon(Icons.code), label: '合约'),
          BottomNavigationBarItem(icon: Icon(Icons.history), label: '历史'),
        ],
      ),
    );
  }

  Widget _walletPage(WalletState ws) {
    return SafeArea(child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        const SizedBox(height: 20),
        const Text('MSG Chain', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700, color: Colors.white)),
        const SizedBox(height: 32),
        Card(child: Padding(
          padding: const EdgeInsets.all(20),
          child: ws.connected && ws.address != null
            ? Column(children: [
                const Icon(Icons.account_balance_wallet, size: 48, color: Color(0xFF58A6FF)),
                const SizedBox(height: 12),
                Text('${ChainConfig.formatBalance(ws.balance)} MSG', style: const TextStyle(fontSize: 32, fontWeight: FontWeight.w700, color: Colors.white)),
                const SizedBox(height: 8),
                Container(padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                  decoration: BoxDecoration(color: const Color(0xFF21262D), borderRadius: BorderRadius.circular(6)),
                  child: Text(MsgUtils.shortenAddress(ws.address!), style: const TextStyle(color: Color(0xFF58A6FF), fontFamily: 'monospace', fontSize: 13))),
              ])
            : Column(children: [
                const Icon(Icons.wallet, size: 64, color: Color(0xFF484F58)),
                const SizedBox(height: 16),
                const Text('连接 Keplr Mobile', style: TextStyle(color: Color(0xFF8B949E))),
                const SizedBox(height: 24),
                ElevatedButton(
                  onPressed: ws.connecting ? null : () => ref.read(walletStateProvider.notifier).connect(),
                  child: ws.connecting
                    ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))
                    : const Text('连接钱包'),
                ),
                if (ws.error != null) Padding(padding: const EdgeInsets.only(top: 12), child: Text(ws.error!, style: const TextStyle(color: Color(0xFFF85149)))),
              ]),
        )),
      ]),
    ));
  }

  Widget _sendPage(WalletState ws) {
    return SafeArea(child: SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(children: [
        const Text('发送 MSG', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)),
        const SizedBox(height: 16),
        TextField(controller: _toCtrl, decoration: const InputDecoration(labelText: '接收地址 (msg1...)'), style: const TextStyle(color: Colors.white)),
        const SizedBox(height: 12),
        TextField(controller: _amtCtrl, decoration: const InputDecoration(labelText: '数量'), keyboardType: TextInputType.number, style: const TextStyle(color: Colors.white)),
        const SizedBox(height: 24),
        ElevatedButton(onPressed: ws.connected ? _send : null, child: const Text('发送')),
      ]),
    ));
  }

  Widget _contractPage() {
    return SafeArea(child: SingleChildScrollView(
      padding: const EdgeInsets.all(20),
      child: Column(children: [
        const Text('合约交互', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)),
        const SizedBox(height: 16),
        TextField(controller: _contractCtrl, decoration: const InputDecoration(labelText: '合约地址'), style: const TextStyle(color: Colors.white)),
        const SizedBox(height: 12),
        TextField(controller: _queryCtrl, decoration: const InputDecoration(labelText: '查询消息 (JSON)'), maxLines: 3, style: const TextStyle(color: Colors.white)),
        const SizedBox(height: 16),
        ElevatedButton(onPressed: _query, child: const Text('查询')),
        if (_queryResult.isNotEmpty)
          Container(width: double.infinity, margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(color: const Color(0xFF161B22), borderRadius: BorderRadius.circular(8), border: Border.all(color: const Color(0xFF30363D))),
            child: Text(_queryResult, style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: Color(0xFFC9D1D9)))),
      ]),
    ));
  }

  Widget _historyPage() {
    return SafeArea(child: Padding(
      padding: const EdgeInsets.all(20),
      child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
        const Text('交易历史', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)),
        const SizedBox(height: 16),
        Expanded(child: FutureBuilder<List<TxRecord>>(
          future: ref.read(txHistoryProvider).getAll(),
          builder: (ctx, snap) {
            if (!snap.hasData) return const Center(child: CircularProgressIndicator());
            return ListView.builder(
              itemCount: snap.data!.length,
              itemBuilder: (ctx, i) {
                final tx = snap.data![i];
                return Card(child: ListTile(
                  title: Text(MsgUtils.shortenAddress(tx.txHash, start: 6, end: 6), style: const TextStyle(color: Color(0xFF58A6FF), fontFamily: 'monospace', fontSize: 12)),
                  subtitle: Text('${tx.formattedAmount} MSG  ${MsgUtils.timeAgo(tx.timestamp)}', style: const TextStyle(color: Color(0xFF8B949E))),
                  trailing: Text(tx.isPending ? '⏳' : tx.status == TxStatus.success ? '✓' : '✗', style: TextStyle(color: tx.isPending ? Colors.orange : tx.status == TxStatus.success ? const Color(0xFF3FB950) : const Color(0xFFF85149))),
                ));
              },
            );
          },
        )),
      ]),
    ));
  }
}

4. WalletConnect 协议详解

4.1 协议概述

WalletConnect v2 是一个开放协议,用于在移动端 DApp 和钱包之间建立安全通信。它使用中继服务器(Relay Server)传递加密消息,不依赖中心化基础设施。

核心流程:

  1. DApp 请求连接,生成 WC URI
  2. 用户将 URI 通过 QR 码或 Deep Link 发送给 Keplr Mobile
  3. Keplr 解析 URI,建立与中继服务器的 WebSocket 连接
  4. 双方交换对称密钥,建立加密通道
  5. DApp 发送签名请求,中继转发,Keplr 提示用户确认
  6. 签名结果加密返回,DApp 广播交易到 MSG Chain

4.2 URI 结构

wc:PROJECT_ID@2?relay-protocol=irn&symKey=BASE64URL_KEY&expiry=TIMESTAMP&relayUrl=wss://relay.walletconnect.com
参数 说明
PROJECT_ID WalletConnect Cloud 项目 ID
relay-protocol 中继协议(默认 irn)
symKey 32 字节对称密钥的 base64url 编码
expiry 连接过期时间戳
relayUrl 中继服务器 WebSocket 地址

4.3 命名空间

const requiredNamespaces = {
  cosmos: {
    methods: ['cosmos_signDirect', 'cosmos_signAmino'],
    chains: ['cosmos:msg-chain-1'],
    events: ['chainChanged', 'accountsChanged'],
  },
};

方法说明:

4.4 连接流程

// 初始化
const core = new Core({ projectId: 'YOUR_ID', relayUrl: 'wss://relay.walletconnect.com' });
const wallet = await Web3Wallet.init({
  core,
  metadata: { name: 'DApp', description: '...', url: '...', icons: [...] },
});

// 发起连接
const { uri, approval } = await wallet.connect({
  requiredNamespaces: {
    cosmos: { methods: ['cosmos_signDirect'], chains: ['cosmos:msg-chain-1'], events: [] },
  },
});

// 等待用户批准
const session = await approval();
const address = session.namespaces.cosmos.accounts[0].split(':')[2];

4.5 签名请求

// Sign Direct (推荐)
const result = await wallet.request({
  topic: session.topic,
  chainId: 'cosmos:msg-chain-1',
  request: {
    method: 'cosmos_signDirect',
    params: {
      signerAddress: 'msg1...',
      signDoc: { bodyBytes, authInfoBytes, chainId: 'msg-chain-1', accountNumber: '123' },
    },
  },
});

// Sign Amino (兼容)
const result = await wallet.request({
  topic: session.topic, chainId: 'cosmos:msg-chain-1',
  request: {
    method: 'cosmos_signAmino',
    params: { signerAddress: 'msg1...', signDoc: { chain_id: 'msg-chain-1', ... } },
  },
});

4.6 会话管理

// 持久化会话
await AsyncStorage.setItem('@wc_topic', session.topic);

// 恢复会话
const topic = await AsyncStorage.getItem('@wc_topic');
if (topic) {
  const session = wallet.getActiveSessions()[topic];
  if (session) { /* 会话有效 */ }
}

// 断开连接
await wallet.disconnectSession({
  topic: session.topic,
  reason: { code: 6000, message: 'User disconnected' },
});

// 监听事件
wallet.on('session_proposal', (p) => { /* 自动批准或提示用户 */ });
wallet.on('session_delete', ({ topic }) => { /* 清理会话 */ });
wallet.on('session_expire', ({ topic }) => { /* 处理过期 */ });

4.7 错误码

错误码 含义
0 成功
1000 用户拒绝
2000 方法不支持
3000 链不支持
4000 会话过期
5000 签名失败
6000 用户断开连接
7000 钱包内部错误

4.8 Keplr Mobile 特定方法

Keplr Mobile 支持 WalletConnect v2,以下是其支持的链和方法:

{
  "supported_chains": ["cosmos:msg-chain-1"],
  "supported_methods": [
    "cosmos_signDirect",
    "cosmos_signAmino",
    "keplr_getKey",
    "keplr_enableWallet",
    "keplr_signDirect",
    "keplr_signAmino"
  ]
}
keplrwallet://wc?uri=WC_URI_ENCODED    (Android Deep Link)
https://keplr.app/wc?uri=WC_URI_ENCODED (iOS Universal Link)
import { Linking } from 'react-native';

function openKeplr(wcUri: string) {
  const encoded = encodeURIComponent(wcUri);
  Linking.openURL(`keplrwallet://wc?uri=${encoded}`).catch(() => {
    Linking.openURL(`https://keplr.app/wc?uri=${encoded}`);
  });
}

5. 安全考虑

5.1 私钥安全管理

私钥和助记词绝不能存储在纯文本中。必须使用平台提供的安全硬件存储:

平台 安全存储技术 说明
iOS Keychain Services 硬件加密,支持 Face ID/Touch ID
Android Android Keystore TEE 隔离,支持指纹/生物识别
iOS Secure Enclave 256 位 AES 硬件加密
Android StrongBox 独立安全芯片(需硬件支持)

React Native:使用 react-native-keychain

import * as Keychain from 'react-native-keychain';

// 存储(自动加密)
await Keychain.setGenericPassword('mnemonic', MNEMONIC, {
  service: 'msg_chain_key',
  accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE,
  accessible: Keychain.ACCESSIBILITY.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
});

// 读取(需要生物识别)
const creds = await Keychain.getGenericPassword({
  service: 'msg_chain_key',
  authenticationPrompt: {
    title: '验证身份',
    subtitle: '解锁钱包需要生物识别',
    cancel: '取消',
  },
});

Flutter:使用 flutter_secure_storage

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:local_auth/local_auth.dart';

const storage = FlutterSecureStorage();
await storage.write(key: 'msg_mnemonic', value: mnemonic);

final localAuth = LocalAuthentication();
final authenticated = await localAuth.authenticate(
  localizedReason: '使用生物识别解锁钱包',
  options: AuthenticationOptions(biometricOnly: true),
);

5.2 生物识别确认

所有交易签名操作在发送到 WalletConnect 之前,应在 DApp 侧要求生物识别确认:

import ReactNativeBiometrics from 'react-native-biometrics';

const rnBiometrics = new ReactNativeBiometrics();

async function confirmTransaction(reason: string): Promise<boolean> {
  const { success } = await rnBiometrics.simplePrompt({
    promptMessage: reason,
    cancelButtonText: '取消',
  });
  return success;
}

最佳实践:

5.3 会话超时管理

const SESSION_DURATION = 24 * 60 * 60 * 1000; // 24 小时

function isSessionExpired(timestamp: number): boolean {
  return Date.now() - timestamp > SESSION_DURATION;
}

// 定期检查(每分钟)
setInterval(() => {
  // 检查并清理过期会话
}, 60000);

超时配置建议:

5.4 钓鱼防护

// 验证接收地址
function isValidMsgAddress(addr: string): boolean {
  return /^msg1[a-z0-9]{38}$/.test(addr);
}

// 域名白名单
const ALLOWED_DOMAINS = ['https://app.msg-chain.com', 'https://dapp.msg-chain.com'];

function verifyOrigin(url: string): boolean {
  return ALLOWED_DOMAINS.some(d => url.startsWith(d));
}

安全清单:

5.5 网络通信安全

// 使用 HTTPS/WSS 连接
const RPC_URL = 'https://rpc.msg-chain.com';
const RELAY_URL = 'wss://relay.walletconnect.com';

// Android: android:usesCleartextTraffic="false" 禁用明文
// iOS: App Transport Security (ATS) 默认启用 HTTPS

5.6 其他安全建议

项目 建议措施
日志 不记录私钥、助记词、签名原文
截图 Android 设置 FLAG_SECURE,iOS 使用 UITextField 保护
剪贴板 敏感信息在 30 秒后自动清除剪贴板
后台运行 切到后台时清空钱包界面,锁定应用
版本更新 强制检查签名,防止包篡改
第三方库 定期更新依赖,审计未维护的包
测试网 主网和测试网使用不同的配置文件和存储空间

6. 部署指南

6.1 WalletConnect 项目注册

  1. 访问 https://cloud.walletconnect.com
  2. 注册账号并创建新项目
  3. 获取 Project ID
  4. 配置支持的链:添加 cosmos:msg-chain-1
  5. 填入 DApp 名称、URL 和图标
  6. 在生产环境前验证连接

6.2 环境配置管理

// src/config/env.ts
const ENV = {
  development: {
    rpcUrl: 'https://testnet-rpc.msg-chain.com',
    restUrl: 'https://testnet-api.msg-chain.com',
    chainId: 'msg-chain-test-1',
    wcProjectId: 'TEST_PROJECT_ID',
  },
  production: {
    rpcUrl: 'https://rpc.msg-chain.com',
    restUrl: 'https://api.msg-chain.com',
    chainId: 'msg-chain-1',
    wcProjectId: 'PROD_PROJECT_ID',
  },
}[__DEV__ ? 'development' : 'production'];

6.3 iOS 发布配置

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/>
</dict>

App Store 审核注意事项:

6.4 Android 发布配置

// android/app/build.gradle
android {
  compileSdk 34
  defaultConfig {
    minSdk 24
    targetSdk 34
  }
}

6.5 CI/CD 集成

# .github/workflows/ci.yml
name: MSG DApp CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
  build-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with: { distribution: 'zulu', java-version: '17' }
      - run: npm ci
      - run: npx react-native build-android --mode release

6.6 安全检查清单

6.7 性能优化建议

优化项 说明
连接池 复用 CosmWasmClient 实例,避免频繁创建
缓存 缓存账户序列号和合约查询结果
Gas 估算 先 simulate 再发送,避免 Gas 不足回滚
批量查询 合并多个查询请求,减少 RPC 调用
懒加载 合约查询结果懒加载,分页显示历史
WebSocket 使用 ws 连接替代轮询获取最新区块

6.8 常见问题排查

1. WalletConnect 连接失败

2. 交易广播后无响应

3. 生物识别无法使用

4. 合约查询返回空

6.9 相关资源

资源 地址
MSG Chain 官方 https://msg-chain.com
区块浏览器 https://msg-scan.com
RPC 端点 https://rpc.msg-chain.com
REST API https://api.msg-chain.com
CosmJS 文档 https://cosmjs.tech
WalletConnect 文档 https://docs.walletconnect.com
Keplr 文档 https://docs.keplr.app
CosmWasm 文档 https://docs.cosmwasm.com
React Native 文档 https://reactnative.dev
Flutter 文档 https://flutter.dev