dApp Docs/CosmWasm合约模板库
Development reference. Not independently verified for production.

MSG Chain CosmWasm 智能合约模板库 — 完整开发参考级实现

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

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

版本: v1.0.0
目标链: msg-chain-1
Bech32 前缀: msg
Gas 价格: 1,000,000,000 attoMSG/gas
MSG 精度: 18 位小数
签名方案: Dilithium-5 (后量子密码学)
共识: Round-Robin + DAR


目录

  1. 概述
  2. 项目结构模板
  3. 基础合约模板(Hello World)
  4. DID 身份合约 (aidid_did_registry_v1)
  5. AI Agent 注册合约 (agent_registry_v1)
  6. AI Agent 宪法合约 (ai_agent_constitution_v1)
  7. Agent 支付合约 (agent_payment_v1)
  8. 微支付通道合约 (micropayment_session_v1)
  9. 系统合约接口
  10. 合约测试
  11. 合约部署与迁移
  12. 安全最佳实践

1. 概述

1.1 CosmWasm on MSG Chain

MSG Chain 基于 Cosmos SDK + CosmWasm 构建,提供完整的智能合约运行环境。与标准 CosmWasm 相比,MSG Chain 有以下关键差异:

特性 标准 CosmWasm MSG Chain
签名算法 Secp256k1 / Ed25519 Dilithium-5(后量子密码学)
共识机制 Tendermint BFT Round-Robin + DAR (Dilithium-AR)
地址前缀 cosmos msg
Chain ID 可变 msg-chain-1
Gas 代币 多种 umsg(18 位精度)
合约治理 链上参数 AI Agent 宪法约束

1.2 核心合约体系

MSG Chain 的 AI Agent 经济体由 5 个核心 CosmWasm 合约驱动:

aidid_did_registry_v1    --  W3C DID 去中心化身份
agent_registry_v1        --  Agent 注册/发现/状态管理
ai_agent_constitution_v1 --  行为宪法与策略引擎
agent_payment_v1        --  里程碑式 AIPAY 支付
micropayment_session_v1 --  微支付通道(类闪电网络)

candidate_node_staking_v2  --  质押/委托
dao_governance_v1         --  DAO 治理
emission_schedule_v2      --  代币排放
foundation_treasury_v2    --  金库多签(类似 ERC-1155 的多签)

1.3 Dilithium-5 后量子密码学

MSG Chain 使用 Dilithium-5 作为默认签名方案,而非 ECDSA 或 Ed25519。这意味着:

1.4 合约间交互架构

Client/Human
    |
    v
aidid_did_registry_v1  ---->  为 Agent 提供 DID 身份
    |
    v
agent_registry_v1      ---->  注册 Agent 并绑定 DID
    |
    v
ai_agent_constitution_v1 ---> 检查 Agent 动作是否合规
    |
    v
agent_payment_v1       ---->  执行里程碑式支付
    |
    v
micropayment_session_v1 --->  实时微支付通道

2. 项目结构模板

2.1 标准 CosmWasm 项目结构

每个 MSG Chain CosmWasm 合约遵循以下标准项目结构:

my-msg-contract/
├── Cargo.toml                    # 依赖配置
├── src/
│   ├── lib.rs                    # 库入口,模块声明
│   ├── contract.rs               # 合约入口点 (instantiate/execute/query)
│   ├── msg.rs                    # 消息类型定义
│   ├── state.rs                  # 状态存储定义
│   ├── error.rs                  # 错误类型定义
│   └── helpers.rs                # 辅助函数
├── examples/
│   └── schema.rs                 # Schema 生成
├── tests/
│   └── integration.rs            # 集成测试
└── schema/                       # 自动生成的 JSON Schema

2.2 Cargo.toml 通用模板

[package]
name = "my-msg-contract"
version = "1.0.0"
edition = "2021"
description = "MSG Chain CosmWasm smart contract"
repository = "https://github.com/msgchain/contracts"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cosmwasm-storage = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw-utils = "1.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
cosmwasm-crypto = "1.5"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"
anyhow = "1.0"

2.3 Schema 生成器

// examples/schema.rs
use cosmwasm_schema::write_api;
use my_contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ExecuteMsg,
        query: QueryMsg,
    }
}

2.4 构建脚本

# 构建 WASM 合约
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown

# 优化 WASM 体积
wasm-opt -Os target/wasm32-unknown-unknown/release/my_contract.wasm -o contract.wasm

# 生成 Schema
cargo run --example schema

# 运行测试
cargo test

# 检查合约体积
ls -lh contract.wasm

3. 基础合约模板(Hello World)

完整的 CosmWasm 基础合约,包含 Instantiate/Execute/Query 三个入口点。

3.1 Cargo.toml

[package]
name = "hello-msg"
version = "1.0.0"
edition = "2021"
description = "MSG Chain Hello World contract"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"

3.2 src/msg.rs

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub initial_count: i32,
}

#[cw_serde]
pub enum ExecuteMsg {
    Increment {},
    Reset { count: i32 },
    SetGreeting { greeting: String },
}

#[cw_serde]
pub enum QueryMsg {
    GetCount {},
    GetGreeting {},
    GetAdmin {},
}

#[cw_serde]
pub struct CountResponse {
    pub count: i32,
}

#[cw_serde]
pub struct GreetingResponse {
    pub greeting: String,
}

#[cw_serde]
pub struct AdminResponse {
    pub admin: String,
}

3.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cw_storage_plus::Item;

#[cw_serde]
pub struct State {
    pub count: i32,
    pub greeting: String,
    pub admin: String,
}

pub const STATE: Item<State> = Item::new("state");

3.4 src/error.rs

use cosmwasm_std::StdError;
use thiserror::Error;

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

    #[error("Unauthorized - sender is not admin")]
    Unauthorized {},

    #[error("Count cannot be negative")]
    NegativeCount {},
}

3.5 src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Response, StdResult,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    AdminResponse, CountResponse, ExecuteMsg, GreetingResponse,
    InstantiateMsg, QueryMsg,
};
use crate::state::{State, STATE};

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

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

    let state = State {
        count: msg.initial_count,
        greeting: "Hello, MSG Chain!".to_string(),
        admin: msg.admin,
    };
    STATE.save(deps.storage, &state)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("initial_count", msg.initial_count.to_string()))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Increment {} => execute_increment(deps),
        ExecuteMsg::Reset { count } => execute_reset(deps, info, count),
        ExecuteMsg::SetGreeting { greeting } => execute_set_greeting(deps, info, greeting),
    }
}

fn execute_increment(deps: DepsMut) -> Result<Response, ContractError> {
    STATE.update(deps.storage, |mut state| -> Result<_, ContractError> {
        state.count += 1;
        Ok(state)
    })?;

    Ok(Response::new().add_attribute("method", "increment"))
}

fn execute_reset(
    deps: DepsMut,
    info: MessageInfo,
    count: i32,
) -> Result<Response, ContractError> {
    if count < 0 {
        return Err(ContractError::NegativeCount {});
    }

    let state = STATE.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::Unauthorized {});
    }

    STATE.save(deps.storage, &State { count, ..state })?;

    Ok(Response::new()
        .add_attribute("method", "reset")
        .add_attribute("count", count.to_string()))
}

fn execute_set_greeting(
    deps: DepsMut,
    info: MessageInfo,
    greeting: String,
) -> Result<Response, ContractError> {
    let state = STATE.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::Unauthorized {});
    }

    STATE.save(deps.storage, &State { greeting, ..state })?;

    Ok(Response::new().add_attribute("method", "set_greeting"))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetCount {} => to_binary(&query_count(deps)?),
        QueryMsg::GetGreeting {} => to_binary(&query_greeting(deps)?),
        QueryMsg::GetAdmin {} => to_binary(&query_admin(deps)?),
    }
}

fn query_count(deps: Deps) -> StdResult<CountResponse> {
    let state = STATE.load(deps.storage)?;
    Ok(CountResponse { count: state.count })
}

fn query_greeting(deps: Deps) -> StdResult<GreetingResponse> {
    let state = STATE.load(deps.storage)?;
    Ok(GreetingResponse {
        greeting: state.greeting,
    })
}

fn query_admin(deps: Deps) -> StdResult<AdminResponse> {
    let state = STATE.load(deps.storage)?;
    Ok(AdminResponse { admin: state.admin })
}

3.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod state;

3.7 src/helpers.rs

use cosmwasm_std::{
    to_binary, Addr, CosmosMsg, StdResult, WasmMsg,
};
use crate::msg::ExecuteMsg;

pub fn increment_contract(contract_addr: Addr) -> StdResult<CosmosMsg> {
    Ok(WasmMsg::Execute {
        contract_addr: contract_addr.to_string(),
        msg: to_binary(&ExecuteMsg::Increment {})?,
        funds: vec![],
    }
    .into())
}

pub fn reset_contract(contract_addr: Addr, count: i32) -> StdResult<CosmosMsg> {
    Ok(WasmMsg::Execute {
        contract_addr: contract_addr.to_string(),
        msg: to_binary(&ExecuteMsg::Reset { count })?,
        funds: vec![],
    }
    .into())
}

3.8 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{
        mock_dependencies, mock_env, mock_info,
    };
    use cosmwasm_std::{from_binary, Addr};

    use super::*;
    use crate::msg::{AdminResponse, CountResponse, GreetingResponse};

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("creator", &[]);

        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            initial_count: 42,
        };

        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 2);
        assert_eq!(res.attributes[0].value, "instantiate");
        assert_eq!(res.attributes[1].value, "42");
    }

    #[test]
    fn increment() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("creator", &[]);

        instantiate(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            InstantiateMsg {
                admin: "admin".to_string(),
                initial_count: 0,
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            info,
            ExecuteMsg::Increment {},
        )
        .unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetCount {}).unwrap();
        let count_response: CountResponse = from_binary(&res).unwrap();
        assert_eq!(count_response.count, 1);
    }

    #[test]
    fn reset_with_negative_count_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);

        instantiate(
            deps.as_mut(),
            env.clone(),
            info.clone(),
            InstantiateMsg {
                admin: "admin".to_string(),
                initial_count: 10,
            },
        )
        .unwrap();

        let err = execute(
            deps.as_mut(),
            env,
            info,
            ExecuteMsg::Reset { count: -1 },
        )
        .unwrap_err();
        assert_eq!(err, ContractError::NegativeCount {});
    }

    #[test]
    fn unauthorized_reset_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("not_admin", &[]);

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("creator", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                initial_count: 10,
            },
        )
        .unwrap();

        let err = execute(
            deps.as_mut(),
            env,
            info,
            ExecuteMsg::Reset { count: 5 },
        )
        .unwrap_err();
        assert_eq!(err, ContractError::Unauthorized {});
    }

    #[test]
    fn set_greeting() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("creator", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                initial_count: 0,
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("admin", &[]),
            ExecuteMsg::SetGreeting {
                greeting: "Hello, MSG!".to_string(),
            },
        )
        .unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetGreeting {}).unwrap();
        let greeting_response: GreetingResponse = from_binary(&res).unwrap();
        assert_eq!(greeting_response.greeting, "Hello, MSG!");
    }
}

4. DID 身份合约 (aidid_did_registry_v1)

W3C DID 兼容的去中心化身份注册表,集成 Dilithium-5 后量子签名。

4.1 Cargo.toml

[package]
name = "aidid-did-registry"
version = "1.0.0"
edition = "2021"
description = "MSG Chain AI Decentralized Identity Registry v1"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw-utils = "1.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"

4.2 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Binary;

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub registry_name: String,
}

#[cw_serde]
pub enum ExecuteMsg {
    CreateDID {
        did_id: String,
        controller: String,
        verification_methods: Vec<VerificationMethod>,
        services: Vec<ServiceEndpoint>,
        metadata: Option<Binary>,
    },
    UpdateDID {
        did_id: String,
        verification_methods: Option<Vec<VerificationMethod>>,
        services: Option<Vec<ServiceEndpoint>>,
        metadata: Option<Binary>,
    },
    DeactivateDID {
        did_id: String,
    },
    AddVerificationMethod {
        did_id: String,
        method: VerificationMethod,
    },
    RemoveVerificationMethod {
        did_id: String,
        method_id: String,
    },
    AddService {
        did_id: String,
        service: ServiceEndpoint,
    },
    RemoveService {
        did_id: String,
        service_id: String,
    },
}

#[cw_serde]
pub enum QueryMsg {
    ResolveDID { did_id: String },
    CheckDIDActive { did_id: String },
    ListDIDsByController {
        controller: String,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetDIDMetadata { did_id: String },
}

#[cw_serde]
pub struct VerificationMethod {
    pub id: String,
    pub controller: String,
    pub key_type: String,
    pub public_key_multibase: Option<String>,
    pub dilithium5_public_key: Option<String>,
}

#[cw_serde]
pub struct ServiceEndpoint {
    pub id: String,
    pub service_type: String,
    pub service_endpoint: String,
    pub metadata: Option<Binary>,
}

#[cw_serde]
pub struct DIDDocument {
    pub did_id: String,
    pub controller: String,
    pub verification_methods: Vec<VerificationMethod>,
    pub services: Vec<ServiceEndpoint>,
    pub active: bool,
    pub created: u64,
    pub updated: u64,
    pub metadata: Option<Binary>,
}

4.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cw_storage_plus::{Item, Map};

use crate::msg::DIDDocument;

#[cw_serde]
pub struct Config {
    pub admin: String,
    pub registry_name: String,
}

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

pub const DID_DOCUMENTS: Map<&str, DIDDocument> = Map::new("did_docs");

4.4 src/error.rs

use cosmwasm_std::StdError;
use thiserror::Error;

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

    #[error("Unauthorized - sender is not admin")]
    Unauthorized {},

    #[error("DID {did_id} already exists")]
    DIDAlreadyExists { did_id: String },

    #[error("DID {did_id} not found")]
    DIDNotFound { did_id: String },

    #[error("DID {did_id} is deactivated")]
    DIDDeactivated { did_id: String },

    #[error("Verification method {method_id} not found for DID {did_id}")]
    VerificationMethodNotFound { did_id: String, method_id: String },

    #[error("Service {service_id} not found for DID {did_id}")]
    ServiceNotFound { did_id: String, service_id: String },

    #[error("Controller {controller} does not own DID {did_id}")]
    NotController { did_id: String, controller: String },

    #[error("Invalid verification method key type: {key_type}")]
    InvalidKeyType { key_type: String },
}

