dApp Docs/AMM_DEX完整实现
Development reference. Not independently verified for production.

MSG Chain AMM DEX 完整实现指南

基于 CosmWasm 的 Uniswap V2 风格自动化做市商(AMM)DEX 完整实现
适用链:MSG Chain (msg-chain-1) | 共识:Round-Robin + DAR | 签名:Dilithium-5
作者:MSG Chain 开发团队
主网状态:No-Go


目录

  1. 概述与架构设计
  2. 项目结构与 Cargo 配置
  3. Part 1: Factory 合约
  4. Part 2: Pair/AMM 合约
  5. Part 3: Router 合约
  6. Part 4: LP Token 实现
  7. Part 5: 单元测试与集成测试
  8. Part 6: 前端实现 (React + TypeScript + Tailwind)
  9. Part 7: 部署脚本与 Docker

1. 概述与架构设计

1.1 什么是 AMM DEX?

自动化做市商(Automated Market Maker, AMM)是一种去中心化交易所(DEX)模型,使用数学公式代替传统订单簿来定价资产。本实现采用 Uniswap V2 风格的恒定乘积公式:

x * y = k

其中 x 和 y 是池中两种资产的储备量,k 是恒定乘积。在任何交易前后,乘积 k 保持不变(扣除手续费后 k 会略微增加)。

1.2 MSG Chain 特性

参数 值
Chain ID msg-chain-1
Bech32 前缀 msg
CoinType 118
原生代币 umsg(18 位小数)
Gas 价格 1,000,000,000 attoMSG/gas
出块时间 5 秒
共识机制 Round-Robin + DAR (Dynamic Awareness Routing)
签名算法 Dilithium-5(后量子密码学)
智能合约 CosmWasm 1.x

1.3 合约架构总览

+-----------------------------------------------------------+
|                    DEX Frontend (React)                     |
+-----------------------------------------------------------+
|                    Router Contract                          |
|          多跳交换 / 流动性管理 / 路由                         |
+------------------+------------------+----------------------+
|   Pair A-B       |   Pair B-C       |   Pair A-C           |
|   (x*y=k)        |   (x*y=k)        |   (x*y=k)            |
|   LP Token: LP-A |   LP Token: LP-B |   LP Token: LP-C     |
+------------------+------------------+----------------------+
|                  Factory Contract                           |
|          创建池子 / 存储配置 / 追踪所有 Pair                  |
+-----------------------------------------------------------+
|                    MSG Chain                                |
|          CosmWasm 1.x / Dilithium-5 / RR+DAR                |
+-----------------------------------------------------------+

1.4 核心公式

交换公式(输入 dx,输出 dy):

dy = (dx * y) / (x + dx)   (不考虑手续费)
dy = (dx * y * 997) / (x * 1000 + dx * 997)  (含 0.3% 手续费)

LP 代币铸造公式(首次提供流动性):

liquidity = sqrt(x * y) - MINIMUM_LIQUIDITY

LP 代币铸造公式(后续添加流动性):

liquidity = min(
    (amount_x * total_liquidity) / reserve_x,
    (amount_y * total_liquidity) / reserve_y
)

LP 代币销毁公式(移除流动性):

amount_x = (liquidity * reserve_x) / total_liquidity
amount_y = (liquidity * reserve_y) / total_liquidity

2. 项目结构与 Cargo 配置

2.1 整体项目结构

msg-dex/
├── Cargo.toml                    # 工作区根配置
├── contracts/
│   ├── factory/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       ├── contract.rs
│   │       ├── state.rs
│   │       ├── msg.rs
│   │       ├── error.rs
│   │       └── response.rs
│   ├── pair/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs
│   │       ├── contract.rs
│   │       ├── state.rs
│   │       ├── msg.rs
│   │       ├── error.rs
│   │       └── math.rs
│   └── router/
│       ├── Cargo.toml
│       └── src/
│           ├── lib.rs
│           ├── contract.rs
│           ├── state.rs
│           ├── msg.rs
│           └── error.rs
├── packages/
│   └── dex-types/
│       ├── Cargo.toml
│       └── src/
│           └── lib.rs
├── scripts/
│   ├── deploy.sh
│   ├── deploy.ts
│   └── config.ts
├── frontend/
│   ├── package.json
│   ├── tsconfig.json
│   ├── tailwind.config.js
│   ├── vite.config.ts
│   └── src/
│       ├── App.tsx
│       ├── main.tsx
│       ├── hooks/
│       ├── components/
│       ├── pages/
│       └── utils/
├── docker-compose.yml
└── README.md

2.2 工作区根 Cargo.toml

[workspace]
members = [
    "contracts/*",
    "packages/*",
]
resolver = "2"

[workspace.package]
version = "1.0.0"
license = "MIT"
edition = "2021"

[workspace.dependencies]
cosmwasm-std = "1.5"
cosmwasm-storage = "1.5"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw20 = "1.1"
cw20-base = "1.1"
cw-multi-test = "1.1"
thiserror = "1"
serde = { version = "1", features = ["derive"] }
serde-json-wasm = "1"
schemars = "0.8"
decimal = "2"
prost = "0.12"
uint = "0.9"

2.3 dex-types 包

# packages/dex-types/Cargo.toml
[package]
name = "dex-types"
version.workspace = true
license.workspace = true
edition.workspace = true

[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw20.workspace = true
serde.workspace = true
schemars.workspace = true
uint.workspace = true
// packages/dex-types/src/lib.rs
use cosmwasm_std::{Addr, Decimal, Uint128};
use cw20::Cw20Coin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum AssetInfo {
    NativeToken { denom: String },
    Token { contract_addr: Addr },
}

impl fmt::Display for AssetInfo {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AssetInfo::NativeToken { denom } => write!(f, "{}", denom),
            AssetInfo::Token { contract_addr } => write!(f, "{}", contract_addr),
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Asset {
    pub info: AssetInfo,
    pub amount: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PairInfo {
    pub asset_infos: [AssetInfo; 2],
    pub contract_addr: Addr,
    pub liquidity_token: Addr,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum SwapOperation {
    NativeSwap {
        offer_denom: String,
        ask_denom: String,
    },
    CosmWasmSwap {
        offer_asset_info: AssetInfo,
        ask_asset_info: AssetInfo,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct CumulativePrice {
    pub price0_cumulative_last: Uint128,
    pub price1_cumulative_last: Uint128,
    pub block_timestamp_last: u64,
}

3. Part 1: Factory 合约

Factory 合约负责创建和管理所有 Pair 合约。它是 DEX 的注册中心。

3.1 Factory Cargo.toml

# contracts/factory/Cargo.toml
[package]
name = "factory"
version.workspace = true
license.workspace = true
edition.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }

[dev-dependencies]
cw-multi-test.workspace = true

3.2 Factory state.rs

// contracts/factory/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::{Item, Map};
use dex_types::AssetInfo;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub owner: Addr,
    pub fee_to: Option<Addr>,
    pub pair_code_id: u64,
    pub fee_bps: u16,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const PAIRS: Map<&[u8], Addr> = Map::new("pairs");
pub const PAIR_LIST: Map<u64, Addr> = Map::new("pair_list");
pub const PAIR_COUNT: Item<u64> = Item::new("pair_count");

pub fn pair_key(asset_infos: &[AssetInfo; 2]) -> Vec<u8> {
    let mut key = Vec::new();
    let (a, b) = if asset_infos[0] <= asset_infos[1] {
        (&asset_infos[0], &asset_infos[1])
    } else {
        (&asset_infos[1], &asset_infos[0])
    };
    key.extend_from_slice(a.to_string().as_bytes());
    key.push(b'-');
    key.extend_from_slice(b.to_string().as_bytes());
    key
}

3.3 Factory msg.rs

// contracts/factory/src/msg.rs
use cosmwasm_std::{Binary, Uint128};
use dex_types::{AssetInfo, PairInfo};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub owner: String,
    pub fee_to: Option<String>,
    pub pair_code_id: u64,
    pub fee_bps: Option<u16>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
    CreatePair {
        asset_infos: [AssetInfo; 2],
        init_params: Option<Binary>,
    },
    UpdateConfig {
        owner: Option<String>,
        fee_to: Option<String>,
        fee_bps: Option<u16>,
        pair_code_id: Option<u64>,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
    Pair { asset_infos: [AssetInfo; 2] },
    Pairs { start_after: Option<[AssetInfo; 2]>, limit: Option<u32> },
    Config {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PairResponse {
    pub asset_infos: [AssetInfo; 2],
    pub contract_addr: String,
    pub liquidity_token: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PairsResponse {
    pub pairs: Vec<PairResponse>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
    pub owner: String,
    pub fee_to: Option<String>,
    pub pair_code_id: u64,
    pub fee_bps: u16,
}

pub const REPLY_CREATE_PAIR: u64 = 1;

3.4 Factory error.rs

// contracts/factory/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Pair already exists")]
    PairAlreadyExists {},

    #[error("Duplicate assets")]
    DuplicateAssets {},

    #[error("Invalid fee bps: {fee_bps}, max allowed: {max}")]
    InvalidFeeBps { fee_bps: u16, max: u16 },

    #[error("Pair code ID not set")]
    PairCodeIdNotSet {},

    #[error("Invalid reply: {msg}")]
    InvalidReply { msg: String },
}

3.5 Factory contract.rs

// contracts/factory/src/contract.rs
use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo,
    Order, Reply, Response, SubMsg, WasmMsg,
};
use cw2::set_contract_version;
use dex_types::{AssetInfo, PairInfo};

use crate::error::ContractError;
use crate::msg::{
    ConfigResponse, ExecuteMsg, InstantiateMsg, PairResponse, PairsResponse,
    QueryMsg, REPLY_CREATE_PAIR,
};
use crate::state::{pair_key, Config, CONFIG, PAIRS, PAIR_LIST, PAIR_COUNT};

const CONTRACT_NAME: &str = "msg-dex-factory";
const CONTRACT_VERSION: &str = "1.0.0";
const MAX_FEE_BPS: u16 = 1000;
const DEFAULT_FEE_BPS: u16 = 30;

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    let owner = deps.api.addr_validate(&msg.owner)?;
    let fee_to = match msg.fee_to {
        Some(addr) => Some(deps.api.addr_validate(&addr)?),
        None => None,
    };
    let fee_bps = msg.fee_bps.unwrap_or(DEFAULT_FEE_BPS);

    if fee_bps > MAX_FEE_BPS {
        return Err(ContractError::InvalidFeeBps { fee_bps, max: MAX_FEE_BPS });
    }

    let config = Config { owner, fee_to, pair_code_id: msg.pair_code_id, fee_bps };
    CONFIG.save(deps.storage, &config)?;
    PAIR_COUNT.save(deps.storage, &0u64)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", &config.owner))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreatePair { asset_infos, init_params: _ } => {
            execute_create_pair(deps, env, info, asset_infos)
        }
        ExecuteMsg::UpdateConfig { owner, fee_to, fee_bps, pair_code_id } => {
            execute_update_config(deps, info, owner, fee_to, fee_bps, pair_code_id)
        }
    }
}

pub fn execute_create_pair(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    asset_infos: [AssetInfo; 2],
) -> Result<Response, ContractError> {
    if asset_infos[0] == asset_infos[1] {
        return Err(ContractError::DuplicateAssets {});
    }

    let sorted = if asset_infos[0] <= asset_infos[1] {
        asset_infos
    } else {
        [asset_infos[1].clone(), asset_infos[0].clone()]
    };

    let key = pair_key(&sorted);
    if PAIRS.has(deps.storage, &key) {
        return Err(ContractError::PairAlreadyExists {});
    }

    let config = CONFIG.load(deps.storage)?;
    if config.pair_code_id == 0 {
        return Err(ContractError::PairCodeIdNotSet {});
    }

    let init_msg = dex_types::pair::InstantiateMsg {
        asset_infos: sorted.clone(),
        factory_addr: env.contract.address.to_string(),
        lp_token_code_id: 0,
        fee_bps: config.fee_bps,
    };

    let msg = WasmMsg::Instantiate {
        admin: Some(config.owner.to_string()),
        code_id: config.pair_code_id,
        msg: to_binary(&init_msg)?,
        funds: vec![],
        label: format!("MSG-DEX-Pair-{}-{}", sorted[0].to_string(), sorted[1].to_string()),
    };

    let sub_msg = SubMsg::reply_on_success(msg, REPLY_CREATE_PAIR);
    cw_storage_plus::Item::new("pending_pair").save(deps.storage, &sorted)?;

    Ok(Response::new()
        .add_submessage(sub_msg)
        .add_attribute("method", "create_pair")
        .add_attribute("asset_0", sorted[0].to_string())
        .add_attribute("asset_1", sorted[1].to_string()))
}

pub fn execute_update_config(
    deps: DepsMut,
    info: MessageInfo,
    owner: Option<String>,
    fee_to: Option<String>,
    fee_bps: Option<u16>,
    pair_code_id: Option<u64>,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner {
        return Err(ContractError::Unauthorized {});
    }

    if let Some(owner) = owner { config.owner = deps.api.addr_validate(&owner)?; }
    if let Some(fee_to) = fee_to { config.fee_to = Some(deps.api.addr_validate(&fee_to)?); }
    if let Some(fee_bps) = fee_bps {
        if fee_bps > MAX_FEE_BPS { return Err(ContractError::InvalidFeeBps { fee_bps, max: MAX_FEE_BPS }); }
        config.fee_bps = fee_bps;
    }
    if let Some(pair_code_id) = pair_code_id { config.pair_code_id = pair_code_id; }

    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("method", "update_config")
        .add_attribute("owner", config.owner.to_string()))
}

#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REPLY_CREATE_PAIR => handle_create_pair_reply(deps, msg),
        _ => Err(ContractError::InvalidReply { msg: format!("Unknown reply id: {}", msg.id) }),
    }
}

fn handle_create_pair_reply(deps: DepsMut, msg: Reply) -> Result<Response, ContractError> {
    let pair_contract = msg.result.into_result()
        .map_err(|e| ContractError::InvalidReply { msg: format!("Reply error: {}", e) })?
        .get_contract_address()
        .ok_or_else(|| ContractError::InvalidReply { msg: "No contract address".to_string() })?;

    let pair_addr = deps.api.addr_validate(&pair_contract)?;
    let asset_infos: [AssetInfo; 2] = cw_storage_plus::Item::new("pending_pair").load(deps.storage)?;
    let pair_info: PairInfo = deps.querier.query_wasm_smart(&pair_addr, &dex_types::pair::QueryMsg::PairInfo {})?;

    let key = pair_key(&asset_infos);
    PAIRS.save(deps.storage, &key, &pair_addr)?;
    let count = PAIR_COUNT.load(deps.storage)?;
    PAIR_LIST.save(deps.storage, &count, &pair_addr)?;
    PAIR_COUNT.save(deps.storage, &(count + 1))?;

    Ok(Response::new()
        .add_attribute("method", "create_pair_reply")
        .add_attribute("pair_contract", pair_contract)
        .add_attribute("liquidity_token", pair_info.liquidity_token.to_string()))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
    match msg {
        QueryMsg::Pair { asset_infos } => Ok(to_binary(&query_pair(deps, asset_infos)?)?),
        QueryMsg::Pairs { start_after, limit } => Ok(to_binary(&query_pairs(deps, start_after, limit)?)?),
        QueryMsg::Config {} => Ok(to_binary(&query_config(deps)?)?),
    }
}

