Skip to content

Commit

Permalink
feat(world): add InstalledModules table to store installed modules
Browse files Browse the repository at this point in the history
  • Loading branch information
alvrs committed Mar 14, 2023
1 parent 3699464 commit cac35f5
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 14 deletions.
20 changes: 10 additions & 10 deletions packages/world/gas-report.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16038
(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96409
(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80937
(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72163
(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101534
(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151628
(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96069
(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252155
(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44726
(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277162
(test/World.t.sol) | Delete record [world.deleteRecord(namespace, file, singletonKey)]: 16026
(test/World.t.sol) | Push data to the table [world.pushToField(namespace, file, keyTuple, 0, encodedData)]: 96397
(test/World.t.sol) | Register a fallback system [bytes4 funcSelector1 = world.registerFunctionSelector(namespace, file, "", "")]: 80940
(test/World.t.sol) | Register a root fallback system [bytes4 funcSelector2 = world.registerRootFunctionSelector(namespace, file, worldFunc, 0)]: 72166
(test/World.t.sol) | Register a function selector [bytes4 functionSelector = world.registerFunctionSelector(namespace, file, "msgSender", "()")]: 101537
(test/World.t.sol) | Register a new namespace [world.registerNamespace("test")]: 151631
(test/World.t.sol) | Register a root function selector [bytes4 functionSelector = world.registerRootFunctionSelector(namespace, file, worldFunc, sysFunc)]: 96072
(test/World.t.sol) | Register a new table in the namespace [bytes32 tableSelector = world.registerTable(namespace, table, schema, defaultKeySchema)]: 252158
(test/World.t.sol) | Write data to a table field [world.setField(namespace, file, singletonKey, 0, abi.encodePacked(true))]: 44714
(test/World.t.sol) | Set metadata [world.setMetadata(namespace, file, tableName, fieldNames)]: 277165
(test/World.t.sol) | Write data to the table [Bool.set(tableId, world, true)]: 42598
14 changes: 14 additions & 0 deletions packages/world/mud.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ const config: StoreUserConfig = {
storeArgument: true,
tableIdArgument: true,
},
InstalledModules: {
primaryKeys: {
namespace: SchemaType.BYTES16,
mdouleName: SchemaType.BYTES16,
},
schema: {
moduleAddress: SchemaType.ADDRESS,
},
// TODO: this is a workaround to use `getRecord` instead of `getField` in the autogen library,
// to allow using the table before it is registered. This is because `getRecord` passes the schema
// to store, while `getField` loads it from storage. Remove this once we have support for passing the
// schema in `getField` too.
dataStruct: true,
},
},
userTypes: {
enums: {
Expand Down
18 changes: 18 additions & 0 deletions packages/world/src/World.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { NamespaceOwner } from "./tables/NamespaceOwner.sol";
import { ResourceAccess } from "./tables/ResourceAccess.sol";
import { Systems } from "./tables/Systems.sol";
import { FunctionSelectors } from "./tables/FunctionSelectors.sol";
import { InstalledModules } from "./tables/InstalledModules.sol";

import { IModule } from "./interfaces/IModule.sol";
import { IWorldCore } from "./interfaces/IWorldCore.sol";
Expand All @@ -43,12 +44,20 @@ contract World is Store, IWorldCore, IErrors {
* Install the given module at the given namespace in the World.
*/
function installModule(IModule module, bytes16 namespace) public {
// Prevent the same module from being installed twice in the same namespace
if (InstalledModules.get(ROOT_NAMESPACE, module.getName()).moduleAddress != address(0)) {
revert ModuleAlreadyInstalled(ResourceSelector.from(namespace, module.getName()).toString());
}

Call.withSender({
msgSender: msg.sender,
target: address(module),
funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, namespace),
delegate: false
});

// Register the module in the InstalledModules table
InstalledModules.set(namespace, module.getName(), address(module));
}

/**
Expand All @@ -58,12 +67,21 @@ contract World is Store, IWorldCore, IErrors {
*/
function installRootModule(IModule module) public {
AccessControl.requireOwner(ROOT_NAMESPACE, ROOT_FILE, msg.sender);

// Prevent the same module from being installed twice in the same namespace
if (InstalledModules.get(ROOT_NAMESPACE, module.getName()).moduleAddress != address(0)) {
revert ModuleAlreadyInstalled(ResourceSelector.from(ROOT_NAMESPACE, module.getName()).toString());
}

Call.withSender({
msgSender: msg.sender,
target: address(module),
funcSelectorAndArgs: abi.encodeWithSelector(IModule.install.selector, ROOT_NAMESPACE),
delegate: true // The module is delegate called so it can edit any table
});

// Register the module in the InstalledModules table
InstalledModules.set(ROOT_NAMESPACE, module.getName(), address(module));
}

/************************************************************************
Expand Down
4 changes: 4 additions & 0 deletions packages/world/src/constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ pragma solidity >=0.8.0;

bytes16 constant ROOT_NAMESPACE = 0;
bytes16 constant ROOT_FILE = 0;

// World modules
bytes16 constant CORE_MODULE_NAME = bytes16("core");
bytes16 constant REGISTRATION_MODULE_NAME = bytes16("registration");
1 change: 1 addition & 0 deletions packages/world/src/interfaces/IErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ interface IErrors {
error SystemExists(address system);
error FunctionSelectorExists(bytes4 functionSelector);
error FunctionSelectorNotFound(bytes4 functionSelector);
error ModuleAlreadyInstalled(string module);
}
7 changes: 7 additions & 0 deletions packages/world/src/interfaces/IModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ pragma solidity >=0.8.0;
import { IWorldCore } from "./IWorldCore.sol";

interface IModule {
error RequiredModuleNotFound(string resourceSelector);

/**
* Return the module name as a bytes16.
*/
function getName() external view returns (bytes16 name);

/**
* A module expects to be called via the World contract, and therefore installs itself on its `msg.sender`.
*/
Expand Down
12 changes: 10 additions & 2 deletions packages/world/src/modules/core/CoreModule.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

import { ROOT_NAMESPACE } from "../../constants.sol";
import { ROOT_NAMESPACE, CORE_MODULE_NAME } from "../../constants.sol";
import { WithMsgSender } from "../../WithMsgSender.sol";

import { IModule } from "../../interfaces/IModule.sol";
Expand All @@ -10,6 +10,7 @@ import { NamespaceOwner } from "../../tables/NamespaceOwner.sol";
import { ResourceAccess } from "../../tables/ResourceAccess.sol";
import { Systems } from "../../tables/Systems.sol";
import { FunctionSelectors } from "../../tables/FunctionSelectors.sol";
import { InstalledModules } from "../../tables/InstalledModules.sol";

/**
* The CoreModule registers internal World tables.
Expand All @@ -21,9 +22,16 @@ import { FunctionSelectors } from "../../tables/FunctionSelectors.sol";
* added in `RegistrationModule`, which is installed after `CoreModule`.
*/
contract CoreModule is IModule, WithMsgSender {
function install(bytes16) external override {
function getName() public pure returns (bytes16) {
return CORE_MODULE_NAME;
}

function install(bytes16) public override {
NamespaceOwner.setMetadata();

InstalledModules.registerSchema();
InstalledModules.setMetadata();

ResourceAccess.registerSchema();
ResourceAccess.setMetadata();
ResourceAccess.set(ROOT_NAMESPACE, _msgSender(), true);
Expand Down
16 changes: 15 additions & 1 deletion packages/world/src/modules/registration/RegistrationModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ pragma solidity >=0.8.0;
import { RegistrationSystem } from "./RegistrationSystem.sol";

import { Call } from "../../Call.sol";
import { ROOT_NAMESPACE } from "../../constants.sol";
import { ROOT_NAMESPACE, REGISTRATION_MODULE_NAME } from "../../constants.sol";
import { WithMsgSender } from "../../WithMsgSender.sol";
import { Resource } from "../../types.sol";
import { ResourceSelector } from "../../ResourceSelector.sol";

import { IModule } from "../../interfaces/IModule.sol";

import { InstalledModules } from "../../tables/InstalledModules.sol";

import { ResourceType } from "./tables/ResourceType.sol";
import { SystemRegistry } from "./tables/SystemRegistry.sol";

Expand All @@ -24,13 +27,24 @@ import { SystemRegistry } from "./tables/SystemRegistry.sol";
* If the module is delegatecalled, the StoreCore functions are used directly.
*/
contract RegistrationModule is IModule, WithMsgSender {
using ResourceSelector for bytes32;

// Since the RegistrationSystem only exists once per World and writes to
// known tables, we can deploy it once and register it in multiple Worlds.
address immutable registrationSystem = address(new RegistrationSystem());
bytes16 immutable registrationSystemFile = bytes16("registration");

function getName() public pure returns (bytes16) {
return REGISTRATION_MODULE_NAME;
}

// The namespace argument is not used because the module is always installed in the root namespace
function install(bytes16) public {
// Require the CoreModule to be installed in the root namespace
if (InstalledModules.get(ROOT_NAMESPACE, bytes16("core")).moduleAddress == address(0)) {
revert RequiredModuleNotFound(ResourceSelector.from(ROOT_NAMESPACE, bytes16("core")).toString());
}

// Register tables required by RegistrationSystem
SystemRegistry.registerSchema();
SystemRegistry.setMetadata();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Systems } from "../../tables/Systems.sol";
import { FunctionSelectors } from "../../tables/FunctionSelectors.sol";

import { ISystemHook } from "../../interfaces/ISystemHook.sol";
import { IErrors } from "../interfaces/IErrors.sol";
import { IErrors } from "../../interfaces/IErrors.sol";
import { IRegistrationSystem } from "../../interfaces/systems/IRegistrationSystem.sol";

contract RegistrationSystem is System, IRegistrationSystem, IErrors {
Expand Down
119 changes: 119 additions & 0 deletions packages/world/src/tables/InstalledModules.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: MIT
pragma solidity >=0.8.0;

/* Autogenerated file. Do not edit manually. */

// Import schema type
import { SchemaType } from "@latticexyz/schema-type/src/solidity/SchemaType.sol";

// Import store internals
import { IStore } from "@latticexyz/store/src/IStore.sol";
import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol";
import { StoreCore } from "@latticexyz/store/src/StoreCore.sol";
import { Bytes } from "@latticexyz/store/src/Bytes.sol";
import { SliceLib } from "@latticexyz/store/src/Slice.sol";
import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol";
import { Schema, SchemaLib } from "@latticexyz/store/src/Schema.sol";
import { PackedCounter, PackedCounterLib } from "@latticexyz/store/src/PackedCounter.sol";

uint256 constant _tableId = uint256(bytes32(abi.encodePacked(bytes16(""), bytes16("InstalledModules"))));
uint256 constant InstalledModulesTableId = _tableId;

struct InstalledModulesData {
address moduleAddress;
}

library InstalledModules {
/** Get the table's schema */
function getSchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](1);
_schema[0] = SchemaType.ADDRESS;

return SchemaLib.encode(_schema);
}

function getKeySchema() internal pure returns (Schema) {
SchemaType[] memory _schema = new SchemaType[](2);
_schema[0] = SchemaType.BYTES16;
_schema[1] = SchemaType.BYTES16;

return SchemaLib.encode(_schema);
}

/** Get the table's metadata */
function getMetadata() internal pure returns (string memory, string[] memory) {
string[] memory _fieldNames = new string[](1);
_fieldNames[0] = "moduleAddress";
return ("InstalledModules", _fieldNames);
}

/** Register the table's schema */
function registerSchema() internal {
StoreSwitch.registerSchema(_tableId, getSchema(), getKeySchema());
}

/** Set the table's metadata */
function setMetadata() internal {
(string memory _tableName, string[] memory _fieldNames) = getMetadata();
StoreSwitch.setMetadata(_tableId, _tableName, _fieldNames);
}

/** Get moduleAddress */
function getModuleAddress(bytes16 namespace, bytes16 mdouleName) internal view returns (address moduleAddress) {
bytes32[] memory _primaryKeys = new bytes32[](2);
_primaryKeys[0] = bytes32((namespace));
_primaryKeys[1] = bytes32((mdouleName));

bytes memory _blob = StoreSwitch.getField(_tableId, _primaryKeys, 0);
return (address(Bytes.slice20(_blob, 0)));
}

/** Set moduleAddress */
function setModuleAddress(bytes16 namespace, bytes16 mdouleName, address moduleAddress) internal {
bytes32[] memory _primaryKeys = new bytes32[](2);
_primaryKeys[0] = bytes32((namespace));
_primaryKeys[1] = bytes32((mdouleName));

StoreSwitch.setField(_tableId, _primaryKeys, 0, abi.encodePacked((moduleAddress)));
}

/** Get the full data */
function get(bytes16 namespace, bytes16 mdouleName) internal view returns (InstalledModulesData memory _table) {
bytes32[] memory _primaryKeys = new bytes32[](2);
_primaryKeys[0] = bytes32((namespace));
_primaryKeys[1] = bytes32((mdouleName));

bytes memory _blob = StoreSwitch.getRecord(_tableId, _primaryKeys, getSchema());
return decode(_blob);
}

/** Set the full data using individual values */
function set(bytes16 namespace, bytes16 mdouleName, address moduleAddress) internal {
bytes memory _data = abi.encodePacked(moduleAddress);

bytes32[] memory _primaryKeys = new bytes32[](2);
_primaryKeys[0] = bytes32((namespace));
_primaryKeys[1] = bytes32((mdouleName));

StoreSwitch.setRecord(_tableId, _primaryKeys, _data);
}

/** Set the full data using the data struct */
function set(bytes16 namespace, bytes16 mdouleName, InstalledModulesData memory _table) internal {
set(namespace, mdouleName, _table.moduleAddress);
}

/** Decode the tightly packed blob using this table's schema */
function decode(bytes memory _blob) internal pure returns (InstalledModulesData memory _table) {
_table.moduleAddress = (address(Bytes.slice20(_blob, 0)));
}

/* Delete all data for given keys */
function deleteRecord(bytes16 namespace, bytes16 mdouleName) internal {
bytes32[] memory _primaryKeys = new bytes32[](2);
_primaryKeys[0] = bytes32((namespace));
_primaryKeys[1] = bytes32((mdouleName));

StoreSwitch.deleteRecord(_tableId, _primaryKeys);
}
}

0 comments on commit cac35f5

Please sign in to comment.