4.5 src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Order, Response, StdResult,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    CheckDIDActiveResponse, DIDDocument, ExecuteMsg, GetDIDMetadataResponse,
    InstantiateMsg, ListDIDsByControllerResponse, QueryMsg,
    ResolveDIDResponse, ServiceEndpoint, VerificationMethod,
};
use crate::state::{Config, CONFIG, DID_DOCUMENTS};

const CONTRACT_NAME: &str = "aidid_did_registry_v1";
const CONTRACT_VERSION: &str = "1.0.0";

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

    let config = Config {
        admin: msg.admin,
        registry_name: msg.registry_name,
    };
    CONFIG.save(deps.storage, &config)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender.to_string())
        .add_attribute("registry_name", config.registry_name))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreateDID {
            did_id,
            controller,
            verification_methods,
            services,
            metadata,
        } => execute_create_did(
            deps, env, info, did_id, controller, verification_methods, services, metadata,
        ),
        ExecuteMsg::UpdateDID {
            did_id,
            verification_methods,
            services,
            metadata,
        } => execute_update_did(deps, env, info, did_id, verification_methods, services, metadata),
        ExecuteMsg::DeactivateDID { did_id } => {
            execute_deactivate_did(deps, env, info, did_id)
        }
        ExecuteMsg::AddVerificationMethod { did_id, method } => {
            execute_add_verification_method(deps, env, info, did_id, method)
        }
        ExecuteMsg::RemoveVerificationMethod { did_id, method_id } => {
            execute_remove_verification_method(deps, env, info, did_id, method_id)
        }
        ExecuteMsg::AddService { did_id, service } => {
            execute_add_service(deps, env, info, did_id, service)
        }
        ExecuteMsg::RemoveService { did_id, service_id } => {
            execute_remove_service(deps, env, info, did_id, service_id)
        }
    }
}

fn assert_controller(
    deps: &DepsMut,
    info: &MessageInfo,
    did_id: &str,
) -> Result<(), ContractError> {
    let doc = DID_DOCUMENTS.load(deps.storage, did_id)?;
    if doc.controller != info.sender.to_string() {
        return Err(ContractError::NotController {
            did_id: did_id.to_string(),
            controller: info.sender.to_string(),
        });
    }
    if !doc.active {
        return Err(ContractError::DIDDeactivated {
            did_id: did_id.to_string(),
        });
    }
    Ok(())
}

fn validate_key_type(key_type: &str) -> Result<(), ContractError> {
    let valid_types = [
        "Dilithium5VerificationKey2026",
        "Ed25519VerificationKey2020",
        "Secp256k1VerificationKey2019",
        "RSAVerificationKey2018",
        "JsonWebKey2020",
    ];
    if !valid_types.contains(&key_type) {
        return Err(ContractError::InvalidKeyType {
            key_type: key_type.to_string(),
        });
    }
    Ok(())
}

fn execute_create_did(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    controller: String,
    verification_methods: Vec<VerificationMethod>,
    services: Vec<ServiceEndpoint>,
    metadata: Option<Binary>,
) -> Result<Response, ContractError> {
    if DID_DOCUMENTS.may_load(deps.storage, &did_id)?.is_some() {
        return Err(ContractError::DIDAlreadyExists { did_id });
    }

    if info.sender.to_string() != controller {
        return Err(ContractError::NotController {
            did_id: did_id.clone(),
            controller: info.sender.to_string(),
        });
    }

    for vm in &verification_methods {
        validate_key_type(&vm.key_type)?;
    }

    let now = env.block.time.seconds();
    let document = DIDDocument {
        did_id: did_id.clone(),
        controller,
        verification_methods,
        services,
        active: true,
        created: now,
        updated: now,
        metadata,
    };

    DID_DOCUMENTS.save(deps.storage, &did_id, &document)?;

    Ok(Response::new()
        .add_attribute("method", "create_did")
        .add_attribute("did_id", &did_id)
        .add_attribute("created", now.to_string()))
}

fn execute_update_did(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    verification_methods: Option<Vec<VerificationMethod>>,
    services: Option<Vec<ServiceEndpoint>>,
    metadata: Option<Binary>,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;

    if let Some(vms) = verification_methods {
        for vm in &vms {
            validate_key_type(&vm.key_type)?;
        }
        doc.verification_methods = vms;
    }
    if let Some(svcs) = services {
        doc.services = svcs;
    }
    if let Some(md) = metadata {
        doc.metadata = Some(md);
    }

    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "update_did")
        .add_attribute("did_id", &did_id))
}

fn execute_deactivate_did(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    if !doc.active {
        return Err(ContractError::DIDDeactivated { did_id });
    }
    doc.active = false;
    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "deactivate_did")
        .add_attribute("did_id", &did_id))
}

fn execute_add_verification_method(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    method: VerificationMethod,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    validate_key_type(&method.key_type)?;

    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    if let Some(existing) = doc.verification_methods.iter_mut().find(|m| m.id == method.id) {
        *existing = method.clone();
    } else {
        doc.verification_methods.push(method);
    }
    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "add_verification_method")
        .add_attribute("did_id", &did_id))
}

fn execute_remove_verification_method(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    method_id: String,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    let initial_len = doc.verification_methods.len();
    doc.verification_methods.retain(|m| m.id != method_id);
    if doc.verification_methods.len() == initial_len {
        return Err(ContractError::VerificationMethodNotFound {
            did_id,
            method_id,
        });
    }
    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "remove_verification_method")
        .add_attribute("method_id", &method_id))
}

fn execute_add_service(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    service: ServiceEndpoint,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    if let Some(existing) = doc.services.iter_mut().find(|s| s.id == service.id) {
        *existing = service.clone();
    } else {
        doc.services.push(service);
    }
    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "add_service")
        .add_attribute("did_id", &did_id))
}

fn execute_remove_service(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    did_id: String,
    service_id: String,
) -> Result<Response, ContractError> {
    assert_controller(&deps, &info, &did_id)?;
    let mut doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    let initial_len = doc.services.len();
    doc.services.retain(|s| s.id != service_id);
    if doc.services.len() == initial_len {
        return Err(ContractError::ServiceNotFound {
            did_id,
            service_id,
        });
    }
    doc.updated = env.block.time.seconds();
    DID_DOCUMENTS.save(deps.storage, &did_id, &doc)?;

    Ok(Response::new()
        .add_attribute("method", "remove_service")
        .add_attribute("service_id", &service_id))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::ResolveDID { did_id } => to_binary(&query_resolve_did(deps, did_id)?),
        QueryMsg::CheckDIDActive { did_id } => to_binary(&query_check_did_active(deps, did_id)?),
        QueryMsg::ListDIDsByController {
            controller,
            start_after,
            limit,
        } => to_binary(&query_list_dids_by_controller(
            deps, controller, start_after, limit,
        )?),
        QueryMsg::GetDIDMetadata { did_id } => to_binary(&query_get_did_metadata(deps, did_id)?),
    }
}

fn query_resolve_did(deps: Deps, did_id: String) -> StdResult<ResolveDIDResponse> {
    let document = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    Ok(ResolveDIDResponse { document })
}

fn query_check_did_active(deps: Deps, did_id: String) -> StdResult<CheckDIDActiveResponse> {
    let doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    Ok(CheckDIDActiveResponse { active: doc.active })
}

fn query_list_dids_by_controller(
    deps: Deps,
    controller: String,
    start_after: Option<String>,
    limit: Option<u32>,
) -> StdResult<ListDIDsByControllerResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let start = start_after.as_deref();

    let dids: StdResult<Vec<_>> = DID_DOCUMENTS
        .range(deps.storage, None, None, Order::Ascending)
        .filter(|r| {
            r.as_ref()
                .map(|(_, doc)| doc.controller == controller)
                .unwrap_or(false)
        })
        .skip(start.map(|s| s.len()).unwrap_or(0))
        .take(limit)
        .map(|r| r.map(|(_, doc)| doc))
        .collect();

    Ok(ListDIDsByControllerResponse { dids: dids? })
}

fn query_get_did_metadata(deps: Deps, did_id: String) -> StdResult<GetDIDMetadataResponse> {
    let doc = DID_DOCUMENTS.load(deps.storage, &did_id)?;
    Ok(GetDIDMetadataResponse {
        metadata: doc.metadata,
    })
}

4.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod state;

4.7 src/helpers.rs

use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg};
use crate::msg::{ExecuteMsg, ServiceEndpoint, VerificationMethod};

pub fn create_did(
    contract_addr: Addr,
    did_id: String,
    controller: String,
    verification_methods: Vec<VerificationMethod>,
    services: Vec<ServiceEndpoint>,
) -> StdResult<CosmosMsg> {
    Ok(WasmMsg::Execute {
        contract_addr: contract_addr.to_string(),
        msg: to_binary(&ExecuteMsg::CreateDID {
            did_id,
            controller,
            verification_methods,
            services,
            metadata: None,
        })?,
        funds: vec![],
    }
    .into())
}

pub fn deactivate_did(contract_addr: Addr, did_id: String) -> StdResult<CosmosMsg> {
    Ok(WasmMsg::Execute {
        contract_addr: contract_addr.to_string(),
        msg: to_binary(&ExecuteMsg::DeactivateDID { did_id })?,
        funds: vec![],
    }
    .into())
}

4.8 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{from_binary, Binary};

    use super::*;

    fn create_test_vm(id: &str) -> VerificationMethod {
        VerificationMethod {
            id: id.to_string(),
            controller: "controller".to_string(),
            key_type: "Dilithium5VerificationKey2026".to_string(),
            public_key_multibase: Some("z6Mk".to_string()),
            dilithium5_public_key: Some("dilithium5_pk".to_string()),
        }
    }

    fn create_test_service(id: &str) -> ServiceEndpoint {
        ServiceEndpoint {
            id: id.to_string(),
            service_type: "LinkedDomains".to_string(),
            service_endpoint: "https://example.com".to_string(),
            metadata: None,
        }
    }

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);

        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            registry_name: "MSG DID Registry".to_string(),
        };
        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 3);

        let config = CONFIG.load(&deps.storage).unwrap();
        assert_eq!(config.admin, "admin");
    }

    #[test]
    fn create_and_resolve_did() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("admin", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                registry_name: "test".to_string(),
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("controller", &[]),
            ExecuteMsg::CreateDID {
                did_id: "did:msg:agent:test-1".to_string(),
                controller: "controller".to_string(),
                verification_methods: vec![create_test_vm("vm-1")],
                services: vec![create_test_service("svc-1")],
                metadata: None,
            },
        )
        .unwrap();

        let res = query(
            deps.as_ref(),
            env.clone(),
            QueryMsg::ResolveDID {
                did_id: "did:msg:agent:test-1".to_string(),
            },
        )
        .unwrap();
        let response: ResolveDIDResponse = from_binary(&res).unwrap();
        assert_eq!(response.document.did_id, "did:msg:agent:test-1");
        assert!(response.document.active);
    }

    #[test]
    fn deactivate_did() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("admin", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                registry_name: "test".to_string(),
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("controller", &[]),
            ExecuteMsg::CreateDID {
                did_id: "did:msg:agent:to-deactivate".to_string(),
                controller: "controller".to_string(),
                verification_methods: vec![],
                services: vec![],
                metadata: None,
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("controller", &[]),
            ExecuteMsg::DeactivateDID {
                did_id: "did:msg:agent:to-deactivate".to_string(),
            },
        )
        .unwrap();

        let res = query(
            deps.as_ref(),
            env,
            QueryMsg::CheckDIDActive {
                did_id: "did:msg:agent:to-deactivate".to_string(),
            },
        )
        .unwrap();
        let response: CheckDIDActiveResponse = from_binary(&res).unwrap();
        assert!(!response.active);
    }

    #[test]
    fn create_duplicate_did_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("admin", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                registry_name: "test".to_string(),
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("controller", &[]),
            ExecuteMsg::CreateDID {
                did_id: "did:msg:agent:dup".to_string(),
                controller: "controller".to_string(),
                verification_methods: vec![],
                services: vec![],
                metadata: None,
            },
        )
        .unwrap();

        let err = execute(
            deps.as_mut(),
            env,
            mock_info("controller", &[]),
            ExecuteMsg::CreateDID {
                did_id: "did:msg:agent:dup".to_string(),
                controller: "controller".to_string(),
                verification_methods: vec![],
                services: vec![],
                metadata: None,
            },
        )
        .unwrap_err();
        assert_eq!(
            err,
            ContractError::DIDAlreadyExists {
                did_id: "did:msg:agent:dup".to_string()
            }
        );
    }

    #[test]
    fn only_controller_can_modify_did() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(
            deps.as_mut(),
            env.clone(),
            mock_info("admin", &[]),
            InstantiateMsg {
                admin: "admin".to_string(),
                registry_name: "test".to_string(),
            },
        )
        .unwrap();

        execute(
            deps.as_mut(),
            env.clone(),
            mock_info("controller", &[]),
            ExecuteMsg::CreateDID {
                did_id: "did:msg:agent:protected".to_string(),
                controller: "controller".to_string(),
                verification_methods: vec![],
                services: vec![],
                metadata: None,
            },
        )
        .unwrap();

        let err = execute(
            deps.as_mut(),
            env,
            mock_info("attacker", &[]),
            ExecuteMsg::UpdateDID {
                did_id: "did:msg:agent:protected".to_string(),
                verification_methods: None,
                services: None,
                metadata: Some(Binary::from(b"evil".as_ref())),
            },
        )
        .unwrap_err();
        assert_eq!(
            err,
            ContractError::NotController {
                did_id: "did:msg:agent:protected".to_string(),
                controller: "attacker".to_string(),
            }
        );
    }
}

5. AI Agent 注册合约 (agent_registry_v1)

Agent 注册与发现合约,提供完整的 CRUD 操作和状态管理。

5.1 Cargo.toml

[package]
name = "agent-registry"
version = "1.0.0"
edition = "2021"
description = "MSG Chain AI Agent Registry v1"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw-utils = "1.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"

5.2 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Binary;

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub did_registry_address: String,
}

