模组

模组是一种链上脚本,通过各种资源的注册和配置,以插件的形式为自主世界添加特定的功能。 模组可以在自主世界间共享。

实现过程

我们以官方提供的 uniqueentity 模组为例介绍模组如何为自主世界引入特定的功能。 这个模组能够为自主世界添加一个获取唯一实体 ID 的功能。 该功能本质上由一个 uint256 的单例表和一个操作这个表实现自增的系统组成。 使用该功能获取到的 ID 就是从 1 开始自增的整数。

首先看一下模组文件的目录结构:

node_modules/@latticexyz/world-modules/src/modules/uniqueentity
├── UniqueEntityModule.sol
├── UniqueEntitySystem.sol
├── constants.sol
├── getUniqueEntity.sol
└── tables
    └── UniqueEntity.sol

UniqueEntityModule.sol 是模组的安装脚本,通过调用该脚本合约完成模组的安装。 UniqueEntitySystem.sol 是实现单例表自增的系统。 constants.sol 是模组的常量定义, 包含即将引入的功能所在的命名空间,所需的各类资源的 ResourceIdgetUniqueEntity.sol 是模组引入的功能也就是获取唯一实体 ID 的使用脚本。 tables/UniqueEntity.soluint256 的单例表。

模组的安装方式有两种:

interface IModule is IERC165, IModuleErrors {
  /**
  * @notice 安装根模组。
  * @dev 这个函数会被 World 合约在执行 `installRootModule` 过程中调用。
  * @param encodedArgs 安装过程可能需要的 ABI 编码的数据。
  */
  function installRoot(bytes memory encodedArgs) external;

  /**
  * @notice 安装模组。
  * @dev 这个函数会被 World 合约在执行 `installModule` 过程中调用。
  * @param encodedArgs 安装过程可能需要的 ABI 编码的数据。
  */
  function install(bytes memory encodedArgs) external;
}

两种安装方法的区别主要体现在安装入口和安装脚本权限的不同。

--> call, ==> delegatecall

// 安装根模组
User --> IWorld.installRootModule ==> IModule.installRoot
// 安装模组
User --> IWorld.installModule --> IModule.install

安装根模组时,我们需要调用 World 合约的 installRootModule 方法,传入模组的地址和需要的参数。 而安转普通模组时,我们需要调用 installModule 方法。另外在根模组安装过程中, World 合约通过 delegatecall 调用模组的 installRoot 方法,因此模组的安装脚本在执行时具有最高权限, 相比于普通模组安装脚本能做的事情更多,例如在 root 命名空间内注册资源和配置访问权限。

uniqueentity 模组的安装脚本主要完成以下几个事情:

  1. 注册一个命名空间,用于存放 UniqueEntity 表和 UniqueEntitySystem 系统。

  2. 注册 UniqueEntity 表。

  3. 注册 UniqueEntitySystem 系统,注册系统方法的函数选择器 getUniqueEntity()

官方模组

官方提供了一些常用的模组,源码见 此处

  • uniqueentity:提供获取唯一实体 ID 的功能。

  • keysintable:为指定表统计存在的键,提供获取所有存在的键的功能。

  • keyswithvalue:为指定表的值建立索引,提供通过值查询键的功能。

  • callwithsignature :提供通过 EIP712 签名调用合约的功能。

  • std-delegations:提供几种常见的委托调用模式,包括限定次数、限定系统、限定时间。

  • puppet:提供为系统合约创建代理合约的功能,可以通过代理合约调用系统合约。

  • erc20-puppet:一种基于 puppet 模组实现的 ERC20 代币合约。

  • erc721-puppet:一种基于 puppet 模组实现的 ERC721 代币合约。

模组安装

通过配置文件安装

我们建议通过编辑配置文件 mud.config.ts 安装模组。

import { defineWorld } from "@latticexyz/world";
import { resolveTableId } from "@latticexyz/world/internal";
import { encodeAbiParameters, parseAbiParameters, toHex } from 'viem';

export default defineWorld({
  tables: {
    StoredUint: {
      schema:{
        value: "uint256",
      },
      key: [],
    },
  },
  modules: [
    {
      artifactPath: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json",
      root: true,
      args: [resolveTableId("StoredUint")],
    },
    {
      artifactPath: "@latticexyz/world-modules/out/PuppetModule.sol/PuppetModule.json",
      root: true,
      args: [],
    },
    {
      artifactPath: "@latticexyz/world-modules/out/ERC20Module.sol/ERC20Module.json",
      root: false,
      args: [
        {type: "bytes", value: encodeAbiParameters(
          parseAbiParameters('bytes14 namespace, (uint8 decimals, string name, string symbol)'),
          [toHex("token", { size: 14 }), {decimals: 18, name: "muddoc", symbol: "MUDOC"}],
        )}
      ],
    },
  ],
});

上面的配置文件完成了三个模组的安装,分别是 keysintablepuppeterc20-puppet。 需要留意的是模组的安装方式,有些模组同时支持以跟模组或普通模组的形式安装,而有的只支持其中的一种。 安装一个模组前,先查看它支持的安装方式。其次我们要提供必要的安装参数,例如安装 keysintable 模组时, 我们需要指定为哪个表统计存在的键。再比如,安装 erc20-puppet 模组时,我们需要提供 ERC20 代币系统所在的命名空间、 代币的名称、符号和精度。

模组的配置项:

  • artifactPath:模组合约编译后的 JSON 文件路径。既支持本地相对路径,又支持包引入路径。

  • root:是否以根模组形式安装。

  • args:安装模组时需要的参数。

手动安装

手动安装模组需要根据安装方式调用 World 合约的 installModuleinstallRootModule 方法。

与上面的配置文件安装内容等价的手动安装脚本如下:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import { Script } from "forge-std/Script.sol";
import { StoredUint } from "../src/codegen/index.sol";
import { IWorld } from "../src/codegen/world/IWorld.sol";
import { KeysInTableModule } from "@latticexyz/world-modules/src/modules/keysintable/KeysInTableModule.sol";
import { PuppetModule } from "@latticexyz/world-modules/src/modules/puppet/PuppetModule.sol";
import { registerERC20 } from "@latticexyz/world-modules/src/modules/erc20-puppet/registerERC20.sol";
import { ERC20MetadataData } from "@latticexyz/world-modules/src/modules/erc20-puppet/tables/ERC20Metadata.sol";
import { IERC20Mintable } from "@latticexyz/world-modules/src/modules/erc20-puppet/IERC20Mintable.sol";

contract InstallModule is Script {
  function run(address worldAddress) public {
    // 安装根模组 `keysintable` 统计 `StoredUint` 表中的键
    IWorld(worldAddress).installRootModule(new KeysInTableModule(), abi.encode(StoredUint._tableId));
    // 安装根模组 `puppet`
    IWorld(worldAddress).installRootModule(new PuppetModule(), new bytes(0));
    // 安装模组 `erc20-puppet`,创建一个 `ERC20` 代币
    IERC20Mintable token = registerERC20(
      IWorld(worldAddress),
      bytes14("token"),
      ERC20MetadataData({ decimals: 6, name: "muddoc", symbol: "MUDOC" })
    );
  }
}

编写模组

这里有一份官方的 模组编写教程