开发智能合约的区别
读写合约状态
这是一个简单的存储并读取一个数字的例子。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedUint;
function setUint(uint x) public {
storedUint = x;
}
function getUint() public view returns (uint) {
return storedUint;
}
}
使用Mud框架,你应该这样写:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { StoredUint } from "../codegen/index.sol";
contract SimpleStorageSystem is System {
function setUint(uint x) public {
StoredUint.set(x);
}
function getUint() public view returns (uint) {
return StoredUint.get();
}
}
首先你会注意到,同样都是 contract 的 SimpleStorageSystem 额外继承了
@latticexyz/world/src/System.sol。这是因为使用 Mud 构建的项目所具备的功能都由一个个系统来实现。
SimpleStorageSystem 也是一个系统,它提供了存储和读取一个数字的功能。
其次你可能会好奇 StoredUint 是在哪里被声明的,又是如何被存储和读取的?
这就是Mud框架的重要特性之一,它把合约状态封装成了人们熟知的数据库,你可以像操作库表一样声明、读取和更新合约状态。 为了达到上面的效果,你只需要搭配一个这样的“库表”配置文件即可:
import { defineWorld } from "@latticexyz/world";
export default defineWorld({
namespace: "muddoc",
tables: {
StoredUint: {
schema:{
value: "uint256",
},
key: [],
},
},
});
看上去如果只是存储和读取一个 uint ,似乎用原生语言更简单和直接。
是的,即使是 Array 或 Mapping 类型的变量,在单个合约的使用场景下, Mud 只能画蛇添足。
这也是我们不推荐在简单合约中使用Mud的原因之一。但我们仍鼓励大家尝试使用Mud。
因为我们经常会需要复杂的数据结构和频繁的合约交互,那就需要为数据结构定义易用的方法,为其他合约定义数据接口。所有的这些都要自己手动去写,还要做好管理,真的是一件耗时伤神的事情。
那么 Mud ,它到底能为数据读写带来什么好处?
自动化的生成数据接口, 以
library形式存在,随用随取。相比于原生
Solidity更紧致的数据打包,相比于Diamon更方便的管理和维护内部状态。通过一组通用的
event集合实现链下标准化、自动化的数据索引,不再需要创建数据更新的event和相应的监听器。
备注
目前,合约无法像查看自己的 slot 一般查看其他合约的 slot ,只能依赖开放的数据接口。然而任何链上数据对于任何链下程序都是透明的。
开放式、标准化的数据读取接口,方便了任何外部合约读取合约的任意数据。
跨合约调用
这是一个简单的调用 SimpleStorage 合约,获取和更新存储的数字的例子。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorageCaller {
SimpleStorage simpleStorage;
constructor(address _simpleStorage) {
simpleStorage = SimpleStorage(_simpleStorage);
}
function setUintToSimpleStorage(uint x) public {
simpleStorage.setUint(x);
}
function getUintFromSimpleStorage() public view returns (uint) {
return simpleStorage.getUint();
}
}
使用Mud框架,你应该这样写:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { IWorld } from "../codegen/world/IWorld.sol";
contract SimpleStorageCallerSystem is System {
function setUintToSimpleStorageSystem(uint x) public {
IWorld(_world()).muddoc__setUint(x);
}
function getUintFromSimpleStorageSystem() public view returns (uint) {
return IWorld(_world()).muddoc__getUint();
}
}
这里, SimpleStorageCallerSystem 也是一个系统,是与 SimpleStorageSystem 地址不同的另一份合约。
但不同于原生的合约交互写法, SimpleStorageCallerSystem 并没有直接调用 SimpleStorageSystem 的方法,
而是调用了另一个由 _world() 返回的合约地址,方法名也改成了带前缀的 muddoc__setUint()。
这就是 Mud 框架的另一个重要特性,所有的系统方法入口都集中于一个被称为 World 的合约上。它就像一个路由器,
负责把所有的系统方法调用分发到正确的系统上,并且还带着方法名称的解析。这就是为什么当一个系统调用另一个系统的方法时,
调用指向的是 _world() 返回的 World 合约,并且方法名称也发生了一些细微的变化。
如果你了解 Diamond 协议,就不难猜到这大概是如何做到的。 World 合约十分类似 Diamond 合约。
其实它们都有一个集中的数据存储合约,所有的业务逻辑 System 或 Facet 合约都以链上代码库的形式存在,
它们不实际存储数据,而是通过 delegateCall 或 call 的形式与管理数据的合约进行连接,以此完成对数据的操作。
有人可能会问,如果 World 合约类似 Diamond ,所有的数据都是集中存储的,
为什么不让 SimpleStorageCallerSystem 直接读写 StoredUint 呢,反而要走合约交互的方式?
确实,如果跨合约交互的需求只是读写一个具体的数据的话,也可以这么写:
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.24;
import { System } from "@latticexyz/world/src/System.sol";
import { StoredUint } from "../codegen/index.sol";
contract SimpleStorageCallerSystem is System {
function setUint2(uint x) public {
StoredUint.set(x);
}
function getUint2() public view returns (uint) {
return StoredUint.get();
}
}
重要
即使合约交互逻辑简单到只是修改一个状态,也不一定能直接对状态所在的表进行操作。Mud 有一套严格的权限控制机制。
如果你的项目只有一个自定义的命名空间,比如 muddoc,且所有的表和系统都隶属它,那么上面的转换就是可行的。否则,仍需具体情况具体分析。
备注
这里我们只是希望通过这个极简的例子,来表现库表资源是在一定范围内共享的,不需要专门写相关的交互方法。在真实应用场景中,每一个系统方法都应该是认真设计的,并尽可能地复用。