#[cw_serde]
pub enum ExecuteMsg {
    RegisterAgent {
        agent_id: String,
        did_id: String,
        owner: String,
        name: String,
        description: Option<String>,
        endpoint: Option<String>,
        metadata: Option<Binary>,
        agent_type: String,
        capabilities: Vec<String>,
    },
    UpdateAgent {
        agent_id: String,
        name: Option<String>,
        description: Option<String>,
        endpoint: Option<String>,
        metadata: Option<Binary>,
        capabilities: Option<Vec<String>>,
    },
    DeregisterAgent { agent_id: String },
    UpdateMetadata { agent_id: String, metadata: Binary },
    SetStatus { agent_id: String, status: AgentStatus },
}

#[cw_serde]
pub enum QueryMsg {
    GetAgent { agent_id: String },
    ListAgents {
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetAgentsByOwner {
        owner: String,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetAgentByDID { did_id: String },
    ListAgentsByStatus {
        status: AgentStatus,
        start_after: Option<String>,
        limit: Option<u32>,
    },
}

#[cw_serde]
pub enum AgentStatus {
    Active,
    Inactive,
    Suspended,
    Deregistered,
}

#[cw_serde]
pub struct Agent {
    pub agent_id: String,
    pub did_id: String,
    pub owner: String,
    pub name: String,
    pub description: Option<String>,
    pub endpoint: Option<String>,
    pub metadata: Option<Binary>,
    pub agent_type: String,
    pub capabilities: Vec<String>,
    pub status: AgentStatus,
    pub registered_at: u64,
    pub updated_at: u64,
}

#[cw_serde]
pub struct GetAgentResponse {
    pub agent: Agent,
}

#[cw_serde]
pub struct ListAgentsResponse {
    pub agents: Vec<Agent>,
}

5.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex};

use crate::msg::Agent;

#[cw_serde]
pub struct Config {
    pub admin: String,
    pub did_registry_address: String,
}

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

pub struct AgentIndexes<'a> {
    pub owner: MultiIndex<'a, (String, Vec<u8>), Agent>,
    pub did_id: MultiIndex<'a, (String, Vec<u8>), Agent>,
    pub status: MultiIndex<'a, (String, Vec<u8>), Agent>,
}

impl<'a> IndexList<Agent> for AgentIndexes<'a> {
    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<Agent>> + '_> {
        let v: Vec<&dyn Index<Agent>> = vec![&self.owner, &self.did_id, &self.status];
        Box::new(v.into_iter())
    }
}

pub fn agents<'a>() -> IndexedMap<'a, &'a str, Agent, AgentIndexes<'a>> {
    let indexes = AgentIndexes {
        owner: MultiIndex::new(
            |a: &Agent| (a.owner.clone(), a.agent_id.as_bytes().to_vec()),
            "agents",
            "agents_owner",
        ),
        did_id: MultiIndex::new(
            |a: &Agent| (a.did_id.clone(), a.agent_id.as_bytes().to_vec()),
            "agents",
            "agents_did",
        ),
        status: MultiIndex::new(
            |a: &Agent| (format!("{:?}", a.status), a.agent_id.as_bytes().to_vec()),
            "agents",
            "agents_status",
        ),
    };
    IndexedMap::new("agents", indexes)
}

5.4 src/error.rs

use cosmwasm_std::StdError;
use thiserror::Error;

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

    #[error("Unauthorized - sender is not admin or owner")]
    Unauthorized {},

    #[error("Agent {agent_id} already exists")]
    AgentAlreadyExists { agent_id: String },

    #[error("Agent {agent_id} not found")]
    AgentNotFound { agent_id: String },

    #[error("Agent {agent_id} is deregistered and cannot be modified")]
    AgentDeregistered { agent_id: String },
}

5.5 src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Order, Response, StdResult,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    Agent, AgentStatus, ExecuteMsg, GetAgentResponse, InstantiateMsg,
    ListAgentsResponse, QueryMsg,
};
use crate::state::{agents, Config, CONFIG};

const CONTRACT_NAME: &str = "agent_registry_v1";
const CONTRACT_VERSION: &str = "1.0.0";

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

    let config = Config {
        admin: msg.admin,
        did_registry_address: msg.did_registry_address,
    };
    CONFIG.save(deps.storage, &config)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender.to_string())
        .add_attribute("did_registry", &config.did_registry_address))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::RegisterAgent {
            agent_id, did_id, owner, name, description,
            endpoint, metadata, agent_type, capabilities,
        } => execute_register_agent(
            deps, env, info, agent_id, did_id, owner, name,
            description, endpoint, metadata, agent_type, capabilities,
        ),
        ExecuteMsg::UpdateAgent {
            agent_id, name, description, endpoint, metadata, capabilities,
        } => execute_update_agent(
            deps, env, info, agent_id, name, description,
            endpoint, metadata, capabilities,
        ),
        ExecuteMsg::DeregisterAgent { agent_id } => {
            execute_deregister_agent(deps, env, info, agent_id)
        }
        ExecuteMsg::UpdateMetadata { agent_id, metadata } => {
            execute_update_metadata(deps, env, info, agent_id, metadata)
        }
        ExecuteMsg::SetStatus { agent_id, status } => {
            execute_set_status(deps, env, info, agent_id, status)
        }
    }
}

fn assert_owner_or_admin(
    deps: &DepsMut,
    info: &MessageInfo,
    agent_id: &str,
) -> Result<Agent, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let agent = agents().load(deps.storage, agent_id)?;
    if info.sender.to_string() != agent.owner && info.sender.to_string() != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    Ok(agent)
}

fn execute_register_agent(
    deps: DepsMut,
    env: Env,
    _info: MessageInfo,
    agent_id: String,
    did_id: String,
    owner: String,
    name: String,
    description: Option<String>,
    endpoint: Option<String>,
    metadata: Option<Binary>,
    agent_type: String,
    capabilities: Vec<String>,
) -> Result<Response, ContractError> {
    if agents().may_load(deps.storage, &agent_id)?.is_some() {
        return Err(ContractError::AgentAlreadyExists { agent_id });
    }

    let now = env.block.time.seconds();
    let agent = Agent {
        agent_id: agent_id.clone(),
        did_id, owner, name, description, endpoint, metadata,
        agent_type, capabilities,
        status: AgentStatus::Active,
        registered_at: now,
        updated_at: now,
    };

    agents().save(deps.storage, &agent_id, &agent)?;

    Ok(Response::new()
        .add_attribute("method", "register_agent")
        .add_attribute("agent_id", &agent_id))
}

fn execute_update_agent(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    agent_id: String,
    name: Option<String>,
    description: Option<String>,
    endpoint: Option<String>,
    metadata: Option<Binary>,
    capabilities: Option<Vec<String>>,
) -> Result<Response, ContractError> {
    let mut agent = assert_owner_or_admin(&deps, &info, &agent_id)?;

    if let AgentStatus::Deregistered = agent.status {
        return Err(ContractError::AgentDeregistered { agent_id });
    }

    if let Some(n) = name { agent.name = n; }
    if let Some(d) = description { agent.description = Some(d); }
    if let Some(e) = endpoint { agent.endpoint = Some(e); }
    if let Some(m) = metadata { agent.metadata = Some(m); }
    if let Some(c) = capabilities { agent.capabilities = c; }

    agent.updated_at = env.block.time.seconds();
    agents().save(deps.storage, &agent_id, &agent)?;

    Ok(Response::new()
        .add_attribute("method", "update_agent")
        .add_attribute("agent_id", &agent_id))
}

fn execute_deregister_agent(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    agent_id: String,
) -> Result<Response, ContractError> {
    let mut agent = assert_owner_or_admin(&deps, &info, &agent_id)?;
    agent.status = AgentStatus::Deregistered;
    agent.updated_at = env.block.time.seconds();
    agents().save(deps.storage, &agent_id, &agent)?;

    Ok(Response::new()
        .add_attribute("method", "deregister_agent")
        .add_attribute("agent_id", &agent_id))
}

fn execute_update_metadata(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    agent_id: String,
    metadata: Binary,
) -> Result<Response, ContractError> {
    let mut agent = assert_owner_or_admin(&deps, &info, &agent_id)?;
    if let AgentStatus::Deregistered = agent.status {
        return Err(ContractError::AgentDeregistered { agent_id });
    }
    agent.metadata = Some(metadata);
    agent.updated_at = env.block.time.seconds();
    agents().save(deps.storage, &agent_id, &agent)?;

    Ok(Response::new()
        .add_attribute("method", "update_metadata")
        .add_attribute("agent_id", &agent_id))
}

fn execute_set_status(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    agent_id: String,
    status: AgentStatus,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender.to_string() != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    let mut agent = agents().load(deps.storage, &agent_id)?;
    agent.status = status;
    agent.updated_at = env.block.time.seconds();
    agents().save(deps.storage, &agent_id, &agent)?;

    Ok(Response::new()
        .add_attribute("method", "set_status")
        .add_attribute("agent_id", &agent_id)
        .add_attribute("status", format!("{:?}", agent.status)))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetAgent { agent_id } => to_binary(&query_get_agent(deps, agent_id)?),
        QueryMsg::ListAgents { start_after, limit } => {
            to_binary(&query_list_agents(deps, start_after, limit)?)
        }
        QueryMsg::GetAgentsByOwner { owner, start_after, limit } => {
            to_binary(&query_get_agents_by_owner(deps, owner, start_after, limit)?)
        }
        QueryMsg::GetAgentByDID { did_id } => to_binary(&query_get_agent_by_did(deps, did_id)?),
        QueryMsg::ListAgentsByStatus { status, start_after, limit } => {
            to_binary(&query_list_agents_by_status(deps, status, start_after, limit)?)
        }
    }
}

fn query_get_agent(deps: Deps, agent_id: String) -> StdResult<GetAgentResponse> {
    let agent = agents().load(deps.storage, &agent_id)?;
    Ok(GetAgentResponse { agent })
}

fn query_list_agents(
    deps: Deps, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListAgentsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let agents: StdResult<Vec<_>> = agents()
        .range(deps.storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit)
        .map(|r| r.map(|(_, a)| a))
        .collect();
    Ok(ListAgentsResponse { agents: agents? })
}

fn query_get_agents_by_owner(
    deps: Deps, owner: String, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListAgentsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let agents: StdResult<Vec<_>> = agents()
        .idx.owner.prefix(owner)
        .range(deps.storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit)
        .map(|r| r.map(|(_, a)| a))
        .collect();
    Ok(ListAgentsResponse { agents: agents? })
}

fn query_get_agent_by_did(deps: Deps, did_id: String) -> StdResult<GetAgentResponse> {
    let agent = agents()
        .idx.did_id.prefix(did_id)
        .range(deps.storage, None, None, Order::Ascending)
        .next()
        .transpose()?
        .map(|(_, a)| a)
        .ok_or_else(|| cosmwasm_std::StdError::not_found("Agent by DID"))?;
    Ok(GetAgentResponse { agent })
}

fn query_list_agents_by_status(
    deps: Deps, status: AgentStatus, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListAgentsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let status_key = format!("{:?}", status);
    let agents: StdResult<Vec<_>> = agents()
        .idx.status.prefix(status_key)
        .range(deps.storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit)
        .map(|r| r.map(|(_, a)| a))
        .collect();
    Ok(ListAgentsResponse { agents: agents? })
}

5.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod state;

5.7 src/helpers.rs

use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg};
use crate::msg::ExecuteMsg;

pub fn register_agent(
    contract_addr: Addr, agent_id: String, did_id: String,
    owner: String, name: String, agent_type: String, capabilities: Vec<String>,
) -> StdResult<CosmosMsg> {
    Ok(WasmMsg::Execute {
        contract_addr: contract_addr.to_string(),
        msg: to_binary(&ExecuteMsg::RegisterAgent {
            agent_id, did_id, owner, name,
            description: None, endpoint: None, metadata: None,
            agent_type, capabilities,
        })?,
        funds: vec![],
    }
    .into())
}

5.8 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::from_binary;
    use super::*;

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);
        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            did_registry_address: "msg1didregistry".to_string(),
        };
        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 3);
    }

    #[test]
    fn register_and_get_agent() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), did_registry_address: "msg1didregistry".to_string() }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("owner", &[]),
            ExecuteMsg::RegisterAgent {
                agent_id: "agent-001".to_string(),
                did_id: "did:msg:agent:agent-001".to_string(),
                owner: "owner".to_string(),
                name: "Test Agent".to_string(),
                description: Some("A test agent".to_string()),
                endpoint: Some("https://agent.example.com".to_string()),
                metadata: None,
                agent_type: "ai".to_string(),
                capabilities: vec!["text-generation".to_string(), "code-review".to_string()],
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetAgent { agent_id: "agent-001".to_string() }).unwrap();
        let response: GetAgentResponse = from_binary(&res).unwrap();
        assert_eq!(response.agent.name, "Test Agent");
        assert_eq!(response.agent.capabilities.len(), 2);
        assert_eq!(response.agent.status, AgentStatus::Active);
    }

    #[test]
    fn duplicate_agent_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), did_registry_address: "msg1didregistry".to_string() }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("owner", &[]),
            ExecuteMsg::RegisterAgent {
                agent_id: "agent-001".to_string(), did_id: "did:msg:agent:agent-001".to_string(),
                owner: "owner".to_string(), name: "Test".to_string(),
                description: None, endpoint: None, metadata: None,
                agent_type: "ai".to_string(), capabilities: vec![],
            }
        ).unwrap();

        let err = execute(deps.as_mut(), env, mock_info("owner", &[]),
            ExecuteMsg::RegisterAgent {
                agent_id: "agent-001".to_string(), did_id: "did:msg:agent:agent-001".to_string(),
                owner: "owner".to_string(), name: "Duplicate".to_string(),
                description: None, endpoint: None, metadata: None,
                agent_type: "ai".to_string(), capabilities: vec![],
            }
        ).unwrap_err();
        assert_eq!(err, ContractError::AgentAlreadyExists { agent_id: "agent-001".to_string() });
    }

    #[test]
    fn update_agent() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), did_registry_address: "msg1didregistry".to_string() }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("owner", &[]),
            ExecuteMsg::RegisterAgent {
                agent_id: "agent-001".to_string(), did_id: "did:msg:agent:agent-001".to_string(),
                owner: "owner".to_string(), name: "Original".to_string(),
                description: None, endpoint: None, metadata: None,
                agent_type: "ai".to_string(), capabilities: vec!["text-generation".to_string()],
            }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("owner", &[]),
            ExecuteMsg::UpdateAgent {
                agent_id: "agent-001".to_string(),
                name: Some("Updated Name".to_string()),
                description: Some("Updated desc".to_string()),
                endpoint: None, metadata: None,
                capabilities: Some(vec!["text-generation".to_string(), "data-analysis".to_string()]),
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetAgent { agent_id: "agent-001".to_string() }).unwrap();
        let response: GetAgentResponse = from_binary(&res).unwrap();
        assert_eq!(response.agent.name, "Updated Name");
        assert_eq!(response.agent.capabilities.len(), 2);
    }

    #[test]
    fn set_status() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), did_registry_address: "msg1didregistry".to_string() }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("owner", &[]),
            ExecuteMsg::RegisterAgent {
                agent_id: "agent-001".to_string(), did_id: "did:msg:agent:agent-001".to_string(),
                owner: "owner".to_string(), name: "Test".to_string(),
                description: None, endpoint: None, metadata: None,
                agent_type: "ai".to_string(), capabilities: vec![],
            }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            ExecuteMsg::SetStatus { agent_id: "agent-001".to_string(), status: AgentStatus::Suspended }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetAgent { agent_id: "agent-001".to_string() }).unwrap();
        let response: GetAgentResponse = from_binary(&res).unwrap();
        assert_eq!(response.agent.status, AgentStatus::Suspended);
    }
}