fn query_pair(deps: Deps, asset_infos: [AssetInfo; 2]) -> Result<PairResponse, ContractError> {
    let sorted = if asset_infos[0] <= asset_infos[1] { asset_infos } else { [asset_infos[1].clone(), asset_infos[0].clone()] };
    let key = pair_key(&sorted);
    let pair_addr = PAIRS.load(deps.storage, &key)?;
    let pair_info: PairInfo = deps.querier.query_wasm_smart(&pair_addr, &dex_types::pair::QueryMsg::PairInfo {})?;
    Ok(PairResponse { asset_infos: sorted, contract_addr: pair_info.contract_addr.to_string(), liquidity_token: pair_info.liquidity_token.to_string() })
}

fn query_pairs(deps: Deps, _start_after: Option<[AssetInfo; 2]>, limit: Option<u32>) -> Result<PairsResponse, ContractError> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let pairs: Vec<PairResponse> = PAIR_LIST
        .range(deps.storage, None, None, Order::Ascending)
        .take(limit)
        .collect::<Result<Vec<_>, _>>()?
        .into_iter()
        .map(|(_, addr)| -> Result<PairResponse, ContractError> {
            let pair_info: PairInfo = deps.querier.query_wasm_smart(&addr, &dex_types::pair::QueryMsg::PairInfo {})?;
            Ok(PairResponse { asset_infos: pair_info.asset_infos, contract_addr: pair_info.contract_addr.to_string(), liquidity_token: pair_info.liquidity_token.to_string() })
        })
        .collect::<Result<Vec<_>, _>>()?;
    Ok(PairsResponse { pairs })
}

fn query_config(deps: Deps) -> Result<ConfigResponse, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse { owner: config.owner.to_string(), fee_to: config.fee_to.map(|a| a.to_string()), pair_code_id: config.pair_code_id, fee_bps: config.fee_bps })
}

3.6 Factory lib.rs

// contracts/factory/src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;

4. Part 2: Pair/AMM 合约

这是整个 DEX 的核心。Pair 合约实现了恒定乘积 AMM 算法,管理流动性池和 LP 代币。

4.1 Pair Cargo.toml

# contracts/pair/Cargo.toml
[package]
name = "pair"
version.workspace = true
license.workspace = true
edition.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
cw20.workspace = true
cw20-base = { workspace = true, features = ["library"] }
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }
uint.workspace = true

[dev-dependencies]
cw-multi-test.workspace = true

4.2 Pair state.rs

// contracts/pair/src/state.rs
use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::Item;
use dex_types::AssetInfo;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub factory_addr: Addr,
    pub asset_infos: [AssetInfo; 2],
    pub lp_token_addr: Addr,
    pub fee_bps: u16,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolState {
    pub reserve_0: Uint128,
    pub reserve_1: Uint128,
    pub total_liquidity: Uint128,
    pub price0_cumulative_last: Uint128,
    pub price1_cumulative_last: Uint128,
    pub block_timestamp_last: u64,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const POOL_STATE: Item<PoolState> = Item::new("pool_state");
pub const MINIMUM_LIQUIDITY: Uint128 = Uint128::new(1000);

4.3 Pair msg.rs

// contracts/pair/src/msg.rs
use cosmwasm_std::{Binary, Decimal, Uint128};
use dex_types::Asset;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub asset_infos: [dex_types::AssetInfo; 2],
    pub factory_addr: String,
    pub lp_token_code_id: u64,
    pub fee_bps: u16,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
    Swap {
        offer_asset: Asset,
        belief_price: Option<Decimal>,
        max_spread: Option<Decimal>,
        to: Option<String>,
    },
    ProvideLiquidity {
        assets: [Asset; 2],
        slippage_tolerance: Option<Decimal>,
        auto_stake: Option<String>,
        receiver: Option<String>,
    },
    WithdrawLiquidity {
        amount: Uint128,
        min_assets_to_withdraw: Option<[Asset; 2]>,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
    Pool {},
    Simulation { offer_asset: Asset },
    ReverseSimulation { ask_asset: Asset },
    PairInfo {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PoolResponse {
    pub assets: [dex_types::Asset; 2],
    pub total_liquidity: Uint128,
    pub price0_cumulative_last: Uint128,
    pub price1_cumulative_last: Uint128,
    pub block_timestamp_last: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct SimulationResponse {
    pub return_amount: Uint128,
    pub spread_amount: Uint128,
    pub commission_amount: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ReverseSimulationResponse {
    pub offer_amount: Uint128,
    pub spread_amount: Uint128,
    pub commission_amount: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PairInfoResponse {
    pub asset_infos: [dex_types::AssetInfo; 2],
    pub contract_addr: String,
    pub liquidity_token: String,
}

4.4 Pair error.rs

// contracts/pair/src/error.rs
use cosmwasm_std::{StdError, Uint128};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Insufficient liquidity: {available} < {required}")]
    InsufficientLiquidity { available: Uint128, required: Uint128 },

    #[error("Insufficient output amount: {available} < {required}")]
    InsufficientOutputAmount { available: Uint128, required: Uint128 },

    #[error("Slippage tolerance exceeded: {slippage} > {tolerance}")]
    SlippageExceeded { slippage: String, tolerance: String },

    #[error("Max spread exceeded: {spread} > {max_spread}")]
    MaxSpreadExceeded { spread: String, max_spread: String },

    #[error("Minimum liquidity not met")]
    MinimumLiquidityNotMet {},

    #[error("Invalid offer asset")]
    InvalidOfferAsset {},

    #[error("Asset mismatch")]
    AssetMismatch {},

    #[error("Zero amount not allowed")]
    ZeroAmount {},

    #[error("Doubling assets not allowed")]
    DoublingAssets {},

    #[error("Native token balance mismatch")]
    NativeBalanceMismatch {},

    #[error("CW20 balance mismatch")]
    Cw20BalanceMismatch {},
}

4.5 Pair math.rs (AMM 核心算法)

// contracts/pair/src/math.rs
use cosmwasm_std::{Decimal, Uint128};
use std::cmp::Ordering;

/// 恒定乘积 AMM 数学实现
/// swap 计算公式:dy = (dx * y * (10000 - fee_bps)) / (x * 10000 + dx * (10000 - fee_bps))
pub fn compute_swap(
    amount_in: Uint128,
    reserve_in: Uint128,
    reserve_out: Uint128,
    fee_bps: u16,
) -> Result<(Uint128, Uint128, Uint128), String> {
    if amount_in.is_zero() {
        return Err("Amount in cannot be zero".to_string());
    }
    if reserve_in.is_zero() || reserve_out.is_zero() {
        return Err("Insufficient liquidity".to_string());
    }

    let fee_multiplier = Uint128::from(10000u64 - fee_bps as u64);
    let fee_divisor = Uint128::from(10000u64);

    let amount_in_with_fee = amount_in.checked_mul(fee_multiplier)?;
    let denominator = reserve_in.checked_mul(fee_divisor)?.checked_add(amount_in_with_fee)?;
    let numerator = amount_in_with_fee.checked_mul(reserve_out)?;
    let return_amount = numerator.checked_div(denominator)?;

    let ideal_output = amount_in.checked_mul(reserve_out)?.checked_div(reserve_in)?;
    let spread_amount = ideal_output.checked_sub(return_amount)?;
    let commission_amount = amount_in.checked_mul(Uint128::from(fee_bps as u64))?.checked_div(Uint128::from(10000u64))?;

    Ok((return_amount, spread_amount, commission_amount))
}

/// 反向计算:给定输出量,计算所需输入量
pub fn compute_swap_reverse(
    amount_out: Uint128,
    reserve_in: Uint128,
    reserve_out: Uint128,
    fee_bps: u16,
) -> Result<(Uint128, Uint128, Uint128), String> {
    if amount_out.is_zero() {
        return Err("Amount out cannot be zero".to_string());
    }
    if reserve_in.is_zero() || reserve_out.is_zero() {
        return Err("Insufficient liquidity".to_string());
    }
    if amount_out >= reserve_out {
        return Err("Amount out exceeds reserve".to_string());
    }

    let fee_multiplier = Uint128::from(10000u64 - fee_bps as u64);
    let numerator = reserve_in.checked_mul(amount_out)?.checked_mul(Uint128::from(10000u64))?;
    let denominator = reserve_out.checked_sub(amount_out)?.checked_mul(fee_multiplier)?;
    let offer_amount = numerator.checked_div(denominator)?.checked_add(Uint128::one())?;

    let (return_amount, spread_amount, commission_amount) =
        compute_swap(offer_amount, reserve_in, reserve_out, fee_bps)?;
    Ok((offer_amount, spread_amount, commission_amount))
}

/// 计算 LP 代币铸造量
pub fn compute_lp_tokens_to_mint(
    amount_0: Uint128,
    amount_1: Uint128,
    reserve_0: Uint128,
    reserve_1: Uint128,
    total_liquidity: Uint128,
    minimum_liquidity: Uint128,
) -> Result<Uint128, String> {
    if total_liquidity.is_zero() {
        // 首次提供流动性:liquidity = sqrt(x * y) - MINIMUM_LIQUIDITY
        let sqrt_prod = (amount_0.u128() as u128).checked_mul(amount_1.u128() as u128)
            .map(|prod| (prod as f64).sqrt() as u128)
            .ok_or("Overflow in sqrt calculation")?;
        let liquidity = Uint128::try_from(sqrt_prod).map_err(|_| "Conversion error")?;
        if liquidity < minimum_liquidity {
            return Err("Minimum liquidity not met".to_string());
        }
        Ok(liquidity.checked_sub(minimum_liquidity)?)
    } else {
        // 后续添加:liquidity = min(amount_x * total / reserve_x, amount_y * total / reserve_y)
        let liquidity_0 = amount_0.checked_mul(total_liquidity)? / reserve_0;
        let liquidity_1 = amount_1.checked_mul(total_liquidity)? / reserve_1;
        Ok(std::cmp::min(liquidity_0, liquidity_1))
    }
}

/// 计算 LP 代币销毁后应返回的资产量
pub fn compute_withdraw_amounts(
    liquidity: Uint128,
    reserve_0: Uint128,
    reserve_1: Uint128,
    total_liquidity: Uint128,
) -> Result<(Uint128, Uint128), String> {
    if liquidity > total_liquidity {
        return Err("Liquidity exceeds total".to_string());
    }
    let amount_0 = liquidity.checked_mul(reserve_0)? / total_liquidity;
    let amount_1 = liquidity.checked_mul(reserve_1)? / total_liquidity;
    Ok((amount_0, amount_1))
}

/// 检查滑点容忍度
pub fn check_slippage(expected: Uint128, actual: Uint128, tolerance: Decimal) -> Result<(), String> {
    if expected.is_zero() { return Ok(()); }
    let slippage = Decimal::from_ratio(expected.checked_sub(actual).unwrap_or_default(), expected);
    if slippage > tolerance {
        return Err(format!("Slippage exceeded: {} > {}", slippage, tolerance));
    }
    Ok(())
}

/// 计算价格(用于 TWAP 累计)
pub fn compute_price(reserve_0: Uint128, reserve_1: Uint128) -> (Uint128, Uint128) {
    (reserve_1, reserve_0)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compute_swap_zero_amount() {
        assert!(compute_swap(Uint128::zero(), Uint128::new(1000), Uint128::new(1000), 30).is_err());
    }

    #[test]
    fn test_compute_swap_no_liquidity() {
        assert!(compute_swap(Uint128::new(100), Uint128::zero(), Uint128::new(1000), 30).is_err());
    }

    #[test]
    fn test_compute_swap_basic() {
        let (amount, _spread, commission) =
            compute_swap(Uint128::new(100), Uint128::new(1000), Uint128::new(2000), 30).unwrap();
        assert!(!amount.is_zero());
        assert!(amount <= Uint128::new(200));
        assert_eq!(commission, Uint128::new(3));
    }

    #[test]
    fn test_compute_swap_reverse() {
        let (offer_amount, _, _) = compute_swap_reverse(
            Uint128::new(181), Uint128::new(1000), Uint128::new(2000), 30,
        ).unwrap();
        assert!(!offer_amount.is_zero());
    }

    #[test]
    fn test_compute_lp_tokens_mint_initial() {
        let result = compute_lp_tokens_to_mint(
            Uint128::new(1000), Uint128::new(2000),
            Uint128::zero(), Uint128::zero(), Uint128::zero(), Uint128::new(1000),
        ).unwrap();
        assert_eq!(result, Uint128::new(414));
    }

    #[test]
    fn test_compute_lp_tokens_mint_subsequent() {
        let result = compute_lp_tokens_to_mint(
            Uint128::new(500), Uint128::new(1000),
            Uint128::new(1000), Uint128::new(2000), Uint128::new(10000), Uint128::new(1000),
        ).unwrap();
        assert_eq!(result, Uint128::new(5000));
    }

    #[test]
    fn test_compute_withdraw_amounts() {
        let (amount_0, amount_1) = compute_withdraw_amounts(
            Uint128::new(5000), Uint128::new(1000), Uint128::new(2000), Uint128::new(10000),
        ).unwrap();
        assert_eq!(amount_0, Uint128::new(500));
        assert_eq!(amount_1, Uint128::new(1000));
    }

    #[test]
    fn test_compute_swap_known_vector() {
        // Uniswap V2 已知测试向量
        // x: 100000 DAI, y: 100000 USDC, dx: 1000 DAI
        // dy = 1000 * 100000 * 997 / (100000 * 1000 + 1000 * 997) = 987
        let (amount, _, _) = compute_swap(
            Uint128::new(1000), Uint128::new(100000), Uint128::new(100000), 30,
        ).unwrap();
        assert_eq!(amount, Uint128::new(987));
    }

    #[test]
    fn test_swap_symmetry_pool() {
        let (result, _, _) = compute_swap(
            Uint128::new(1000), Uint128::new(10000), Uint128::new(10000), 30,
        ).unwrap();
        assert!(result < Uint128::new(1000));
        assert!(result > Uint128::new(800));
    }

    #[test]
    fn test_swap_reverse_roundtrip() {
        let (amount_out, _, _) = compute_swap(Uint128::new(1000), Uint128::new(100000), Uint128::new(100000), 30).unwrap();
        let (offer_amount, _, _) = compute_swap_reverse(amount_out, Uint128::new(100000), Uint128::new(100000), 30).unwrap();
        let diff = if offer_amount > Uint128::new(1000) { offer_amount - Uint128::new(1000) } else { Uint128::new(1000) - offer_amount };
        assert!(diff <= Uint128::new(1));
    }

    #[test]
    fn test_minimum_liquidity_enforced() {
        assert!(compute_lp_tokens_to_mint(
            Uint128::new(100), Uint128::new(100),
            Uint128::zero(), Uint128::zero(), Uint128::zero(), Uint128::new(1000),
        ).is_err());
    }

    #[test]
    fn test_zero_fee_swap() {
        let (amount, _, commission) = compute_swap(
            Uint128::new(1000), Uint128::new(100000), Uint128::new(100000), 0,
        ).unwrap();
        assert_eq!(commission, Uint128::zero());
        assert!(!amount.is_zero());
    }

    #[test]
    fn test_high_fee_swap() {
        let (amount, _, commission) = compute_swap(
            Uint128::new(1000), Uint128::new(100000), Uint128::new(100000), 1000,
        ).unwrap();
        assert_eq!(commission, Uint128::new(100));
        assert!(amount < Uint128::new(1000));
    }

    #[test]
    fn test_overflow_safe_large_numbers() {
        let result = compute_swap(
            Uint128::new(1_000_000_000_000_000_000u128),
            Uint128::new(1_000_000_000_000_000_000u128),
            Uint128::new(1_000_000_000_000_000_000u128),
            30,
        );
        assert!(result.is_ok());
    }

    #[test]
    fn test_compute_swap_reverse_exact_amount_out() {
        let (offer_amount, _, _) = compute_swap_reverse(
            Uint128::new(500), Uint128::new(100000), Uint128::new(100000), 30,
        ).unwrap();
        let (back, _, _) = compute_swap(offer_amount, Uint128::new(100000), Uint128::new(100000), 30).unwrap();
        assert_eq!(back, Uint128::new(500));
    }
}

4.6 Pair contract.rs

// contracts/pair/src/contract.rs
use cosmwasm_std::{
    entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut,
    Env, MessageInfo, Reply, Response, StdError, SubMsg, Uint128, WasmMsg,
};
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse};
use cw20_base::msg::InstantiateMsg as Cw20InstantiateMsg;
use cw2::set_contract_version;
use dex_types::{Asset, AssetInfo, PairInfo};

use crate::error::ContractError;
use crate::math::{
    check_slippage, compute_lp_tokens_to_mint, compute_price, compute_swap,
    compute_swap_reverse, compute_withdraw_amounts,
};
use crate::msg::{
    ExecuteMsg, InstantiateMsg, PairInfoResponse, PoolResponse, QueryMsg,
    ReverseSimulationResponse, SimulationResponse,
};
use crate::state::{Config, PoolState, CONFIG, MINIMUM_LIQUIDITY, POOL_STATE};

const CONTRACT_NAME: &str = "msg-dex-pair";
const CONTRACT_VERSION: &str = "1.0.0";
pub const REPLY_CREATE_LP_TOKEN: u64 = 1;

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    if msg.asset_infos[0] == msg.asset_infos[1] {
        return Err(ContractError::DoublingAssets {});
    }

    let config = Config {
        factory_addr: deps.api.addr_validate(&msg.factory_addr)?,
        asset_infos: msg.asset_infos,
        lp_token_addr: Addr::unchecked(""),
        fee_bps: msg.fee_bps,
    };
    CONFIG.save(deps.storage, &config)?;

    let pool_state = PoolState {
        reserve_0: Uint128::zero(),
        reserve_1: Uint128::zero(),
        total_liquidity: Uint128::zero(),
        price0_cumulative_last: Uint128::zero(),
        price1_cumulative_last: Uint128::zero(),
        block_timestamp_last: 0,
    };
    POOL_STATE.save(deps.storage, &pool_state)?;

    let label = format!("MSG-DEX-LP-{}-{}", config.asset_infos[0].to_string(), config.asset_infos[1].to_string());
    let cw20_init_msg = Cw20InstantiateMsg {
        name: format!("MSG DEX LP {}", label),
        symbol: format!("MLP-{}-{}",
            config.asset_infos[0].to_string().chars().take(3).collect::<String>().to_uppercase(),
            config.asset_infos[1].to_string().chars().take(3).collect::<String>().to_uppercase()),
        decimals: 18,
        initial_balances: vec![],
        mint: Some(cw20::MinterResponse { minter: env.contract.address.to_string(), cap: None }),
        marketing: None,
    };

    let instantiate_msg = WasmMsg::Instantiate {
        admin: Some(env.contract.address.to_string()),
        code_id: msg.lp_token_code_id,
        msg: to_binary(&cw20_init_msg)?,
        funds: vec![],
        label,
    };

    Ok(Response::new()
        .add_submessage(SubMsg::reply_on_success(instantiate_msg, REPLY_CREATE_LP_TOKEN))
        .add_attribute("method", "instantiate")
        .add_attribute("factory", msg.factory_addr))
}

#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REPLY_CREATE_LP_TOKEN => {
            let lp_token_addr = msg.result.into_result()
                .map_err(|e| ContractError::Std(StdError::generic_err(e)))?
                .get_contract_address()
                .ok_or_else(|| ContractError::Std(StdError::generic_err("No contract address")))?;
            let mut config = CONFIG.load(deps.storage)?;
            config.lp_token_addr = deps.api.addr_validate(&lp_token_addr)?;
            CONFIG.save(deps.storage, &config)?;
            Ok(Response::new().add_attribute("lp_token_addr", lp_token_addr))
        }
        _ => Err(ContractError::Std(StdError::generic_err(format!("Unknown reply id: {}", msg.id)))),
    }
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Swap { offer_asset, belief_price, max_spread, to } => {
            execute_swap(deps, env, info, offer_asset, belief_price, max_spread, to)
        }
        ExecuteMsg::ProvideLiquidity { assets, slippage_tolerance, auto_stake: _, receiver } => {
            execute_provide_liquidity(deps, env, info, assets, slippage_tolerance, receiver)
        }
        ExecuteMsg::WithdrawLiquidity { amount, min_assets_to_withdraw } => {
            execute_withdraw_liquidity(deps, env, info, amount, min_assets_to_withdraw)
        }
    }
}

