name: multiversx-smart-contracts description: 使用Rust构建MultiversX智能合约。当应用需要区块链逻辑、代币创建、NFT铸造、质押、众筹或任何需要自定义智能合约的链上功能时使用。
MultiversX智能合约开发
使用Rust、sc-meta和mxpy构建、测试和部署MultiversX智能合约。
前提条件
VM上可用的工具:
- Rust(版本1.85.0+)
- sc-meta - 智能合约元工具
- mxpy - 用于部署的MultiversX Python CLI
如果未安装sc-meta:
cargo install multiversx-sc-meta --locked
创建新合约
使用sc-meta从模板创建新合约:
# 列出可用模板
sc-meta templates
# 从模板创建
sc-meta new --template adder --name my-contract
# 可用模板:
# - empty: 最小合约结构
# - adder: 基本算术操作
# - crypto-zombies: NFT游戏示例
项目结构
创建合约后,获得以下结构:
my-contract/
├── Cargo.toml # 依赖项
├── src/
│ └── lib.rs # 合约代码
├── meta/
│ ├── Cargo.toml # 元包依赖项
│ └── src/
│ └── main.rs # 构建工具入口
├── wasm/
│ ├── Cargo.toml # WASM输出配置
│ └── src/
│ └── lib.rs # WASM入口点
└── scenarios/ # 测试文件(可选)
Cargo.toml配置
[package]
name = "my-contract"
version = "0.0.0"
edition = "2024"
[lib]
path = "src/lib.rs"
[dependencies.multiversx-sc]
version = "0.64.0"
[dev-dependencies.multiversx-sc-scenario]
version = "0.64.0"
基本合约结构
#![no_std]
multiversx_sc::imports!();
#[multiversx_sc::contract]
pub trait MyContract {
#[init]
fn init(&self, initial_value: BigUint) {
self.stored_value().set(initial_value);
}
#[upgrade]
fn upgrade(&self) {
// 合约升级时调用
}
#[endpoint]
fn add(&self, value: BigUint) {
self.stored_value().update(|v| *v += value);
}
#[view(getValue)]
fn get_value(&self) -> BigUint {
self.stored_value().get()
}
#[storage_mapper("storedValue")]
fn stored_value(&self) -> SingleValueMapper<BigUint>;
}
核心注解
合约和模块级别
| 注解 | 目的 |
|---|---|
#[multiversx_sc::contract] |
标记特质为主合约(每个包一个) |
#[multiversx_sc::module] |
标记特质为可重用模块 |
#[multiversx_sc::proxy] |
创建用于调用其他合约的代理 |
方法级别
| 注解 | 目的 |
|---|---|
#[init] |
构造函数,部署时调用 |
#[upgrade] |
合约升级时调用 |
#[endpoint] |
公共可调用方法 |
#[view] |
只读公共方法 |
#[endpoint(customName)] |
自定义ABI名称的端点 |
#[view(customName)] |
自定义ABI名称的视图 |
支付注解
| 注解 | 目的 |
|---|---|
#[payable] |
接受任何代币支付(自SDK v0.64.0起是#[payable("*")]的简写) |
#[payable("EGLD")] |
仅接受EGLD |
#[payable("TOKEN-ID")] |
接受特定代币 |
事件注解
| 注解 | 目的 |
|---|---|
#[event("eventName")] |
定义合约事件 |
#[indexed] |
标记事件字段为可搜索主题 |
回调注解
| 注解 | 目的 |
|---|---|
#[callback] |
用于传统异步调用的回调 |
#[promises_callback] |
用于基于承诺的异步调用的回调(与register_promise()一起使用) |
存储映射器
SingleValueMapper
存储单个值。
#[storage_mapper("owner")]
fn owner(&self) -> SingleValueMapper<ManagedAddress>;
// 使用
self.owner().set(caller);
let owner = self.owner().get();
self.owner().is_empty();
self.owner().clear();
self.owner().update(|v| *v = new_value);
VecMapper
存储索引数组(1起始索引)。
#[storage_mapper("items")]
fn items(&self) -> VecMapper<BigUint>;
// 使用
self.items().push(&value);
let item = self.items().get(1); // 1起始索引!
self.items().set(1, &new_value);
let len = self.items().len();
for item in self.items().iter() { }
self.items().swap_remove(1);
SetMapper
唯一集合,O(1)查找,保持插入顺序。
#[storage_mapper("whitelist")]
fn whitelist(&self) -> SetMapper<ManagedAddress>;
// 使用
self.whitelist().insert(address); // 如果重复返回false
self.whitelist().contains(&address); // O(1)
self.whitelist().remove(&address);
for addr in self.whitelist().iter() { }
UnorderedSetMapper
类似SetMapper,但当顺序不重要时更高效。
#[storage_mapper("participants")]
fn participants(&self) -> UnorderedSetMapper<ManagedAddress>;
MapMapper
键值对。昂贵 - 不需要迭代时避免使用。
#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;
// 使用
self.balances().insert(address, amount);
let balance = self.balances().get(&address); // 返回Option
self.balances().contains_key(&address);
self.balances().remove(&address);
for (addr, bal) in self.balances().iter() { }
LinkedListMapper
双链表,用于队列操作。
#[storage_mapper("queue")]
fn queue(&self) -> LinkedListMapper<BigUint>;
// 使用
self.queue().push_back(value);
self.queue().push_front(value);
self.queue().pop_front();
self.queue().pop_back();
FungibleTokenMapper
管理可替代代币,内置ESDT操作。
#[storage_mapper("token")]
fn token(&self) -> FungibleTokenMapper;
// 使用
self.token().issue_and_set_all_roles(...);
self.token().mint(amount);
self.token().burn(amount);
self.token().get_balance();
NonFungibleTokenMapper
管理NFT/SFT/META-ESDT代币。
#[storage_mapper("nft")]
fn nft(&self) -> NonFungibleTokenMapper;
// 使用
self.nft().nft_create(amount, &attributes);
self.nft().nft_add_quantity(nonce, amount);
self.nft().get_all_token_data(nonce);
WhitelistMapper
非迭代O(1)成员检查。当不需要迭代时比SetMapper更轻量。
#[storage_mapper("allowedTokens")]
fn allowed_tokens(&self) -> WhitelistMapper<TokenId>;
// 使用
self.allowed_tokens().add(&token_id);
self.allowed_tokens().contains(&token_id); // O(1)
self.allowed_tokens().require_whitelisted(&token_id); // 如果缺失则恐慌
self.allowed_tokens().remove(&token_id);
BiDiMapper
双向映射 - 键和值之间的双向查找(两者必须唯一)。
#[storage_mapper("idToAddress")]
fn id_to_address(&self) -> BiDiMapper<u64, ManagedAddress>;
// 使用
self.id_to_address().insert(1u64, address.clone());
let addr = self.id_to_address().get_value(&1u64);
let id = self.id_to_address().get_id(&address);
self.id_to_address().contains_id(&1u64);
self.id_to_address().remove_by_id(&1u64);
UniqueIdMapper
管理唯一ID池,用于随机或顺序分配。
#[storage_mapper("nftIds")]
fn nft_ids(&self) -> UniqueIdMapper<Self::Api>;
// 使用
self.nft_ids().set_initial_len(1000); // ID 1..=1000
let id = self.nft_ids().swap_remove(index); // 弹出随机ID
self.nft_ids().len(); // 剩余ID
OrderedBinaryTreeMapper
自平衡二叉搜索树,用于有序链上数据。
#[storage_mapper("orderBook")]
fn order_book(&self) -> OrderedBinaryTreeMapper<Self::Api, u64>;
QueueMapper
FIFO队列,支持后推和前弹。
#[storage_mapper("pending")]
fn pending(&self) -> QueueMapper<ManagedAddress>;
// 使用
self.pending().push_back(address);
let next = self.pending().pop_front(); // Option
self.pending().len();
AddressToIdMapper
地址和自增u64 ID之间的双向映射。对于通过数字ID跟踪许多用户的合约,Gas高效。
#[storage_mapper("users")]
fn users(&self) -> AddressToIdMapper;
// 使用
let id: u64 = self.users().get_or_create_id(&caller); // 自动分配下一个ID
let addr: ManagedAddress = self.users().get_address(id);
self.users().contains(&caller); // O(1)检查
UserMapper
类似AddressToIdMapper,但专门设计用于用户管理,带有计数跟踪。
#[storage_mapper("user")]
fn user_mapper(&self) -> UserMapper;
// 使用
let user_id = self.user_mapper().get_or_create_user(&address);
let user_count = self.user_mapper().get_user_count();
let address = self.user_mapper().get_user_address(user_id);
MapStorageMapper
映射,其中值本身是存储映射器(嵌套存储)。当每个键需要自己的复杂存储结构时使用。
#[storage_mapper("vaults")]
fn vaults(&self) -> MapStorageMapper<ManagedAddress, SingleValueMapper<BigUint>>;
TimelockMapper
带时间锁的值 - 存储当前和未来值以及解锁时间戳。当区块时间超过解锁点时自动转换值。
#[storage_mapper("admin")]
fn admin(&self) -> TimelockMapper<ManagedAddress>;
// 使用 - 带延迟调度更改
self.admin().update_and_lock(new_admin, unlock_timestamp);
let current = self.admin().get(); // 解锁前返回当前值,解锁后返回未来值
数据类型
核心类型
| 类型 | 描述 |
|---|---|
BigUint |
无符号任意精度整数 |
BigInt |
有符号任意精度整数 |
ManagedBuffer |
字节数组(字符串、原始数据) |
ManagedAddress |
32字节地址 |
EsdtTokenIdentifier |
ESDT代币ID(例如,“TOKEN-abc123”) |
TokenId |
EGLD和ESDT的统一代币标识符(EGLD表示为EGLD-000000) |
Payment |
统一支付:TokenId + nonce + NonZeroBigUint金额 |
NonZeroBigUint |
BigUint,在构造时保证非零 |
EgldOrEsdtTokenIdentifier |
遗留:EGLD或ESDT代币ID(首选TokenId) |
EsdtTokenPayment |
遗留:代币ID + nonce + 金额(首选Payment) |
创建值
// BigUint
let amount = BigUint::from(1000u64);
let zero = BigUint::zero();
// ManagedBuffer(字符串)
let buffer = ManagedBuffer::from("hello");
// 地址
let caller = self.blockchain().get_caller();
// 代币标识符
let token = EsdtTokenIdentifier::from("TOKEN-abc123");
// 统一代币标识符(EGLD或ESDT)
let token_id = TokenId::from("EGLD-000000"); // EGLD
let token_id = TokenId::from("TOKEN-abc123"); // ESDT
// NonZeroBigUint
let nz_amount = NonZeroBigUint::new_or_panic(BigUint::from(1000u64));
支付类型:Payment vs EgldOrEsdtTokenPayment
| 方面 | Payment<M>(v0.64+) |
EgldOrEsdtTokenPayment<M>(遗留) |
|---|---|---|
| 代币ID | TokenId(EGLD = EGLD-000000) |
EgldOrEsdtTokenIdentifier |
| 金额 | NonZeroBigUint(拒绝零) |
BigUint(允许零) |
| 状态 | 新代码首选 | 生产环境中广泛使用 |
// 新(v0.64+):带TokenId + NonZeroBigUint的Payment
let payment: Payment<Self::Api> = self.call_value().single();
// payment.token_identifier是TokenId,payment.amount是NonZeroBigUint
// 遗留:带BigUint的EgldOrEsdtTokenPayment
let legacy = self.call_value().egld_or_single_esdt();
// legacy.token_identifier是EgldOrEsdtTokenIdentifier,legacy.amount是BigUint
支付处理
错误
// 不要:使用BigUint处理支付金额 — 允许零值转账
let amount: BigUint = self.call_value().egld_value().clone_value();
let payment = EsdtTokenPayment::new(token, 0, amount); // 遗留类型,无零检查
正确
// 做:使用NonZeroBigUint和Payment — 零在类型级别被拒绝
let payment = self.call_value().single(); // 返回带NonZeroBigUint的Payment
// payment.amount是NonZeroBigUint — 保证非零
错误
// 不要:使用遗留的EgldOrEsdtTokenIdentifier
let token: EgldOrEsdtTokenIdentifier = self.call_value().egld_or_single_esdt().token_identifier;
正确
// 做:使用统一的TokenId — 统一处理EGLD和ESDT
let token: TokenId = self.call_value().single().token_identifier;
// EGLD表示为"EGLD-000000"
错误
// 不要:为跨合约调用使用遗留send() API
self.send().direct_esdt(&recipient, &token, 0, &amount);
self.send_raw().direct_egld_execute(&to, &amount, 0, &ManagedBuffer::new());
正确
// 做:为所有转账和跨合约调用使用Tx构建器API
self.tx().to(&recipient).payment(payment).transfer();
self.tx().to(&addr).typed(proxy::Proxy).some_endpoint(arg).sync_call();
接收EGLD
#[payable("EGLD")]
#[endpoint]
fn deposit_egld(&self) {
let payment = self.call_value().egld();
// payment是BigUint(EGLD金额)
}
接收任何单一支付(EGLD或ESDT,统一)
#[payable]
#[endpoint]
fn deposit(&self) {
let payment = self.call_value().single();
// payment.token_identifier : TokenId
// payment.token_nonce : u64
// payment.amount : NonZeroBigUint(保证非零)
}
接收多个支付
#[payable]
#[endpoint]
fn multi_deposit(&self) {
let payments = self.call_value().all();
// 返回ManagedRef<PaymentVec> — 统一的EGLD + ESDT支付
for payment in payments.iter() {
// payment是Payment
}
}
接收精确N个支付
#[payable]
#[endpoint]
fn dual_deposit(&self) {
let [token_a, token_b] = self.call_value().array();
// 正好2个支付,否则崩溃
}
可选单一支付
#[payable]
#[endpoint]
fn optional_deposit(&self) {
let maybe_payment = self.call_value().single_optional();
// 返回Option<Ref<Payment>> — 零个或一个支付
}
发送代币
// 发送EGLD
self.tx()
.to(&recipient)
.egld(&amount)
.transfer();
// 发送Payment(需要NonZeroBigUint)
if let Some(amount_nz) = amount.into_non_zero() {
self.tx()
.to(&recipient)
.payment(Payment::new(token_id, nonce, amount_nz))
.transfer();
}
// 发送多个支付
self.tx()
.to(&recipient)
.payment(&payments)
.transfer();
// 仅当非空时转账
self.tx()
.to(&recipient)
.egld(&amount)
.transfer_if_not_empty();
注意: 自SDK v0.55.0起,EGLD和ESDT可以在同一多转账交易中一起发送。
事件
#[event("deposit")]
fn deposit_event(
&self,
#[indexed] caller: &ManagedAddress,
#[indexed] token: &TokenId,
amount: &BigUint,
);
// 发出事件
self.deposit_event(&caller, &token_id, &amount);
模块
将大型合约拆分为模块:
// 在src/storage.rs中
#[multiversx_sc::module]
pub trait StorageModule {
#[storage_mapper("owner")]
fn owner(&self) -> SingleValueMapper<ManagedAddress>;
}
// 在src/lib.rs中
mod storage;
#[multiversx_sc::contract]
pub trait MyContract: storage::StorageModule {
#[init]
fn init(&self) {
self.owner().set(self.blockchain().get_caller());
}
}
错误处理
// 使用require!
#[endpoint]
fn withdraw(&self, amount: BigUint) {
let caller = self.blockchain().get_caller();
require!(
caller == self.owner().get(),
"只有所有者可以提现"
);
require!(amount > 0, "金额必须为正");
}
// 使用sc_panic!
if condition_failed {
sc_panic!("操作失败");
}
构建合约
构建工作区中的所有合约
sc-meta all build
构建单个合约
cd my-contract/meta
cargo run build
带选项构建
# 带锁定依赖项构建
sc-meta all build --locked
# 调试构建,带WAT输出
cd meta && cargo run build-dbg
构建输出
构建后,在output/中找到输出:
my-contract.wasm- 合约字节码my-contract.abi.json- 合约ABI
测试
基于Rust的测试
在tests/文件夹中创建测试:
use multiversx_sc_scenario::*;
fn world() -> ScenarioWorld {
let mut blockchain = ScenarioWorld::new();
blockchain.register_contract(
"mxsc:output/my-contract.mxsc.json",
my_contract::ContractBuilder,
);
blockchain
}
#[test]
fn test_deploy() {
let mut world = world();
world.run("scenarios/deploy.scen.json");
}
运行测试
# 运行所有测试
sc-meta test
# 运行特定测试
cargo test test_deploy
使用mxpy部署
部署新合约
mxpy contract deploy \
--bytecode output/my-contract.wasm \
--proxy https://devnet-api.multiversx.com \
--chain D \
--pem wallet.pem \
--gas-limit 60000000 \
--arguments 1000 \
--send
升级现有合约
mxpy contract upgrade <contract-address> \
--bytecode output/my-contract.wasm \
--proxy https://devnet-api.multiversx.com \
--chain D \
--pem wallet.pem \
--gas-limit 60000000 \
--send
调用合约端点
mxpy contract call <contract-address> \
--proxy https://devnet-api.multiversx.com \
--chain D \
--pem wallet.pem \
--gas-limit 5000000 \
--function "add" \
--arguments 100 \
--send
查询视图函数
mxpy contract query <contract-address> \
--proxy https://devnet-api.multiversx.com \
--function "getValue"
网络端点
| 网络 | 代理URL | 链ID |
|---|---|---|
| Devnet | https://devnet-api.multiversx.com |
D |
| Testnet | https://testnet-api.multiversx.com |
T |
| Mainnet | https://api.multiversx.com |
1 |
高级模式
使用代理的跨合约调用
// 定义代理特质
#[multiversx_sc::proxy]
pub trait OtherContract {
#[endpoint]
fn some_endpoint(&self, value: BigUint);
}
// 使用代理
#[endpoint]
fn call_other(&self, other_address: ManagedAddress, value: BigUint) {
self.tx()
.to(&other_address)
.typed(other_contract_proxy::OtherContractProxy)
.some_endpoint(value)
.sync_call();
}
从同步调用中检索反向转账
当被调用的合约发送代币回来时(例如,DEX交换返回),使用ReturnsBackTransfers捕获它们:
#[endpoint]
fn swap_and_store(&self, dex: ManagedAddress, token_in: TokenId, amount: NonZeroBigUint) {
let back_transfers = self.tx()
.to(&dex)
.typed(dex_proxy::DexProxy)
.swap(token_in, amount)
.returns(ReturnsBackTransfersReset) // 重置避免陈旧积累
.sync_call();
let payments = back_transfers.into_payment_vec();
for payment in &payments {
self.received_tokens(&payment.token_identifier)
.update(|bal| *bal += payment.amount.as_big_uint());
}
}
关键规则:
- 在一个端点中多次同步调用时,使用
ReturnsBackTransfersReset(而不是ReturnsBackTransfers) — 否则反向转账会积累。 - 当只需要EGLD金额时,使用
ReturnsBackTransfersEGLD。 - 当期望正好一个ESDT代币返回时,使用
ReturnsBackTransfersSingleESDT。 - 调用
.into_payment_vec()以获取带v0.64Payment项(TokenId+NonZeroBigUint)的PaymentVec。
带回调的异步调用
#[endpoint]
fn async_call(&self, other_address: ManagedAddress) {
self.tx()
.to(&other_address)
.typed(other_contract_proxy::OtherContractProxy)
.some_endpoint()
.callback(self.callbacks().my_callback())
.async_call_and_exit();
}
#[callback]
fn my_callback(&self, #[call_result] result: ManagedAsyncCallResult<BigUint>) {
match result {
ManagedAsyncCallResult::Ok(value) => {
// 处理成功
}
ManagedAsyncCallResult::Err(err) => {
// 处理错误
}
}
}
带反向转账的承诺
对于返回代币的跨分片调用,使用承诺+回调:
#[endpoint]
fn cross_shard_swap(&self, dex: ManagedAddress, token: EgldOrEsdtTokenIdentifier, nonce: u64, amount: BigUint) {
let gas_limit = self.blockchain().get_gas_left() - 20_000_000;
self.tx()
.to(&dex)
.typed(dex_proxy::DexProxy)
.swap(token, nonce, amount)
.gas(gas_limit)
.callback(self.callbacks().swap_callback())
.gas_for_callback(10_000_000)
.register_promise();
}
#[promises_callback]
fn swap_callback(&self) {
let back_transfers = self.blockchain().get_back_transfers();
let payments = back_transfers.into_payment_vec();
for payment in &payments {
self.received_tokens(&payment.token_identifier)
.update(|bal| *bal += payment.amount.as_big_uint());
}
}
代币发行(模块化方法)
处理代币发行的推荐方法是从框架(multiversx-sc-modules)导入和继承EsdtModule。此模块提供统一的issue_token方法,可用于在MultiversX上发行任何类型的代币(可替代、不可替代、半可替代、元、动态)。
#[multiversx_sc::contract]
pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule {
// 注意:只有可替代和元代币有小数位数
// 示例:发行可替代代币
#[payable("EGLD")]
#[endpoint(issueFungible)]
fn issue_fungible(
&self,
token_display_name: ManagedBuffer,
token_ticker: ManagedBuffer,
num_decimals: usize,
) {
// 调用从EsdtModule继承的issue_token方法
self.issue_token(
token_display_name,
token_ticker,
EsdtTokenType::Fungible,
OptionalValue::Some(num_decimals),
);
}
// 示例:发行NFT
#[payable("EGLD")]
#[endpoint(issueNft)]
fn issue_nft(
&self,
token_display_name: ManagedBuffer,
token_ticker: ManagedBuffer,
) {
self.issue_token(
token_display_name,
token_ticker,
EsdtTokenType::NonFungible,
OptionalValue::None,
);
}
}
代币铸造
类似地,EsdtModule提供mint方法,用于创建已由合约发行的代币的新单位。
#[multiversx_sc::contract]
pub trait MyContract: multiversx_sc_modules::esdt::EsdtModule {
#[endpoint(mintTokens)]
fn mint_tokens(&self, amount: BigUint) {
// 使用从EsdtModule继承的mint方法铸造代币。
// token_id由模块的存储管理。
// 对于可替代代币,nonce为0。
self.mint(0, &amount);
}
}
代码示例
众筹合约模式
#![no_std]
use multiversx_sc::{chain_core::types::TimestampSeconds, imports::*};
#[multiversx_sc::contract]
pub trait Crowdfunding {
#[init]
fn init(&self, target: BigUint, deadline: TimestampSeconds, token_id: TokenId) {
self.target().set(target);
self.deadline().set(deadline);
require!(token_id.is_valid(), "提供的代币无效");
self.cf_token_identifier().set(token_id);
}
#[payable]
#[endpoint]
fn fund(&self) {
require!(self.blockchain().get_block_timestamp_millis() < self.deadline().get(), "筹资期已结束");
let payment = self.call_value().single();
require!(payment.token_identifier == self.cf_token_identifier().get(), "代币错误");
self.deposit(&self.blockchain().get_caller())
.update(|deposit| *deposit += payment.amount.as_big_uint());
}
#[endpoint]
fn claim(&self) {
require!(self.blockchain().get_block_timestamp_millis() >= self.deadline().get(), "筹资期未结束");
let caller = self.blockchain().get_caller();
let token_id = self.cf_token_identifier().get();
if self.get_current_funds() >= self.target().get() {
require!(caller == self.blockchain().get_owner_address(), "非所有者");
if let Some(bal) = self.get_current_funds().into_non_zero() {
self.tx().to(&caller).payment(Payment::new(token_id, 0, bal)).transfer();
}
} else {
let deposit = self.deposit(&caller).get();
require!(deposit > 0, "无存款");
self.deposit(&caller).clear();
if let Some(dep) = deposit.into_non_zero() {
self.tx().to(&caller).payment(Payment::new(token_id, 0, dep)).transfer();
}
}
}
#[view(getCurrentFunds)]
fn get_current_funds(&self) -> BigUint {
self.blockchain().get_sc_balance(&self.cf_token_identifier().get(), 0)
}
#[storage_mapper("target")]
fn target(&self) -> SingleValueMapper<BigUint>;
#[storage_mapper("deadline")]
fn deadline(&self) -> SingleValueMapper<TimestampSeconds>;
#[storage_mapper("tokenIdentifier")]
fn cf_token_identifier(&self) -> SingleValueMapper<TokenId>;
#[storage_mapper("deposit")]
fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper<BigUint>;
}
关键知识
错误:不需要迭代时使用MapMapper
// 错误 - MapMapper昂贵(4*N + 1存储条目)
#[storage_mapper("balances")]
fn balances(&self) -> MapMapper<ManagedAddress, BigUint>;
正确:使用带地址键的SingleValueMapper
// 正确 - 当不需要迭代时高效
#[storage_mapper("balance")]
fn balance(&self, user: &ManagedAddress) -> SingleValueMapper<BigUint>;
错误:大型模块和函数
// 错误 - 所有内容在一个文件中
#[multiversx_sc::contract]
pub trait MyContract {
// 500+行代码...
}
正确:拆分为模块
// 正确 - 组织化模块
mod storage;
mod logic;
mod events;
#[multiversx_sc::contract]
pub trait MyContract:
storage::StorageModule +
logic::LogicModule +
events::EventsModule
{
#[init]
fn init(&self) { }
}
错误:重复错误消息
// 错误 - 重复字符串增加合约大小
require!(amount > 0, "金额必须为正");
require!(other_amount > 0, "金额必须为正");
正确:静态错误消息
// 正确 - 单一定义
const ERR_AMOUNT_POSITIVE: &str = "金额必须为正";
require!(amount > 0, ERR_AMOUNT_POSITIVE);
require!(other_amount > 0, ERR_AMOUNT_POSITIVE);
一起发送EGLD + ESDT(自v0.55.0起)
// 支持:EGLD和ESDT可以在多转账中组合
// 使用带TokenId的Payment进行统一处理
let mut payments = ManagedVec::new();
if let Some(egld_nz) = egld_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from("EGLD-000000"), 0, egld_nz));
}
if let Some(esdt_nz) = esdt_amount.into_non_zero() {
payments.push(Payment::new(TokenId::from(token_id), 0, esdt_nz));
}
self.tx().to(&recipient).payment(&payments).transfer();
生产模式
对于生产级合约,这些附加技能涵盖高级模式:
Gas优化的存储缓存
使用基于Drop的缓存在入口批量存储读取和在出口批量写入。参见multiversx-cache-patterns技能。
// 一次性加载所有状态,在内存中修改,在作用域退出时提交
let mut cache = StorageCache::new(self);
cache.total_supply += &amount;
cache.fee_reserve += &fee;
// Drop自动写回两个值
跨合约存储读取
使用#[storage_mapper_from_address]直接读取另一个合约的存储,无需异步调用。参见multiversx-cross-contract-storage技能。
#[storage_mapper_from_address("reserve")]
fn external_reserve(
&self,
contract_address: ManagedAddress,
token_id: &TokenIdentifier,
) -> SingleValueMapper<BigUint, ManagedAddress>;
生产项目结构
对于多模块合约,遵循模块化架构模式。参见multiversx-project-architecture技能。
src/
lib.rs # 仅特质组合
storage.rs # 所有存储映射器
cache/mod.rs # 基于Drop的缓存
views.rs # #[view]端点
config.rs # 管理员配置端点
events.rs # 事件定义
validation.rs # 输入验证
errors.rs # 静态错误常量
helpers.rs # 业务逻辑
DeFi金融数学
对于借贷、质押或DEX合约,使用半向上取整和标准化精度级别。参见multiversx-defi-math技能。
附加专业化技能
multiversx-flash-loan-patterns— 带安全保护的闪电贷实现multiversx-factory-manager— 部署和管理子合约multiversx-vault-pattern— 用于多步操作的内存代币跟踪
文档链接
始终查阅官方文档:
- 智能合约概述: https://docs.multiversx.com/developers/smart-contracts
- sc-meta工具: https://docs.multiversx.com/developers/meta/sc-meta
- 存储映射器: https://docs.multiversx.com/developers/developer-reference/storage-mappers
- 注解: https://docs.multiversx.com/developers/developer-reference/sc-annotations
- 支付: https://docs.multiversx.com/developers/developer-reference/sc-payments
- 示例合约: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples
- 框架仓库: https://github.com/multiversx/mx-sdk-rs
验证清单
完成前验证:
- [ ] 使用
sc-meta new --template <template> --name <name>创建合约 - [ ] lib.rs顶部有
#![no_std] - [ ] 主特质上有
#[multiversx_sc::contract] - [ ] 为部署定义了
#[init]函数 - [ ] 存储映射器使用适当类型(不需要迭代时避免MapMapper)
- [ ] 支付端点有
#[payable(...)]注解 - [ ] 错误消息使用
require!宏 - [ ] 合约使用
sc-meta all build成功构建 - [ ] 输出文件存在:
output/<name>.wasm和output/<name>.abi.json - [ ] 测试通过
sc-meta test(如果测试存在) - [ ] 部署前在devnet上测试