6. AI Agent 宪法合约 (ai_agent_constitution_v1)

AI Agent 宪法合约定义了 Agent 的行为边界和规则引擎,是 MSG Chain 的核心差异化功能。

6.1 Cargo.toml

[package]
name = "ai-agent-constitution"
version = "1.0.0"
edition = "2021"
description = "MSG Chain AI Agent Constitution v1 - Rule Engine"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"

6.2 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Binary;

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub constitution_name: String,
    pub preamble: Option<String>,
}

#[cw_serde]
pub enum ExecuteMsg {
    UpdateConstitution {
        preamble: Option<String>,
        active: Option<bool>,
    },
    AddRule {
        rule_id: String,
        rule_text: String,
        category: String,
        priority: u32,
        constraints: Option<Binary>,
    },
    RemoveRule { rule_id: String },
    UpdateRule {
        rule_id: String,
        rule_text: Option<String>,
        category: Option<String>,
        priority: Option<u32>,
        active: Option<bool>,
        constraints: Option<Binary>,
    },
    SetActive { active: bool },
}

#[cw_serde]
pub enum QueryMsg {
    GetConstitution {},
    GetRule { rule_id: String },
    ListRules {
        category: Option<String>,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    IsActionAllowed {
        action: String,
        context: Option<Binary>,
    },
    GetActiveStatus {},
}

#[cw_serde]
pub struct Rule {
    pub rule_id: String,
    pub rule_text: String,
    pub category: String,
    pub priority: u32,
    pub active: bool,
    pub constraints: Option<Binary>,
    pub created_at: u64,
    pub updated_at: u64,
}

#[cw_serde]
pub struct Constitution {
    pub name: String,
    pub preamble: Option<String>,
    pub active: bool,
    pub rule_count: u32,
    pub created_at: u64,
    pub updated_at: u64,
}

#[cw_serde]
pub struct ConstitutionResponse {
    pub constitution: Constitution,
}

#[cw_serde]
pub struct RuleResponse {
    pub rule: Rule,
}

#[cw_serde]
pub struct ListRulesResponse {
    pub rules: Vec<Rule>,
}

#[cw_serde]
pub struct IsActionAllowedResponse {
    pub allowed: bool,
    pub reasons: Vec<String>,
}

#[cw_serde]
pub struct ActiveStatusResponse {
    pub active: bool,
}

6.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex};

use crate::msg::{Constitution, Rule};

#[cw_serde]
pub struct Config {
    pub admin: String,
}

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

pub const CONSTITUTION: Item<Constitution> = Item::new("constitution");

pub struct RuleIndexes<'a> {
    pub category: MultiIndex<'a, (String, Vec<u8>), Rule>,
}

impl<'a> IndexList<Rule> for RuleIndexes<'a> {
    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<Rule>> + '_> {
        let v: Vec<&dyn Index<Rule>> = vec![&self.category];
        Box::new(v.into_iter())
    }
}

pub fn rules<'a>() -> IndexedMap<'a, &'a str, Rule, RuleIndexes<'a>> {
    let indexes = RuleIndexes {
        category: MultiIndex::new(
            |r: &Rule| (r.category.clone(), r.rule_id.as_bytes().to_vec()),
            "rules",
            "rules_category",
        ),
    };
    IndexedMap::new("rules", indexes)
}

6.4 src/error.rs

use cosmwasm_std::StdError;
use thiserror::Error;

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

    #[error("Unauthorized - sender is not admin")]
    Unauthorized {},

    #[error("Rule {rule_id} not found")]
    RuleNotFound { rule_id: String },

    #[error("Rule {rule_id} already exists")]
    RuleAlreadyExists { rule_id: String },

    #[error("Constitution is not active")]
    ConstitutionNotActive {},

    #[error("Action {action} denied by rule {rule_id}: {reason}")]
    ActionDenied { action: String, rule_id: String, reason: String },
}

6.5 src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Order, Response, StdResult,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    ActiveStatusResponse, Constitution, ConstitutionResponse, ExecuteMsg,
    InstantiateMsg, IsActionAllowedResponse, ListRulesResponse, QueryMsg,
    Rule, RuleResponse,
};
use crate::state::{rules, Config, CONSTITUTION, CONFIG};

const CONTRACT_NAME: &str = "ai_agent_constitution_v1";
const CONTRACT_VERSION: &str = "1.0.0";

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

    let config = Config {
        admin: msg.admin.clone(),
    };
    CONFIG.save(deps.storage, &config)?;

    let now = env.block.time.seconds();
    let constitution = Constitution {
        name: msg.constitution_name,
        preamble: msg.preamble,
        active: true,
        rule_count: 0,
        created_at: now,
        updated_at: now,
    };
    CONSTITUTION.save(deps.storage, &constitution)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender.to_string())
        .add_attribute("constitution_name", &constitution.name))
}

#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::UpdateConstitution { preamble, active } => {
            execute_update_constitution(deps, env, info, preamble, active)
        }
        ExecuteMsg::AddRule { rule_id, rule_text, category, priority, constraints } => {
            execute_add_rule(deps, env, info, rule_id, rule_text, category, priority, constraints)
        }
        ExecuteMsg::RemoveRule { rule_id } => execute_remove_rule(deps, env, info, rule_id),
        ExecuteMsg::UpdateRule { rule_id, rule_text, category, priority, active, constraints } => {
            execute_update_rule(deps, env, info, rule_id, rule_text, category, priority, active, constraints)
        }
        ExecuteMsg::SetActive { active } => execute_set_active(deps, env, info, active),
    }
}

fn assert_admin(deps: &DepsMut, info: &MessageInfo) -> Result<(), ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender.to_string() != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    Ok(())
}

fn execute_update_constitution(
    deps: DepsMut, env: Env, info: MessageInfo,
    preamble: Option<String>, active: Option<bool>,
) -> Result<Response, ContractError> {
    assert_admin(&deps, &info)?;
    let mut constitution = CONSTITUTION.load(deps.storage)?;
    if let Some(p) = preamble { constitution.preamble = Some(p); }
    if let Some(a) = active { constitution.active = a; }
    constitution.updated_at = env.block.time.seconds();
    CONSTITUTION.save(deps.storage, &constitution)?;
    Ok(Response::new()
        .add_attribute("method", "update_constitution")
        .add_attribute("active", constitution.active.to_string()))
}

fn execute_add_rule(
    deps: DepsMut, env: Env, info: MessageInfo,
    rule_id: String, rule_text: String, category: String,
    priority: u32, constraints: Option<Binary>,
) -> Result<Response, ContractError> {
    assert_admin(&deps, &info)?;
    if rules().may_load(deps.storage, &rule_id)?.is_some() {
        return Err(ContractError::RuleAlreadyExists { rule_id });
    }
    let now = env.block.time.seconds();
    let rule = Rule {
        rule_id: rule_id.clone(), rule_text, category, priority,
        active: true, constraints,
        created_at: now, updated_at: now,
    };
    rules().save(deps.storage, &rule_id, &rule)?;

    let mut constitution = CONSTITUTION.load(deps.storage)?;
    constitution.rule_count += 1;
    constitution.updated_at = now;
    CONSTITUTION.save(deps.storage, &constitution)?;

    Ok(Response::new()
        .add_attribute("method", "add_rule")
        .add_attribute("rule_id", &rule_id))
}

fn execute_remove_rule(
    deps: DepsMut, env: Env, info: MessageInfo, rule_id: String,
) -> Result<Response, ContractError> {
    assert_admin(&deps, &info)?;
    if rules().may_load(deps.storage, &rule_id)?.is_none() {
        return Err(ContractError::RuleNotFound { rule_id });
    }
    rules().remove(deps.storage, &rule_id)?;

    let mut constitution = CONSTITUTION.load(deps.storage)?;
    constitution.rule_count = constitution.rule_count.saturating_sub(1);
    constitution.updated_at = env.block.time.seconds();
    CONSTITUTION.save(deps.storage, &constitution)?;

    Ok(Response::new()
        .add_attribute("method", "remove_rule")
        .add_attribute("rule_id", &rule_id))
}

fn execute_update_rule(
    deps: DepsMut, env: Env, info: MessageInfo,
    rule_id: String, rule_text: Option<String>, category: Option<String>,
    priority: Option<u32>, active: Option<bool>, constraints: Option<Binary>,
) -> Result<Response, ContractError> {
    assert_admin(&deps, &info)?;
    let mut rule = rules().load(deps.storage, &rule_id)
        .map_err(|_| ContractError::RuleNotFound { rule_id: rule_id.clone() })?;
    if let Some(t) = rule_text { rule.rule_text = t; }
    if let Some(c) = category { rule.category = c; }
    if let Some(p) = priority { rule.priority = p; }
    if let Some(a) = active { rule.active = a; }
    if let Some(c) = constraints { rule.constraints = Some(c); }
    rule.updated_at = env.block.time.seconds();
    rules().save(deps.storage, &rule_id, &rule)?;
    Ok(Response::new()
        .add_attribute("method", "update_rule")
        .add_attribute("rule_id", &rule_id))
}

fn execute_set_active(
    deps: DepsMut, _env: Env, info: MessageInfo, active: bool,
) -> Result<Response, ContractError> {
    assert_admin(&deps, &info)?;
    let mut constitution = CONSTITUTION.load(deps.storage)?;
    constitution.active = active;
    CONSTITUTION.save(deps.storage, &constitution)?;
    Ok(Response::new()
        .add_attribute("method", "set_active")
        .add_attribute("active", active.to_string()))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetConstitution {} => to_binary(&query_get_constitution(deps)?),
        QueryMsg::GetRule { rule_id } => to_binary(&query_get_rule(deps, rule_id)?),
        QueryMsg::ListRules { category, start_after, limit } => {
            to_binary(&query_list_rules(deps, category, start_after, limit)?)
        }
        QueryMsg::IsActionAllowed { action, context } => {
            to_binary(&query_is_action_allowed(deps, action, context)?)
        }
        QueryMsg::GetActiveStatus {} => to_binary(&query_get_active_status(deps)?),
    }
}

fn query_get_constitution(deps: Deps) -> StdResult<ConstitutionResponse> {
    Ok(ConstitutionResponse { constitution: CONSTITUTION.load(deps.storage)? })
}

fn query_get_rule(deps: Deps, rule_id: String) -> StdResult<RuleResponse> {
    Ok(RuleResponse { rule: rules().load(deps.storage, &rule_id)? })
}

fn query_list_rules(
    deps: Deps, category: Option<String>, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListRulesResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let rules: StdResult<Vec<_>> = if let Some(cat) = category {
        rules().idx.category.prefix(cat)
            .range(deps.storage, None, None, Order::Ascending)
            .skip(start_after.map(|s| s.len()).unwrap_or(0))
            .take(limit).map(|r| r.map(|(_, rule)| rule)).collect()
    } else {
        rules().range(deps.storage, None, None, Order::Ascending)
            .skip(start_after.map(|s| s.len()).unwrap_or(0))
            .take(limit).map(|r| r.map(|(_, rule)| rule)).collect()
    };
    Ok(ListRulesResponse { rules: rules? })
}

fn query_is_action_allowed(
    deps: Deps, action: String, _context: Option<Binary>,
) -> StdResult<IsActionAllowedResponse> {
    let constitution = CONSTITUTION.load(deps.storage)?;
    if !constitution.active {
        return Ok(IsActionAllowedResponse {
            allowed: false,
            reasons: vec!["Constitution is not active".to_string()],
        });
    }

    let mut allowed = true;
    let mut reasons = vec![];

    let all_rules: StdResult<Vec<Rule>> = rules()
        .range(deps.storage, None, None, Order::Ascending)
        .map(|r| r.map(|(_, rule)| rule))
        .collect();

    if let Ok(mut sorted_rules) = all_rules {
        sorted_rules.sort_by_key(|r| r.priority);
        for rule in sorted_rules {
            if !rule.active { continue; }
            if rule.category == "all" || action.starts_with(&rule.category) {
                if rule.rule_text.starts_with("deny:") {
                    let denied_action = rule.rule_text.trim_start_matches("deny:");
                    if action == denied_action || denied_action == "*" {
                        allowed = false;
                        reasons.push(format!("Denied by rule {}: {}", rule.rule_id, rule.rule_text));
                    }
                }
            }
        }
    }

    Ok(IsActionAllowedResponse { allowed, reasons })
}

fn query_get_active_status(deps: Deps) -> StdResult<ActiveStatusResponse> {
    let constitution = CONSTITUTION.load(deps.storage)?;
    Ok(ActiveStatusResponse { active: constitution.active })
}

6.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