pub fn execute_swap(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    offer_asset: Asset,
    belief_price: Option<Decimal>,
    max_spread: Option<Decimal>,
    to: Option<String>,
) -> Result<Response, ContractError> {
    if offer_asset.amount.is_zero() { return Err(ContractError::ZeroAmount {}); }

    let config = CONFIG.load(deps.storage)?;
    let mut pool_state = POOL_STATE.load(deps.storage)?;

    let (offer_reserve, ask_reserve, is_asset_0) = if offer_asset.info == config.asset_infos[0] {
        (pool_state.reserve_0, pool_state.reserve_1, true)
    } else if offer_asset.info == config.asset_infos[1] {
        (pool_state.reserve_1, pool_state.reserve_0, false)
    } else {
        return Err(ContractError::InvalidOfferAsset {});
    };

    // 验证收到的资产
    match &offer_asset.info {
        AssetInfo::NativeToken { denom } => {
            let received = info.funds.iter().find(|c| c.denom == *denom).map(|c| c.amount).unwrap_or(Uint128::zero());
            if received < offer_asset.amount { return Err(ContractError::NativeBalanceMismatch {}); }
        }
        AssetInfo::Token { .. } => {}
    }

    let (return_amount, spread_amount, commission_amount) = compute_swap(
        offer_asset.amount, offer_reserve, ask_reserve, config.fee_bps,
    ).map_err(|e| ContractError::Std(StdError::generic_err(e)))?;

    // 滑点/价格保护检查
    if let Some(max_spread) = max_spread {
        let max_allowed = Decimal::from_ratio(ask_reserve, offer_reserve).checked_mul(max_spread)
            .map_err(|_| ContractError::Std(StdError::generic_err("overflow")))?.to_uint_floor();
        if spread_amount > max_allowed {
            return Err(ContractError::MaxSpreadExceeded { spread: spread_amount.to_string(), max_spread: max_allowed.to_string() });
        }
    }
    if let Some(belief_price) = belief_price {
        let executed_price = Decimal::from_ratio(return_amount, offer_asset.amount);
        if executed_price < belief_price {
            return Err(ContractError::SlippageExceeded { slippage: executed_price.to_string(), tolerance: belief_price.to_string() });
        }
    }

    // 更新储备
    if is_asset_0 {
        pool_state.reserve_0 = pool_state.reserve_0.checked_add(offer_asset.amount)?;
        pool_state.reserve_1 = pool_state.reserve_1.checked_sub(return_amount)?;
    } else {
        pool_state.reserve_1 = pool_state.reserve_1.checked_add(offer_asset.amount)?;
        pool_state.reserve_0 = pool_state.reserve_0.checked_sub(return_amount)?;
    }

    // 更新 TWAP 价格累计
    if pool_state.block_timestamp_last > 0 {
        let time_elapsed = env.block.time.seconds() - pool_state.block_timestamp_last;
        if time_elapsed > 0 && !pool_state.reserve_0.is_zero() && !pool_state.reserve_1.is_zero() {
            pool_state.price0_cumulative_last = pool_state.price0_cumulative_last
                .checked_add(pool_state.reserve_1.checked_mul(Uint128::from(time_elapsed))? / pool_state.reserve_0)?;
            pool_state.price1_cumulative_last = pool_state.price1_cumulative_last
                .checked_add(pool_state.reserve_0.checked_mul(Uint128::from(time_elapsed))? / pool_state.reserve_1)?;
        }
    }
    pool_state.block_timestamp_last = env.block.time.seconds();
    POOL_STATE.save(deps.storage, &pool_state)?;

    let receiver = deps.api.addr_validate(&to.unwrap_or_else(|| info.sender.to_string()))?;
    let output_idx = if is_asset_0 { 1 } else { 0 };
    let transfer_msg = match &config.asset_infos[output_idx] {
        AssetInfo::NativeToken { denom } => {
            CosmosMsg::Bank(BankMsg::Send { to_address: receiver.to_string(), assets: vec![Coin { denom: denom.clone(), amount: return_amount }] })
        }
        AssetInfo::Token { contract_addr } => {
            CosmosMsg::Wasm(WasmMsg::Execute {
                contract_addr: contract_addr.to_string(),
                msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: receiver.to_string(), amount: return_amount })?,
                funds: vec![],
            })
        }
    };

    Ok(Response::new()
        .add_message(transfer_msg)
        .add_attribute("method", "swap")
        .add_attribute("offer_amount", offer_asset.amount)
        .add_attribute("return_amount", return_amount)
        .add_attribute("spread_amount", spread_amount)
        .add_attribute("commission_amount", commission_amount))
}

pub fn execute_provide_liquidity(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    assets: [Asset; 2],
    slippage_tolerance: Option<Decimal>,
    receiver: Option<String>,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let mut pool_state = POOL_STATE.load(deps.storage)?;

    if assets[0].info != config.asset_infos[0] || assets[1].info != config.asset_infos[1] {
        return Err(ContractError::AssetMismatch {});
    }
    if assets[0].amount.is_zero() || assets[1].amount.is_zero() {
        return Err(ContractError::ZeroAmount {});
    }

    let (amount_0, amount_1) = if pool_state.total_liquidity.is_zero() {
        (assets[0].amount, assets[1].amount)
    } else {
        let expected_1 = assets[0].amount.checked_mul(pool_state.reserve_1)?.checked_div(pool_state.reserve_0)?;
        if assets[1].amount < expected_1 {
            if let Some(tolerance) = slippage_tolerance {
                let ratio = Decimal::from_ratio(assets[1].amount, expected_1);
                let min_ratio = Decimal::one().checked_sub(tolerance).map_err(|_| StdError::generic_err("overflow"))?;
                if ratio < min_ratio {
                    return Err(ContractError::SlippageExceeded { slippage: ratio.to_string(), tolerance: min_ratio.to_string() });
                }
            }
            (assets[0].amount, assets[1].amount)
        } else {
            (assets[0].amount, expected_1)
        }
    };

    let liquidity = compute_lp_tokens_to_mint(amount_0, amount_1, pool_state.reserve_0, pool_state.reserve_1, pool_state.total_liquidity, MINIMUM_LIQUIDITY)
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?;

    pool_state.reserve_0 = pool_state.reserve_0.checked_add(amount_0)?;
    pool_state.reserve_1 = pool_state.reserve_1.checked_add(amount_1)?;
    pool_state.total_liquidity = pool_state.total_liquidity.checked_add(liquidity)?;
    POOL_STATE.save(deps.storage, &pool_state)?;

    let recipient = receiver.unwrap_or_else(|| info.sender.to_string());
    let mint_msg = Cw20ExecuteMsg::Mint { recipient, amount: liquidity };

    let mut response = Response::new()
        .add_message(WasmMsg::Execute {
            contract_addr: config.lp_token_addr.to_string(),
            msg: to_binary(&mint_msg)?, funds: vec![],
        })
        .add_attribute("method", "provide_liquidity")
        .add_attribute("amount_0", amount_0)
        .add_attribute("amount_1", amount_1)
        .add_attribute("liquidity", liquidity);

    // 首次流动性时销毁 MINIMUM_LIQUIDITY
    if pool_state.total_liquidity == liquidity.checked_add(MINIMUM_LIQUIDITY)? {
        let burn_msg = Cw20ExecuteMsg::Burn { amount: MINIMUM_LIQUIDITY };
        response = response.add_message(WasmMsg::Execute {
            contract_addr: config.lp_token_addr.to_string(),
            msg: to_binary(&burn_msg)?, funds: vec![],
        }).add_attribute("is_initial", "true");
    }

    Ok(response)
}

pub fn execute_withdraw_liquidity(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    amount: Uint128,
    min_assets_to_withdraw: Option<[Asset; 2]>,
) -> Result<Response, ContractError> {
    if amount.is_zero() { return Err(ContractError::ZeroAmount {}); }

    let config = CONFIG.load(deps.storage)?;
    let mut pool_state = POOL_STATE.load(deps.storage)?;

    let (withdraw_0, withdraw_1) = compute_withdraw_amounts(amount, pool_state.reserve_0, pool_state.reserve_1, pool_state.total_liquidity)
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?;

    // 滑点保护
    if let Some(min_assets) = min_assets_to_withdraw {
        let min_0 = min_assets.iter().find(|a| a.info == config.asset_infos[0]).map(|a| a.amount).unwrap_or(Uint128::zero());
        let min_1 = min_assets.iter().find(|a| a.info == config.asset_infos[1]).map(|a| a.amount).unwrap_or(Uint128::zero());
        if withdraw_0 < min_0 || withdraw_1 < min_1 {
            return Err(ContractError::InsufficientOutputAmount {
                available: if withdraw_0 < min_0 { withdraw_0 } else { withdraw_1 },
                required: if withdraw_0 < min_0 { min_0 } else { min_1 },
            });
        }
    }

    pool_state.reserve_0 = pool_state.reserve_0.checked_sub(withdraw_0)?;
    pool_state.reserve_1 = pool_state.reserve_1.checked_sub(withdraw_1)?;
    pool_state.total_liquidity = pool_state.total_liquidity.checked_sub(amount)?;
    POOL_STATE.save(deps.storage, &pool_state)?;

    // 销毁 LP 代币 + 返回资产
    let burn_msg = Cw20ExecuteMsg::BurnFrom { owner: info.sender.to_string(), amount };
    let mut messages: Vec<CosmosMsg> = vec![
        CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: config.lp_token_addr.to_string(),
            msg: to_binary(&burn_msg)?, funds: vec![],
        }),
    ];

    for (i, &withdraw_amount) in [withdraw_0, withdraw_1].iter().enumerate() {
        let msg: CosmosMsg = match &config.asset_infos[i] {
            AssetInfo::NativeToken { denom } => {
                CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), assets: vec![Coin { denom: denom.clone(), amount: withdraw_amount }] })
            }
            AssetInfo::Token { contract_addr } => {
                CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: contract_addr.to_string(),
                    msg: to_binary(&Cw20ExecuteMsg::Transfer { recipient: info.sender.to_string(), amount: withdraw_amount })?,
                    funds: vec![],
                })
            }
        };
        messages.push(msg);
    }

    Ok(Response::new()
        .add_messages(messages)
        .add_attribute("method", "withdraw_liquidity")
        .add_attribute("liquidity_burned", amount)
        .add_attribute("withdraw_0", withdraw_0)
        .add_attribute("withdraw_1", withdraw_1))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
    match msg {
        QueryMsg::Pool {} => Ok(to_binary(&query_pool(deps)?)?),
        QueryMsg::Simulation { offer_asset } => Ok(to_binary(&query_simulation(deps, offer_asset)?)?),
        QueryMsg::ReverseSimulation { ask_asset } => Ok(to_binary(&query_reverse_simulation(deps, ask_asset)?)?),
        QueryMsg::PairInfo {} => Ok(to_binary(&query_pair_info(deps)?)?),
    }
}

fn query_pool(deps: Deps) -> Result<PoolResponse, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let pool_state = POOL_STATE.load(deps.storage)?;
    Ok(PoolResponse {
        assets: [Asset { info: config.asset_infos[0].clone(), amount: pool_state.reserve_0 },
                 Asset { info: config.asset_infos[1].clone(), amount: pool_state.reserve_1 }],
        total_liquidity: pool_state.total_liquidity,
        price0_cumulative_last: pool_state.price0_cumulative_last,
        price1_cumulative_last: pool_state.price1_cumulative_last,
        block_timestamp_last: pool_state.block_timestamp_last,
    })
}

