From cd069f189eb537df8ef7a51b50e97c982d66b8b8 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 9 Sep 2025 12:07:37 +0900 Subject: [PATCH 1/3] add batch store module --- packages/world-module-batchstore/.gitignore | 2 + .../world-module-batchstore/.solhint.json | 8 + packages/world-module-batchstore/README.md | 1 + packages/world-module-batchstore/foundry.toml | 15 ++ .../world-module-batchstore/mud.config.ts | 9 + packages/world-module-batchstore/package.json | 58 ++++++ .../world-module-batchstore/remappings.txt | 2 + .../src/BatchStoreModule.sol | 43 +++++ .../src/BatchStoreSystem.sol | 60 ++++++ .../systems/BatchStoreSystemLib.sol | 176 ++++++++++++++++++ .../src/codegen/world/IBatchStoreSystem.sol | 23 +++ .../src/codegen/world/IWorld.sol | 16 ++ .../world-module-batchstore/src/common.sol | 11 ++ .../test/BatchStoreModule.t.sol | 99 ++++++++++ packages/world-module-batchstore/ts/build.ts | 20 ++ .../world-module-batchstore/tsconfig.json | 7 + .../world-module-batchstore/tsup.config.ts | 9 + .../render-solidity/renderSystemLibrary.ts | 4 +- pnpm-lock.yaml | 65 +++++-- 19 files changed, 606 insertions(+), 22 deletions(-) create mode 100644 packages/world-module-batchstore/.gitignore create mode 100644 packages/world-module-batchstore/.solhint.json create mode 100644 packages/world-module-batchstore/README.md create mode 100644 packages/world-module-batchstore/foundry.toml create mode 100644 packages/world-module-batchstore/mud.config.ts create mode 100644 packages/world-module-batchstore/package.json create mode 100644 packages/world-module-batchstore/remappings.txt create mode 100644 packages/world-module-batchstore/src/BatchStoreModule.sol create mode 100644 packages/world-module-batchstore/src/BatchStoreSystem.sol create mode 100644 packages/world-module-batchstore/src/codegen/experimental/systems/BatchStoreSystemLib.sol create mode 100644 packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol create mode 100644 packages/world-module-batchstore/src/codegen/world/IWorld.sol create mode 100644 packages/world-module-batchstore/src/common.sol create mode 100644 packages/world-module-batchstore/test/BatchStoreModule.t.sol create mode 100644 packages/world-module-batchstore/ts/build.ts create mode 100644 packages/world-module-batchstore/tsconfig.json create mode 100644 packages/world-module-batchstore/tsup.config.ts diff --git a/packages/world-module-batchstore/.gitignore b/packages/world-module-batchstore/.gitignore new file mode 100644 index 0000000000..1e4ded714a --- /dev/null +++ b/packages/world-module-batchstore/.gitignore @@ -0,0 +1,2 @@ +cache +out diff --git a/packages/world-module-batchstore/.solhint.json b/packages/world-module-batchstore/.solhint.json new file mode 100644 index 0000000000..4e2baa8be7 --- /dev/null +++ b/packages/world-module-batchstore/.solhint.json @@ -0,0 +1,8 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": ["error", ">=0.8.0"], + "avoid-low-level-calls": "off", + "func-visibility": ["warn", { "ignoreConstructors": true }] + } +} diff --git a/packages/world-module-batchstore/README.md b/packages/world-module-batchstore/README.md new file mode 100644 index 0000000000..7f931ee060 --- /dev/null +++ b/packages/world-module-batchstore/README.md @@ -0,0 +1 @@ +# BatchStore world module diff --git a/packages/world-module-batchstore/foundry.toml b/packages/world-module-batchstore/foundry.toml new file mode 100644 index 0000000000..e19b3bb38a --- /dev/null +++ b/packages/world-module-batchstore/foundry.toml @@ -0,0 +1,15 @@ +[profile.default] +solc = "0.8.24" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 3 +allow_paths = ["../../node_modules", "../"] +src = "src" +out = "out" +bytecode_hash = "none" +extra_output_files = [ + "abi", + "evm.bytecode" +] diff --git a/packages/world-module-batchstore/mud.config.ts b/packages/world-module-batchstore/mud.config.ts new file mode 100644 index 0000000000..b51def30e9 --- /dev/null +++ b/packages/world-module-batchstore/mud.config.ts @@ -0,0 +1,9 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + codegen: { + generateSystemLibraries: true, + // generate into experimental dir until these are stable/audited + systemLibrariesDirectory: "experimental/systems", + }, +}); diff --git a/packages/world-module-batchstore/package.json b/packages/world-module-batchstore/package.json new file mode 100644 index 0000000000..3f6f4c8012 --- /dev/null +++ b/packages/world-module-batchstore/package.json @@ -0,0 +1,58 @@ +{ + "name": "@latticexyz/world-module-batchstore", + "version": "2.2.23", + "description": "BatchStore world module", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/world-module-batchstore" + }, + "license": "MIT", + "type": "module", + "exports": { + "./mud.config": "./dist/mud.config.js", + "./out/*": "./out/*" + }, + "typesVersions": { + "*": { + "mud.config": [ + "./dist/mud.config.d.ts" + ] + } + }, + "files": [ + "dist", + "out", + "src" + ], + "scripts": { + "build": "pnpm run build:mud && pnpm run build:abi && pnpm run build:abi-ts && pnpm run build:js", + "build:abi": "forge build", + "build:abi-ts": "abi-ts", + "build:js": "tsup", + "build:mud": "tsx ./ts/build.ts", + "clean": "pnpm run clean:abi && pnpm run clean:js && pnpm run clean:mud", + "clean:abi": "forge clean", + "clean:js": "shx rm -rf dist", + "clean:mud": "shx rm -rf src/**/codegen", + "dev": "tsup --watch", + "gas-report": "gas-report --save gas-report.json", + "lint": "solhint --config ./.solhint.json 'src/**/*.sol'", + "test": "tsc --noEmit && forge test", + "test:ci": "pnpm run test" + }, + "dependencies": { + "@latticexyz/schema-type": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/world": "workspace:*" + }, + "devDependencies": { + "@latticexyz/abi-ts": "workspace:*", + "@latticexyz/gas-report": "workspace:*", + "forge-std": "https://github.com/foundry-rs/forge-std.git#60acb7aaadcce2d68e52986a0a66fe79f07d138f", + "solhint": "^3.3.7" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/world-module-batchstore/remappings.txt b/packages/world-module-batchstore/remappings.txt new file mode 100644 index 0000000000..e2614d0aa8 --- /dev/null +++ b/packages/world-module-batchstore/remappings.txt @@ -0,0 +1,2 @@ +forge-std/=node_modules/forge-std/src/ +@latticexyz/=node_modules/@latticexyz/ diff --git a/packages/world-module-batchstore/src/BatchStoreModule.sol b/packages/world-module-batchstore/src/BatchStoreModule.sol new file mode 100644 index 0000000000..f8875fed32 --- /dev/null +++ b/packages/world-module-batchstore/src/BatchStoreModule.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; + +import { Module } from "@latticexyz/world/src/Module.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; +import { worldRegistrationSystem } from "@latticexyz/world/src/codegen/experimental/systems/WorldRegistrationSystemLib.sol"; + +import { BatchStoreSystem } from "./BatchStoreSystem.sol"; +import { batchStoreSystem } from "./codegen/experimental/systems/BatchStoreSystemLib.sol"; + +contract BatchStoreModule is Module { + BatchStoreSystem private immutable systemAddress = new BatchStoreSystem(); + + function installRoot(bytes memory encodedArgs) public override { + ResourceId systemId = batchStoreSystem.toResourceId(); + + if (!ResourceIds.getExists(systemId)) { + worldRegistrationSystem.callAsRoot().registerSystem(systemId, systemAddress, true); + // TODO: since this is a scary/internal and use-with-caution module/system, should we not register function selectors? + worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( + systemId, + "getTableRecords(bytes32,bytes32[][])", + "getTableRecords(bytes32,bytes32[][])" + ); + worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( + systemId, + "setTableRecords(bytes32,(bytes32[],bytes,bytes32,bytes)[])", + "setTableRecords(bytes32,(bytes32[],bytes,bytes32,bytes)[])" + ); + worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( + systemId, + "deleteTableRecords(bytes32,bytes32[][])", + "deleteTableRecords(bytes32,bytes32[][])" + ); + } else if (batchStoreSystem.getAddress() != address(systemAddress)) { + // upgrade system + worldRegistrationSystem.callAsRoot().registerSystem(systemId, systemAddress, true); + } + } +} diff --git a/packages/world-module-batchstore/src/BatchStoreSystem.sol b/packages/world-module-batchstore/src/BatchStoreSystem.sol new file mode 100644 index 0000000000..c984c7b9c6 --- /dev/null +++ b/packages/world-module-batchstore/src/BatchStoreSystem.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { System } from "@latticexyz/world/src/System.sol"; +import { AccessControl } from "@latticexyz/world/src/AccessControl.sol"; +import { StoreCore } from "@latticexyz/store/src/StoreCore.sol"; +import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { TableRecord } from "./common.sol"; + +contract BatchStoreSystem is System { + function getTableRecords( + ResourceId tableId, + bytes32[][] memory keyTuples + ) external view returns (TableRecord[] memory records) { + AccessControl._requireAccess(tableId, _msgSender()); + + FieldLayout fieldLayout = StoreCore.getFieldLayout(tableId); + records = new TableRecord[](keyTuples.length); + + for (uint256 i = 0; i < keyTuples.length; i++) { + (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData) = StoreCore.getRecord( + tableId, + keyTuples[i], + fieldLayout + ); + records[i] = TableRecord({ + keyTuple: keyTuples[i], + staticData: staticData, + encodedLengths: encodedLengths, + dynamicData: dynamicData + }); + } + } + + function setTableRecords(ResourceId tableId, TableRecord[] memory records) external { + AccessControl._requireAccess(tableId, _msgSender()); + + for (uint256 i = 0; i < records.length; i++) { + StoreCore.setRecord( + tableId, + records[i].keyTuple, + records[i].staticData, + records[i].encodedLengths, + records[i].dynamicData + ); + } + } + + function deleteTableRecords(ResourceId tableId, bytes32[][] memory keyTuples) external { + AccessControl._requireAccess(tableId, _msgSender()); + + FieldLayout fieldLayout = StoreCore.getFieldLayout(tableId); + + for (uint256 i = 0; i < keyTuples.length; i++) { + StoreCore.deleteRecord(tableId, keyTuples[i], fieldLayout); + } + } +} diff --git a/packages/world-module-batchstore/src/codegen/experimental/systems/BatchStoreSystemLib.sol b/packages/world-module-batchstore/src/codegen/experimental/systems/BatchStoreSystemLib.sol new file mode 100644 index 0000000000..0f3fce2f18 --- /dev/null +++ b/packages/world-module-batchstore/src/codegen/experimental/systems/BatchStoreSystemLib.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { BatchStoreSystem } from "../../../BatchStoreSystem.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { TableRecord } from "../../../common.sol"; +import { revertWithBytes } from "@latticexyz/world/src/revertWithBytes.sol"; +import { IWorldCall } from "@latticexyz/world/src/IWorldKernel.sol"; +import { SystemCall } from "@latticexyz/world/src/SystemCall.sol"; +import { WorldContextConsumerLib } from "@latticexyz/world/src/WorldContext.sol"; +import { Systems } from "@latticexyz/world/src/codegen/tables/Systems.sol"; +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +type BatchStoreSystemType is bytes32; + +// equivalent to WorldResourceIdLib.encode({ typeId: RESOURCE_SYSTEM, namespace: "", name: "BatchStoreSystem" })) +BatchStoreSystemType constant batchStoreSystem = BatchStoreSystemType.wrap( + 0x73790000000000000000000000000000426174636853746f726553797374656d +); + +struct CallWrapper { + ResourceId systemId; + address from; +} + +struct RootCallWrapper { + ResourceId systemId; + address from; +} + +/** + * @title BatchStoreSystemLib + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This library is automatically generated from the corresponding system contract. Do not edit manually. + */ +library BatchStoreSystemLib { + error BatchStoreSystemLib_CallingFromRootSystem(); + + function getTableRecords( + BatchStoreSystemType self, + ResourceId tableId, + bytes32[][] memory keyTuples + ) internal view returns (TableRecord[] memory records) { + return CallWrapper(self.toResourceId(), address(0)).getTableRecords(tableId, keyTuples); + } + + function setTableRecords(BatchStoreSystemType self, ResourceId tableId, TableRecord[] memory records) internal { + return CallWrapper(self.toResourceId(), address(0)).setTableRecords(tableId, records); + } + + function deleteTableRecords(BatchStoreSystemType self, ResourceId tableId, bytes32[][] memory keyTuples) internal { + return CallWrapper(self.toResourceId(), address(0)).deleteTableRecords(tableId, keyTuples); + } + + function getTableRecords( + CallWrapper memory self, + ResourceId tableId, + bytes32[][] memory keyTuples + ) internal view returns (TableRecord[] memory records) { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert BatchStoreSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall( + _getTableRecords_ResourceId_bytes32ArrayArray.getTableRecords, + (tableId, keyTuples) + ); + bytes memory worldCall = self.from == address(0) + ? abi.encodeCall(IWorldCall.call, (self.systemId, systemCall)) + : abi.encodeCall(IWorldCall.callFrom, (self.from, self.systemId, systemCall)); + (bool success, bytes memory returnData) = address(_world()).staticcall(worldCall); + if (!success) revertWithBytes(returnData); + + bytes memory result = abi.decode(returnData, (bytes)); + // skip decoding an empty result, which can happen after expectRevert + if (result.length != 0) { + return abi.decode(result, (TableRecord[])); + } + } + + function setTableRecords(CallWrapper memory self, ResourceId tableId, TableRecord[] memory records) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert BatchStoreSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall( + _setTableRecords_ResourceId_TableRecordArray.setTableRecords, + (tableId, records) + ); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function deleteTableRecords(CallWrapper memory self, ResourceId tableId, bytes32[][] memory keyTuples) internal { + // if the contract calling this function is a root system, it should use `callAsRoot` + if (address(_world()) == address(this)) revert BatchStoreSystemLib_CallingFromRootSystem(); + + bytes memory systemCall = abi.encodeCall( + _deleteTableRecords_ResourceId_bytes32ArrayArray.deleteTableRecords, + (tableId, keyTuples) + ); + self.from == address(0) + ? _world().call(self.systemId, systemCall) + : _world().callFrom(self.from, self.systemId, systemCall); + } + + function setTableRecords(RootCallWrapper memory self, ResourceId tableId, TableRecord[] memory records) internal { + bytes memory systemCall = abi.encodeCall( + _setTableRecords_ResourceId_TableRecordArray.setTableRecords, + (tableId, records) + ); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function deleteTableRecords(RootCallWrapper memory self, ResourceId tableId, bytes32[][] memory keyTuples) internal { + bytes memory systemCall = abi.encodeCall( + _deleteTableRecords_ResourceId_bytes32ArrayArray.deleteTableRecords, + (tableId, keyTuples) + ); + SystemCall.callWithHooksOrRevert(self.from, self.systemId, systemCall, msg.value); + } + + function callFrom(BatchStoreSystemType self, address from) internal pure returns (CallWrapper memory) { + return CallWrapper(self.toResourceId(), from); + } + + function callAsRoot(BatchStoreSystemType self) internal view returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), WorldContextConsumerLib._msgSender()); + } + + function callAsRootFrom(BatchStoreSystemType self, address from) internal pure returns (RootCallWrapper memory) { + return RootCallWrapper(self.toResourceId(), from); + } + + function toResourceId(BatchStoreSystemType self) internal pure returns (ResourceId) { + return ResourceId.wrap(BatchStoreSystemType.unwrap(self)); + } + + function fromResourceId(ResourceId resourceId) internal pure returns (BatchStoreSystemType) { + return BatchStoreSystemType.wrap(resourceId.unwrap()); + } + + function getAddress(BatchStoreSystemType self) internal view returns (address) { + return Systems.getSystem(self.toResourceId()); + } + + function _world() private view returns (IWorldCall) { + return IWorldCall(StoreSwitch.getStoreAddress()); + } +} + +/** + * System Function Interfaces + * + * We generate an interface for each system function, which is then used for encoding system calls. + * This is necessary to handle function overloading correctly (which abi.encodeCall cannot). + * + * Each interface is uniquely named based on the function name and parameters to prevent collisions. + */ + +interface _getTableRecords_ResourceId_bytes32ArrayArray { + function getTableRecords(ResourceId tableId, bytes32[][] memory keyTuples) external; +} + +interface _setTableRecords_ResourceId_TableRecordArray { + function setTableRecords(ResourceId tableId, TableRecord[] memory records) external; +} + +interface _deleteTableRecords_ResourceId_bytes32ArrayArray { + function deleteTableRecords(ResourceId tableId, bytes32[][] memory keyTuples) external; +} + +using BatchStoreSystemLib for BatchStoreSystemType global; +using BatchStoreSystemLib for CallWrapper global; +using BatchStoreSystemLib for RootCallWrapper global; diff --git a/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol b/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol new file mode 100644 index 0000000000..630bf59dfc --- /dev/null +++ b/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; +import { TableRecord } from "../../common.sol"; + +/** + * @title IBatchStoreSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface IBatchStoreSystem { + function getTableRecords( + ResourceId tableId, + bytes32[][] memory keyTuples + ) external view returns (TableRecord[] memory records); + + function setTableRecords(ResourceId tableId, TableRecord[] memory records) external; + + function deleteTableRecords(ResourceId tableId, bytes32[][] memory keyTuples) external; +} diff --git a/packages/world-module-batchstore/src/codegen/world/IWorld.sol b/packages/world-module-batchstore/src/codegen/world/IWorld.sol new file mode 100644 index 0000000000..eaf440439f --- /dev/null +++ b/packages/world-module-batchstore/src/codegen/world/IWorld.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; +import { IBatchStoreSystem } from "./IBatchStoreSystem.sol"; + +/** + * @title IWorld + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @notice This interface integrates all systems and associated function selectors + * that are dynamically registered in the World during deployment. + * @dev This is an autogenerated file; do not edit manually. + */ +interface IWorld is IBaseWorld, IBatchStoreSystem {} diff --git a/packages/world-module-batchstore/src/common.sol b/packages/world-module-batchstore/src/common.sol new file mode 100644 index 0000000000..b70d32f37c --- /dev/null +++ b/packages/world-module-batchstore/src/common.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol"; + +struct TableRecord { + bytes32[] keyTuple; + bytes staticData; + EncodedLengths encodedLengths; + bytes dynamicData; +} diff --git a/packages/world-module-batchstore/test/BatchStoreModule.t.sol b/packages/world-module-batchstore/test/BatchStoreModule.t.sol new file mode 100644 index 0000000000..6e0f58ff5f --- /dev/null +++ b/packages/world-module-batchstore/test/BatchStoreModule.t.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { Test } from "forge-std/Test.sol"; +import { GasReporter } from "@latticexyz/gas-report/src/GasReporter.sol"; + +import { StoreSwitch } from "@latticexyz/store/src/StoreSwitch.sol"; + +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { createWorld } from "@latticexyz/world/test/createWorld.sol"; +import { WorldTestSystem } from "@latticexyz/world/test/World.t.sol"; +import { IWorld } from "../src/codegen/world/IWorld.sol"; +import { BatchStoreModule } from "../src/BatchStoreModule.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; +import { batchStoreSystem, BatchStoreSystemType } from "../src/codegen/experimental/systems/BatchStoreSystemLib.sol"; +import { TableRecord } from "../src/common.sol"; +import { EncodedLengths } from "@latticexyz/store/src/EncodedLengths.sol"; + +contract BatchStoreModuleTest is Test, GasReporter { + using WorldResourceIdInstance for ResourceId; + + IWorld world; + BatchStoreModule batchStoreModule = new BatchStoreModule(); + + function setUp() public { + world = IWorld(address(createWorld())); + StoreSwitch.setStoreAddress(address(world)); + } + + function testInstall() public { + startGasReport("install batch store module"); + world.installRootModule(batchStoreModule, new bytes(0)); + endGasReport(); + } + + function testGetTableRecords() public { + world.installRootModule(batchStoreModule, new bytes(0)); + + bytes32[][] memory keys = new bytes32[][](1); + keys[0] = NamespaceOwner.encodeKeyTuple(WorldResourceIdLib.encodeNamespace("")); + + startGasReport("get table records"); + world.getTableRecords(NamespaceOwner._tableId, keys); + endGasReport(); + + TableRecord[] memory records = world.getTableRecords(NamespaceOwner._tableId, keys); + assertEq(records.length, 1); + assertEq(records[0].keyTuple, keys[0]); + + (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData) = NamespaceOwner.encode( + address(this) + ); + assertEq(records[0].staticData, staticData); + assertEq(records[0].encodedLengths.unwrap(), encodedLengths.unwrap()); + assertEq(records[0].dynamicData, dynamicData); + } + + function testSetTableRecords() public { + world.installRootModule(batchStoreModule, new bytes(0)); + + ResourceId namespace = WorldResourceIdLib.encodeNamespace("example"); + assertEq(NamespaceOwner.get(namespace), address(0)); + + TableRecord[] memory records = new TableRecord[](1); + (bytes memory staticData, EncodedLengths encodedLengths, bytes memory dynamicData) = NamespaceOwner.encode( + address(this) + ); + records[0] = TableRecord({ + keyTuple: NamespaceOwner.encodeKeyTuple(WorldResourceIdLib.encodeNamespace("example")), + staticData: staticData, + encodedLengths: encodedLengths, + dynamicData: dynamicData + }); + + startGasReport("set table records"); + world.setTableRecords(NamespaceOwner._tableId, records); + endGasReport(); + + assertEq(NamespaceOwner.get(namespace), address(this)); + } + + function testDeleteTableRecords() public { + world.installRootModule(batchStoreModule, new bytes(0)); + + ResourceId namespace = WorldResourceIdLib.encodeNamespace("example"); + NamespaceOwner.set(namespace, address(this)); + + assertEq(NamespaceOwner.get(namespace), address(this)); + + bytes32[][] memory keys = new bytes32[][](1); + keys[0] = NamespaceOwner.encodeKeyTuple(namespace); + + startGasReport("delete table records"); + world.deleteTableRecords(NamespaceOwner._tableId, keys); + endGasReport(); + + assertEq(NamespaceOwner.get(namespace), address(0)); + } +} diff --git a/packages/world-module-batchstore/ts/build.ts b/packages/world-module-batchstore/ts/build.ts new file mode 100644 index 0000000000..267e5bfdb3 --- /dev/null +++ b/packages/world-module-batchstore/ts/build.ts @@ -0,0 +1,20 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { tablegen } from "@latticexyz/store/codegen"; +import { worldgen } from "@latticexyz/world/node"; + +/** + * To avoid circular dependencies, we run a very similar `build` step as `cli` package here. + */ + +// TODO: move tablegen/worldgen to CLI commands from store/world we can run in package.json instead of a custom script +// (https://github.com/latticexyz/mud/issues/3030) + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const configPath = "../mud.config"; + +const { default: config } = await import(configPath); +const rootDir = path.dirname(path.join(__dirname, configPath)); + +await tablegen({ rootDir, config }); +await worldgen({ rootDir, config }); diff --git a/packages/world-module-batchstore/tsconfig.json b/packages/world-module-batchstore/tsconfig.json new file mode 100644 index 0000000000..9b0bf57752 --- /dev/null +++ b/packages/world-module-batchstore/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["mud.config.ts", "ts"] +} diff --git a/packages/world-module-batchstore/tsup.config.ts b/packages/world-module-batchstore/tsup.config.ts new file mode 100644 index 0000000000..6b360b4e59 --- /dev/null +++ b/packages/world-module-batchstore/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "tsup"; +import { baseConfig } from "../../tsup.config.base"; + +export default defineConfig((opts) => ({ + ...baseConfig(opts), + entry: { + "mud.config": "mud.config.ts", + }, +})); diff --git a/packages/world/ts/node/render-solidity/renderSystemLibrary.ts b/packages/world/ts/node/render-solidity/renderSystemLibrary.ts index a487662329..3449e4b087 100644 --- a/packages/world/ts/node/render-solidity/renderSystemLibrary.ts +++ b/packages/world/ts/node/render-solidity/renderSystemLibrary.ts @@ -298,8 +298,8 @@ function functionInterfaceName(contractFunction: ContractInterfaceFunction) { const { name, parameters } = contractFunction; const paramTypes = parameters .map((param) => param.split(" ")[0]) - .map((type) => type.replace(".", "_")) - .map((type) => type.replace("[]", "Array")) + .map((type) => type.replace(/\./g, "_")) + .map((type) => type.replace(/\[\]/g, "Array")) // Static arrays may contain multiple disallowed symbols, for name uniqueness toHex is easier than escaping .map((type) => type.replace(/\[.+\]/, (match) => stringToHex(match))) .join("_"); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9825e5cfe3..c8438028f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1469,6 +1469,31 @@ importers: specifier: ^3.3.7 version: 3.3.7 + packages/world-module-batchstore: + dependencies: + '@latticexyz/schema-type': + specifier: workspace:* + version: link:../schema-type + '@latticexyz/store': + specifier: workspace:* + version: link:../store + '@latticexyz/world': + specifier: workspace:* + version: link:../world + devDependencies: + '@latticexyz/abi-ts': + specifier: workspace:* + version: link:../abi-ts + '@latticexyz/gas-report': + specifier: workspace:* + version: link:../gas-report + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#60acb7aaadcce2d68e52986a0a66fe79f07d138f + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/60acb7aaadcce2d68e52986a0a66fe79f07d138f + solhint: + specifier: ^3.3.7 + version: 3.3.7 + packages/world-module-callwithsignature: dependencies: '@latticexyz/schema-type': @@ -19826,9 +19851,9 @@ snapshots: deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.1 - call-bind: 1.0.7 + call-bind: 1.0.8 es-get-iterator: 1.1.3 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 is-arguments: 1.1.1 is-array-buffer: 3.0.4 is-date-object: 1.0.5 @@ -19842,7 +19867,7 @@ snapshots: side-channel: 1.0.6 which-boxed-primitive: 1.0.2 which-collection: 1.0.2 - which-typed-array: 1.1.15 + which-typed-array: 1.1.19 deep-extend@0.6.0: {} @@ -20138,8 +20163,8 @@ snapshots: es-get-iterator@1.1.3: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 + call-bind: 1.0.8 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 is-arguments: 1.1.1 is-map: 2.0.3 @@ -21389,7 +21414,7 @@ snapshots: is-arguments@1.1.1: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 has-tostringtag: 1.0.2 is-array-buffer@3.0.4: @@ -21413,7 +21438,7 @@ snapshots: is-boolean-object@1.1.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 has-tostringtag: 1.0.2 is-callable@1.2.7: {} @@ -21428,7 +21453,7 @@ snapshots: is-data-view@1.0.1: dependencies: - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 is-date-object@1.0.5: dependencies: @@ -21442,7 +21467,7 @@ snapshots: is-finalizationregistry@1.0.2: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 is-fullwidth-code-point@2.0.0: {} @@ -21533,8 +21558,8 @@ snapshots: is-weakset@2.0.3: dependencies: - call-bind: 1.0.7 - get-intrinsic: 1.2.4 + call-bind: 1.0.8 + get-intrinsic: 1.3.0 is-what@4.1.15: {} @@ -21600,7 +21625,7 @@ snapshots: iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 has-symbols: 1.0.3 reflect.getprototypeof: 1.0.6 set-function-name: 2.0.2 @@ -22774,7 +22799,7 @@ snapshots: object-is@1.1.6: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 object-keys@1.1.1: {} @@ -23815,11 +23840,11 @@ snapshots: reflect.getprototypeof@1.0.6: dependencies: - call-bind: 1.0.7 + call-bind: 1.0.8 define-properties: 1.2.1 es-abstract: 1.23.3 es-errors: 1.3.0 - get-intrinsic: 1.2.4 + get-intrinsic: 1.3.0 globalthis: 1.0.4 which-builtin-type: 1.1.4 @@ -24946,7 +24971,7 @@ snapshots: dependencies: call-bind: 1.0.7 es-errors: 1.3.0 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 typed-array-buffer@1.0.3: dependencies: @@ -24960,7 +24985,7 @@ snapshots: for-each: 0.3.3 gopd: 1.2.0 has-proto: 1.0.3 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 typed-array-byte-offset@1.0.2: dependencies: @@ -24969,7 +24994,7 @@ snapshots: for-each: 0.3.3 gopd: 1.2.0 has-proto: 1.0.3 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 typed-array-length@1.0.6: dependencies: @@ -24977,7 +25002,7 @@ snapshots: for-each: 0.3.3 gopd: 1.2.0 has-proto: 1.0.3 - is-typed-array: 1.1.13 + is-typed-array: 1.1.15 possible-typed-array-names: 1.0.0 typescript@5.4.2: {} @@ -25562,7 +25587,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.0.2 which-collection: 1.0.2 - which-typed-array: 1.1.15 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: From 5de9cce14d599fdea29a0e70595507397e9cf5b5 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 9 Sep 2025 12:09:29 +0900 Subject: [PATCH 2/3] install as a default module --- packages/cli/package.json | 1 + packages/cli/src/deploy/configToModules.ts | 49 ++++++++++++++-------- pnpm-lock.yaml | 3 ++ 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index ef90f88616..db5b219f45 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -52,6 +52,7 @@ "@latticexyz/store-sync": "workspace:*", "@latticexyz/utils": "workspace:*", "@latticexyz/world": "workspace:*", + "@latticexyz/world-module-batchstore": "workspace:*", "@latticexyz/world-module-callwithsignature": "workspace:*", "@latticexyz/world-module-metadata": "workspace:*", "abitype": "1.0.9", diff --git a/packages/cli/src/deploy/configToModules.ts b/packages/cli/src/deploy/configToModules.ts index f9cc341503..d2bd2407e0 100644 --- a/packages/cli/src/deploy/configToModules.ts +++ b/packages/cli/src/deploy/configToModules.ts @@ -8,35 +8,48 @@ import { World } from "@latticexyz/world"; import { importContractArtifact } from "../utils/importContractArtifact"; import { resolveWithContext } from "@latticexyz/world/internal"; import callWithSignatureModule from "@latticexyz/world-module-callwithsignature/out/CallWithSignatureModule.sol/CallWithSignatureModule.json" with { type: "json" }; +import batchStoreModule from "@latticexyz/world-module-batchstore/out/BatchStoreModule.sol/BatchStoreModule.json" with { type: "json" }; import { getContractArtifact } from "../utils/getContractArtifact"; import { excludeCallWithSignatureModule } from "./compat/excludeUnstableCallWithSignatureModule"; import { moduleArtifactPathFromName } from "./compat/moduleArtifactPathFromName"; const callWithSignatureModuleArtifact = getContractArtifact(callWithSignatureModule); +const batchStoreModuleArtifact = getContractArtifact(batchStoreModule); + +// metadata module is installed inside `ensureResourceTags` +const defaultModules: Module[] = [ + { + // optional for now + // TODO: figure out approach to install on existing worlds where deployer may not own root namespace + optional: true, + name: "CallWithSignatureModule", + installStrategy: "root", + installData: "0x", + prepareDeploy: createPrepareDeploy( + callWithSignatureModuleArtifact.bytecode, + callWithSignatureModuleArtifact.placeholders, + ), + deployedBytecodeSize: callWithSignatureModuleArtifact.deployedBytecodeSize, + abi: callWithSignatureModuleArtifact.abi, + }, + { + // optional for now + // TODO: figure out approach to install on existing worlds where deployer may not own root namespace + optional: true, + name: "BatchStoreModule", + installStrategy: "root", + installData: "0x", + prepareDeploy: createPrepareDeploy(batchStoreModuleArtifact.bytecode, batchStoreModuleArtifact.placeholders), + deployedBytecodeSize: batchStoreModuleArtifact.deployedBytecodeSize, + abi: batchStoreModuleArtifact.abi, + }, +]; export async function configToModules( config: config, // TODO: remove/replace `forgeOutDir` forgeOutDir: string, ): Promise { - // metadata module is installed inside `ensureResourceTags` - const defaultModules: Module[] = [ - { - // optional for now - // TODO: figure out approach to install on existing worlds where deployer may not own root namespace - optional: true, - name: "CallWithSignatureModule", - installStrategy: "root", - installData: "0x", - prepareDeploy: createPrepareDeploy( - callWithSignatureModuleArtifact.bytecode, - callWithSignatureModuleArtifact.placeholders, - ), - deployedBytecodeSize: callWithSignatureModuleArtifact.deployedBytecodeSize, - abi: callWithSignatureModuleArtifact.abi, - }, - ]; - const modules = await Promise.all( config.modules .filter(excludeCallWithSignatureModule) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8438028f7..13d9c02637 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -233,6 +233,9 @@ importers: '@latticexyz/world': specifier: workspace:* version: link:../world + '@latticexyz/world-module-batchstore': + specifier: workspace:* + version: link:../world-module-batchstore '@latticexyz/world-module-callwithsignature': specifier: workspace:* version: link:../world-module-callwithsignature From bdad072e8e95e91ebb5a00faf7d6cb929f884bbc Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Tue, 9 Sep 2025 12:34:17 +0900 Subject: [PATCH 3/3] remove world functions --- .../world-module-batchstore/mud.config.ts | 8 +++++++ .../src/BatchStoreModule.sol | 22 ++---------------- .../src/codegen/world/IBatchStoreSystem.sol | 23 ------------------- .../src/codegen/world/IWorld.sol | 3 +-- .../test/BatchStoreModule.t.sol | 8 +++---- 5 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol diff --git a/packages/world-module-batchstore/mud.config.ts b/packages/world-module-batchstore/mud.config.ts index b51def30e9..a5c8c7e763 100644 --- a/packages/world-module-batchstore/mud.config.ts +++ b/packages/world-module-batchstore/mud.config.ts @@ -6,4 +6,12 @@ export default defineWorld({ // generate into experimental dir until these are stable/audited systemLibrariesDirectory: "experimental/systems", }, + systems: { + BatchStoreSystem: { + deploy: { + disabled: true, + registerWorldFunctions: false, + }, + }, + }, }); diff --git a/packages/world-module-batchstore/src/BatchStoreModule.sol b/packages/world-module-batchstore/src/BatchStoreModule.sol index f8875fed32..fcb0a8ce94 100644 --- a/packages/world-module-batchstore/src/BatchStoreModule.sol +++ b/packages/world-module-batchstore/src/BatchStoreModule.sol @@ -17,26 +17,8 @@ contract BatchStoreModule is Module { function installRoot(bytes memory encodedArgs) public override { ResourceId systemId = batchStoreSystem.toResourceId(); - if (!ResourceIds.getExists(systemId)) { - worldRegistrationSystem.callAsRoot().registerSystem(systemId, systemAddress, true); - // TODO: since this is a scary/internal and use-with-caution module/system, should we not register function selectors? - worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( - systemId, - "getTableRecords(bytes32,bytes32[][])", - "getTableRecords(bytes32,bytes32[][])" - ); - worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( - systemId, - "setTableRecords(bytes32,(bytes32[],bytes,bytes32,bytes)[])", - "setTableRecords(bytes32,(bytes32[],bytes,bytes32,bytes)[])" - ); - worldRegistrationSystem.callAsRoot().registerRootFunctionSelector( - systemId, - "deleteTableRecords(bytes32,bytes32[][])", - "deleteTableRecords(bytes32,bytes32[][])" - ); - } else if (batchStoreSystem.getAddress() != address(systemAddress)) { - // upgrade system + if (batchStoreSystem.getAddress() != address(systemAddress)) { + // install or upgrade system worldRegistrationSystem.callAsRoot().registerSystem(systemId, systemAddress, true); } } diff --git a/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol b/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol deleted file mode 100644 index 630bf59dfc..0000000000 --- a/packages/world-module-batchstore/src/codegen/world/IBatchStoreSystem.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.24; - -/* Autogenerated file. Do not edit manually. */ - -import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; -import { TableRecord } from "../../common.sol"; - -/** - * @title IBatchStoreSystem - * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) - * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. - */ -interface IBatchStoreSystem { - function getTableRecords( - ResourceId tableId, - bytes32[][] memory keyTuples - ) external view returns (TableRecord[] memory records); - - function setTableRecords(ResourceId tableId, TableRecord[] memory records) external; - - function deleteTableRecords(ResourceId tableId, bytes32[][] memory keyTuples) external; -} diff --git a/packages/world-module-batchstore/src/codegen/world/IWorld.sol b/packages/world-module-batchstore/src/codegen/world/IWorld.sol index eaf440439f..4761e84790 100644 --- a/packages/world-module-batchstore/src/codegen/world/IWorld.sol +++ b/packages/world-module-batchstore/src/codegen/world/IWorld.sol @@ -4,7 +4,6 @@ pragma solidity >=0.8.24; /* Autogenerated file. Do not edit manually. */ import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; -import { IBatchStoreSystem } from "./IBatchStoreSystem.sol"; /** * @title IWorld @@ -13,4 +12,4 @@ import { IBatchStoreSystem } from "./IBatchStoreSystem.sol"; * that are dynamically registered in the World during deployment. * @dev This is an autogenerated file; do not edit manually. */ -interface IWorld is IBaseWorld, IBatchStoreSystem {} +interface IWorld is IBaseWorld {} diff --git a/packages/world-module-batchstore/test/BatchStoreModule.t.sol b/packages/world-module-batchstore/test/BatchStoreModule.t.sol index 6e0f58ff5f..dadf0debc0 100644 --- a/packages/world-module-batchstore/test/BatchStoreModule.t.sol +++ b/packages/world-module-batchstore/test/BatchStoreModule.t.sol @@ -40,10 +40,10 @@ contract BatchStoreModuleTest is Test, GasReporter { keys[0] = NamespaceOwner.encodeKeyTuple(WorldResourceIdLib.encodeNamespace("")); startGasReport("get table records"); - world.getTableRecords(NamespaceOwner._tableId, keys); + batchStoreSystem.getTableRecords(NamespaceOwner._tableId, keys); endGasReport(); - TableRecord[] memory records = world.getTableRecords(NamespaceOwner._tableId, keys); + TableRecord[] memory records = batchStoreSystem.getTableRecords(NamespaceOwner._tableId, keys); assertEq(records.length, 1); assertEq(records[0].keyTuple, keys[0]); @@ -73,7 +73,7 @@ contract BatchStoreModuleTest is Test, GasReporter { }); startGasReport("set table records"); - world.setTableRecords(NamespaceOwner._tableId, records); + batchStoreSystem.setTableRecords(NamespaceOwner._tableId, records); endGasReport(); assertEq(NamespaceOwner.get(namespace), address(this)); @@ -91,7 +91,7 @@ contract BatchStoreModuleTest is Test, GasReporter { keys[0] = NamespaceOwner.encodeKeyTuple(namespace); startGasReport("delete table records"); - world.deleteTableRecords(NamespaceOwner._tableId, keys); + batchStoreSystem.deleteTableRecords(NamespaceOwner._tableId, keys); endGasReport(); assertEq(NamespaceOwner.get(namespace), address(0));