6.7 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::from_binary;
    use super::*;

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);
        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            constitution_name: "Agent Constitution".to_string(),
            preamble: Some("We the AI agents...".to_string()),
        };
        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 3);
    }

    #[test]
    fn add_and_query_rules() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let admin_info = mock_info("admin", &[]);
        instantiate(deps.as_mut(), env.clone(), admin_info.clone(),
            InstantiateMsg { admin: "admin".to_string(), constitution_name: "Test".to_string(), preamble: None }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), admin_info.clone(),
            ExecuteMsg::AddRule {
                rule_id: "rule-001".to_string(), rule_text: "deny:transfer".to_string(),
                category: "payment".to_string(), priority: 10, constraints: None,
            }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), admin_info.clone(),
            ExecuteMsg::AddRule {
                rule_id: "rule-002".to_string(), rule_text: "allow:*".to_string(),
                category: "all".to_string(), priority: 100, constraints: None,
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetConstitution {}).unwrap();
        let response: ConstitutionResponse = from_binary(&res).unwrap();
        assert_eq!(response.constitution.rule_count, 2);
    }

    #[test]
    fn is_action_allowed() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let admin_info = mock_info("admin", &[]);
        instantiate(deps.as_mut(), env.clone(), admin_info.clone(),
            InstantiateMsg { admin: "admin".to_string(), constitution_name: "Test".to_string(), preamble: None }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), admin_info.clone(),
            ExecuteMsg::AddRule {
                rule_id: "deny-transfer".to_string(), rule_text: "deny:transfer".to_string(),
                category: "payment".to_string(), priority: 10, constraints: None,
            }
        ).unwrap();

        let res = query(deps.as_ref(), env.clone(),
            QueryMsg::IsActionAllowed { action: "transfer".to_string(), context: None }
        ).unwrap();
        let response: IsActionAllowedResponse = from_binary(&res).unwrap();
        assert!(!response.allowed);

        let res = query(deps.as_ref(), env,
            QueryMsg::IsActionAllowed { action: "query".to_string(), context: None }
        ).unwrap();
        let response: IsActionAllowedResponse = from_binary(&res).unwrap();
        assert!(response.allowed);
    }

    #[test]
    fn set_active_status() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let admin_info = mock_info("admin", &[]);
        instantiate(deps.as_mut(), env.clone(), admin_info.clone(),
            InstantiateMsg { admin: "admin".to_string(), constitution_name: "Test".to_string(), preamble: None }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), admin_info, ExecuteMsg::SetActive { active: false }).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetActiveStatus {}).unwrap();
        let response: ActiveStatusResponse = from_binary(&res).unwrap();
        assert!(!response.active);
    }
}

7. Agent 支付合约 (agent_payment_v1)

里程碑式支付合约,支持分期付款和争议处理。

7.1 Cargo.toml

[package]
name = "agent-payment"
version = "1.0.0"
edition = "2021"
description = "MSG Chain Agent Payment v1 - Milestone-based payments"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw-utils = "1.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"

7.2 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Binary, Uint128};

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub agent_registry_address: String,
    pub constitution_address: Option<String>,
    pub platform_fee_bps: Option<u64>,
}

#[cw_serde]
pub enum ExecuteMsg {
    CreateSession {
        agent_id: String,
        client: String,
        total_amount: Uint128,
        denom: String,
        milestones: Option<Vec<Milestone>>,
        expiry: u64,
        metadata: Option<Binary>,
    },
    FundSession { session_id: String },
    ReleasePayment {
        session_id: String,
        milestone_id: Option<String>,
        amount: Option<Uint128>,
    },
    DisputePayment {
        session_id: String,
        reason: String,
        evidence: Option<Binary>,
    },
    CloseSession {
        session_id: String,
        reason: Option<String>,
    },
    AddMilestone {
        session_id: String,
        milestone: Milestone,
    },
    CompleteMilestone {
        session_id: String,
        milestone_id: String,
        proof: Option<Binary>,
    },
}

#[cw_serde]
pub enum QueryMsg {
    GetSession { session_id: String },
    GetAgentSessions {
        agent_id: String,
        status: Option<SessionStatus>,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    ListSessions {
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetBalance { address: String },
    GetDisputeStatus { session_id: String },
}

#[cw_serde]
pub struct Milestone {
    pub milestone_id: String,
    pub description: String,
    pub amount: Uint128,
    pub completed: bool,
    pub released: bool,
}

#[cw_serde]
pub enum SessionStatus {
    PendingFunding,
    Active,
    Completed,
    Disputed,
    Cancelled,
    Expired,
}

#[cw_serde]
pub struct PaymentSession {
    pub session_id: String,
    pub agent_id: String,
    pub client: String,
    pub total_amount: Uint128,
    pub released_amount: Uint128,
    pub denom: String,
    pub milestones: Vec<Milestone>,
    pub status: SessionStatus,
    pub expiry: u64,
    pub created_at: u64,
    pub updated_at: u64,
    pub metadata: Option<Binary>,
    pub dispute_reason: Option<String>,
    pub dispute_evidence: Option<Binary>,
}

#[cw_serde]
pub struct GetSessionResponse {
    pub session: PaymentSession,
}

#[cw_serde]
pub struct ListSessionsResponse {
    pub sessions: Vec<PaymentSession>,
}

#[cw_serde]
pub struct BalanceResponse {
    pub address: String,
    pub balance: Uint128,
    pub denom: String,
}

#[cw_serde]
pub struct DisputeStatusResponse {
    pub session_id: String,
    pub disputed: bool,
    pub reason: Option<String>,
}

7.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex};
use cosmwasm_std::Uint128;

use crate::msg::PaymentSession;

#[cw_serde]
pub struct Config {
    pub admin: String,
    pub agent_registry_address: String,
    pub constitution_address: Option<String>,
    pub platform_fee_bps: u64,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const ESCROW_BALANCES: Map<&str, (Uint128, String)> = Map::new("escrow");

pub struct SessionIndexes<'a> {
    pub agent: MultiIndex<'a, (String, Vec<u8>), PaymentSession>,
}

impl<'a> IndexList<PaymentSession> for SessionIndexes<'a> {
    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<PaymentSession>> + '_> {
        let v: Vec<&dyn Index<PaymentSession>> = vec![&self.agent];
        Box::new(v.into_iter())
    }
}

pub fn sessions<'a>() -> IndexedMap<'a, &'a str, PaymentSession, SessionIndexes<'a>> {
    let indexes = SessionIndexes {
        agent: MultiIndex::new(
            |s: &PaymentSession| (s.agent_id.clone(), s.session_id.as_bytes().to_vec()),
            "sessions",
            "sessions_agent",
        ),
    };
    IndexedMap::new("sessions", indexes)
}

7.4 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("Session {session_id} not found")]
    SessionNotFound { session_id: String },

    #[error("Session {session_id} already exists")]
    SessionAlreadyExists { session_id: String },

    #[error("Session {session_id} is not in the expected state")]
    InvalidSessionState { session_id: String },

    #[error("Milestone {milestone_id} not found in session {session_id}")]
    MilestoneNotFound { session_id: String, milestone_id: String },

    #[error("Insufficient funds: need {needed}, have {available}")]
    InsufficientFunds { needed: Uint128, available: Uint128 },

    #[error("Session {session_id} has expired")]
    SessionExpired { session_id: String },
}

7.5 src/contract.rs

use cosmwasm_std::{
    coins, entry_point, to_binary, BankMsg, Binary, Deps, DepsMut, Env,
    MessageInfo, Order, Response, StdResult, Uint128,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    BalanceResponse, DisputeStatusResponse, ExecuteMsg, GetSessionResponse,
    InstantiateMsg, ListSessionsResponse, Milestone, PaymentSession, QueryMsg,
    SessionStatus,
};
use crate::state::{sessions, Config, CONFIG, ESCROW_BALANCES};

const CONTRACT_NAME: &str = "agent_payment_v1";
const CONTRACT_VERSION: &str = "1.0.0";

#[entry_point]
pub fn instantiate(
    deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg,
) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    let config = Config {
        admin: msg.admin,
        agent_registry_address: msg.agent_registry_address,
        constitution_address: msg.constitution_address,
        platform_fee_bps: msg.platform_fee_bps.unwrap_or(0),
    };
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender.to_string()))
}

#[entry_point]
pub fn execute(
    deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreateSession { agent_id, client, total_amount, denom, milestones, expiry, metadata } => {
            execute_create_session(deps, env, info, agent_id, client, total_amount, denom, milestones, expiry, metadata)
        }
        ExecuteMsg::FundSession { session_id } => execute_fund_session(deps, env, info, session_id),
        ExecuteMsg::ReleasePayment { session_id, milestone_id, amount } => {
            execute_release_payment(deps, env, info, session_id, milestone_id, amount)
        }
        ExecuteMsg::DisputePayment { session_id, reason, evidence } => {
            execute_dispute_payment(deps, env, info, session_id, reason, evidence)
        }
        ExecuteMsg::CloseSession { session_id, reason } => execute_close_session(deps, env, info, session_id, reason),
        ExecuteMsg::AddMilestone { session_id, milestone } => execute_add_milestone(deps, env, info, session_id, milestone),
        ExecuteMsg::CompleteMilestone { session_id, milestone_id, proof } => {
            execute_complete_milestone(deps, env, info, session_id, milestone_id, proof)
        }
    }
}

fn execute_create_session(
    deps: DepsMut, env: Env, _info: MessageInfo,
    agent_id: String, client: String, total_amount: Uint128, denom: String,
    milestones: Option<Vec<Milestone>>, expiry: u64, metadata: Option<Binary>,
) -> Result<Response, ContractError> {
    let now = env.block.time.seconds();
    let session_id = format!("session-{}-{}", agent_id, now);
    let session = PaymentSession {
        session_id: session_id.clone(), agent_id, client, total_amount,
        released_amount: Uint128::zero(), denom,
        milestones: milestones.unwrap_or_default(),
        status: SessionStatus::PendingFunding, expiry,
        created_at: now, updated_at: now,
        metadata, dispute_reason: None, dispute_evidence: None,
    };
    sessions().save(deps.storage, &session_id, &session)?;
    Ok(Response::new()
        .add_attribute("method", "create_session")
        .add_attribute("session_id", &session_id))
}

fn execute_fund_session(
    deps: DepsMut, env: Env, info: MessageInfo, session_id: String,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;

    if session.status != SessionStatus::PendingFunding {
        return Err(ContractError::InvalidSessionState { session_id: session_id.clone() });
    }
    if env.block.time.seconds() > session.expiry {
        session.status = SessionStatus::Expired;
        sessions().save(deps.storage, &session_id, &session)?;
        return Err(ContractError::SessionExpired { session_id });
    }

    let sent = info.funds.iter()
        .find(|c| c.denom == session.denom)
        .map(|c| c.amount)
        .unwrap_or(Uint128::zero());

    if sent < session.total_amount {
        return Err(ContractError::InsufficientFunds {
            needed: session.total_amount, available: sent,
        });
    }

    session.status = SessionStatus::Active;
    session.updated_at = env.block.time.seconds();
    sessions().save(deps.storage, &session_id, &session)?;
    ESCROW_BALANCES.save(deps.storage, &session_id, &(session.total_amount, session.denom.clone()))?;

    Ok(Response::new()
        .add_attribute("method", "fund_session")
        .add_attribute("session_id", &session_id))
}

fn execute_release_payment(
    deps: DepsMut, env: Env, _info: MessageInfo,
    session_id: String, milestone_id: Option<String>, amount: Option<Uint128>,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;

    if session.status != SessionStatus::Active {
        return Err(ContractError::InvalidSessionState { session_id: session_id.clone() });
    }

    let release_amount = if let Some(mid) = &milestone_id {
        let milestone = session.milestones.iter_mut()
            .find(|m| m.milestone_id == *mid)
            .ok_or(ContractError::MilestoneNotFound {
                session_id: session_id.clone(), milestone_id: mid.clone(),
            })?;
        if !milestone.completed {
            return Err(ContractError::InvalidSessionState { session_id: session_id.clone() });
        }
        milestone.released = true;
        milestone.amount
    } else if let Some(amt) = amount {
        amt
    } else {
        session.total_amount - session.released_amount
    };

    session.released_amount += release_amount;
    session.updated_at = env.block.time.seconds();
    if session.released_amount >= session.total_amount {
        session.status = SessionStatus::Completed;
    }
    sessions().save(deps.storage, &session_id, &session)?;

    if let Some((balance, denom)) = ESCROW_BALANCES.may_load(deps.storage, &session_id)? {
        let new_balance = balance.checked_sub(release_amount).unwrap_or(Uint128::zero());
        if new_balance.is_zero() {
            ESCROW_BALANCES.remove(deps.storage, &session_id);
        } else {
            ESCROW_BALANCES.save(deps.storage, &session_id, &(new_balance, denom))?;
        }
    }

    let transfer = BankMsg::Send {
        to_address: session.agent_id.clone(),
        amount: coins(release_amount.u128(), &session.denom),
    };

    Ok(Response::new()
        .add_message(transfer)
        .add_attribute("method", "release_payment")
        .add_attribute("session_id", &session_id)
        .add_attribute("amount", release_amount.to_string()))
}

fn execute_dispute_payment(
    deps: DepsMut, env: Env, _info: MessageInfo,
    session_id: String, reason: String, evidence: Option<Binary>,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;
    if session.status != SessionStatus::Active {
        return Err(ContractError::InvalidSessionState { session_id: session_id.clone() });
    }
    session.status = SessionStatus::Disputed;
    session.dispute_reason = Some(reason);
    session.dispute_evidence = evidence;
    session.updated_at = env.block.time.seconds();
    sessions().save(deps.storage, &session_id, &session)?;
    Ok(Response::new()
        .add_attribute("method", "dispute_payment")
        .add_attribute("session_id", &session_id))
}

fn execute_close_session(
    deps: DepsMut, env: Env, _info: MessageInfo,
    session_id: String, _reason: Option<String>,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;
    session.status = SessionStatus::Cancelled;
    session.updated_at = env.block.time.seconds();
    sessions().save(deps.storage, &session_id, &session)?;

    let response = if let Some((balance, denom)) = ESCROW_BALANCES.may_load(deps.storage, &session_id)? {
        ESCROW_BALANCES.remove(deps.storage, &session_id);
        if !balance.is_zero() {
            Response::new().add_message(BankMsg::Send {
                to_address: session.client.clone(),
                amount: coins(balance.u128(), &denom),
            })
        } else { Response::new() }
    } else { Response::new() };

    Ok(response
        .add_attribute("method", "close_session")
        .add_attribute("session_id", &session_id))
}

fn execute_add_milestone(
    deps: DepsMut, env: Env, _info: MessageInfo,
    session_id: String, milestone: Milestone,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;
    session.milestones.push(milestone);
    session.updated_at = env.block.time.seconds();
    sessions().save(deps.storage, &session_id, &session)?;
    Ok(Response::new()
        .add_attribute("method", "add_milestone")
        .add_attribute("session_id", &session_id))
}