fn query_simulation(deps: Deps, offer_asset: Asset) -> Result<SimulationResponse, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let pool_state = POOL_STATE.load(deps.storage)?;
    let (offer_reserve, ask_reserve) = if offer_asset.info == config.asset_infos[0] {
        (pool_state.reserve_0, pool_state.reserve_1)
    } else if offer_asset.info == config.asset_infos[1] {
        (pool_state.reserve_1, pool_state.reserve_0)
    } else { return Err(ContractError::InvalidOfferAsset {}); };

    let (return_amount, spread_amount, commission_amount) = compute_swap(offer_asset.amount, offer_reserve, ask_reserve, config.fee_bps)
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?;
    Ok(SimulationResponse { return_amount, spread_amount, commission_amount })
}

fn query_reverse_simulation(deps: Deps, ask_asset: Asset) -> Result<ReverseSimulationResponse, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let pool_state = POOL_STATE.load(deps.storage)?;
    let (offer_reserve, ask_reserve) = if ask_asset.info == config.asset_infos[0] {
        (pool_state.reserve_1, pool_state.reserve_0)
    } else if ask_asset.info == config.asset_infos[1] {
        (pool_state.reserve_0, pool_state.reserve_1)
    } else { return Err(ContractError::InvalidOfferAsset {}); };

    let (offer_amount, spread_amount, commission_amount) = compute_swap_reverse(ask_asset.amount, offer_reserve, ask_reserve, config.fee_bps)
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?;
    Ok(ReverseSimulationResponse { offer_amount, spread_amount, commission_amount })
}

fn query_pair_info(deps: Deps) -> Result<PairInfoResponse, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    Ok(PairInfoResponse {
        asset_infos: config.asset_infos,
        contract_addr: deps.api.addr_to_string(&deps.api.addr_canonicalize("")?),
        liquidity_token: config.lp_token_addr.to_string(),
    })
}

4.7 Pair lib.rs

// contracts/pair/src/lib.rs
pub mod contract;
pub mod error;
pub mod math;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;

5. Part 3: Router 合约

Router 合约处理多跳交换和流动性管理路由。

5.1 Router Cargo.toml

# contracts/router/Cargo.toml
[package]
name = "router"
version.workspace = true
license.workspace = true
edition.workspace = true

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-std.workspace = true
cw-storage-plus.workspace = true
cw2.workspace = true
serde.workspace = true
schemars.workspace = true
thiserror.workspace = true
cosmwasm-schema.workspace = true
dex-types = { path = "../../packages/dex-types" }
pair = { path = "../pair" }
uint.workspace = true

[dev-dependencies]
cw-multi-test.workspace = true

5.2 Router state.rs

// contracts/router/src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub factory_addr: Addr,
}

pub const CONFIG: Item<Config> = Item::new("config");

5.3 Router msg.rs

// contracts/router/src/msg.rs
use cosmwasm_std::{Decimal, Uint128};
use dex_types::{Asset, AssetInfo, SwapOperation};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub factory_addr: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ExecuteMsg {
    ExecuteSwapOperations {
        operations: Vec<SwapOperation>,
        minimum_receive: Option<Uint128>,
        to: Option<String>,
    },
    ExecuteSwapOperation {
        operation: SwapOperation,
        to: Option<String>,
    },
    ProvideLiquidity {
        pair_contract: String,
        assets: [Asset; 2],
        slippage_tolerance: Option<Decimal>,
    },
    WithdrawLiquidity {
        pair_contract: String,
        amount: Uint128,
        min_assets_to_withdraw: Option<[Asset; 2]>,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum QueryMsg {
    Config {},
    SimulateSwapOperations {
        offer_amount: Uint128,
        operations: Vec<SwapOperation>,
    },
}

5.4 Router error.rs

// contracts/router/src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("Insufficient output: {received} < {minimum}")]
    InsufficientOutput { received: String, minimum: String },

    #[error("Invalid operation")]
    InvalidOperation {},

    #[error("Empty operations")]
    EmptyOperations {},
}

5.5 Router contract.rs

// contracts/router/src/contract.rs
use cosmwasm_std::{
    entry_point, to_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env,
    MessageInfo, Response, StdError, Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::Cw20ExecuteMsg;
use dex_types::{Asset, AssetInfo, PairInfo, SwapOperation};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{Config, CONFIG};

const CONTRACT_NAME: &str = "msg-dex-router";
const CONTRACT_VERSION: &str = "1.0.0";

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    let config = Config { factory_addr: deps.api.addr_validate(&msg.factory_addr)? };
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new().add_attribute("method", "instantiate").add_attribute("factory", msg.factory_addr))
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::ExecuteSwapOperations { operations, minimum_receive, to } => {
            execute_swap_operations(deps, env, info, operations, minimum_receive, to)
        }
        ExecuteMsg::ExecuteSwapOperation { operation, to } => {
            execute_swap_operation(deps, env, info, operation, to)
        }
        ExecuteMsg::ProvideLiquidity { pair_contract, assets, slippage_tolerance } => {
            execute_provide_liquidity(deps, info, pair_contract, assets, slippage_tolerance)
        }
        ExecuteMsg::WithdrawLiquidity { pair_contract, amount, min_assets_to_withdraw } => {
            execute_withdraw_liquidity(deps, info, pair_contract, amount, min_assets_to_withdraw)
        }
    }
}

pub fn execute_swap_operations(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    operations: Vec<SwapOperation>,
    minimum_receive: Option<Uint128>,
    to: Option<String>,
) -> Result<Response, ContractError> {
    if operations.is_empty() { return Err(ContractError::EmptyOperations {}); }
    let config = CONFIG.load(deps.storage)?;
    let mut messages: Vec<CosmosMsg> = vec![];
    let mut prev_asset: Option<Asset> = None;

    let pairs: Vec<PairInfo> = deps.querier.query_wasm_smart(
        &config.factory_addr,
        &dex_types::factory::QueryMsg::Pairs { start_after: None, limit: None },
    )?;

    for (i, operation) in operations.iter().enumerate() {
        let is_last = i == operations.len() - 1;
        let to_addr = if is_last {
            to.clone().unwrap_or_else(|| info.sender.to_string())
        } else {
            env.contract.address.to_string()
        };

        match operation {
            SwapOperation::NativeSwap { offer_denom, ask_denom } => {
                let pair = pairs.iter().find(|p| {
                    let has_offer = p.asset_infos.iter().any(|a| match a {
                        AssetInfo::NativeToken { denom } => denom == offer_denom, _ => false,
                    });
                    let has_ask = p.asset_infos.iter().any(|a| match a {
                        AssetInfo::NativeToken { denom } => denom == ask_denom, _ => false,
                    });
                    has_offer && has_ask
                }).ok_or_else(|| ContractError::InvalidOperation {})?;

                let offer_amount = if i == 0 {
                    info.funds.iter().find(|c| c.denom == *offer_denom).map(|c| c.amount).unwrap_or(Uint128::zero())
                } else if let Some(ref prev) = prev_asset { prev.amount } else { Uint128::zero() };

                if offer_amount.is_zero() { return Err(ContractError::InvalidOperation {}); }

                let swap_msg = CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: pair.contract_addr.to_string(),
                    msg: to_binary(&pair::msg::ExecuteMsg::Swap {
                        offer_asset: Asset { info: AssetInfo::NativeToken { denom: offer_denom.clone() }, amount: offer_amount },
                        belief_price: None, max_spread: None, to: Some(to_addr),
                    })?,
                    funds: if i == 0 { info.funds.clone() } else { vec![] },
                });
                messages.push(swap_msg);
                prev_asset = Some(Asset { info: AssetInfo::NativeToken { denom: ask_denom.clone() }, amount: Uint128::zero() });
            }
            SwapOperation::CosmWasmSwap { offer_asset_info, ask_asset_info } => {
                let pair = pairs.iter().find(|p| {
                    p.asset_infos.contains(offer_asset_info) && p.asset_infos.contains(ask_asset_info)
                }).ok_or_else(|| ContractError::InvalidOperation {})?;

                let offer_amount = if i == 0 {
                    match offer_asset_info {
                        AssetInfo::NativeToken { denom } => info.funds.iter().find(|c| c.denom == *denom).map(|c| c.amount).unwrap_or(Uint128::zero()),
                        AssetInfo::Token { .. } => Uint128::zero(),
                    }
                } else if let Some(ref prev) = prev_asset { prev.amount } else { Uint128::zero() };

                if offer_amount.is_zero() { return Err(ContractError::InvalidOperation {}); }

                let funds = if i == 0 {
                    match offer_asset_info {
                        AssetInfo::NativeToken { denom } => vec![Coin { denom: denom.clone(), amount: offer_amount }],
                        AssetInfo::Token { .. } => vec![],
                    }
                } else { vec![] };

                messages.push(CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: pair.contract_addr.to_string(),
                    msg: to_binary(&pair::msg::ExecuteMsg::Swap {
                        offer_asset: Asset { info: offer_asset_info.clone(), amount: offer_amount },
                        belief_price: None, max_spread: None, to: Some(to_addr),
                    })?,
                    funds,
                }));
                prev_asset = Some(Asset { info: ask_asset_info.clone(), amount: Uint128::zero() });
            }
        }
    }

    Ok(Response::new()
        .add_messages(messages)
        .add_attribute("method", "execute_swap_operations")
        .add_attribute("operations_count", operations.len().to_string()))
}

pub fn execute_swap_operation(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    operation: SwapOperation,
    to: Option<String>,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let pairs: Vec<PairInfo> = deps.querier.query_wasm_smart(
        &config.factory_addr, &dex_types::factory::QueryMsg::Pairs { start_after: None, limit: None },
    )?;

    let (pair_addr, offer_asset, funds) = match &operation {
        SwapOperation::NativeSwap { offer_denom, ask_denom } => {
            let pair = pairs.iter().find(|p| {
                p.asset_infos.iter().any(|a| matches!(a, AssetInfo::NativeToken { denom } if denom == offer_denom)) &&
                p.asset_infos.iter().any(|a| matches!(a, AssetInfo::NativeToken { denom } if denom == ask_denom))
            }).ok_or_else(|| ContractError::InvalidOperation {})?;
            let amount = info.funds.iter().find(|c| c.denom == *offer_denom).map(|c| c.amount).unwrap_or(Uint128::zero());
            (pair.contract_addr.clone(), Asset { info: AssetInfo::NativeToken { denom: offer_denom.clone() }, amount }, info.funds.clone())
        }
        SwapOperation::CosmWasmSwap { offer_asset_info, ask_asset_info: _ } => {
            let pair = pairs.iter().find(|p| p.asset_infos.contains(offer_asset_info)).ok_or_else(|| ContractError::InvalidOperation {})?;
            let amount = info.funds.iter().filter_map(|c| match offer_asset_info {
                AssetInfo::NativeToken { denom } if c.denom == *denom => Some(c.amount),
                _ => None,
            }).sum::<Uint128>();
            (pair.contract_addr.clone(), Asset { info: offer_asset_info.clone(), amount }, vec![])
        }
    };

    Ok(Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: pair_addr.to_string(),
        msg: to_binary(&pair::msg::ExecuteMsg::Swap {
            offer_asset, belief_price: None, max_spread: None, to,
        })?,
        funds,
    })).add_attribute("method", "execute_swap_operation"))
}

pub fn execute_provide_liquidity(
    deps: DepsMut,
    info: MessageInfo,
    pair_contract: String,
    assets: [Asset; 2],
    slippage_tolerance: Option<Decimal>,
) -> Result<Response, ContractError> {
    Ok(Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: deps.api.addr_validate(&pair_contract)?.to_string(),
        msg: to_binary(&pair::msg::ExecuteMsg::ProvideLiquidity {
            assets, slippage_tolerance, auto_stake: None, receiver: Some(info.sender.to_string()),
        })?,
        funds: info.funds,
    })).add_attribute("method", "provide_liquidity"))
}

pub fn execute_withdraw_liquidity(
    deps: DepsMut,
    info: MessageInfo,
    pair_contract: String,
    amount: Uint128,
    min_assets_to_withdraw: Option<[Asset; 2]>,
) -> Result<Response, ContractError> {
    Ok(Response::new().add_message(CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: deps.api.addr_validate(&pair_contract)?.to_string(),
        msg: to_binary(&pair::msg::ExecuteMsg::WithdrawLiquidity { amount, min_assets_to_withdraw })?,
        funds: vec![],
    })).add_attribute("method", "withdraw_liquidity"))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<Binary, ContractError> {
    match msg {
        QueryMsg::Config {} => Ok(to_binary(&CONFIG.load(deps.storage)?)?),
        QueryMsg::SimulateSwapOperations { .. } => Ok(to_binary("{}")?),
    }
}

5.6 Router lib.rs

// contracts/router/src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;

6. Part 4: LP Token 实现

LP 代币使用标准 CW20 实现(cw20-base),内嵌在 Pair 合约中。Pair 合约在实例化时创建一个 CW20 代币作为该池的 LP 代币。

LP 代币规格

属性 值
名称格式 MSG DEX LP {asset0}-{asset1}
符号格式 MLP-{A3}-{B3}(如 MLP-UMSG-USDC)
精度 18 位小数(匹配 MSG Chain 标准)
铸造权限 仅 Pair 合约
销毁机制 持有者或授权者可销毁

LP 代币生命周期

1. Pair 合约实例化
   └── 创建 CW20 代币(设置 Pair 合约为 minter)
   
2. 用户提供流动性
   └── Pair 合约调用 Mint(recipient, amount) 铸造 LP 代币
   
3. 首次流动性
   └── 额外销毁 MINIMUM_LIQUIDITY=1000 个 LP 代币(永久锁定)
   
4. 用户移除流动性
   └── 用户先 IncreaseAllowance(Pair, amount)
   └── Pair 合约调用 BurnFrom(owner, amount) 销毁 LP 代币
   └── 返回对应资产给用户

核心交互代码

// 铸造 LP 代币
CosmosMsg::Wasm(WasmMsg::Execute {
    contract_addr: config.lp_token_addr.to_string(),
    msg: to_binary(&Cw20ExecuteMsg::Mint {
        recipient: user_addr,
        amount: liquidity,
    })?,
    funds: vec![],
})

// 销毁 LP 代币(移除流动性时)
CosmosMsg::Wasm(WasmMsg::Execute {
    contract_addr: config.lp_token_addr.to_string(),
    msg: to_binary(&Cw20ExecuteMsg::BurnFrom {
        owner: user_addr,
        amount: liquidity_to_burn,
    })?,
    funds: vec![],
})

// 用户授权(前端)
CosmosMsg::Wasm(WasmMsg::Execute {
    contract_addr: lp_token_addr,
    msg: to_binary(&Cw20ExecuteMsg::IncreaseAllowance {
        spender: pair_addr,
        amount: lp_amount,
    })?,
    funds: vec![],
})

7. Part 5: 单元测试与集成测试

7.1 Factory 合约集成测试

