name: multiversx-cross-contract-storage description: 使用 storage_mapper_from_address 直接读取另一个合约的存储,无需异步调用。在构建聚合器、控制器或任何需要从其他同分片合约读取状态而无需代理调用开销的合约时使用。
MultiversX 跨合约存储读取
注意: 此技能使用
TokenIdentifier(仅限 ESDT 别名)。对于统一的 EGLD+ESDT 标识符,请使用TokenId。
直接读取另一个合约的存储映射器——零代理调用的 gas 开销,无异步复杂性。
解决了什么问题?
当您的合约需要从另一个同分片合约读取状态时,您有两个选项:
- 代理调用 — 在目标上执行视图,消耗执行 gas,需要 ABI 知识
- 直接存储读取 — 读取原始存储键,仅消耗存储读取成本,需要知道键名
storage_mapper_from_address 为您提供选项 2。
何时使用
| 标准 | storage_mapper_from_address |
代理调用 |
|---|---|---|
| 仅同分片 | 是(必需) | 跨分片工作 |
| 只读 | 是 | 读 + 写 |
| 需要在目标上计算 | 否 | 是 |
| Gas 成本 | ~存储读取成本 | ~执行 + 存储 |
| 需要知道存储键 | 是 | 否(使用 ABI) |
| 数据新鲜度 | 当前区块状态 | 当前区块状态 |
核心模式
#[multiversx_sc::module]
pub trait ExternalStorageModule {
// 从另一个合约读取简单值
#[storage_mapper_from_address("total_supply")]
fn external_total_supply(
&self,
contract_address: ManagedAddress,
) -> SingleValueMapper<BigUint, ManagedAddress>;
// 读取具有复合键的值(特定代币的余额)
#[storage_mapper_from_address("balance")]
fn external_balance(
&self,
contract_address: ManagedAddress,
token_id: &TokenIdentifier,
) -> SingleValueMapper<BigUint, ManagedAddress>;
// 读取布尔标志(模块前缀键)
#[storage_mapper_from_address("pause_module:paused")]
fn external_paused(
&self,
contract_address: ManagedAddress,
) -> SingleValueMapper<bool, ManagedAddress>;
}
关键语法规则
#[storage_mapper_from_address("key")]中的字符串必须与目标合约中的存储键完全匹配- 第一个参数必须是
ManagedAddress— 目标合约地址 - 返回类型的第二个泛型必须是
ManagedAddress(例如,SingleValueMapper<BigUint, ManagedAddress>) - 地址之后的附加参数成为复合存储键的一部分(与带参数的普通存储映射器相同)
通用示例
示例 1:读取代币余额
#[storage_mapper_from_address("balance")]
fn external_token_balance(
&self,
contract_address: ManagedAddress,
token_id: &TokenIdentifier,
) -> SingleValueMapper<BigUint, ManagedAddress>;
fn get_external_balance(&self, addr: &ManagedAddress, token: &TokenIdentifier) -> BigUint {
let mapper = self.external_token_balance(addr.clone(), token);
if mapper.is_empty() { BigUint::zero() } else { mapper.get() }
}
示例 2:读取配置标志
#[storage_mapper_from_address("is_active")]
fn external_is_active(
&self,
contract_address: ManagedAddress,
) -> SingleValueMapper<bool, ManagedAddress>;
示例 3:读取复合键(多参数)
// 目标合约有:#[storage_mapper("rate")] fn rate(&self, asset: &TokenIdentifier, tier: u32) -> ...
#[storage_mapper_from_address("rate")]
fn external_rate(
&self,
contract_address: ManagedAddress,
asset: &TokenIdentifier,
tier: u32,
) -> SingleValueMapper<BigUint, ManagedAddress>;
如何发现存储键
- 阅读源代码:在目标合约中查找
#[storage_mapper("key")] - 检查 ABI:
.abi.json文件列出了所有存储键 - 复合键:带参数的存储映射器将键编码为
key+ 嵌套编码的参数 - 模块键:某些模块使用前缀键,如
pause_module:paused
安全考虑
1. 仅同分片
storage_mapper_from_address 仅当两个合约在同一分片上时才有效。跨分片读取会静默返回空值/默认值——无错误!
// 危险:如果 target_address 在不同分片上,此操作会静默返回 0
let value = self.external_total_supply(target_address).get();
2. 陈旧数据意识
数据与当前区块一样新鲜。但如果目标合约在本区块中未被调用,其状态反映的是最后一个活跃区块。
3. 存储键冲突
如果目标合约在升级中更改其存储键名,您的读取将静默中断(返回默认值)。
4. 无写访问
此模式为只读。您无法写入另一个合约的存储。
反模式
1. 未验证空结果
// 错误 — 如果键不存在,您会得到默认值(0, false, 空)
let value = self.external_balance(addr, &token).get();
// 正确 — 检查映射器是否有值
let mapper = self.external_balance(addr, &token);
require!(!mapper.is_empty(), "外部值未设置");
let value = mapper.get();
2. 假设跨分片工作
// 错误 — 无验证
let balance = self.external_balance(unknown_address, &token).get();
// 正确 — 先验证分片
let my_shard = self.blockchain().get_shard_of_address(&self.blockchain().get_sc_address());
let target_shard = self.blockchain().get_shard_of_address(&target_address);
require!(my_shard == target_shard, "跨分片读取不支持");
模板
multiversx_sc::imports!();
#[multiversx_sc::module]
pub trait ExternalStorageModule {
// 为您需要的外部存储键定义一个映射器
#[storage_mapper_from_address("storage_key_name")]
fn external_value(
&self,
contract_address: ManagedAddress,
) -> SingleValueMapper<YourType, ManagedAddress>;
// 带验证的读取辅助函数
fn read_external_value(&self, addr: &ManagedAddress) -> YourType {
let mapper = self.external_value(addr.clone());
require!(!mapper.is_empty(), "外部值未设置");
mapper.get()
}
}