fn execute_complete_milestone(
    deps: DepsMut, env: Env, _info: MessageInfo,
    session_id: String, milestone_id: String, _proof: Option<Binary>,
) -> Result<Response, ContractError> {
    let mut session = sessions().load(deps.storage, &session_id)
        .map_err(|_| ContractError::SessionNotFound { session_id: session_id.clone() })?;
    let milestone = session.milestones.iter_mut()
        .find(|m| m.milestone_id == milestone_id)
        .ok_or(ContractError::MilestoneNotFound { session_id: session_id.clone(), milestone_id: milestone_id.clone() })?;
    milestone.completed = true;
    session.updated_at = env.block.time.seconds();
    sessions().save(deps.storage, &session_id, &session)?;
    Ok(Response::new()
        .add_attribute("method", "complete_milestone")
        .add_attribute("session_id", &session_id)
        .add_attribute("milestone_id", &milestone_id))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetSession { session_id } => to_binary(&query_get_session(deps, session_id)?),
        QueryMsg::GetAgentSessions { agent_id, status, start_after, limit } => {
            to_binary(&query_get_agent_sessions(deps, agent_id, status, start_after, limit)?)
        }
        QueryMsg::ListSessions { start_after, limit } => to_binary(&query_list_sessions(deps, start_after, limit)?),
        QueryMsg::GetBalance { address } => to_binary(&query_get_balance(deps, address)?),
        QueryMsg::GetDisputeStatus { session_id } => to_binary(&query_get_dispute_status(deps, session_id)?),
    }
}

fn query_get_session(deps: Deps, session_id: String) -> StdResult<GetSessionResponse> {
    Ok(GetSessionResponse { session: sessions().load(deps.storage, &session_id)? })
}

fn query_get_agent_sessions(
    deps: Deps, agent_id: String, _status: Option<SessionStatus>,
    start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListSessionsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let sessions: StdResult<Vec<_>> = sessions().idx.agent.prefix(agent_id)
        .range(deps.storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit).map(|r| r.map(|(_, s)| s)).collect();
    Ok(ListSessionsResponse { sessions: sessions? })
}

fn query_list_sessions(
    deps: Deps, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListSessionsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let sessions: StdResult<Vec<_>> = sessions()
        .range(deps.storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit).map(|r| r.map(|(_, s)| s)).collect();
    Ok(ListSessionsResponse { sessions: sessions? })
}

fn query_get_balance(deps: Deps, address: String) -> StdResult<BalanceResponse> {
    let mut total = Uint128::zero();
    let mut denom = String::new();
    for r in sessions().range(deps.storage, None, None, Order::Ascending) {
        if let Ok((_, session)) = r {
            if session.client == address || session.agent_id == address {
                if let Ok(Some((balance, d))) = ESCROW_BALANCES.may_load(deps.storage, &session.session_id) {
                    total += balance; denom = d;
                }
            }
        }
    }
    Ok(BalanceResponse { address, balance: total, denom })
}

fn query_get_dispute_status(deps: Deps, session_id: String) -> StdResult<DisputeStatusResponse> {
    let session = sessions().load(deps.storage, &session_id)?;
    Ok(DisputeStatusResponse {
        session_id, disputed: session.status == SessionStatus::Disputed, reason: session.dispute_reason,
    })
}

7.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

7.7 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{from_binary, Uint128};
    use super::*;

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);
        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            agent_registry_address: "msg1registry".to_string(),
            constitution_address: None, platform_fee_bps: Some(100),
        };
        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 2);
    }

    #[test]
    fn create_session_flow() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg {
                admin: "admin".to_string(), agent_registry_address: "msg1registry".to_string(),
                constitution_address: None, platform_fee_bps: None,
            }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("client", &[]),
            ExecuteMsg::CreateSession {
                agent_id: "agent-001".to_string(), client: "client".to_string(),
                total_amount: Uint128::new(1000), denom: "umsg".to_string(),
                milestones: None, expiry: env.block.time.seconds() + 86400, metadata: None,
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::ListSessions { start_after: None, limit: None }).unwrap();
        let list: ListSessionsResponse = from_binary(&res).unwrap();
        assert_eq!(list.sessions.len(), 1);
        assert_eq!(list.sessions[0].status, SessionStatus::PendingFunding);
    }
}

8. 微支付通道合约 (micropayment_session_v1)

微支付通道合约,支持链下状态更新和链上结算。类似比特币闪电网络的支付通道,通过 off-chain 状态更新实现高频微支付,仅在最需要时上链结算。

8.1 Cargo.toml

[package]
name = "micropayment-channel"
version = "1.0.0"
edition = "2021"
description = "MSG Chain Micropayment Channel v1 - Off-chain state channels"

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

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"

8.2 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Binary, Uint128};

#[cw_serde]
pub struct InstantiateMsg {
    pub admin: String,
    pub min_settlement: Uint128,
    pub max_channel_duration: u64,
}

#[cw_serde]
pub enum ExecuteMsg {
    OpenChannel {
        counterparty: String,
        denom: String,
        channel_capacity: Uint128,
        expiry: u64,
        metadata: Option<Binary>,
    },
    Deposit { channel_id: String },
    Claim {
        channel_id: String,
        amount: Uint128,
        signature: Binary,
    },
    CloseChannel {
        channel_id: String,
        reason: Option<String>,
    },
    ExtendExpiry {
        channel_id: String,
        new_expiry: u64,
    },
    UpdateState {
        channel_id: String,
        new_balance: Uint128,
        nonce: u64,
        signature: Binary,
    },
}

#[cw_serde]
pub enum QueryMsg {
    GetChannel { channel_id: String },
    ListChannelsByParticipant {
        participant: String,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetChannelState { channel_id: String },
    GetChannelBalance { channel_id: String },
}

#[cw_serde]
pub enum ChannelStatus {
    Open,
    Closing,
    Closed,
    Expired,
}

#[cw_serde]
pub struct PaymentChannel {
    pub channel_id: String,
    pub initiator: String,
    pub counterparty: String,
    pub denom: String,
    pub capacity: Uint128,
    pub balance_initiator: Uint128,
    pub balance_counterparty: Uint128,
    pub nonce: u64,
    pub status: ChannelStatus,
    pub expiry: u64,
    pub created_at: u64,
    pub updated_at: u64,
    pub metadata: Option<Binary>,
}

#[cw_serde]
pub struct GetChannelResponse {
    pub channel: PaymentChannel,
}

#[cw_serde]
pub struct ListChannelsResponse {
    pub channels: Vec<PaymentChannel>,
}

#[cw_serde]
pub struct ChannelStateResponse {
    pub channel_id: String,
    pub nonce: u64,
    pub balance_initiator: Uint128,
    pub balance_counterparty: Uint128,
    pub status: ChannelStatus,
}

#[cw_serde]
pub struct ChannelBalanceResponse {
    pub channel_id: String,
    pub total_capacity: Uint128,
    pub balance_initiator: Uint128,
    pub balance_counterparty: Uint128,
    pub denom: String,
}

8.3 src/state.rs

use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex};
use cosmwasm_std::Uint128;

use crate::msg::PaymentChannel;

pub struct Config {
    pub admin: String,
    pub min_settlement: Uint128,
    pub max_channel_duration: u64,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const NONCES: Map<&str, u64> = Map::new("nonces");

pub struct ChannelIndexes<'a> {
    pub participant: MultiIndex<'a, (String, Vec<u8>), PaymentChannel>,
}

impl<'a> IndexList<PaymentChannel> for ChannelIndexes<'a> {
    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<PaymentChannel>> + '_> {
        let v: Vec<&dyn Index<PaymentChannel>> = vec![&self.participant];
        Box::new(v.into_iter())
    }
}

pub fn channels<'a>() -> IndexedMap<'a, &'a str, PaymentChannel, ChannelIndexes<'a>> {
    let indexes = ChannelIndexes {
        participant: MultiIndex::new(
            |c: &PaymentChannel| {
                let key = if c.initiator < c.counterparty {
                    format!("{}-{}", c.initiator, c.counterparty)
                } else {
                    format!("{}-{}", c.counterparty, c.initiator)
                };
                (key, c.channel_id.as_bytes().to_vec())
            },
            "channels",
            "channels_participant",
        ),
    };
    IndexedMap::new("channels", indexes)
}

8.4 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("Channel {channel_id} not found")]
    ChannelNotFound { channel_id: String },

    #[error("Channel {channel_id} already exists")]
    ChannelAlreadyExists { channel_id: String },

    #[error("Channel {channel_id} is not open")]
    ChannelNotOpen { channel_id: String },

    #[error("Channel expired at {expiry}")]
    ChannelExpired { expiry: u64 },

    #[error("Invalid nonce: expected {expected}, got {got}")]
    InvalidNonce { expected: u64, got: u64 },

    #[error("Invalid signature for channel {channel_id}")]
    InvalidSignature { channel_id: String },

    #[error("Insufficient capacity")]
    InsufficientCapacity,
}

8.5 src/contract.rs

use cosmwasm_std::{
    coins, entry_point, to_binary, BankMsg, Binary, Deps, DepsMut, Env,
    MessageInfo, Order, Response, StdResult, Uint128,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    ChannelBalanceResponse, ChannelStateResponse, ChannelStatus, ExecuteMsg,
    GetChannelResponse, InstantiateMsg, ListChannelsResponse, PaymentChannel, QueryMsg,
};
use crate::state::{channels, Config, CONFIG};

const CONTRACT_NAME: &str = "micropayment_session_v1";
const CONTRACT_VERSION: &str = "1.0.0";

#[entry_point]
pub fn instantiate(
    deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg,
) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    let config = Config {
        admin: msg.admin,
        min_settlement: msg.min_settlement,
        max_channel_duration: msg.max_channel_duration,
    };
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", info.sender.to_string()))
}

#[entry_point]
pub fn execute(
    deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::OpenChannel { counterparty, denom, channel_capacity, expiry, metadata } => {
            execute_open_channel(deps, env, info, counterparty, denom, channel_capacity, expiry, metadata)
        }
        ExecuteMsg::Deposit { channel_id } => execute_deposit(deps, env, info, channel_id),
        ExecuteMsg::Claim { channel_id, amount, signature } => {
            execute_claim(deps, env, info, channel_id, amount, signature)
        }
        ExecuteMsg::CloseChannel { channel_id, reason } => execute_close_channel(deps, env, info, channel_id, reason),
        ExecuteMsg::ExtendExpiry { channel_id, new_expiry } => execute_extend_expiry(deps, env, info, channel_id, new_expiry),
        ExecuteMsg::UpdateState { channel_id, new_balance, nonce, signature } => {
            execute_update_state(deps, env, info, channel_id, new_balance, nonce, signature)
        }
    }
}

fn execute_open_channel(
    deps: DepsMut, env: Env, info: MessageInfo,
    counterparty: String, denom: String, channel_capacity: Uint128,
    expiry: u64, metadata: Option<Binary>,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if expiry - env.block.time.seconds() > config.max_channel_duration {
        return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("Channel duration exceeds maximum")));
    }

    let now = env.block.time.seconds();
    let channel_id = format!("ch-{}-{}", info.sender, now);

    let sent = info.funds.iter()
        .find(|c| c.denom == denom)
        .map(|c| c.amount)
        .unwrap_or(Uint128::zero());

    if sent < channel_capacity {
        return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("Insufficient initial deposit")));
    }

    let channel = PaymentChannel {
        channel_id: channel_id.clone(), initiator: info.sender.to_string(),
        counterparty, denom, capacity: channel_capacity,
        balance_initiator: channel_capacity, balance_counterparty: Uint128::zero(),
        nonce: 0, status: ChannelStatus::Open, expiry,
        created_at: now, updated_at: now, metadata,
    };
    channels().save(deps.storage, &channel_id, &channel)?;

    Ok(Response::new()
        .add_attribute("method", "open_channel")
        .add_attribute("channel_id", &channel_id))
}

fn execute_deposit(
    deps: DepsMut, env: Env, info: MessageInfo, channel_id: String,
) -> Result<Response, ContractError> {
    let mut channel = channels().load(deps.storage, &channel_id)
        .map_err(|_| ContractError::ChannelNotFound { channel_id: channel_id.clone() })?;

    if channel.status != ChannelStatus::Open {
        return Err(ContractError::ChannelNotOpen { channel_id: channel_id.clone() });
    }

    let sent = info.funds.iter()
        .find(|c| c.denom == channel.denom)
        .map(|c| c.amount)
        .unwrap_or(Uint128::zero());

    if sent.is_zero() {
        return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("No funds sent")));
    }

    channel.capacity += sent;
    if info.sender.to_string() == channel.initiator {
        channel.balance_initiator += sent;
    } else if info.sender.to_string() == channel.counterparty {
        channel.balance_counterparty += sent;
    }
    channel.updated_at = env.block.time.seconds();
    channels().save(deps.storage, &channel_id, &channel)?;

    Ok(Response::new()
        .add_attribute("method", "deposit")
        .add_attribute("channel_id", &channel_id)
        .add_attribute("amount", sent.to_string()))
}

fn execute_claim(
    deps: DepsMut, env: Env, _info: MessageInfo,
    channel_id: String, amount: Uint128, _signature: Binary,
) -> Result<Response, ContractError> {
    let mut channel = channels().load(deps.storage, &channel_id)
        .map_err(|_| ContractError::ChannelNotFound { channel_id: channel_id.clone() })?;

    if channel.status != ChannelStatus::Open {
        return Err(ContractError::ChannelNotOpen { channel_id: channel_id.clone() });
    }

    if env.block.time.seconds() > channel.expiry {
        channel.status = ChannelStatus::Expired;
        channels().save(deps.storage, &channel_id, &channel)?;
        return Err(ContractError::ChannelExpired { expiry: channel.expiry });
    }

    if amount > channel.balance_initiator {
        return Err(ContractError::InsufficientCapacity);
    }

    channel.balance_initiator -= amount;
    channel.balance_counterparty += amount;
    channel.nonce += 1;
    channel.updated_at = env.block.time.seconds();
    channels().save(deps.storage, &channel_id, &channel)?;

    let transfer = BankMsg::Send {
        to_address: channel.counterparty.clone(),
        amount: coins(amount.u128(), &channel.denom),
    };

    Ok(Response::new()
        .add_message(transfer)
        .add_attribute("method", "claim")
        .add_attribute("channel_id", &channel_id)
        .add_attribute("amount", amount.to_string()))
}

fn execute_close_channel(
    deps: DepsMut, env: Env, _info: MessageInfo,
    channel_id: String, _reason: Option<String>,
) -> Result<Response, ContractError> {
    let channel = channels().load(deps.storage, &channel_id)
        .map_err(|_| ContractError::ChannelNotFound { channel_id: channel_id.clone() })?;

    if channel.status != ChannelStatus::Open && channel.status != ChannelStatus::Closing {
        return Err(ContractError::ChannelNotOpen { channel_id: channel_id.clone() });
    }

    let closing = ChannelStatus::Closed;
    let mut ch = channel;
    ch.status = closing;
    ch.updated_at = env.block.time.seconds();
    channels().save(deps.storage, &channel_id, &ch)?;

    let mut response = Response::new()
        .add_attribute("method", "close_channel")
        .add_attribute("channel_id", &channel_id);

    if !ch.balance_initiator.is_zero() {
        response = response.add_message(BankMsg::Send {
            to_address: ch.initiator.clone(),
            amount: coins(ch.balance_initiator.u128(), &ch.denom),
        });
    }
    if !ch.balance_counterparty.is_zero() {
        response = response.add_message(BankMsg::Send {
            to_address: ch.counterparty.clone(),
            amount: coins(ch.balance_counterparty.u128(), &ch.denom),
        });
    }

    Ok(response)
}

fn execute_extend_expiry(
    deps: DepsMut, env: Env, _info: MessageInfo,
    channel_id: String, new_expiry: u64,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let mut channel = channels().load(deps.storage, &channel_id)
        .map_err(|_| ContractError::ChannelNotFound { channel_id: channel_id.clone() })?;

    if channel.status != ChannelStatus::Open {
        return Err(ContractError::ChannelNotOpen { channel_id: channel_id.clone() });
    }

    if new_expiry - env.block.time.seconds() > config.max_channel_duration {
        return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("New expiry exceeds maximum duration")));
    }

    channel.expiry = new_expiry;
    channel.updated_at = env.block.time.seconds();
    channels().save(deps.storage, &channel_id, &channel)?;

    Ok(Response::new()
        .add_attribute("method", "extend_expiry")
        .add_attribute("channel_id", &channel_id)
        .add_attribute("new_expiry", new_expiry.to_string()))
}

fn execute_update_state(
    deps: DepsMut, env: Env, _info: MessageInfo,
    channel_id: String, new_balance: Uint128, nonce: u64, _signature: Binary,
) -> Result<Response, ContractError> {
    let mut channel = channels().load(deps.storage, &channel_id)
        .map_err(|_| ContractError::ChannelNotFound { channel_id: channel_id.clone() })?;

    if channel.status != ChannelStatus::Open {
        return Err(ContractError::ChannelNotOpen { channel_id: channel_id.clone() });
    }

    if nonce <= channel.nonce {
        return Err(ContractError::InvalidNonce { expected: channel.nonce + 1, got: nonce });
    }

    if new_balance > channel.capacity {
        return Err(ContractError::InsufficientCapacity);
    }

    let counterparty_balance = channel.capacity - new_balance;
    channel.balance_initiator = new_balance;
    channel.balance_counterparty = counterparty_balance;
    channel.nonce = nonce;
    channel.updated_at = env.block.time.seconds();
    channels().save(deps.storage, &channel_id, &channel)?;

    Ok(Response::new()
        .add_attribute("method", "update_state")
        .add_attribute("channel_id", &channel_id)
        .add_attribute("nonce", nonce.to_string()))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::GetChannel { channel_id } => to_binary(&query_get_channel(deps, channel_id)?),
        QueryMsg::ListChannelsByParticipant { participant, start_after, limit } => {
            to_binary(&query_list_channels_by_participant(deps, participant, start_after, limit)?)
        }
        QueryMsg::GetChannelState { channel_id } => to_binary(&query_get_channel_state(deps, channel_id)?),
        QueryMsg::GetChannelBalance { channel_id } => to_binary(&query_get_channel_balance(deps, channel_id)?),
    }
}

fn query_get_channel(deps: Deps, channel_id: String) -> StdResult<GetChannelResponse> {
    Ok(GetChannelResponse { channel: channels().load(deps.storage, &channel_id)? })
}

fn query_list_channels_by_participant(
    deps: Deps, participant: String, start_after: Option<String>, limit: Option<u32>,
) -> StdResult<ListChannelsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let channels: StdResult<Vec<_>> = channels()
        .range(deps.storage, None, None, Order::Ascending)
        .filter(|r| {
            r.as_ref().map(|(_, c)| c.initiator == participant || c.counterparty == participant).unwrap_or(false)
        })
        .skip(start_after.map(|s| s.len()).unwrap_or(0))
        .take(limit).map(|r| r.map(|(_, c)| c)).collect();
    Ok(ListChannelsResponse { channels: channels? })
}

fn query_get_channel_state(deps: Deps, channel_id: String) -> StdResult<ChannelStateResponse> {
    let channel = channels().load(deps.storage, &channel_id)?;
    Ok(ChannelStateResponse {
        channel_id, nonce: channel.nonce,
        balance_initiator: channel.balance_initiator,
        balance_counterparty: channel.balance_counterparty,
        status: channel.status,
    })
}

fn query_get_channel_balance(deps: Deps, channel_id: String) -> StdResult<ChannelBalanceResponse> {
    let channel = channels().load(deps.storage, &channel_id)?;
    Ok(ChannelBalanceResponse {
        channel_id: channel.channel_id,
        total_capacity: channel.capacity,
        balance_initiator: channel.balance_initiator,
        balance_counterparty: channel.balance_counterparty,
        denom: channel.denom,
    })
}

8.6 src/lib.rs

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

8.7 单元测试

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{from_binary, Binary, Uint128};
    use super::*;

    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &[]);
        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            min_settlement: Uint128::new(1000),
            max_channel_duration: 86400 * 30,
        };
        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.attributes.len(), 2);
    }

    #[test]
    fn open_and_close_channel() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), min_settlement: Uint128::new(1000), max_channel_duration: 86400 * 30 }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("initiator", &[]),
            ExecuteMsg::OpenChannel {
                counterparty: "counterparty".to_string(),
                denom: "umsg".to_string(),
                channel_capacity: Uint128::new(100000),
                expiry: env.block.time.seconds() + 86400,
                metadata: None,
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::ListChannelsByParticipant {
            participant: "initiator".to_string(), start_after: None, limit: None,
        }).unwrap();
        let list: ListChannelsResponse = from_binary(&res).unwrap();
        assert_eq!(list.channels.len(), 1);
    }

    #[test]
    fn update_state() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), min_settlement: Uint128::new(1000), max_channel_duration: 86400 * 30 }
        ).unwrap();

        execute(deps.as_mut(), env.clone(), mock_info("initiator", &[]),
            ExecuteMsg::OpenChannel {
                counterparty: "counterparty".to_string(),
                denom: "umsg".to_string(),
                channel_capacity: Uint128::new(100000),
                expiry: env.block.time.seconds() + 86400,
                metadata: None,
            }
        ).unwrap();

        let res = query(deps.as_ref(), env.clone(), QueryMsg::ListChannelsByParticipant {
            participant: "initiator".to_string(), start_after: None, limit: None,
        }).unwrap();
        let list: ListChannelsResponse = from_binary(&res).unwrap();
        let channel_id = list.channels[0].channel_id.clone();

        execute(deps.as_mut(), env.clone(), mock_info("initiator", &[]),
            ExecuteMsg::UpdateState {
                channel_id: channel_id.clone(),
                new_balance: Uint128::new(80000),
                nonce: 1,
                signature: Binary::from(b"test_sig".as_ref()),
            }
        ).unwrap();

        let res = query(deps.as_ref(), env, QueryMsg::GetChannelState { channel_id: channel_id.clone() }).unwrap();
        let state: ChannelStateResponse = from_binary(&res).unwrap();
        assert_eq!(state.balance_initiator, Uint128::new(80000));
        assert_eq!(state.balance_counterparty, Uint128::new(20000));
        assert_eq!(state.nonce, 1);
    }
}

9. 系统合约接口

系统合约由 MSG Chain 在 genesis 时部署,通过 genesis_registry_v1 合约进行地址解析。以下为参考接口。

9.1 candidate_node_staking_v2 (质押合约)

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Uint128;

#[cw_serde]
pub enum ExecuteMsg {
    Stake { validator: String, amount: Uint128 },
    Unstake { validator: String, amount: Uint128 },
    ClaimRewards { validator: Option<String> },
    UpdateStake { validator: String, new_amount: Uint128 },
    Delegate { validator: String, amount: Uint128, delegator: String },
    Undelegate { validator: String, amount: Uint128 },
}

#[cw_serde]
pub enum QueryMsg {
    GetStake { address: String, validator: String },
    GetValidatorStakes { validator: String },
    GetDelegations { delegator: String },
    GetRewards { address: String, validator: Option<String> },
    GetStakingParams {},
}

9.2 dao_governance_v1 (DAO 治理合约)

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub enum ExecuteMsg {
    SubmitProposal {
        title: String,
        description: String,
        target_contract: Option<String>,
        execute_msg: Option<String>,
        deposit_amount: Option<String>,
    },
    Vote { proposal_id: u64, vote: VoteOption },
    ExecuteProposal { proposal_id: u64 },
    CancelProposal { proposal_id: u64 },
    UpdateConfig {
        quorum: Option<String>,
        voting_period: Option<u64>,
        threshold: Option<String>,
    },
}

#[cw_serde]
pub enum VoteOption {
    Yes, No, Abstain, Veto,
}

#[cw_serde]
pub enum QueryMsg {
    GetProposal { proposal_id: u64 },
    ListProposals {
        status: Option<String>,
        start_after: Option<u64>,
        limit: Option<u32>,
    },
    GetVote { proposal_id: u64, voter: String },
    GetConfig {},
    GetProposalTally { proposal_id: u64 },
}

9.3 emission_schedule_v2 (代币排放合约)

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Uint128;

// 不可用户执行,由链模块自动控制

#[cw_serde]
pub enum QueryMsg {
    GetCurrentEmission {},
    GetEmissionSchedule {},
    GetTotalEmitted {},
    GetRemainingEmission {},
}

#[cw_serde]
pub struct CurrentEmissionResponse {
    pub block_reward: Uint128,
    pub annual_inflation_rate: String,
    pub denom: String,
}

#[cw_serde]
pub struct EmissionScheduleResponse {
    pub phases: Vec<EmissionPhase>,
    pub start_height: u64,
}

#[cw_serde]
pub struct EmissionPhase {
    pub phase: u32,
    pub start_block: u64,
    pub end_block: u64,
    pub reward_per_block: Uint128,
    pub total_emitted_in_phase: Uint128,
}

9.4 foundation_treasury_v2 (金库多签合约)

use cosmwasm_schema::cw_serde;
use cosmwasm_std::Uint128;

#[cw_serde]
pub enum ExecuteMsg {
    ProposeSpend {
        recipient: String,
        amount: Uint128,
        denom: String,
        purpose: String,
    },
    ApproveSpend { proposal_id: u64 },
    ExecuteSpend { proposal_id: u64 },
    AddSigner { address: String },
    RemoveSigner { address: String },
}

#[cw_serde]
pub enum QueryMsg {
    GetBalance { denom: Option<String> },
    GetProposal { proposal_id: u64 },
    ListProposals {
        status: Option<String>,
        start_after: Option<u64>,
        limit: Option<u32>,
    },
    GetSigners {},
    GetConfig {},
}

9.5 genesis_registry_v1 (合约地址注册中心)

use cosmwasm_schema::cw_serde;

#[cw_serde]
pub enum ExecuteMsg {
    RegisterContract { canonical_key: String, contract_address: String },
    UpdateContract { canonical_key: String, contract_address: String },
    RemoveContract { canonical_key: String },
}

#[cw_serde]
pub enum QueryMsg {
    ResolveCanonical { key: String },
    ListContracts {
        start_after: Option<String>,
        limit: Option<u32>,
    },
    GetContractCount {},
}

9.6 合约地址解析示例

import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate";

const GENESIS_REGISTRY = "msg1...genesisRegistryAddress";

async function resolveContract(
  client: CosmWasmClient,
  canonicalKey: string
): Promise<string> {
  const result: any = await client.queryContractSmart(GENESIS_REGISTRY, {
    resolve_canonical: { key: canonicalKey },
  });
  return result.address;
}

// 使用示例
const aididAddress = await resolveContract(client, "aidid_did_registry_v1");
const constitutionAddress = await resolveContract(client, "ai_agent_constitution_v1");

10. 合约测试

10.1 单元测试模式

每个合约包含内嵌的 #[cfg(test)] mod tests 测试模块。以下为标准测试模式:

#[cfg(test)]
mod tests {
    use cosmwasm_std::testing::{
        mock_dependencies, mock_env, mock_info, MOCK_CONTRACT_ADDR,
    };
    use cosmwasm_std::{coins, from_binary, Addr, Uint128, Binary};

    use super::*;

    // ── 1. 初始化测试 ──
    #[test]
    fn proper_initialization() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("admin", &coins(1000, "umsg"));

        let msg = InstantiateMsg {
            admin: "admin".to_string(),
            // ... 其他字段
        };

        let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
        assert_eq!(res.messages.len(), 0);
        assert!(res.attributes.iter().any(|a| a.key == "method"));
    }

    // ── 2. 正常执行测试 ──
    #[test]
    fn successful_execution() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        let info = mock_info("sender", &[]);

        // Arrange - 先初始化
        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]), 
            InstantiateMsg { /* ... */ }
        ).unwrap();

        // Act - 执行
        let res = execute(deps.as_mut(), env.clone(), info,
            ExecuteMsg::SomeAction { /* ... */ }
        ).unwrap();

        // Assert
        assert_eq!(res.attributes.len(), 2);
    }

    // ── 3. 权限检查测试 ──
    #[test]
    fn unauthorized_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), /* ... */ }
        ).unwrap();

        let err = execute(deps.as_mut(), env, mock_info("unauthorized_user", &[]),
            ExecuteMsg::AdminOnlyAction { /* ... */ }
        ).unwrap_err();

        // 使用 match 检查自定义错误类型
        match err {
            ContractError::Unauthorized {} => {},
            _ => panic!("Expected Unauthorized error"),
        }
    }

    // ── 4. 边界条件测试 ──
    #[test]
    fn invalid_input_fails() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { admin: "admin".to_string(), /* ... */ }
        ).unwrap();

        let err = execute(deps.as_mut(), env, mock_info("admin", &[]),
            ExecuteMsg::Create { id: "".to_string(), /* 空 ID */ }
        ).unwrap_err();

        // 或者检查结果
        let res = query(deps.as_ref(), env.clone(), QueryMsg::Get { id: "nonexistent".to_string() });
        assert!(res.is_err());
    }

    // ── 5. 查询测试 ──
    #[test]
    fn query_returns_expected_data() {
        let mut deps = mock_dependencies();
        let env = mock_env();

        instantiate(deps.as_mut(), env.clone(), mock_info("admin", &[]),
            InstantiateMsg { /* ... */ }
        ).unwrap();

        let bin = query(deps.as_ref(), env, QueryMsg::GetData {}).unwrap();
        let response: DataResponse = from_binary(&bin).unwrap();
        assert_eq!(response.value, "expected");
    }
}