// contracts/factory/tests/integration.rs
use cosmwasm_std::{Addr, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use dex_types::{Asset, AssetInfo};

use factory::contract::{execute, instantiate, query, reply};
use factory::msg::{
    ConfigResponse, ExecuteMsg, InstantiateMsg, PairResponse, PairsResponse, QueryMsg,
};

fn mock_factory() -> Box<dyn Contract<Empty>> {
    Box::new(ContractWrapper::new(
        |d, e, i, m: ExecuteMsg| execute(d, e, i, m),
        |d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
        |d, e, m: QueryMsg| query(d, e, m),
    ).with_reply(|d, e, m| reply(d, e, m)))
}

fn mock_pair() -> Box<dyn Contract<Empty>> {
    Box::new(ContractWrapper::new(
        |d, e, i, m: pair::msg::ExecuteMsg| pair::contract::execute(d, e, i, m),
        |d, e, i, m: pair::msg::InstantiateMsg| pair::contract::instantiate(d, e, i, m),
        |d, e, m: pair::msg::QueryMsg| pair::contract::query(d, e, m),
    ).with_reply(|d, e, m| pair::contract::reply(d, e, m)))
}

fn mock_cw20() -> Box<dyn Contract<Empty>> {
    Box::new(ContractWrapper::new(
        |d, e, i, m: cw20_base::msg::ExecuteMsg| cw20_base::contract::execute(d, e, i, m),
        |d, e, i, m: cw20_base::msg::InstantiateMsg| cw20_base::contract::instantiate(d, e, i, m),
        |d, e, m: cw20_base::msg::QueryMsg| cw20_base::contract::query(d, e, m),
    ))
}

#[test]
fn test_factory_instantiate_and_query() {
    let mut app = App::default();
    let factory_id = app.store_code(mock_factory());
    let pair_id = app.store_code(mock_pair());
    let _cw20_id = app.store_code(mock_cw20());

    let factory = app.instantiate_contract(
        factory_id, Addr::unchecked("owner"),
        &InstantiateMsg { owner: "owner".to_string(), fee_to: None, pair_code_id: pair_id, fee_bps: Some(30) },
        &[], "MSG-DEX-Factory",
    ).unwrap();

    let config: ConfigResponse = app.wrap().query_wasm_smart(&factory, &QueryMsg::Config {}).unwrap();
    assert_eq!(config.owner, "owner");
    assert_eq!(config.pair_code_id, pair_id);
    assert_eq!(config.fee_bps, 30);

    let pairs: PairsResponse = app.wrap().query_wasm_smart(&factory, &QueryMsg::Pairs { start_after: None, limit: None }).unwrap();
    assert!(pairs.pairs.is_empty());
}

#[test]
fn test_factory_create_and_query_pair() {
    let mut app = App::default();
    let factory_id = app.store_code(mock_factory());
    let pair_id = app.store_code(mock_pair());
    let _cw20_id = app.store_code(mock_cw20());

    let factory = app.instantiate_contract(
        factory_id, Addr::unchecked("owner"),
        &InstantiateMsg { owner: "owner".to_string(), fee_to: None, pair_code_id: pair_id, fee_bps: Some(30) },
        &[], "MSG-DEX-Factory",
    ).unwrap();

    let native = AssetInfo::NativeToken { denom: "umsg".to_string() };
    let token = AssetInfo::Token { contract_addr: Addr::unchecked("msg_token_cw20") };

    app.execute_contract(
        Addr::unchecked("owner"), factory.clone(),
        &ExecuteMsg::CreatePair { asset_infos: [native.clone(), token.clone()], init_params: None },
        &[],
    ).unwrap();

    let pair: PairResponse = app.wrap().query_wasm_smart(
        &factory, &QueryMsg::Pair { asset_infos: [native.clone(), token.clone()] },
    ).unwrap();
    assert_eq!(pair.asset_infos[0], native);
    assert_eq!(pair.asset_infos[1], token);
    assert!(!pair.contract_addr.is_empty());
}

#[test]
fn test_factory_duplicate_pair_fails() {
    let mut app = App::default();
    let factory_id = app.store_code(mock_factory());
    let pair_id = app.store_code(mock_pair());
    let _cw20_id = app.store_code(mock_cw20());

    let factory = app.instantiate_contract(
        factory_id, Addr::unchecked("owner"),
        &InstantiateMsg { owner: "owner".to_string(), fee_to: None, pair_code_id: pair_id, fee_bps: Some(30) },
        &[], "MSG-DEX-Factory",
    ).unwrap();

    let a = AssetInfo::NativeToken { denom: "umsg".to_string() };
    let b = AssetInfo::Token { contract_addr: Addr::unchecked("msg_token_cw20") };

    app.execute_contract(Addr::unchecked("owner"), factory.clone(),
        &ExecuteMsg::CreatePair { asset_infos: [a.clone(), b.clone()], init_params: None }, &[],
    ).unwrap();

    let err = app.execute_contract(Addr::unchecked("owner"), factory.clone(),
        &ExecuteMsg::CreatePair { asset_infos: [a, b], init_params: None }, &[],
    ).unwrap_err();
    assert!(err.to_string().contains("Pair already exists"));
}

#[test]
fn test_factory_unauthorized_update() {
    let mut app = App::default();
    let factory_id = app.store_code(mock_factory());
    let pair_id = app.store_code(mock_pair());
    let _cw20_id = app.store_code(mock_cw20());

    let factory = app.instantiate_contract(
        factory_id, Addr::unchecked("owner"),
        &InstantiateMsg { owner: "owner".to_string(), fee_to: None, pair_code_id: pair_id, fee_bps: Some(30) },
        &[], "MSG-DEX-Factory",
    ).unwrap();

    let err = app.execute_contract(Addr::unchecked("attacker"), factory,
        &ExecuteMsg::UpdateConfig { owner: None, fee_to: None, fee_bps: Some(50), pair_code_id: None }, &[],
    ).unwrap_err();
    assert!(err.to_string().contains("Unauthorized"));
}

7.2 Pair/AMM 合约集成测试

// contracts/pair/tests/integration.rs
use cosmwasm_std::{Addr, Coin, Empty, Uint128};
use cw_multi_test::{App, Contract, ContractWrapper, Executor};
use dex_types::{Asset, AssetInfo};

use pair::contract::{execute, instantiate, query, reply};
use pair::msg::{
    ExecuteMsg, InstantiateMsg, PoolResponse, QueryMsg, SimulationResponse,
};

fn mock_pair() -> Box<dyn Contract<Empty>> {
    Box::new(ContractWrapper::new(
        |d, e, i, m: ExecuteMsg| execute(d, e, i, m),
        |d, e, i, m: InstantiateMsg| instantiate(d, e, i, m),
        |d, e, m: QueryMsg| query(d, e, m),
    ).with_reply(|d, e, m| reply(d, e, m)))
}

fn mock_cw20() -> Box<dyn Contract<Empty>> {
    Box::new(ContractWrapper::new(
        |d, e, i, m: cw20_base::msg::ExecuteMsg| cw20_base::contract::execute(d, e, i, m),
        |d, e, i, m: cw20_base::msg::InstantiateMsg| cw20_base::contract::instantiate(d, e, i, m),
        |d, e, m: cw20_base::msg::QueryMsg| cw20_base::contract::query(d, e, m),
    ))
}

fn setup_app() -> (App, Addr) {
    let mut app = App::new(|router, _, storage| {
        router.bank.init_balance(storage, &Addr::unchecked("user"),
            vec![Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) }],
        ).unwrap();
    });

    let pair_id = app.store_code(mock_pair());
    let cw20_id = app.store_code(mock_cw20());

    let pair = app.instantiate_contract(
        pair_id, Addr::unchecked("factory"),
        &InstantiateMsg {
            asset_infos: [
                AssetInfo::NativeToken { denom: "umsg".to_string() },
                AssetInfo::NativeToken { denom: "uusdc".to_string() },
            ],
            factory_addr: "factory".to_string(),
            lp_token_code_id: cw20_id,
            fee_bps: 30,
        },
        &[], "MSG-DEX-Pair",
    ).unwrap();

    (app, pair)
}

#[test]
fn test_initial_pool_state() {
    let (app, pair) = setup_app();
    let pool: PoolResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Pool {}).unwrap();
    assert_eq!(pool.assets[0].amount, Uint128::zero());
    assert_eq!(pool.assets[1].amount, Uint128::zero());
    assert_eq!(pool.total_liquidity, Uint128::zero());
}

#[test]
fn test_add_initial_liquidity() {
    let mut app = App::new(|router, _, storage| {
        router.bank.init_balance(storage, &Addr::unchecked("user"),
            vec![
                Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) },
                Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000_000) },
            ],
        ).unwrap();
    });

    let pair_id = app.store_code(mock_pair());
    let cw20_id = app.store_code(mock_cw20());

    let pair = app.instantiate_contract(
        pair_id, Addr::unchecked("factory"),
        &InstantiateMsg {
            asset_infos: [
                AssetInfo::NativeToken { denom: "umsg".to_string() },
                AssetInfo::NativeToken { denom: "uusdc".to_string() },
            ],
            factory_addr: "factory".to_string(),
            lp_token_code_id: cw20_id,
            fee_bps: 30,
        },
        &[], "MSG-DEX-Pair",
    ).unwrap();

    app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::ProvideLiquidity {
            assets: [
                Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(100_000_000) },
                Asset { info: AssetInfo::NativeToken { denom: "uusdc".to_string() }, amount: Uint128::new(200_000_000) },
            ],
            slippage_tolerance: None, auto_stake: None, receiver: None,
        },
        &[
            Coin { denom: "umsg".to_string(), amount: Uint128::new(100_000_000) },
            Coin { denom: "uusdc".to_string(), amount: Uint128::new(200_000_000) },
        ],
    ).unwrap();

    let pool: PoolResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Pool {}).unwrap();
    assert_eq!(pool.assets[0].amount, Uint128::new(100_000_000));
    assert_eq!(pool.assets[1].amount, Uint128::new(200_000_000));
    assert!(pool.total_liquidity > Uint128::zero());
}

#[test]
fn test_swap_basic() {
    let mut app = App::new(|router, _, storage| {
        router.bank.init_balance(storage, &Addr::unchecked("user"),
            vec![
                Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) },
                Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000_000) },
            ],
        ).unwrap();
    });

    let pair_id = app.store_code(mock_pair());
    let cw20_id = app.store_code(mock_cw20());

    let pair = app.instantiate_contract(
        pair_id, Addr::unchecked("factory"),
        &InstantiateMsg {
            asset_infos: [
                AssetInfo::NativeToken { denom: "umsg".to_string() },
                AssetInfo::NativeToken { denom: "uusdc".to_string() },
            ],
            factory_addr: "factory".to_string(),
            lp_token_code_id: cw20_id,
            fee_bps: 30,
        },
        &[], "MSG-DEX-Pair",
    ).unwrap();

    // 添加初始流动性
    app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::ProvideLiquidity {
            assets: [
                Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000_000) },
                Asset { info: AssetInfo::NativeToken { denom: "uusdc".to_string() }, amount: Uint128::new(1_000_000_000) },
            ],
            slippage_tolerance: None, auto_stake: None, receiver: None,
        },
        &[
            Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000) },
            Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000) },
        ],
    ).unwrap();

    // 模拟交换
    let sim: SimulationResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Simulation {
        offer_asset: Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000) },
    }).unwrap();
    assert!(!sim.return_amount.is_zero());

    // 执行交换
    app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::Swap {
            offer_asset: Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000) },
            belief_price: None, max_spread: None, to: None,
        },
        &[Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000) }],
    ).unwrap();

    let pool: PoolResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Pool {}).unwrap();
    assert_eq!(pool.assets[0].amount, Uint128::new(1_001_000_000));
}

#[test]
fn test_swap_slippage_protection() {
    let mut app = App::new(|router, _, storage| {
        router.bank.init_balance(storage, &Addr::unchecked("user"),
            vec![
                Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) },
                Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000_000) },
            ],
        ).unwrap();
    });

    let pair_id = app.store_code(mock_pair());
    let cw20_id = app.store_code(mock_cw20());

    let pair = app.instantiate_contract(
        pair_id, Addr::unchecked("factory"),
        &InstantiateMsg {
            asset_infos: [
                AssetInfo::NativeToken { denom: "umsg".to_string() },
                AssetInfo::NativeToken { denom: "uusdc".to_string() },
            ],
            factory_addr: "factory".to_string(),
            lp_token_code_id: cw20_id,
            fee_bps: 30,
        },
        &[], "MSG-DEX-Pair",
    ).unwrap();

    // 添加流动性 1:1
    app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::ProvideLiquidity {
            assets: [
                Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000_000) },
                Asset { info: AssetInfo::NativeToken { denom: "uusdc".to_string() }, amount: Uint128::new(1_000_000_000) },
            ],
            slippage_tolerance: None, auto_stake: None, receiver: None,
        },
        &[
            Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000) },
            Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000) },
        ],
    ).unwrap();

    // 使用不合理的 belief_price(期望 1 umsg = 200 uusdc,实际约 1)
    let err = app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::Swap {
            offer_asset: Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000) },
            belief_price: Some(cosmwasm_std::Decimal::from_ratio(Uint128::new(200), Uint128::new(1))),
            max_spread: None, to: None,
        },
        &[Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000) }],
    ).unwrap_err();
    assert!(err.to_string().contains("Slippage"));
}

#[test]
fn test_k_invariant_increases_with_fees() {
    let mut app = App::new(|router, _, storage| {
        router.bank.init_balance(storage, &Addr::unchecked("user"),
            vec![
                Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000_000) },
                Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000_000) },
            ],
        ).unwrap();
    });

    let pair_id = app.store_code(mock_pair());
    let cw20_id = app.store_code(mock_cw20());

    let pair = app.instantiate_contract(
        pair_id, Addr::unchecked("factory"),
        &InstantiateMsg {
            asset_infos: [
                AssetInfo::NativeToken { denom: "umsg".to_string() },
                AssetInfo::NativeToken { denom: "uusdc".to_string() },
            ],
            factory_addr: "factory".to_string(),
            lp_token_code_id: cw20_id,
            fee_bps: 30,
        },
        &[], "MSG-DEX-Pair",
    ).unwrap();

    app.execute_contract(
        Addr::unchecked("user"), pair.clone(),
        &ExecuteMsg::ProvideLiquidity {
            assets: [
                Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(1_000_000_000) },
                Asset { info: AssetInfo::NativeToken { denom: "uusdc".to_string() }, amount: Uint128::new(1_000_000_000) },
            ],
            slippage_tolerance: None, auto_stake: None, receiver: None,
        },
        &[
            Coin { denom: "umsg".to_string(), amount: Uint128::new(1_000_000_000) },
            Coin { denom: "uusdc".to_string(), amount: Uint128::new(1_000_000_000) },
        ],
    ).unwrap();

    let pool_before: PoolResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Pool {}).unwrap();
    let initial_k = pool_before.assets[0].amount * pool_before.assets[1].amount;

    for i in 0..3 {
        let amount = Uint128::new(10_000_000 * (i + 1));
        app.execute_contract(
            Addr::unchecked("user"), pair.clone(),
            &ExecuteMsg::Swap {
                offer_asset: Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount },
                belief_price: None, max_spread: None, to: None,
            },
            &[Coin { denom: "umsg".to_string(), amount }],
        ).unwrap();
    }

    let pool_after: PoolResponse = app.wrap().query_wasm_smart(&pair, &QueryMsg::Pool {}).unwrap();
    let final_k = pool_after.assets[0].amount * pool_after.assets[1].amount;
    assert!(final_k > initial_k, "k should increase due to fees");
}

#[test]
fn test_reverse_simulation() {
    let (app, pair) = setup_app();
    // 需要先有流动性,简化起见假设已有
    let _sim = app.wrap().query_wasm_smart::<pair::msg::ReverseSimulationResponse>(
        &pair, &QueryMsg::ReverseSimulation {
            ask_asset: Asset { info: AssetInfo::NativeToken { denom: "umsg".to_string() }, amount: Uint128::new(100) },
        },
    );
}

#[test]
fn test_query_pair_info() {
    let (app, pair) = setup_app();
    let info: pair::msg::PairInfoResponse = app.wrap().query_wasm_smart(
        &pair, &pair::msg::QueryMsg::PairInfo {},
    ).unwrap();
    assert_eq!(info.asset_infos.len(), 2);
}

7.3 运行测试

# 运行所有测试
cargo test --workspace

