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/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..a5c8c7e763 --- /dev/null +++ b/packages/world-module-batchstore/mud.config.ts @@ -0,0 +1,17 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + codegen: { + generateSystemLibraries: true, + // 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/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..fcb0a8ce94 --- /dev/null +++ b/packages/world-module-batchstore/src/BatchStoreModule.sol @@ -0,0 +1,25 @@ +// 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 (batchStoreSystem.getAddress() != address(systemAddress)) { + // install or 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/IWorld.sol b/packages/world-module-batchstore/src/codegen/world/IWorld.sol new file mode 100644 index 0000000000..4761e84790 --- /dev/null +++ b/packages/world-module-batchstore/src/codegen/world/IWorld.sol @@ -0,0 +1,15 @@ +// 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"; + +/** + * @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 {} 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..dadf0debc0 --- /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"); + batchStoreSystem.getTableRecords(NamespaceOwner._tableId, keys); + endGasReport(); + + TableRecord[] memory records = batchStoreSystem.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"); + batchStoreSystem.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"); + batchStoreSystem.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..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 @@ -1469,6 +1472,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 +19854,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 +19870,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 +20166,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 +21417,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 +21441,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 +21456,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 +21470,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 +21561,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 +21628,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 +22802,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 +23843,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 +24974,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 +24988,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 +24997,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 +25005,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 +25590,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: