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
目录
- 概述
- 项目结构模板
- 基础合约模板(Hello World)
- DID 身份合约 (aidid_did_registry_v1)
- AI Agent 注册合约 (agent_registry_v1)
- AI Agent 宪法合约 (ai_agent_constitution_v1)
- Agent 支付合约 (agent_payment_v1)
- 微支付通道合约 (micropayment_session_v1)
- 系统合约接口
- 合约测试
- 合约部署与迁移
- 安全最佳实践
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。这意味着:
- 交易签名使用 Dilithium-5 算法
- 账户公钥类型为
/cosmos.crypto.dilithium.PubKey - 合约内可验证 Dilithium-5 签名(通过
msg_pq_verify_v1查询) - 公钥编码使用 multibase 格式
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 合约升级策略
- 兼容性迁移:仅添加新字段,不修改已有存储结构。使用
cw2::set_contract_version记录版本 - 存储迁移:在
migrate入口点中处理存储格式变更 - 管理权限:合约必须设置
admin地址,只有 admin 可以执行迁移 - 暂停机制:建议实现
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 后量子签名注意事项
- 公钥大小:Dilithium-5 公钥约为 1.3KB,签名约为 2.5KB,比 ECDSA 大得多。存储和 gas 成本更高
- 存储优化:考虑将公钥存储在链下,仅在需要验证时传入
- multibase 编码:使用
public_key_multibase字段编码 Dilithium-5 公钥 - 密钥轮换:建议定期轮换 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 安全检查清单
- [ ] 所有
ExecuteMsg变体都有权限检查 - [ ] 所有数学运算使用
checked_*方法 - [ ] 查询使用分页限制(默认 20,最大 100)
- [ ] 存储读取使用
may_load而非load当键可能不存在时 - [ ] 避免在
execute中执行 gas 消耗大的循环 - [ ] 使用
cw2::set_contract_version记录合约版本 - [ ] 验证输入数据(非空字符串、有效地址、正数金额等)
- [ ] 在
Submit/Response后清理临时存储 - [ ] 使用
overflow-checks = true编译标志 - [ ] 所有跨合约调用使用
CosmosMsg而非直接execute - [ ] 实现 KillSwitch / 暂停机制用于紧急情况
- [ ] 测试覆盖:单元测试 + 集成测试 + 边界测试
- [ ] 合约 admin 权限分离(部署 admin ≠ 治理 admin)
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 特有安全考虑
- Dilithium-5 gas 成本:签名验证操作 gas 成本更高,设计合约时应尽量减少链上验证次数
- DAR 共识影响:Round-Robin + DAR 共识下,区块提议者轮换,合约应避免依赖特定验证者
- AI Agent 宪法约束:所有 Agent 合约应实现
IsActionAllowed查询调用,在执行关键操作前检查宪法 - 跨合约安全性: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/