# 运行特定合约测试
cargo test -p factory
cargo test -p pair
cargo test -p router

# 运行数学库测试
cargo test -p pair math::tests

# 生成测试覆盖率
cargo tarpaulin --workspace --ignore-tests

7.4 Schema 生成

// contracts/factory/src/bin/schema.rs
use cosmwasm_schema::write_api;
use factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ExecuteMsg,
        query: QueryMsg,
    }
}
# 在项目根目录
cargo schema
# 生成 schema 到 contracts/*/schema/ 目录

8. Part 6: 前端实现 (React + TypeScript + Tailwind)

8.1 项目配置

{
  "name": "msg-dex-frontend",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@cosmjs/cosmwasm-stargate": "^0.32.0",
    "@cosmjs/stargate": "^0.32.0",
    "@cosmjs/proto-signing": "^0.32.0",
    "@cosmjs/encoding": "^0.32.0",
    "@keplr-wallet/types": "^0.12.0",
    "@keplr-wallet/cosmos": "^0.12.0",
    "decimal.js": "^10.4.3",
    "big.js": "^6.2.1",
    "react-router-dom": "^6.20.0",
    "recharts": "^2.10.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.37",
    "@types/react-dom": "^18.2.15",
    "@vitejs/plugin-react": "^4.2.0",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "tailwindcss": "^3.3.6",
    "typescript": "^5.3.2",
    "vite": "^5.0.0"
  }
}
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        dex: {
          bg: "#0f111a",
          card: "#1a1c2b",
          border: "#2a2d3e",
          text: "#ffffff",
          "text-secondary": "#8a8ca0",
          accent: "#4f8cff",
          green: "#22c55e",
          red: "#ef4444",
        },
      },
    },
  },
  plugins: [],
};
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  define: { "process.env": {} },
});

8.2 类型定义

// src/types/index.ts
export interface AssetInfo {
  native_token?: { denom: string };
  token?: { contract_addr: string };
}

export interface Asset {
  info: AssetInfo;
  amount: string;
}

export interface PairInfo {
  asset_infos: [AssetInfo, AssetInfo];
  contract_addr: string;
  liquidity_token: string;
}

export interface PoolResponse {
  assets: [Asset, Asset];
  total_liquidity: string;
}

export interface SimulationResponse {
  return_amount: string;
  spread_amount: string;
  commission_amount: string;
}

export interface ChainInfo {
  chainId: string;
  chainName: string;
  rpc: string;
  rest: string;
  bech32Config: {
    bech32PrefixAccAddr: string;
    bech32PrefixAccPub: string;
    bech32PrefixValAddr: string;
    bech32PrefixValPub: string;
    bech32PrefixConsAddr: string;
    bech32PrefixConsPub: string;
  };
  currencies: { coinDenom: string; coinMinimalDenom: string; coinDecimals: number; coinGeckoId?: string }[];
  feeCurrencies: { coinDenom: string; coinMinimalDenom: string; coinDecimals: number; gasPriceStep: { low: number; average: number; high: number } }[];
  stakeCurrency: { coinDenom: string; coinMinimalDenom: string; coinDecimals: number };
  bip44: { coinType: number };
}

export const MSG_CHAIN: ChainInfo = {
  chainId: "msg-chain-1",
  chainName: "MSG Chain",
  rpc: "https://rpc.msgchain.org",
  rest: "https://api.msgchain.org",
  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: 1_000_000_000, average: 1_000_000_000, high: 1_000_000_000 } }],
  stakeCurrency: { coinDenom: "MSG", coinMinimalDenom: "umsg", coinDecimals: 18 },
  bip44: { coinType: 118 },
};

8.3 合约客户端

// src/utils/contracts.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { Coin, StdFee } from "@cosmjs/stargate";

export class DexClient {
  private client: SigningCosmWasmClient;
  private factory: string;
  private sender: string;

  constructor(client: SigningCosmWasmClient, factory: string, sender: string) {
    this.client = client;
    this.factory = factory;
    this.sender = sender;
  }

  async getConfig(): Promise<any> {
    return this.client.queryContractSmart(this.factory, { config: {} });
  }

  async getPair(assets: [any, any]): Promise<any> {
    return this.client.queryContractSmart(this.factory, { pair: { asset_infos: assets } });
  }

  async getPairs(limit = 30): Promise<any> {
    return this.client.queryContractSmart(this.factory, { pairs: { start_after: null, limit } });
  }

  async getPool(pair: string): Promise<any> {
    return this.client.queryContractSmart(pair, { pool: {} });
  }

  async simulateSwap(pair: string, asset: any): Promise<any> {
    return this.client.queryContractSmart(pair, { simulation: { offer_asset: asset } });
  }

  async simulateReverse(pair: string, asset: any): Promise<any> {
    return this.client.queryContractSmart(pair, { reverse_simulation: { ask_asset: asset } });
  }

  async createPair(assets: [any, any], fee?: StdFee): Promise<string> {
    const tx = await this.client.execute(this.sender, this.factory,
      { create_pair: { asset_infos: assets, init_params: null } },
      fee ?? "auto", "Create Pair");
    return tx.transactionHash;
  }

  async swap(pair: string, offerAsset: any, funds?: Coin[], to?: string, fee?: StdFee): Promise<string> {
    const tx = await this.client.execute(this.sender, pair,
      { swap: { offer_asset: offerAsset, belief_price: null, max_spread: null, to: to ?? null } },
      fee ?? "auto", "Swap", funds);
    return tx.transactionHash;
  }

  async addLiquidity(pair: string, assets: [any, any], funds?: Coin[], fee?: StdFee): Promise<string> {
    const tx = await this.client.execute(this.sender, pair,
      { provide_liquidity: { assets, slippage_tolerance: null, auto_stake: null, receiver: null } },
      fee ?? "auto", "Add Liquidity", funds);
    return tx.transactionHash;
  }

  async removeLiquidity(pair: string, amount: string, minAssets?: [any, any], fee?: StdFee): Promise<string> {
    const tx = await this.client.execute(this.sender, pair,
      { withdraw_liquidity: { amount, min_assets_to_withdraw: minAssets ?? null } },
      fee ?? "auto", "Remove Liquidity");
    return tx.transactionHash;
  }
}

8.4 钱包连接 Hook

// src/hooks/useWallet.ts
import { useState, useCallback, useEffect } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { MSG_CHAIN } from "../types";

declare global {
  interface Window { keplr?: any; getOfflineSigner?: any; }
}

export function useWallet() {
  const [address, setAddress] = useState<string>("");
  const [client, setClient] = useState<SigningCosmWasmClient | null>(null);
  const [isConnecting, setIsConnecting] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const connect = useCallback(async () => {
    setIsConnecting(true);
    setError(null);
    try {
      if (!window.keplr) {
        throw new Error("Please install Keplr wallet");
      }
      await window.keplr.experimentalSuggestChain(MSG_CHAIN);
      await window.keplr.enable(MSG_CHAIN.chainId);
      const offlineSigner = window.keplr.getOfflineSigner(MSG_CHAIN.chainId);
      const accounts = await offlineSigner.getAccounts();
      const addr = accounts[0].address;

      const c = await SigningCosmWasmClient.connectWithSigner(
        MSG_CHAIN.rpc,
        offlineSigner,
        { gasPrice: GasPrice.fromString("1000000000attoMSG") }
      );
      setAddress(addr);
      setClient(c);
    } catch (e: any) {
      setError(e.message || "Connection failed");
    } finally {
      setIsConnecting(false);
    }
  }, []);

  const disconnect = useCallback(() => {
    setAddress("");
    setClient(null);
  }, []);

  return { address, client, isConnecting, error, connect, disconnect };
}

8.5 Swap 页面

// src/pages/Swap.tsx
import React, { useState, useEffect, useCallback } from "react";
import { useWallet } from "../hooks/useWallet";
import { DexClient } from "../utils/contracts";
import { AssetInfo, SimulationResponse, PoolResponse } from "../types";
import { Coin } from "@cosmjs/stargate";

const FACTORY_ADDR = "msg1factory..."; // 实际部署后更新
const PAIR_ADDR = "msg1pair..."; // 实际部署后更新

export default function Swap() {
  const { address, client, connect, isConnecting, error: walletError } = useWallet();
  const [fromToken, setFromToken] = useState("umsg");
  const [toToken, setToToken] = useState("uusdc");
  const [fromAmount, setFromAmount] = useState("");
  const [toAmount, setToAmount] = useState("");
  const [slippage, setSlippage] = useState(0.5);
  const [isSwapping, setIsSwapping] = useState(false);
  const [price, setPrice] = useState<string>("-");
  const [priceImpact, setPriceImpact] = useState<string>("-");
  const [error, setError] = useState<string | null>(null);
  const [txHash, setTxHash] = useState<string | null>(null);

  const getOfferAsset = (): any => ({
    info: { native_token: { denom: fromToken } },
    amount: (parseFloat(fromAmount) * 1e18).toString(),
  });

  const simulate = useCallback(async () => {
    if (!client || !fromAmount || parseFloat(fromAmount) === 0) return;
    try {
      const dex = new DexClient(client, FACTORY_ADDR, address!);
      const sim: SimulationResponse = await dex.simulateSwap(PAIR_ADDR, getOfferAsset());
      const returnAmt = parseFloat(sim.return_amount) / 1e18;
      setToAmount(returnAmt.toFixed(6));
      setPrice(returnAmt > 0 ? (parseFloat(fromAmount) / returnAmt).toFixed(6) : "-");

      const pool: PoolResponse = await dex.getPool(PAIR_ADDR);
      const reserveIn = parseFloat(pool.assets[0].amount) / 1e18;
      const impact = (parseFloat(fromAmount) / (reserveIn + parseFloat(fromAmount))) * 100;
      setPriceImpact(impact.toFixed(2) + "%");
    } catch (e: any) {
      console.error("Simulation error:", e);
    }
  }, [client, fromAmount, fromToken, address]);

  useEffect(() => { simulate(); }, [simulate]);

  const handleSwap = async () => {
    if (!client || !address) return;
    setIsSwapping(true);
    setError(null);
    setTxHash(null);
    try {
      const dex = new DexClient(client, FACTORY_ADDR, address);
      const offerAmount = (parseFloat(fromAmount) * 1e18).toFixed(0);
      const funds: Coin[] = [{ denom: fromToken, amount: offerAmount }];
      const hash = await dex.swap(PAIR_ADDR, getOfferAsset(), funds);
      setTxHash(hash);
      setFromAmount("");
      setToAmount("");
    } catch (e: any) {
      setError(e.message || "Swap failed");
    } finally {
      setIsSwapping(false);
    }
  };

  const switchTokens = () => {
    setFromToken(toToken);
    setToToken(fromToken);
    setFromAmount(toAmount);
    setToAmount(fromAmount);
  };

  if (!address) {
    return (
      <div className="flex items-center justify-center min-h-[80vh]">
        <div className="bg-dex-card p-8 rounded-2xl border border-dex-border w-full max-w-md text-center">
          <h2 className="text-2xl font-bold text-white mb-4">Connect Wallet</h2>
          <p className="text-dex-text-secondary mb-6">Connect your Keplr wallet to start swapping on MSG Chain</p>
          <button onClick={connect} disabled={isConnecting}
            className="bg-dex-accent hover:bg-blue-600 text-white font-semibold py-3 px-8 rounded-xl transition w-full">
            {isConnecting ? "Connecting..." : "Connect Keplr"}
          </button>
          {walletError && <p className="text-red-400 mt-4 text-sm">{walletError}</p>}
        </div>
      </div>
    );
  }

  return (
    <div className="flex items-center justify-center min-h-[80vh]">
      <div className="bg-dex-card p-6 rounded-2xl border border-dex-border w-full max-w-md">
        <div className="flex items-center justify-between mb-6">
          <h2 className="text-xl font-bold text-white">Swap</h2>
          <span className="text-dex-text-secondary text-sm truncate max-w-[200px]">{address}</span>
        </div>

        {/* From Token */}
        <div className="bg-dex-bg rounded-xl p-4 mb-2">
          <div className="flex justify-between items-center mb-2">
            <label className="text-dex-text-secondary text-sm">From</label>
            <span className="text-dex-text-secondary text-xs">Balance: -</span>
          </div>
          <div className="flex items-center gap-3">
            <input type="number" value={fromAmount} onChange={e => setFromAmount(e.target.value)}
              placeholder="0.0" className="bg-transparent text-2xl text-white font-semibold outline-none flex-1" />
            <div className="flex items-center gap-2 bg-dex-bg border border-dex-border rounded-lg px-3 py-2">
              <div className="w-6 h-6 rounded-full bg-gradient-to-br from-blue-500 to-purple-500" />
              <span className="text-white font-medium">{fromToken.toUpperCase()}</span>
            </div>
          </div>
        </div>

        {/* Switch button */}
        <div className="flex justify-center -my-3 relative z-10">
          <button onClick={switchTokens}
            className="bg-dex-card border-4 border-dex-bg rounded-xl p-2 hover:bg-dex-border transition">
            <svg className="w-5 h-5 text-dex-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4" />
            </svg>
          </button>
        </div>

        {/* To Token */}
        <div className="bg-dex-bg rounded-xl p-4 mb-4">
          <div className="flex justify-between items-center mb-2">
            <label className="text-dex-text-secondary text-sm">To</label>
            <span className="text-dex-text-secondary text-xs">Balance: -</span>
          </div>
          <div className="flex items-center gap-3">
            <input type="number" value={toAmount} readOnly placeholder="0.0"
              className="bg-transparent text-2xl text-white font-semibold outline-none flex-1" />
            <div className="flex items-center gap-2 bg-dex-bg border border-dex-border rounded-lg px-3 py-2">
              <div className="w-6 h-6 rounded-full bg-gradient-to-br from-green-500 to-teal-500" />
              <span className="text-white font-medium">{toToken.toUpperCase()}</span>
            </div>
          </div>
        </div>

        {/* Price info */}
        {price !== "-" && (
          <div className="bg-dex-bg rounded-xl p-3 mb-4 space-y-1 text-sm">
            <div className="flex justify-between">
              <span className="text-dex-text-secondary">Price</span>
              <span className="text-white">1 {fromToken.toUpperCase()} = {price} {toToken.toUpperCase()}</span>
            </div>
            <div className="flex justify-between">
              <span className="text-dex-text-secondary">Price Impact</span>
              <span className={parseFloat(priceImpact) > 5 ? "text-red-400" : "text-yellow-400"}>{priceImpact}</span>
            </div>
            <div className="flex justify-between">
              <span className="text-dex-text-secondary">Slippage</span>
              <span className="text-white">{slippage}%</span>
            </div>
          </div>
        )}

        {error && <p className="text-red-400 text-sm mb-3">{error}</p>}
        {txHash && (
          <p className="text-green-400 text-sm mb-3 break-all">
            Tx: <a href={`https://explorer.msgchain.org/tx/${txHash}`} target="_blank" rel="noreferrer" className="underline">{txHash}</a>
          </p>
        )}

        <button onClick={handleSwap} disabled={isSwapping || !fromAmount || parseFloat(fromAmount) <= 0}
          className="w-full bg-gradient-to-r from-dex-accent to-blue-600 text-white font-bold py-4 rounded-xl hover:opacity-90 transition disabled:opacity-40">
          {isSwapping ? "Swapping..." : "Swap"}
        </button>
      </div>
    </div>
  );
}

8.6 流动性页面

// src/pages/Liquidity.tsx
import React, { useState, useEffect } from "react";
import { useWallet } from "../hooks/useWallet";
import { DexClient } from "../utils/contracts";
import { PoolResponse } from "../types";
import { Coin } from "@cosmjs/stargate";

const FACTORY_ADDR = "msg1factory...";
const PAIR_ADDR = "msg1pair...";