10.2 集成测试 (cw-multi-test)

// tests/integration.rs
use cosmwasm_std::testing::{mock_env, mock_info, MUS};
use cosmwasm_std::{coins, Addr, Empty, Uint128};
use cw_multi_test::{App, ContractWrapper, Executor};

use my_contract::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

fn mock_app() -> App {
    App::default()
}

fn store_contract_code(app: &mut App) -> u64 {
    let contract = ContractWrapper::new(
        crate::contract::execute,
        crate::contract::instantiate,
        crate::contract::query,
    );
    app.store_code(Box::new(contract))
}

#[test]
fn full_workflow_integration() {
    let mut app = mock_app();
    let admin = Addr::unchecked("admin");

    // 1. 上传代码
    let code_id = store_contract_code(&mut app);

    // 2. 实例化合约
    let contract_addr = app
        .instantiate_contract(
            code_id,
            admin.clone(),
            &InstantiateMsg {
                admin: "admin".to_string(),
                // ... 其他字段
            },
            &[],
            "test-contract",
            None,
        )
        .unwrap();

    // 3. 执行
    app.execute_contract(
        admin.clone(),
        contract_addr.clone(),
        &ExecuteMsg::SomeAction { /* ... */ },
        &[],
    )
    .unwrap();

    // 4. 查询
    let result: DataResponse = app
        .wrap()
        .query_wasm_smart(contract_addr, &QueryMsg::GetData {})
        .unwrap();

    assert_eq!(result.value, "expected");
}

10.3 Property-Based Testing (使用 proptest)

#[cfg(test)]
mod proptests {
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn doesnt_crash_on_any_count(count: i32) {
            let mut deps = mock_dependencies();
            let env = mock_env();

            instantiate(
                deps.as_mut(),
                env.clone(),
                mock_info("admin", &[]),
                InstantiateMsg {
                    admin: "admin".to_string(),
                    initial_count: count,
                },
            )
            .unwrap();

            // 即使 count 为负数,合约也应优雅处理
            let result = execute(
                deps.as_mut(),
                env,
                mock_info("admin", &[]),
                ExecuteMsg::Reset { count },
            );

            if count < 0 {
                assert_eq!(result.unwrap_err(), ContractError::NegativeCount {});
            } else {
                assert!(result.is_ok());
            }
        }
    }
}

11. 合约部署与迁移

11.1 部署流程

MSG Chain 的合约部署遵循标准 CosmWasm 流程:

1. 编译 WASM → 2. 优化体积 → 3. StoreCode → 4. InstantiateContract → 5. 验证

11.2 部署脚本 (TypeScript)

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

const CHAIN_ID = "msg-chain-1";
const RPC_ENDPOINT = "http://localhost:26657";
const PREFIX = "msg";

const gasPrice = GasPrice.fromString("1000000000attoMSG");

async function deployContract(
  client: SigningCosmWasmClient,
  deployer: string,
  wasmPath: string,
  instantiateMsg: Record<string, unknown>,
  label: string,
  admin: string
) {
  // 1. 读取 WASM 文件
  const wasm = readFileSync(wasmPath);

  // 2. 上传合约代码
  const uploadResult = await client.upload(
    deployer,
    wasm,
    calculateFee(3_000_000, gasPrice),
    "uploading contract"
  );
  console.log(`Upload success, code_id: ${uploadResult.codeId}`);

  // 3. 实例化合约
  const instantiateResult = await client.instantiate(
    deployer,
    uploadResult.codeId,
    instantiateMsg,
    label,
    calculateFee(500_000, gasPrice),
    { admin }
  );
  console.log(`Contract instantiated at: ${instantiateResult.contractAddress}`);

  return {
    codeId: uploadResult.codeId,
    contractAddress: instantiateResult.contractAddress,
  };
}

async function main() {
  const mnemonic = process.env.DEPLOYER_MNEMONIC!;
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: PREFIX });
  const [account] = await wallet.getAccounts();

  const client = await SigningCosmWasmClient.connectWithSigner(
    RPC_ENDPOINT,
    wallet
  );

  // 部署 DID Registry
  const didResult = await deployContract(
    client,
    account.address,
    "./artifacts/aidid_did_registry.wasm",
    {
      admin: account.address,
      registry_name: "MSG Chain AI DID Registry",
    },
    "aidid_did_registry_v1",
    account.address
  );

  // 部署 Agent Registry
  const agentResult = await deployContract(
    client,
    account.address,
    "./artifacts/agent_registry.wasm",
    {
      admin: account.address,
      did_registry_address: didResult.contractAddress,
    },
    "agent_registry_v1",
    account.address
  );

  // 部署 Constitution
  const constitutionResult = await deployContract(
    client,
    account.address,
    "./artifacts/ai_agent_constitution.wasm",
    {
      admin: account.address,
      constitution_name: "MSG AI Agent Constitution",
      preamble: "This constitution governs all AI Agents on MSG Chain.",
    },
    "ai_agent_constitution_v1",
    account.address
  );

  // 部署 Payment
  const paymentResult = await deployContract(
    client,
    account.address,
    "./artifacts/agent_payment.wasm",
    {
      admin: account.address,
      agent_registry_address: agentResult.contractAddress,
      constitution_address: constitutionResult.contractAddress,
      platform_fee_bps: 50,
    },
    "agent_payment_v1",
    account.address
  );

  // 部署 Micropayment
  const microResult = await deployContract(
    client,
    account.address,
    "./artifacts/micropayment_channel.wasm",
    {
      admin: account.address,
      min_settlement: "1000",
      max_channel_duration: 2592000,
    },
    "micropayment_session_v1",
    account.address
  );

  console.log("\n=== Deployment Summary ===");
  console.log(`DID Registry:         ${didResult.contractAddress}`);
  console.log(`Agent Registry:       ${agentResult.contractAddress}`);
  console.log(`Constitution:         ${constitutionResult.contractAddress}`);
  console.log(`Payment:              ${paymentResult.contractAddress}`);
  console.log(`Micropayment:         ${microResult.contractAddress}`);
}

main().catch(console.error);

11.3 合约迁移 (Contract Migration)

async function migrateContract(
  client: SigningCosmWasmClient,
  admin: string,
  contractAddress: string,
  newCodeId: number,
  migrateMsg: Record<string, unknown>
) {
  const result = await client.migrate(
    admin,
    contractAddress,
    newCodeId,
    migrateMsg,
    calculateFee(500_000, gasPrice)
  );
  console.log(`Migration successful: ${result.transactionHash}`);
  return result;
}

// 使用示例
await migrateContract(client, adminAddress, oldContractAddr, 2, {});

11.4 合约升级策略

  1. 兼容性迁移:仅添加新字段,不修改已有存储结构。使用 cw2::set_contract_version 记录版本
  2. 存储迁移:在 migrate 入口点中处理存储格式变更
  3. 管理权限:合约必须设置 admin 地址,只有 admin 可以执行迁移
  4. 暂停机制:建议实现 SetActive 枚举,在迁移前暂停合约
#[entry_point]
pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
    let version = get_contract_version(deps.storage)?;
    if version.contract != CONTRACT_NAME {
        return Err(StdError::generic_err("Cannot upgrade to a different contract").into());
    }

    // 更新版本号
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    // 处理存储迁移
    // ... 例如添加新字段

    Ok(Response::new()
        .add_attribute("method", "migrate")
        .add_attribute("from_version", version.version)
        .add_attribute("to_version", CONTRACT_VERSION))
}

12. 安全最佳实践

12.1 CosmWasm 常见安全陷阱

// ❌ 危险:没有权限检查
fn execute_critical(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
    // 任何用户都可以调用此函数
    Ok(Response::new())
}

// ✅ 安全:始终检查调用者权限
fn execute_critical(deps: DepsMut, info: MessageInfo) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    if info.sender.to_string() != config.admin {
        return Err(ContractError::Unauthorized {});
    }
    Ok(Response::new())
}
// ❌ 危险:整数溢出(在 release 模式下不会 panic)
fn unsafe_math(a: Uint128, b: Uint128) -> Uint128 {
    a + b  // release 模式下会静默环绕
}

// ✅ 安全:使用 checked_* 方法
fn safe_math(a: Uint128, b: Uint128) -> Result<Uint128, ContractError> {
    a.checked_add(b).map_err(|_| ContractError::Overflow {})
}
// ❌ 危险:在 execute 中执行不受控的循环
fn unsafe_query_all(deps: Deps) -> StdResult<Vec<Item>> {
    let mut results = vec![];
    for item in MAP.range(deps.storage, None, None, Order::Ascending) {
        let (_, v) = item?;
        results.push(v);
    }
    Ok(results)  // 可能导致 gas 耗尽
}

// ✅ 安全:使用分页查询
fn safe_query_all(deps: Deps, limit: u32) -> StdResult<Vec<Item>> {
    MAP.range(deps.storage, None, None, Order::Ascending)
        .take(limit as usize)
        .map(|r| r.map(|(_, v)| v))
        .collect()
}

12.2 Dilithium-5 后量子签名注意事项

  1. 公钥大小:Dilithium-5 公钥约为 1.3KB,签名约为 2.5KB,比 ECDSA 大得多。存储和 gas 成本更高
  2. 存储优化:考虑将公钥存储在链下,仅在需要验证时传入
  3. multibase 编码:使用 public_key_multibase 字段编码 Dilithium-5 公钥
  4. 密钥轮换:建议定期轮换 Dilithium-5 密钥对,支持 AddVerificationMethod / RemoveVerificationMethod

12.3 重入攻击防护

// 使用 mutex/flag 防止重入
pub const EXECUTING: Item<bool> = Item::new("executing");

fn execute_with_reentry_guard(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    if EXECUTING.load(deps.storage)? {
        return Err(ContractError::ReentrancyDetected {});
    }
    EXECUTING.save(deps.storage, &true)?;

    // ... 执行逻辑 ...

    EXECUTING.save(deps.storage, &false)?;
    Ok(Response::new())
}

12.4 安全检查清单

12.5 Gas 优化建议

优化点 说明 节约比例
使用 cw-storage-plus 替代原始 cosmwasm-storage 30-50%
批量存储操作 合并多个 save 调用 10-20%
使用 Uint128 而非 String 更高效的序列化 5-10%
限制查询页大小 默认 20 条 O(n)
使用 may_load 而非 load 省去错误构造 微小
避免不必要的 to_binary 缓存序列化结果 5-15%

12.6 MSG Chain 特有安全考虑

  1. Dilithium-5 gas 成本:签名验证操作 gas 成本更高,设计合约时应尽量减少链上验证次数
  2. DAR 共识影响:Round-Robin + DAR 共识下,区块提议者轮换,合约应避免依赖特定验证者
  3. AI Agent 宪法约束:所有 Agent 合约应实现 IsActionAllowed 查询调用,在执行关键操作前检查宪法
  4. 跨合约安全性:Agent 间调用使用 SubMsg 和 reply 处理错误,避免因一个 Agent 失败导致整个交易回滚

附录 A:快速参考

A.1 合约地址 (Mainnet)

合约 Canonical Key 说明
aidid_did_registry_v1 aidid_did_registry_v1 Genesis 部署
agent_registry_v1 — 动态部署
ai_agent_constitution_v1 ai_agent_constitution_v1 Genesis 部署
agent_payment_v1 — 动态部署
micropayment_session_v1 — 动态部署
candidate_node_staking_v2 candidate_node_staking Genesis 部署
dao_governance_v1 dao_governance Genesis 部署
foundation_treasury_v2 foundation_treasury Genesis 部署
emission_schedule_v2 emission_schedule Genesis 部署

A.2 Gas 价格参考

优先级 价格 (attoMSG/gas) 用途
Low 1,000,000,000 只读查询
Average 1,000,000,000 标准交易
High 1,000,000,000 紧急交易

A.3 常用 CosmWasm 依赖版本

包 版本 说明
cosmwasm-std 1.5.x 标准库
cw-storage-plus 1.2.x 增强存储
cw2 1.1.x 合约版本管理
cw-utils 1.0.x 工具函数
cw-multi-test 0.18.x 集成测试框架
schemars 0.8.x JSON Schema 生成
thiserror 1.0.x 错误类型宏

A.4 CosmJS 客户端版本

@cosmjs/cosmwasm-stargate: ^0.32.0
@cosmjs/proto-signing: ^0.32.0
@cosmjs/stargate: ^0.32.0
@cosmjs/amino: ^0.32.0

附录 B:错误码大全

错误码 含义 处理方式
StdError::NotFound 存储键不存在 检查输入是否正确
StdError::ParseErr 解码失败 验证 JSON 格式
StdError::SerializeErr 编码失败 内部错误
StdError::GenericErr 通用错误 查看错误消息
ContractError::Unauthorized 权限不足 使用正确账户签名
ContractError::DIDAlreadyExists DID 重复 使用不同 DID
ContractError::DIDNotFound DID 不存在 检查 DID 是否正确
ContractError::AgentAlreadyExists Agent 重复 使用不同 Agent ID
ContractError::AgentNotFound Agent 不存在 检查 Agent ID
ContractError::SessionNotFound 会话不存在 检查 Session ID
ContractError::InvalidSessionState 会话状态错误 检查当前状态
ContractError::InsufficientFunds 资金不足 增加附件资金
ContractError::ChannelNotFound 通道不存在 检查 Channel ID
ContractError::ChannelNotOpen 通道未开启 先开启通道
ContractError::InvalidNonce nonce 无效 使用最新 nonce

本文档是 MSG Chain CosmWasm 合约开发的完整参考。


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