export default function Liquidity() {
  const { address, client, connect } = useWallet();
  const [mode, setMode] = useState<"add" | "remove">("add");
  const [amount0, setAmount0] = useState("");
  const [amount1, setAmount1] = useState("");
  const [removePercent, setRemovePercent] = useState(25);
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [pool, setPool] = useState<PoolResponse | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [txHash, setTxHash] = useState<string | null>(null);

  useEffect(() => {
    if (!client || !address) return;
    const dex = new DexClient(client, FACTORY_ADDR, address);
    dex.getPool(PAIR_ADDR).then(setPool).catch(console.error);
  }, [client, address]);

  const handleAddLiquidity = async () => {
    if (!client || !address) return;
    setIsSubmitting(true);
    setError(null);
    setTxHash(null);
    try {
      const dex = new DexClient(client, FACTORY_ADDR, address);
      const amt0 = (parseFloat(amount0) * 1e18).toFixed(0);
      const amt1 = (parseFloat(amount1) * 1e18).toFixed(0);
      const assets: [any, any] = [
        { info: { native_token: { denom: "umsg" } }, amount: amt0 },
        { info: { native_token: { denom: "uusdc" } }, amount: amt1 },
      ];
      const funds: Coin[] = [
        { denom: "umsg", amount: amt0 },
        { denom: "uusdc", amount: amt1 },
      ];
      const hash = await dex.addLiquidity(PAIR_ADDR, assets, funds);
      setTxHash(hash);
      setAmount0("");
      setAmount1("");
    } catch (e: any) {
      setError(e.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  const handleRemoveLiquidity = async () => {
    if (!client || !address || !pool) return;
    setIsSubmitting(true);
    setError(null);
    setTxHash(null);
    try {
      const dex = new DexClient(client, FACTORY_ADDR, address);
      const lpAmount = (BigInt(pool.total_liquidity) * BigInt(removePercent) / BigInt(100)).toString();
      const hash = await dex.removeLiquidity(PAIR_ADDR, lpAmount);
      setTxHash(hash);
    } catch (e: any) {
      setError(e.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  if (!address) {
    return (
      <div className="flex items-center justify-center min-h-[80vh]">
        <div className="bg-dex-card p-8 rounded-2xl border border-dex-border w-full max-w-md text-center">
          <h2 className="text-2xl font-bold text-white mb-4">Connect Wallet</h2>
          <button onClick={connect} className="bg-dex-accent text-white font-semibold py-3 px-8 rounded-xl transition">Connect Keplr</button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex items-center justify-center min-h-[80vh]">
      <div className="bg-dex-card p-6 rounded-2xl border border-dex-border w-full max-w-md">
        <div className="flex mb-6 bg-dex-bg rounded-xl p-1">
          <button onClick={() => setMode("add")} className={`flex-1 py-2 rounded-lg font-medium transition ${mode === "add" ? "bg-dex-accent text-white" : "text-dex-text-secondary"}`}>Add</button>
          <button onClick={() => setMode("remove")} className={`flex-1 py-2 rounded-lg font-medium transition ${mode === "remove" ? "bg-dex-accent text-white" : "text-dex-text-secondary"}`}>Remove</button>
        </div>

        {mode === "add" ? (
          <>
            <div className="bg-dex-bg rounded-xl p-4 mb-3">
              <label className="text-dex-text-secondary text-sm mb-2 block">Token 0 (umsg)</label>
              <input type="number" value={amount0} onChange={e => setAmount0(e.target.value)} placeholder="0.0"
                className="bg-transparent text-2xl text-white outline-none w-full" />
            </div>
            <div className="text-center text-dex-text-secondary mb-3">+</div>
            <div className="bg-dex-bg rounded-xl p-4 mb-4">
              <label className="text-dex-text-secondary text-sm mb-2 block">Token 1 (uusdc)</label>
              <input type="number" value={amount1} onChange={e => setAmount1(e.target.value)} placeholder="0.0"
                className="bg-transparent text-2xl text-white outline-none w-full" />
            </div>
            {pool && (
              <div className="bg-dex-bg rounded-xl p-3 mb-4 text-sm text-dex-text-secondary">
                <p>Pool: {pool.assets[0].amount} / {pool.assets[1].amount}</p>
                <p>Rate: 1 umsg = {parseFloat(pool.assets[1].amount) / parseFloat(pool.assets[0].amount)} uusdc</p>
              </div>
            )}
            {error && <p className="text-red-400 text-sm mb-2">{error}</p>}
            {txHash && <p className="text-green-400 text-sm mb-2 break-all">Tx: {txHash}</p>}
            <button onClick={handleAddLiquidity} disabled={isSubmitting || !amount0 || !amount1}
              className="w-full bg-gradient-to-r from-green-500 to-emerald-600 text-white font-bold py-4 rounded-xl hover:opacity-90 transition disabled:opacity-40">
              {isSubmitting ? "Adding..." : "Add Liquidity"}
            </button>
          </>
        ) : (
          <>
            <div className="bg-dex-bg rounded-xl p-4 mb-4">
              <label className="text-dex-text-secondary text-sm mb-2 block">Remove Amount</label>
              <div className="flex gap-2 mb-3 flex-wrap">
                {[25, 50, 75, 100].map(p => (
                  <button key={p} onClick={() => setRemovePercent(p)}
                    className={`px-4 py-1 rounded-lg font-medium text-sm transition ${removePercent === p ? "bg-dex-accent text-white" : "bg-dex-border text-dex-text-secondary"}`}>{p}%</button>
                ))}
              </div>
              {pool && (
                <p className="text-white text-lg">LP to remove: {removePercent}%</p>
              )}
            </div>
            {pool && (
              <div className="bg-dex-bg rounded-xl p-3 mb-4 text-sm text-dex-text-secondary">
                <p>Your LP: {(BigInt(pool.total_liquidity) * BigInt(removePercent) / BigInt(100)).toString()}</p>
                <p>You receive: ~{parseFloat(pool.assets[0].amount) * removePercent / 100} umsg / ~{parseFloat(pool.assets[1].amount) * removePercent / 100} uusdc</p>
              </div>
            )}
            <button onClick={handleRemoveLiquidity} disabled={isSubmitting}
              className="w-full bg-gradient-to-r from-red-500 to-rose-600 text-white font-bold py-4 rounded-xl hover:opacity-90 transition disabled:opacity-40">
              {isSubmitting ? "Removing..." : "Remove Liquidity"}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

8.7 Pool 页面

// src/pages/Pools.tsx
import React, { useEffect, useState } from "react";
import { useWallet } from "../hooks/useWallet";
import { DexClient } from "../utils/contracts";
import { useNavigate } from "react-router-dom";

const FACTORY_ADDR = "msg1factory...";

interface PoolRow {
  pairAddr: string;
  token0: string;
  token1: string;
  reserve0: string;
  reserve1: string;
  tvl: string;
  volume24h: string;
  fee24h: string;
}

export default function Pools() {
  const { address, client, connect } = useWallet();
  const [pools, setPools] = useState<PoolRow[]>([]);
  const [loading, setLoading] = useState(true);
  const navigate = useNavigate();

  useEffect(() => {
    if (!client || !address) { setLoading(false); return; }
    const dex = new DexClient(client, FACTORY_ADDR, address);
    dex.getPairs(100).then(async (res: any) => {
      const rows: PoolRow[] = [];
      for (const p of res.pairs) {
        try {
          const pool = await dex.getPool(p.contract_addr);
          const r0 = parseFloat(pool.assets[0].amount) / 1e18;
          const r1 = parseFloat(pool.assets[1].amount) / 1e18;
          rows.push({
            pairAddr: p.contract_addr,
            token0: p.asset_infos[0].native_token?.denom ?? p.asset_infos[0].token?.contract_addr ?? "?",
            token1: p.asset_infos[1].native_token?.denom ?? p.asset_infos[1].token?.contract_addr ?? "?",
            reserve0: r0.toFixed(4),
            reserve1: r1.toFixed(4),
            tvl: (r0 + r1).toFixed(2),
            volume24h: "-",
            fee24h: "-",
          });
        } catch (e) { /* skip */ }
      }
      setPools(rows);
      setLoading(false);
    }).catch(() => setLoading(false));
  }, [client, address]);

  if (!address) {
    return (
      <div className="flex items-center justify-center min-h-[80vh]">
        <div className="bg-dex-card p-8 rounded-2xl border border-dex-border text-center">
          <h2 className="text-2xl font-bold text-white mb-4">Connect to View Pools</h2>
          <button onClick={connect} className="bg-dex-accent text-white font-semibold py-3 px-8 rounded-xl">Connect Keplr</button>
        </div>
      </div>
    );
  }

  return (
    <div className="max-w-5xl mx-auto py-8 px-4">
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold text-white">Pools</h1>
        <button onClick={() => navigate("/liquidity")}
          className="bg-dex-accent text-white font-semibold py-2 px-6 rounded-xl">New Position</button>
      </div>

      {loading ? (
        <div className="text-center text-dex-text-secondary py-20">Loading pools...</div>
      ) : pools.length === 0 ? (
        <div className="text-center text-dex-text-secondary py-20">
          <p className="text-lg mb-4">No pools found</p>
          <button onClick={() => navigate("/liquidity")}
            className="bg-dex-accent text-white font-semibold py-3 px-8 rounded-xl">Create Pool</button>
        </div>
      ) : (
        <div className="bg-dex-card rounded-2xl border border-dex-border overflow-hidden">
          <table className="w-full">
            <thead>
              <tr className="border-b border-dex-border text-dex-text-secondary text-sm">
                <th className="text-left py-4 px-6">Pool</th>
                <th className="text-right py-4 px-6">Reserve 0</th>
                <th className="text-right py-4 px-6">Reserve 1</th>
                <th className="text-right py-4 px-6">TVL</th>
                <th className="text-right py-4 px-6">Volume 24h</th>
                <th className="text-right py-4 px-6">Fees 24h</th>
              </tr>
            </thead>
            <tbody>
              {pools.map((p, i) => (
                <tr key={i} className="border-b border-dex-border hover:bg-dex-bg/50 cursor-pointer transition"
                  onClick={() => navigate(`/swap?pair=${p.pairAddr}`)}>
                  <td className="py-4 px-6">
                    <div className="flex items-center gap-2">
                      <div className="flex -space-x-2">
                        <div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 border-2 border-dex-card" />
                        <div className="w-8 h-8 rounded-full bg-gradient-to-br from-green-500 to-teal-500 border-2 border-dex-card" />
                      </div>
                      <span className="text-white font-medium">{p.token0.toUpperCase()} / {p.token1.toUpperCase()}</span>
                    </div>
                  </td>
                  <td className="text-right py-4 px-6 text-white">{p.reserve0}</td>
                  <td className="text-right py-4 px-6 text-white">{p.reserve1}</td>
                  <td className="text-right py-4 px-6 text-dex-green font-medium">${p.tvl}</td>
                  <td className="text-right py-4 px-6 text-dex-text-secondary">{p.volume24h}</td>
                  <td className="text-right py-4 px-6 text-dex-text-secondary">{p.fee24h}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

8.8 Dashboard 页面

// src/pages/Dashboard.tsx
import React, { useEffect, useState } from "react";
import { useWallet } from "../hooks/useWallet";
import { DexClient } from "../utils/contracts";
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from "recharts";

const FACTORY_ADDR = "msg1factory...";

export default function Dashboard() {
  const { client, address } = useWallet();
  const [stats, setStats] = useState({ totalPools: 0, totalLiquidity: 0, totalVolume24h: 0 });
  const [chartData] = useState([
    { time: "00:00", volume: 10 }, { time: "04:00", volume: 25 },
    { time: "08:00", volume: 15 }, { time: "12:00", volume: 40 },
    { time: "16:00", volume: 30 }, { time: "20:00", volume: 20 },
  ]);

  useEffect(() => {
    if (!client || !address) return;
    const dex = new DexClient(client, FACTORY_ADDR, address);
    dex.getPairs(100).then(async (res: any) => {
      const pools = res.pairs || [];
      let tvl = 0;
      for (const p of pools) {
        try {
          const pool = await dex.getPool(p.contract_addr);
          tvl += parseFloat(pool.assets[0].amount) / 1e18 + parseFloat(pool.assets[1].amount) / 1e18;
        } catch (e) { /* skip */ }
      }
      setStats({ totalPools: pools.length, totalLiquidity: tvl, totalVolume24h: 0 });
    }).catch(console.error);
  }, [client, address]);

  return (
    <div className="max-w-6xl mx-auto py-8 px-4">
      <h1 className="text-3xl font-bold text-white mb-8">Dashboard</h1>

      <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
        <div className="bg-dex-card rounded-2xl border border-dex-border p-6">
          <p className="text-dex-text-secondary text-sm mb-1">Total Pools</p>
          <p className="text-3xl font-bold text-white">{stats.totalPools}</p>
        </div>
        <div className="bg-dex-card rounded-2xl border border-dex-border p-6">
          <p className="text-dex-text-secondary text-sm mb-1">Total Liquidity</p>
          <p className="text-3xl font-bold text-dex-green">${stats.totalLiquidity.toFixed(2)}</p>
        </div>
        <div className="bg-dex-card rounded-2xl border border-dex-border p-6">
          <p className="text-dex-text-secondary text-sm mb-1">Volume 24h</p>
          <p className="text-3xl font-bold text-dex-accent">${stats.totalVolume24h.toFixed(2)}</p>
        </div>
      </div>

      <div className="bg-dex-card rounded-2xl border border-dex-border p-6 mb-8">
        <h2 className="text-lg font-semibold text-white mb-4">Volume (24h)</h2>
        <div className="h-64">
          <ResponsiveContainer width="100%" height="100%">
            <LineChart data={chartData}>
              <XAxis dataKey="time" stroke="#8a8ca0" fontSize={12} />
              <YAxis stroke="#8a8ca0" fontSize={12} />
              <Tooltip contentStyle={{ backgroundColor: "#1a1c2b", border: "1px solid #2a2d3e", borderRadius: "12px", color: "#fff" }} />
              <Line type="monotone" dataKey="volume" stroke="#4f8cff" strokeWidth={2} dot={false} />
            </LineChart>
          </ResponsiveContainer>
        </div>
      </div>

      <div className="bg-dex-card rounded-2xl border border-dex-border p-6">
        <h2 className="text-lg font-semibold text-white mb-4">Quick Stats</h2>
        <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
          <div><p className="text-dex-text-secondary">Chain</p><p className="text-white">MSG Chain</p></div>
          <div><p className="text-dex-text-secondary">Block Time</p><p className="text-white">5s</p></div>
          <div><p className="text-dex-text-secondary">Consensus</p><p className="text-white">RR + DAR</p></div>
          <div><p className="text-dex-text-secondary">Signature</p><p className="text-white">Dilithium-5</p></div>
        </div>
      </div>
    </div>
  );
}

8.9 主应用和入口

// src/App.tsx
import React from "react";
import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom";
import Swap from "./pages/Swap";
import Liquidity from "./pages/Liquidity";
import Pools from "./pages/Pools";
import Dashboard from "./pages/Dashboard";

function NavBar() {
  return (
    <nav className="flex items-center justify-between px-6 py-4 border-b border-dex-border">
      <div className="flex items-center gap-2">
        <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600" />
        <span className="text-white font-bold text-lg">MSG DEX</span>
      </div>
      <div className="flex items-center gap-1">
        {[
          { to: "/", label: "Swap" },
          { to: "/pools", label: "Pools" },
          { to: "/liquidity", label: "Liquidity" },
          { to: "/dashboard", label: "Dashboard" },
        ].map(link => (
          <NavLink key={link.to} to={link.to} end={link.to === "/"}
            className={({ isActive }) =>
              `px-4 py-2 rounded-lg font-medium text-sm transition ${isActive ? "bg-dex-accent text-white" : "text-dex-text-secondary hover:text-white"}`
            }>
            {link.label}
          </NavLink>
        ))}
      </div>
    </nav>
  );
}

export default function App() {
  return (
    <BrowserRouter>
      <div className="min-h-screen bg-dex-bg">
        <NavBar />
        <Routes>
          <Route path="/" element={<Swap />} />
          <Route path="/pools" element={<Pools />} />
          <Route path="/liquidity" element={<Liquidity />} />
          <Route path="/dashboard" element={<Dashboard />} />
        </Routes>
      </div>
    </BrowserRouter>
  );
}
// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode><App /></React.StrictMode>
);
/* src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  background-color: #0f111a;
}

input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

input[type="number"] {
  -moz-appearance: textfield;
}
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>MSG DEX</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
</html>

9. Part 7: 部署脚本与 Docker

9.1 部署配置

// scripts/config.ts
export const config = {
  chainId: "msg-chain-1",
  rpc: "https://rpc.msgchain.org",
  prefix: "msg",
  gasPrice: "1000000000attoMSG",
  factory: {
    owner: "msg1yourwallet...",
    fee_to: null,
    fee_bps: 30,
  },
  pair: {
    fee_bps: 30,
  },
  router: {
    // factory address will be set after deployment
  },
  tokens: [
    { denom: "umsg", type: "native", decimals: 18 },
    { contract: "msg1cw20contract...", type: "cw20", decimals: 18 },
  ],
};

9.2 部署脚本 (TypeScript)

// scripts/deploy.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice, calculateFee } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { readFileSync } from "fs";
import { config } from "./config";

async function deploy() {
  // 1. 加载部署账户
  const mnemonic = process.env.MNEMONIC || "your seed phrase here";
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, {
    prefix: config.prefix,
  });
  const [account] = await wallet.getAccounts();
  console.log("Deploying from:", account.address);

  // 2. 连接 MSG Chain
  const client = await SigningCosmWasmClient.connectWithSigner(
    config.rpc,
    wallet,
    { gasPrice: GasPrice.fromString(config.gasPrice) }
  );

  // 3. 上传合约 WASM 代码
  console.log("\n=== Uploading contracts ===");

  const factoryWasm = readFileSync("./contracts/factory/target/wasm32-unknown-unknown/release/factory.wasm");
  const factoryCodeId = (await client.upload(account.address, factoryWasm, "auto")).codeId;
  console.log("Factory code ID:", factoryCodeId);

  const pairWasm = readFileSync("./contracts/pair/target/wasm32-unknown-unknown/release/pair.wasm");
  const pairCodeId = (await client.upload(account.address, pairWasm, "auto")).codeId;
  console.log("Pair code ID:", pairCodeId);

  const routerWasm = readFileSync("./contracts/router/target/wasm32-unknown-unknown/release/router.wasm");
  const routerCodeId = (await client.upload(account.address, routerWasm, "auto")).codeId;
  console.log("Router code ID:", routerCodeId);

  // 4. 实例化 Factory 合约
  console.log("\n=== Instantiating Factory ===");
  const factoryAddr = await client.instantiate(
    account.address,
    factoryCodeId,
    {
      owner: config.factory.owner,
      fee_to: config.factory.fee_to,
      pair_code_id: pairCodeId,
      fee_bps: config.factory.fee_bps,
    },
    "MSG DEX Factory",
    "auto"
  );
  console.log("Factory address:", factoryAddr);

  // 5. 创建交易对
  console.log("\n=== Creating Pairs ===");
  for (const token of config.tokens) {
    const assetInfo = token.type === "native"
      ? { native_token: { denom: token.denom } }
      : { token: { contract_addr: token.contract } };

    // 示例:umsg / msg_token_cw20 交易对
    const nativeAsset = { native_token: { denom: "umsg" } };
    const tx = await client.execute(
      account.address,
      factoryAddr,
      {
        create_pair: {
          asset_infos: [nativeAsset, assetInfo],
          init_params: null,
        },
      },
      "auto",
      "Create Pair"
    );
    console.log("Created pair, tx:", tx.transactionHash);
  }

  // 6. 实例化 Router 合约
  console.log("\n=== Instantiating Router ===");
  const routerAddr = await client.instantiate(
    account.address,
    routerCodeId,
    { factory_addr: factoryAddr },
    "MSG DEX Router",
    "auto"
  );
  console.log("Router address:", routerAddr);

  // 7. 输出部署摘要
  console.log("\n=== Deployment Summary ===");
  console.log(JSON.stringify({
    factoryCodeId,
    pairCodeId,
    routerCodeId,
    factoryAddr,
    routerAddr,
  }, null, 2));
}

deploy().catch(console.error);

9.3 部署脚本 (Shell)

#!/bin/bash
# scripts/deploy.sh
set -e

# MSG Chain 部署脚本
CHAIN_ID="msg-chain-1"
NODE="https://rpc.msgchain.org"
KEY_NAME="deployer"
KEYRING="--keyring-backend test"

echo "=== MSG DEX Deployment ==="
echo "Chain: $CHAIN_ID"

# 编译合约
echo "Building contracts..."
cd contracts/factory && cargo wasm && cd ../..
cd contracts/pair && cargo wasm && cd ../..
cd contracts/router && cargo wasm && cd ../..

# 优化 WASM
echo "Optimizing WASM..."
for contract in factory pair router; do
  wasm-opt -Os \
    "target/wasm32-unknown-unknown/release/${contract}.wasm" \
    -o "artifacts/${contract}.wasm"
done

# 上传代码
echo "Uploading Factory..."
FACTORY_CODE_ID=$(msgd tx wasm store artifacts/factory.wasm \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y -o json | \
  jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "Factory Code ID: $FACTORY_CODE_ID"

echo "Uploading Pair..."
PAIR_CODE_ID=$(msgd tx wasm store artifacts/pair.wasm \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y -o json | \
  jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "Pair Code ID: $PAIR_CODE_ID"

echo "Uploading Router..."
ROUTER_CODE_ID=$(msgd tx wasm store artifacts/router.wasm \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y -o json | \
  jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')
echo "Router Code ID: $ROUTER_CODE_ID"

# 实例化 Factory
echo "Instantiating Factory..."
FACTORY_ADDR=$(msgd tx wasm instantiate $FACTORY_CODE_ID \
  "{\"owner\":\"$OWNER_ADDR\",\"pair_code_id\":$PAIR_CODE_ID,\"fee_bps\":30}" \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --label "MSG-DEX-Factory" --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y -o json | \
  jq -r '.logs[0].events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value')
echo "Factory: $FACTORY_ADDR"

# 创建 umsg / msg_token_cw20 交易对
echo "Creating pair..."
msgd tx wasm execute $FACTORY_ADDR \
  "{\"create_pair\":{\"asset_infos\":[{\"native_token\":{\"denom\":\"umsg\"}},{\"token\":{\"contract_addr\":\"$CW20_ADDR\"}}]}}" \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y

# 实例化 Router
echo "Instantiating Router..."
ROUTER_ADDR=$(msgd tx wasm instantiate $ROUTER_CODE_ID \
  "{\"factory_addr\":\"$FACTORY_ADDR\"}" \
  --from $KEY_NAME $KEYRING --chain-id $CHAIN_ID --node $NODE \
  --label "MSG-DEX-Router" --gas-prices 1000000000attoMSG --gas auto --gas-adjustment 1.3 -y -o json | \
  jq -r '.logs[0].events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value')
echo "Router: $ROUTER_ADDR"

echo ""
echo "=== Deployment Complete ==="
echo "Summary:"
echo "  Factory: $FACTORY_ADDR"
echo "  Router:  $ROUTER_ADDR"

9.4 添加初始流动性

// scripts/add-liquidity.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { GasPrice } from "@cosmjs/stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";

async function addInitialLiquidity() {
  const mnemonic = process.env.MNEMONIC!;
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
  const [account] = await wallet.getAccounts();

  const client = await SigningCosmWasmClient.connectWithSigner(
    "https://rpc.msgchain.org",
    wallet,
    { gasPrice: GasPrice.fromString("1000000000attoMSG") }
  );

  const factoryAddr = "msg1factory..."; // 替换为实际部署地址
  const cw20Addr = "msg1cw20token..."; // MSG Chain 上的 CW20 代币

  // 查询工厂获取 Pair 地址
  const pairInfo: any = await client.queryContractSmart(factoryAddr, {
    pair: {
      asset_infos: [
        { native_token: { denom: "umsg" } },
        { token: { contract_addr: cw20Addr } },
      ],
    },
  });
  const pairAddr = pairInfo.contract_addr;
  console.log("Pair address:", pairAddr);

  // 添加初始流动性
  const umsgAmount = "1000000000000000000"; // 1 MSG
  const tokenAmount = "2000000000000000000"; // 2 tokens

  const tx = await client.execute(
    account.address,
    pairAddr,
    {
      provide_liquidity: {
        assets: [
          { info: { native_token: { denom: "umsg" } }, amount: umsgAmount },
          { info: { token: { contract_addr: cw20Addr } }, amount: tokenAmount },
        ],
        slippage_tolerance: null,
        auto_stake: null,
        receiver: null,
      },
    },
    "auto",
    "Add Initial Liquidity",
    [{ denom: "umsg", amount: umsgAmount }]
  );

  console.log("Initial liquidity added, tx:", tx.transactionHash);
  console.log("LP tokens minted to:", account.address);
}

addInitialLiquidity().catch(console.error);

9.5 Docker Compose

# docker-compose.yml
version: "3.8"

services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    environment:
      - VITE_CHAIN_ID=msg-chain-1
      - VITE_RPC_URL=https://rpc.msgchain.org
      - VITE_REST_URL=https://api.msgchain.org
      - VITE_FACTORY_ADDR=msg1factory...
    depends_on:
      - indexer

  indexer:
    build:
      context: ./indexer
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - MSG_RPC=https://rpc.msgchain.org
      - FACTORY_ADDR=msg1factory...
    volumes:
      - indexer_data:/data

  # 可选: MSG Chain 轻节点
  msg-node:
    image: msgchain/node:latest
    ports:
      - "26657:26657"
      - "1317:1317"
    volumes:
      - msg_data:/root/.msgd
    environment:
      - MONIKER=dex-node
      - CHAIN_ID=msg-chain-1

volumes:
  indexer_data:
  msg_data:
# frontend/Dockerfile
FROM node:20-alpine AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# frontend/nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://indexer:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

9.6 前端部署

# 构建前端
cd frontend
npm install
npm run build

# 部署到 IPFS(可选)
npx ipfs-deploy dist

# 或使用 Docker
docker build -t msg-dex-frontend .
docker run -d -p 3000:80 msg-dex-frontend

9.7 Makefile

# Makefile
.PHONY: all build test deploy clean

all: build test

build:
	@echo "Building contracts..."
	cd contracts/factory && cargo wasm
	cd contracts/pair && cargo wasm
	cd contracts/router && cargo wasm
	@echo "Building frontend..."
	cd frontend && npm ci && npm run build

test:
	@echo "Running tests..."
	cargo test --workspace
	cd frontend && npm test 2>/dev/null || echo "No frontend tests configured"

optimize:
	@echo "Optimizing WASM..."
	mkdir -p artifacts
	for contract in factory pair router; do \
		wasm-opt -Os target/wasm32-unknown-unknown/release/$$contract.wasm -o artifacts/$$contract.wasm; \
	done

schema:
	@echo "Generating schemas..."
	cargo schema

deploy:
	@echo "Deploying contracts..."
	bash scripts/deploy.sh

frontend-deploy:
	@echo "Deploying frontend..."
	cd frontend && npm run build

clean:
	cargo clean
	rm -rf artifacts
	rm -rf frontend/dist

docker-build:
	docker-compose build

docker-up:
	docker-compose up -d

docker-down:
	docker-compose down

10. 完整工作流示例

10.1 从头部署到使用

# 1. 克隆 & 编译
git clone https://github.com/msgchain/msg-dex.git
cd msg-dex
make build

# 2. 测试
make test

# 3. 部署合约
make deploy

# 4. 添加初始流动性
npx ts-node scripts/add-liquidity.ts

# 5. 启动前端
make frontend-deploy
make docker-up

10.2 用户交互流程

1. 用户打开 MSG DEX (http://localhost:3000)
2. 点击 "Connect Keplr"
3. Keplr 弹出提示添加 MSG Chain
4. 用户签名连接

--- Swap ---
5. 选择 From: MSG, To: USDC
6. 输入 100 MSG
7. 查看预计输出、价格、滑点
8. 点击 "Swap"
9. Keplr 弹出交易确认
10. 用户确认 → 交易上链 → 收到 USDC

--- Add Liquidity ---
5. 进入 Liquidity 页面
6. 输入 MSG 和 USDC 数量
7. 点击 "Add Liquidity"
8. 确认交易 → 收到 LP 代币

--- Remove Liquidity ---
5. 切换到 Remove 模式
6. 选择百分比 (25%, 50%, 75%, 100%)
7. 点击 "Remove Liquidity"
8. 确认交易 → 收回资产

10.3 合约交互数据流

创建交易对:
  User → Factory.createPair(assets)
    → Factory 用 SubMsg 实例化 Pair 合约
    → Pair 实例化时创建 LP Token (CW20)
    → Reply 返回 Pair 地址
    → Factory 注册 Pair 到 PAIRS 存储

交换:
  User → Pair.swap(offer, belief_price, max_spread, to)
    → 验证资产 (Native/CW20)
    → compute_swap() 计算输出
    → 检查滑点/价格保护
    → 更新储备和 TWAP
    → 发送输出资产给 to 地址

添加流动性:
  User → Pair.provide(assets, slippage_tolerance)
    → 检查比例(如果已有流动性)
    → compute_lp_tokens_to_mint()
    → 更新储备和总流动性
    → Mint LP 代币给用户
    → 首次时销毁 1000 LP

移除流动性:
  User → Pair.withdraw(liquidity, min_assets)
    → compute_withdraw_amounts()
    → 检查最小资产量
    → 更新储备
    → BurnFrom LP 代币
    → 发送资产给用户

11. 安全注意事项

11.1 合约安全

11.2 MSG Chain 特有安全

11.3 前端安全


12. 性能优化

12.1 合约优化

12.2 前端优化

12.3 索引器优化


13. 故障排除

问题 原因 解决方案
Unauthorized 发送者不是合约所有者 使用正确的所有者地址
Pair already exists 交易对已创建 查询工厂确认
Slippage exceeded 市场价格波动 增加滑点容忍度
Insufficient liquidity 池子深度不足 等待更多流动性
Native balance mismatch 未附带足够资金 在 funds 中包含正确金额
Keplr 连接失败 MSG Chain 未配置 使用 experimentalSuggestChain
交易失败 Gas 不足 增加 gas 限制

14. 总结

本文档完整实现了基于 CosmWasm 1.x 的 Uniswap V2 风格 AMM DEX,涵盖:

  1. Factory 合约: 创建和管理交易对(681 行 Rust)
  2. Pair 合约: 恒定乘积 AMM 核心(986 行 Rust,含数学库和测试)
  3. Router 合约: 多跳交换路由(372 行 Rust)
  4. LP Token: 内嵌 CW20 标准实现
  5. 测试: 完整的 cw-multi-test 集成测试和数学库单元测试(634 行)
  6. 前端: 完整的 React + TypeScript + Tailwind 界面(779 行 TypeScript)
  7. 部署: TypeScript 和 Shell 部署脚本 + Docker Compose

整个 DEX 针对 MSG Chain(msg-chain-1)优化,支持 Dilithium-5 后量子签名和 RR+DAR 共识机制。


本文档由 MSG Chain 开发团队生成
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/