diff --git a/.changeset/spotty-camels-occur.md b/.changeset/spotty-camels-occur.md new file mode 100644 index 0000000000..2344118f72 --- /dev/null +++ b/.changeset/spotty-camels-occur.md @@ -0,0 +1,6 @@ +--- +"@latticexyz/cli": patch +"@latticexyz/world-module-metadata": patch +--- + +Added metadata module to be automatically installed during world deploy. This module allows for tagging any resource with arbitrary metadata. Internally, we'll use this to tag resources with labels onchain so that we can use labels to create a MUD project from an existing world. diff --git a/packages/cli/package.json b/packages/cli/package.json index 5c37abeb09..7884fe6aa4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,7 @@ "@latticexyz/store": "workspace:*", "@latticexyz/utils": "workspace:*", "@latticexyz/world": "workspace:*", + "@latticexyz/world-module-metadata": "workspace:*", "abitype": "1.0.0", "asn1.js": "^5.4.1", "chalk": "^5.0.1", diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 8116043f2b..93cf8797a1 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -100,4 +100,9 @@ export type Module = DeterministicContract & { readonly name: string; readonly installAsRoot: boolean; readonly installData: Hex; // TODO: figure out better naming for this + /** + * @internal + * Optional modules warn instead of throw if they revert while being installed. + */ + readonly optional?: boolean; }; diff --git a/packages/cli/src/deploy/configToModules.ts b/packages/cli/src/deploy/configToModules.ts index 649de451e0..9eeeaa101b 100644 --- a/packages/cli/src/deploy/configToModules.ts +++ b/packages/cli/src/deploy/configToModules.ts @@ -6,14 +6,38 @@ import { bytesToHex } from "viem"; import { createPrepareDeploy } from "./createPrepareDeploy"; import { World } from "@latticexyz/world"; import { getContractArtifact } from "../utils/getContractArtifact"; -import { knownModuleArtifacts } from "../utils/knownModuleArtifacts"; +import { importContractArtifact } from "../utils/importContractArtifact"; import { resolveWithContext } from "@latticexyz/world/internal"; +import metadataModule from "@latticexyz/world-module-metadata/out/MetadataModule.sol/MetadataModule.json" assert { type: "json" }; + +/** Please don't add to this list! These are kept for backwards compatibility and assumes the downstream project has this module installed as a dependency. */ +const knownModuleArtifacts = { + KeysWithValueModule: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json", + KeysInTableModule: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json", + UniqueEntityModule: "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json", + Unstable_CallWithSignatureModule: + "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", +}; + +const metadataModuleArtifact = getContractArtifact(metadataModule); export async function configToModules( config: config, // TODO: remove/replace `forgeOutDir` forgeOutDir: string, ): Promise { + const defaultModules: Module[] = [ + { + optional: true, + name: "MetadataModule", + installAsRoot: false, + installData: "0x", + prepareDeploy: createPrepareDeploy(metadataModuleArtifact.bytecode, metadataModuleArtifact.placeholders), + deployedBytecodeSize: metadataModuleArtifact.deployedBytecodeSize, + abi: metadataModuleArtifact.abi, + }, + ]; + const modules = await Promise.all( config.modules.map(async (mod): Promise => { let artifactPath = mod.artifactPath; @@ -46,7 +70,7 @@ export async function configToModules( } const name = path.basename(artifactPath, ".json"); - const artifact = await getContractArtifact({ artifactPath }); + const artifact = await importContractArtifact({ artifactPath }); // TODO: replace args with something more strongly typed const installArgs = mod.args @@ -71,5 +95,5 @@ export async function configToModules( }), ); - return modules; + return [...defaultModules, ...modules]; } diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 2fa5ef310a..45df2ad83d 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -59,6 +59,12 @@ export async function ensureModules({ args: [moduleAddress, mod.installData], }); } catch (error) { + if (mod.optional) { + debug( + `optional module ${mod.name} install failed, skipping\n ${error instanceof BaseError ? error.shortMessage : error}`, + ); + return; + } if (error instanceof BaseError && error.message.includes("Module_AlreadyInstalled")) { debug(`module ${mod.name} already installed`); return; diff --git a/packages/cli/src/utils/getContractArtifact.ts b/packages/cli/src/utils/getContractArtifact.ts index 63d4abbd28..9b274b2c3a 100644 --- a/packages/cli/src/utils/getContractArtifact.ts +++ b/packages/cli/src/utils/getContractArtifact.ts @@ -3,24 +3,6 @@ import { LibraryPlaceholder } from "../deploy/common"; import { findPlaceholders } from "./findPlaceholders"; import { z } from "zod"; import { Abi as abiSchema } from "abitype/zod"; -import { createRequire } from "node:module"; -import { findUp } from "find-up"; - -export type GetContractArtifactOptions = { - /** - * Path to `package.json` where `artifactPath`s are resolved relative to. - * - * Defaults to nearest `package.json` relative to `process.cwd()`. - */ - packageJsonPath?: string; - /** - * Import path to contract's forge/solc JSON artifact with the contract's compiled bytecode. - * - * This path is resolved using node's module resolution relative to `configPath`, so this supports both - * relative file paths (`../path/to/MyModule.json`) as well as JS import paths (`@latticexyz/world-contracts/out/CallWithSignatureModule.sol/CallWithSignatureModule.json`). - */ - artifactPath: string; -}; export type GetContractArtifactResult = { bytecode: Hex; @@ -49,26 +31,9 @@ const artifactSchema = z.object({ abi: abiSchema, }); -export async function getContractArtifact({ - packageJsonPath, - artifactPath, -}: GetContractArtifactOptions): Promise { - let importedArtifact; - try { - const requirePath = packageJsonPath ?? (await findUp("package.json", { cwd: process.cwd() })); - if (!requirePath) throw new Error("Could not find package.json to import relative to."); - - const require = createRequire(requirePath); - importedArtifact = require(artifactPath); - } catch (error) { - console.error(); - console.error("Could not import contract artifact at", artifactPath); - console.error(); - throw error; - } - +export function getContractArtifact(artifactJson: unknown): GetContractArtifactResult { // TODO: improve errors or replace with arktype? - const artifact = artifactSchema.parse(importedArtifact); + const artifact = artifactSchema.parse(artifactJson); const placeholders = findPlaceholders(artifact.bytecode.linkReferences); return { diff --git a/packages/cli/src/utils/importContractArtifact.ts b/packages/cli/src/utils/importContractArtifact.ts new file mode 100644 index 0000000000..50c804f750 --- /dev/null +++ b/packages/cli/src/utils/importContractArtifact.ts @@ -0,0 +1,40 @@ +import { createRequire } from "node:module"; +import { findUp } from "find-up"; +import { GetContractArtifactResult, getContractArtifact } from "./getContractArtifact"; + +export type ImportContractArtifactOptions = { + /** + * Path to `package.json` where `artifactPath`s are resolved relative to. + * + * Defaults to nearest `package.json` relative to `process.cwd()`. + */ + packageJsonPath?: string; + /** + * Import path to contract's forge/solc JSON artifact with the contract's compiled bytecode. + * + * This path is resolved using node's module resolution relative to `configPath`, so this supports both + * relative file paths (`../path/to/MyModule.json`) as well as JS import paths (`@latticexyz/world-contracts/out/CallWithSignatureModule.sol/CallWithSignatureModule.json`). + */ + artifactPath: string; +}; + +export async function importContractArtifact({ + packageJsonPath, + artifactPath, +}: ImportContractArtifactOptions): Promise { + let artfactJson; + try { + const requirePath = packageJsonPath ?? (await findUp("package.json", { cwd: process.cwd() })); + if (!requirePath) throw new Error("Could not find package.json to import relative to."); + + const require = createRequire(requirePath); + artfactJson = require(artifactPath); + } catch (error) { + console.error(); + console.error("Could not import contract artifact at", artifactPath); + console.error(); + throw error; + } + + return getContractArtifact(artfactJson); +} diff --git a/packages/cli/src/utils/knownModuleArtifacts.ts b/packages/cli/src/utils/knownModuleArtifacts.ts deleted file mode 100644 index a71c4fa2e4..0000000000 --- a/packages/cli/src/utils/knownModuleArtifacts.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** @deprecated Please don't add to this list! These are kept for backwards compatibility and assumes the downstream project has this module installed as a dependency. */ -export const knownModuleArtifacts = { - KeysWithValueModule: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json", - KeysInTableModule: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json", - UniqueEntityModule: "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json", - Unstable_CallWithSignatureModule: - "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", -}; diff --git a/packages/world-module-metadata/.gitignore b/packages/world-module-metadata/.gitignore new file mode 100644 index 0000000000..1e4ded714a --- /dev/null +++ b/packages/world-module-metadata/.gitignore @@ -0,0 +1,2 @@ +cache +out diff --git a/packages/world-module-metadata/.solhint.json b/packages/world-module-metadata/.solhint.json new file mode 100644 index 0000000000..4e2baa8be7 --- /dev/null +++ b/packages/world-module-metadata/.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-metadata/README.md b/packages/world-module-metadata/README.md new file mode 100644 index 0000000000..bb63585de0 --- /dev/null +++ b/packages/world-module-metadata/README.md @@ -0,0 +1 @@ +# Metadata world module diff --git a/packages/world-module-metadata/foundry.toml b/packages/world-module-metadata/foundry.toml new file mode 100644 index 0000000000..f0e017f5a0 --- /dev/null +++ b/packages/world-module-metadata/foundry.toml @@ -0,0 +1,15 @@ +[profile.default] +solc = "0.8.24" +ffi = false +fuzz_runs = 256 +optimizer = true +optimizer_runs = 3000 +verbosity = 2 +allow_paths = ["../../node_modules", "../"] +src = "src" +out = "out" +bytecode_hash = "none" +extra_output_files = [ + "abi", + "evm.bytecode" +] diff --git a/packages/world-module-metadata/gas-report.json b/packages/world-module-metadata/gas-report.json new file mode 100644 index 0000000000..1adfb98428 --- /dev/null +++ b/packages/world-module-metadata/gas-report.json @@ -0,0 +1,20 @@ +[ + { + "file": "test/MetadataModule.t.sol", + "test": "testDeleteResourceTag", + "name": "delete resource tag", + "gasUsed": 70301 + }, + { + "file": "test/MetadataModule.t.sol", + "test": "testInstall", + "name": "install metadata module", + "gasUsed": 1106562 + }, + { + "file": "test/MetadataModule.t.sol", + "test": "testSetResourceTag", + "name": "set resource tag", + "gasUsed": 116708 + } +] diff --git a/packages/world-module-metadata/mud.config.ts b/packages/world-module-metadata/mud.config.ts new file mode 100644 index 0000000000..0982853aac --- /dev/null +++ b/packages/world-module-metadata/mud.config.ts @@ -0,0 +1,18 @@ +import { defineWorld } from "@latticexyz/world"; + +export default defineWorld({ + namespace: "metadata", + userTypes: { + ResourceId: { filePath: "@latticexyz/store/src/ResourceId.sol", type: "bytes32" }, + }, + tables: { + ResourceTag: { + schema: { + resource: "ResourceId", + tag: "bytes32", + value: "bytes", + }, + key: ["resource", "tag"], + }, + }, +}); diff --git a/packages/world-module-metadata/package.json b/packages/world-module-metadata/package.json new file mode 100644 index 0000000000..cd01bfcee4 --- /dev/null +++ b/packages/world-module-metadata/package.json @@ -0,0 +1,63 @@ +{ + "name": "@latticexyz/world-module-metadata", + "version": "2.1.0", + "description": "Metadata world module", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/world-module-metadata" + }, + "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": "rimraf dist", + "clean:mud": "rimraf src/**/codegen", + "dev": "tsup --watch", + "gas-report": "gas-report --save gas-report.json", + "lint": "solhint --config ./.solhint.json 'src/**/*.sol'", + "test": "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:*", + "@types/node": "^18.15.11", + "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", + "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1", + "solhint": "^3.3.7", + "tsup": "^6.7.0", + "tsx": "^3.12.6", + "vitest": "0.34.6" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/world-module-metadata/remappings.txt b/packages/world-module-metadata/remappings.txt new file mode 100644 index 0000000000..66be45ecbd --- /dev/null +++ b/packages/world-module-metadata/remappings.txt @@ -0,0 +1,3 @@ +ds-test/=node_modules/ds-test/src/ +forge-std/=node_modules/forge-std/src/ +@latticexyz/=node_modules/@latticexyz/ \ No newline at end of file diff --git a/packages/world-module-metadata/src/MetadataModule.sol b/packages/world-module-metadata/src/MetadataModule.sol new file mode 100644 index 0000000000..67cae69e85 --- /dev/null +++ b/packages/world-module-metadata/src/MetadataModule.sol @@ -0,0 +1,57 @@ +// 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 { requireOwner } from "./common.sol"; +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; +import { RESOURCE_SYSTEM } from "@latticexyz/world/src/worldResourceTypes.sol"; + +import { MetadataSystem } from "./MetadataSystem.sol"; +import { ResourceTag } from "./codegen/tables/ResourceTag.sol"; + +/** + * @title MetadataModule + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Adds metadata tables and systems for annotating data in MUD apps. + * For example, tagging resources with labels for better UX when reconstructing a MUD project from a world using onchain state. + */ +contract MetadataModule is Module { + using WorldResourceIdInstance for ResourceId; + + MetadataSystem private immutable metadataSystem = new MetadataSystem(); + + function installRoot(bytes memory) public pure { + revert Module_RootInstallNotSupported(); + } + + function install(bytes memory) public { + IBaseWorld world = IBaseWorld(_world()); + + ResourceId namespace = ResourceTag._tableId.getNamespaceId(); + if (!ResourceIds.getExists(namespace)) { + world.registerNamespace(namespace); + } + requireOwner(namespace, address(this)); + + if (!ResourceIds.getExists(ResourceTag._tableId)) { + ResourceTag.register(); + } + + ResourceId metadataSystemId = WorldResourceIdLib.encode( + RESOURCE_SYSTEM, + namespace.getNamespace(), + "MetadataSystem" + ); + // TODO: add support for upgrading system and registering new function selectors + if (!ResourceIds.getExists(metadataSystemId)) { + world.registerSystem(metadataSystemId, metadataSystem, true); + world.registerFunctionSelector(metadataSystemId, "getResourceTag(bytes32,bytes32)"); + world.registerFunctionSelector(metadataSystemId, "setResourceTag(bytes32,bytes32,bytes)"); + world.registerFunctionSelector(metadataSystemId, "deleteResourceTag(bytes32,bytes32)"); + } + + world.transferOwnership(namespace, _msgSender()); + } +} diff --git a/packages/world-module-metadata/src/MetadataSystem.sol b/packages/world-module-metadata/src/MetadataSystem.sol new file mode 100644 index 0000000000..090fec2d54 --- /dev/null +++ b/packages/world-module-metadata/src/MetadataSystem.sol @@ -0,0 +1,25 @@ +// 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 { requireExistence, requireOwner } from "./common.sol"; +import { ResourceTag } from "./codegen/tables/ResourceTag.sol"; + +contract MetadataSystem is System { + function getResourceTag(ResourceId resource, bytes32 tag) public view returns (bytes memory) { + return ResourceTag.get(resource, tag); + } + + function setResourceTag(ResourceId resource, bytes32 tag, bytes memory value) public { + requireExistence(resource); + requireOwner(resource, _msgSender()); + ResourceTag.set(resource, tag, value); + } + + function deleteResourceTag(ResourceId resource, bytes32 tag) public { + requireExistence(resource); + requireOwner(resource, _msgSender()); + ResourceTag.deleteRecord(resource, tag); + } +} diff --git a/packages/world-module-metadata/src/codegen/index.sol b/packages/world-module-metadata/src/codegen/index.sol new file mode 100644 index 0000000000..1ebba0798a --- /dev/null +++ b/packages/world-module-metadata/src/codegen/index.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceTag } from "./tables/ResourceTag.sol"; diff --git a/packages/world-module-metadata/src/codegen/tables/ResourceTag.sol b/packages/world-module-metadata/src/codegen/tables/ResourceTag.sol new file mode 100644 index 0000000000..e275c2abc4 --- /dev/null +++ b/packages/world-module-metadata/src/codegen/tables/ResourceTag.sol @@ -0,0 +1,484 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +// 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 { Memory } from "@latticexyz/store/src/Memory.sol"; +import { SliceLib } from "@latticexyz/store/src/Slice.sol"; +import { EncodeArray } from "@latticexyz/store/src/tightcoder/EncodeArray.sol"; +import { FieldLayout } from "@latticexyz/store/src/FieldLayout.sol"; +import { Schema } from "@latticexyz/store/src/Schema.sol"; +import { EncodedLengths, EncodedLengthsLib } from "@latticexyz/store/src/EncodedLengths.sol"; +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +// Import user types +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +library ResourceTag { + // Hex below is the result of `WorldResourceIdLib.encode({ namespace: "metadata", name: "ResourceTag", typeId: RESOURCE_TABLE });` + ResourceId constant _tableId = ResourceId.wrap(0x74626d657461646174610000000000005265736f757263655461670000000000); + + FieldLayout constant _fieldLayout = + FieldLayout.wrap(0x0000000100000000000000000000000000000000000000000000000000000000); + + // Hex-encoded key schema of (bytes32, bytes32) + Schema constant _keySchema = Schema.wrap(0x004002005f5f0000000000000000000000000000000000000000000000000000); + // Hex-encoded value schema of (bytes) + Schema constant _valueSchema = Schema.wrap(0x00000001c4000000000000000000000000000000000000000000000000000000); + + /** + * @notice Get the table's key field names. + * @return keyNames An array of strings with the names of key fields. + */ + function getKeyNames() internal pure returns (string[] memory keyNames) { + keyNames = new string[](2); + keyNames[0] = "resource"; + keyNames[1] = "tag"; + } + + /** + * @notice Get the table's value field names. + * @return fieldNames An array of strings with the names of value fields. + */ + function getFieldNames() internal pure returns (string[] memory fieldNames) { + fieldNames = new string[](1); + fieldNames[0] = "value"; + } + + /** + * @notice Register the table with its config. + */ + function register() internal { + StoreSwitch.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Register the table with its config. + */ + function _register() internal { + StoreCore.registerTable(_tableId, _fieldLayout, _keySchema, _valueSchema, getKeyNames(), getFieldNames()); + } + + /** + * @notice Get value. + */ + function getValue(ResourceId resource, bytes32 tag) internal view returns (bytes memory value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (bytes(_blob)); + } + + /** + * @notice Get value. + */ + function _getValue(ResourceId resource, bytes32 tag) internal view returns (bytes memory value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (bytes(_blob)); + } + + /** + * @notice Get value. + */ + function get(ResourceId resource, bytes32 tag) internal view returns (bytes memory value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + bytes memory _blob = StoreSwitch.getDynamicField(_tableId, _keyTuple, 0); + return (bytes(_blob)); + } + + /** + * @notice Get value. + */ + function _get(ResourceId resource, bytes32 tag) internal view returns (bytes memory value) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + bytes memory _blob = StoreCore.getDynamicField(_tableId, _keyTuple, 0); + return (bytes(_blob)); + } + + /** + * @notice Set value. + */ + function setValue(ResourceId resource, bytes32 tag, bytes memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((value))); + } + + /** + * @notice Set value. + */ + function _setValue(ResourceId resource, bytes32 tag, bytes memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((value))); + } + + /** + * @notice Set value. + */ + function set(ResourceId resource, bytes32 tag, bytes memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.setDynamicField(_tableId, _keyTuple, 0, bytes((value))); + } + + /** + * @notice Set value. + */ + function _set(ResourceId resource, bytes32 tag, bytes memory value) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.setDynamicField(_tableId, _keyTuple, 0, bytes((value))); + } + + /** + * @notice Get the length of value. + */ + function lengthValue(ResourceId resource, bytes32 tag) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of value. + */ + function _lengthValue(ResourceId resource, bytes32 tag) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of value. + */ + function length(ResourceId resource, bytes32 tag) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + uint256 _byteLength = StoreSwitch.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get the length of value. + */ + function _length(ResourceId resource, bytes32 tag) internal view returns (uint256) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + uint256 _byteLength = StoreCore.getDynamicFieldLength(_tableId, _keyTuple, 0); + unchecked { + return _byteLength / 1; + } + } + + /** + * @notice Get an item of value. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItemValue(ResourceId resource, bytes32 tag, uint256 _index) internal view returns (bytes memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (bytes(_blob)); + } + } + + /** + * @notice Get an item of value. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItemValue(ResourceId resource, bytes32 tag, uint256 _index) internal view returns (bytes memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (bytes(_blob)); + } + } + + /** + * @notice Get an item of value. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function getItem(ResourceId resource, bytes32 tag, uint256 _index) internal view returns (bytes memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _blob = StoreSwitch.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (bytes(_blob)); + } + } + + /** + * @notice Get an item of value. + * @dev Reverts with Store_IndexOutOfBounds if `_index` is out of bounds for the array. + */ + function _getItem(ResourceId resource, bytes32 tag, uint256 _index) internal view returns (bytes memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _blob = StoreCore.getDynamicFieldSlice(_tableId, _keyTuple, 0, _index * 1, (_index + 1) * 1); + return (bytes(_blob)); + } + } + + /** + * @notice Push a slice to value. + */ + function pushValue(ResourceId resource, bytes32 tag, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to value. + */ + function _pushValue(ResourceId resource, bytes32 tag, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to value. + */ + function push(ResourceId resource, bytes32 tag, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Push a slice to value. + */ + function _push(ResourceId resource, bytes32 tag, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.pushToDynamicField(_tableId, _keyTuple, 0, bytes((_slice))); + } + + /** + * @notice Pop a slice from value. + */ + function popValue(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from value. + */ + function _popValue(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from value. + */ + function pop(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Pop a slice from value. + */ + function _pop(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.popFromDynamicField(_tableId, _keyTuple, 0, 1); + } + + /** + * @notice Update a slice of value at `_index`. + */ + function updateValue(ResourceId resource, bytes32 tag, uint256 _index, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of value at `_index`. + */ + function _updateValue(ResourceId resource, bytes32 tag, uint256 _index, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of value at `_index`. + */ + function update(ResourceId resource, bytes32 tag, uint256 _index, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreSwitch.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Update a slice of value at `_index`. + */ + function _update(ResourceId resource, bytes32 tag, uint256 _index, bytes memory _slice) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + unchecked { + bytes memory _encoded = bytes((_slice)); + StoreCore.spliceDynamicData(_tableId, _keyTuple, 0, uint40(_index * 1), uint40(_encoded.length), _encoded); + } + } + + /** + * @notice Delete all data for given keys. + */ + function deleteRecord(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreSwitch.deleteRecord(_tableId, _keyTuple); + } + + /** + * @notice Delete all data for given keys. + */ + function _deleteRecord(ResourceId resource, bytes32 tag) internal { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + StoreCore.deleteRecord(_tableId, _keyTuple, _fieldLayout); + } + + /** + * @notice Tightly pack dynamic data lengths using this table's schema. + * @return _encodedLengths The lengths of the dynamic fields (packed into a single bytes32 value). + */ + function encodeLengths(bytes memory value) internal pure returns (EncodedLengths _encodedLengths) { + // Lengths are effectively checked during copy by 2**40 bytes exceeding gas limits + unchecked { + _encodedLengths = EncodedLengthsLib.pack(bytes(value).length); + } + } + + /** + * @notice Tightly pack dynamic (variable length) data using this table's schema. + * @return The dynamic data, encoded into a sequence of bytes. + */ + function encodeDynamic(bytes memory value) internal pure returns (bytes memory) { + return abi.encodePacked(bytes((value))); + } + + /** + * @notice Encode all of a record's fields. + * @return The static (fixed length) data, encoded into a sequence of bytes. + * @return The lengths of the dynamic fields (packed into a single bytes32 value). + * @return The dynamic (variable length) data, encoded into a sequence of bytes. + */ + function encode(bytes memory value) internal pure returns (bytes memory, EncodedLengths, bytes memory) { + bytes memory _staticData; + EncodedLengths _encodedLengths = encodeLengths(value); + bytes memory _dynamicData = encodeDynamic(value); + + return (_staticData, _encodedLengths, _dynamicData); + } + + /** + * @notice Encode keys as a bytes32 array using this table's field layout. + */ + function encodeKeyTuple(ResourceId resource, bytes32 tag) internal pure returns (bytes32[] memory) { + bytes32[] memory _keyTuple = new bytes32[](2); + _keyTuple[0] = ResourceId.unwrap(resource); + _keyTuple[1] = tag; + + return _keyTuple; + } +} diff --git a/packages/world-module-metadata/src/codegen/world/IMetadataSystem.sol b/packages/world-module-metadata/src/codegen/world/IMetadataSystem.sol new file mode 100644 index 0000000000..e6330de8a1 --- /dev/null +++ b/packages/world-module-metadata/src/codegen/world/IMetadataSystem.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +import { ResourceId } from "@latticexyz/store/src/ResourceId.sol"; + +/** + * @title IMetadataSystem + * @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 IMetadataSystem { + function metadata__getResourceTag(ResourceId resource, bytes32 tag) external view returns (bytes memory); + + function metadata__setResourceTag(ResourceId resource, bytes32 tag, bytes memory value) external; + + function metadata__deleteResourceTag(ResourceId resource, bytes32 tag) external; +} diff --git a/packages/world-module-metadata/src/codegen/world/IWorld.sol b/packages/world-module-metadata/src/codegen/world/IWorld.sol new file mode 100644 index 0000000000..9e9bfa87d3 --- /dev/null +++ b/packages/world-module-metadata/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 { IMetadataSystem } from "./IMetadataSystem.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, IMetadataSystem {} diff --git a/packages/world-module-metadata/src/common.sol b/packages/world-module-metadata/src/common.sol new file mode 100644 index 0000000000..193c9c39ac --- /dev/null +++ b/packages/world-module-metadata/src/common.sol @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { ResourceId, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { ResourceAccess } from "@latticexyz/world/src/codegen/tables/ResourceAccess.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; +import { ResourceIds } from "@latticexyz/store/src/codegen/tables/ResourceIds.sol"; + +using WorldResourceIdInstance for ResourceId; + +// TODO: move these to world + +/** + * @notice Checks if the caller has access to the given resource ID or its namespace. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @return true if the caller has access, false otherwise. + */ +function hasAccess(ResourceId resourceId, address caller) view returns (bool) { + return + // First check access based on the namespace. If caller has no namespace access, check access on the resource. + ResourceAccess.get(resourceId.getNamespaceId(), caller) || ResourceAccess.get(resourceId, caller); +} + +/** + * @notice Check for access at the given namespace or resource. + * @param resourceId The resource ID to check access for. + * @param caller The address of the caller. + * @dev Reverts with IWorldErrors.World_AccessDenied if access is denied. + */ +function requireAccess(ResourceId resourceId, address caller) view { + // Check if the given caller has access to the given namespace or name + if (!hasAccess(resourceId, caller)) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } +} + +/** + * @notice Check for ownership of the namespace of the given resource ID. + * @dev Reverts with IWorldErrors.World_AccessDenied if caller is not owner of the namespace of the resource. + * @param resourceId The resource ID to check ownership for. + * @param caller The address of the caller. + */ +function requireOwner(ResourceId resourceId, address caller) view { + if (NamespaceOwner.get(resourceId.getNamespaceId()) != caller) { + revert IWorldErrors.World_AccessDenied(resourceId.toString(), caller); + } +} + +/** + * @notice Check for existence of the given resource ID. + * @dev Reverts with IWorldErrors.World_ResourceNotFound if the resource does not exist. + * @param resourceId The resource ID to check existence for. + */ +function requireExistence(ResourceId resourceId) view { + if (!ResourceIds.getExists(resourceId)) { + revert IWorldErrors.World_ResourceNotFound(resourceId, resourceId.toString()); + } +} diff --git a/packages/world-module-metadata/test/MetadataModule.t.sol b/packages/world-module-metadata/test/MetadataModule.t.sol new file mode 100644 index 0000000000..4fcc5a92c3 --- /dev/null +++ b/packages/world-module-metadata/test/MetadataModule.t.sol @@ -0,0 +1,114 @@ +// 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 { createWorld } from "@latticexyz/world/test/createWorld.sol"; +import { ResourceId, WorldResourceIdLib, WorldResourceIdInstance } from "@latticexyz/world/src/WorldResourceId.sol"; +import { IWorldErrors } from "@latticexyz/world/src/IWorldErrors.sol"; +import { NamespaceOwner } from "@latticexyz/world/src/codegen/tables/NamespaceOwner.sol"; + +import { MetadataModule } from "../src/MetadataModule.sol"; +import { IWorld } from "../src/codegen/world/IWorld.sol"; +import { ResourceTag } from "../src/codegen/tables/ResourceTag.sol"; + +contract MetadataModuleTest is Test, GasReporter { + using WorldResourceIdInstance for ResourceId; + + IWorld world; + MetadataModule metadataModule = new MetadataModule(); + + function setUp() public { + world = IWorld(address(createWorld())); + StoreSwitch.setStoreAddress(address(world)); + } + + function testInstall() public { + startGasReport("install metadata module"); + world.installModule(metadataModule, new bytes(0)); + endGasReport(); + + ResourceId namespace = ResourceTag._tableId.getNamespaceId(); + assertEq(NamespaceOwner.get(namespace), address(this)); + + // Installing again will revert because metadata namespace isn't owned by the module, so the module is unable to write to it. + vm.expectRevert( + abi.encodeWithSelector(IWorldErrors.World_AccessDenied.selector, namespace.toString(), address(metadataModule)) + ); + world.installModule(metadataModule, new bytes(0)); + + // Transferring the namespace to the module and installing should be a no-op/idempotent and the module will return namespace ownership. + world.transferOwnership(namespace, address(metadataModule)); + world.installModule(metadataModule, new bytes(0)); + assertEq(NamespaceOwner.get(namespace), address(this)); + } + + function testSetResourceTag() public { + world.installModule(metadataModule, new bytes(0)); + ResourceId resource = ResourceTag._tableId; + + assertEq(world.metadata__getResourceTag(resource, "label"), ""); + assertEq(ResourceTag.get(resource, "label"), ""); + + startGasReport("set resource tag"); + world.metadata__setResourceTag(resource, "label", "ResourceTag"); + endGasReport(); + + assertEq(world.metadata__getResourceTag(resource, "label"), "ResourceTag"); + assertEq(ResourceTag.get(resource, "label"), "ResourceTag"); + + // metadata is mutable, so make sure we can mutate it + world.metadata__setResourceTag(resource, "label", "Resource"); + assertEq(world.metadata__getResourceTag(resource, "label"), "Resource"); + assertEq(ResourceTag.get(resource, "label"), "Resource"); + } + + function testDeleteResourceTag() public { + world.installModule(metadataModule, new bytes(0)); + ResourceId resource = ResourceTag._tableId; + + world.metadata__setResourceTag(resource, "label", "ResourceTag"); + assertEq(ResourceTag.get(resource, "label"), "ResourceTag"); + assertEq(world.metadata__getResourceTag(resource, "label"), "ResourceTag"); + + startGasReport("delete resource tag"); + world.metadata__deleteResourceTag(resource, "label"); + endGasReport(); + + assertEq(world.metadata__getResourceTag(resource, "label"), ""); + assertEq(ResourceTag.get(resource, "label"), ""); + } + + function testTagNonexistentResource() public { + world.installModule(metadataModule, new bytes(0)); + ResourceId resource = WorldResourceIdLib.encode("tb", "whatever", "SomeTable"); + + vm.expectRevert( + abi.encodeWithSelector(IWorldErrors.World_ResourceNotFound.selector, resource, resource.toString()) + ); + world.metadata__setResourceTag(resource, "label", "SomeTable"); + + vm.expectRevert( + abi.encodeWithSelector(IWorldErrors.World_ResourceNotFound.selector, resource, resource.toString()) + ); + world.metadata__deleteResourceTag(resource, "label"); + } + + function testTagUnownedResource(address caller) public { + vm.assume(caller != address(0)); + vm.assume(caller != address(this)); + + world.installModule(metadataModule, new bytes(0)); + ResourceId resource = NamespaceOwner._tableId; + + vm.startPrank(caller); + + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_AccessDenied.selector, resource.toString(), caller)); + world.metadata__setResourceTag(resource, "label", "NamespaceOwner"); + + vm.expectRevert(abi.encodeWithSelector(IWorldErrors.World_AccessDenied.selector, resource.toString(), caller)); + world.metadata__deleteResourceTag(resource, "label"); + } +} diff --git a/packages/world-module-metadata/ts/build.ts b/packages/world-module-metadata/ts/build.ts new file mode 100644 index 0000000000..9f24536f54 --- /dev/null +++ b/packages/world-module-metadata/ts/build.ts @@ -0,0 +1,18 @@ +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 Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]); diff --git a/packages/world-module-metadata/tsconfig.json b/packages/world-module-metadata/tsconfig.json new file mode 100644 index 0000000000..9b0bf57752 --- /dev/null +++ b/packages/world-module-metadata/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["mud.config.ts", "ts"] +} diff --git a/packages/world-module-metadata/tsup.config.ts b/packages/world-module-metadata/tsup.config.ts new file mode 100644 index 0000000000..a89281f3d6 --- /dev/null +++ b/packages/world-module-metadata/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: { + "mud.config": "mud.config.ts", + }, + target: "esnext", + format: ["esm"], + dts: !process.env.TSUP_SKIP_DTS, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/packages/world-modules/.gitignore b/packages/world-modules/.gitignore index 5ff362ddd2..97ba578a6d 100644 --- a/packages/world-modules/.gitignore +++ b/packages/world-modules/.gitignore @@ -7,4 +7,7 @@ artifacts yarn-error.log API +# Each module in this package codegens its libraries to each module dir (see MUD config). +# World interface 1) is combined for all modules and 2) doesn't make as much sense for some modules because they're installed to a specific namespace. +# So ignore them for now. We ignore the entire base codegen dir because it includes the table index, which we don't want to propagate either. src/codegen diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0d064ab13..cb9913a32e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,9 @@ importers: '@latticexyz/world': specifier: workspace:* version: link:../world + '@latticexyz/world-module-metadata': + specifier: workspace:* + version: link:../world-module-metadata abitype: specifier: 1.0.0 version: 1.0.0(typescript@5.4.2)(zod@3.23.8) @@ -1079,6 +1082,46 @@ importers: specifier: 0.34.6 version: 0.34.6(jsdom@22.1.0) + packages/world-module-metadata: + 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 + '@types/node': + specifier: ^18.15.11 + version: 18.15.11 + ds-test: + specifier: https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0 + version: https://codeload.github.com/dapphub/ds-test/tar.gz/e282159d5170298eb2455a6c05280ab5a73a4ef0 + forge-std: + specifier: https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1 + version: https://codeload.github.com/foundry-rs/forge-std/tar.gz/74cfb77e308dd188d2f58864aaf44963ae6b88b1 + solhint: + specifier: ^3.3.7 + version: 3.3.7 + tsup: + specifier: ^6.7.0 + version: 6.7.0(postcss@8.4.23)(typescript@5.4.2) + tsx: + specifier: ^3.12.6 + version: 3.12.6 + vitest: + specifier: 0.34.6 + version: 0.34.6(jsdom@22.1.0) + packages/world-modules: dependencies: '@latticexyz/common': @@ -4045,9 +4088,6 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} - get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} @@ -10313,8 +10353,6 @@ snapshots: get-caller-file@2.0.5: {} - get-func-name@2.0.0: {} - get-func-name@2.0.2: {} get-intrinsic@1.1.3: @@ -11435,7 +11473,7 @@ snapshots: loupe@2.3.6: dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 lru-cache@10.3.0: {} diff --git a/tsconfig.paths.json b/tsconfig.paths.json index ac24ee3a3a..f2d16c9e8d 100644 --- a/tsconfig.paths.json +++ b/tsconfig.paths.json @@ -40,6 +40,8 @@ "@latticexyz/world/node": ["./packages/world/ts/node/index.ts"], "@latticexyz/world/out/*": ["./packages/world/out/*"], "@latticexyz/world/*": ["./packages/world/ts/exports/*.ts"], + "@latticexyz/world-module-metadata/mud.config": ["./packages/world-module-metadata/mud.config.ts"], + "@latticexyz/world-module-metadata/out/*": ["./packages/world-module-metadata/out/*"], "@latticexyz/world-modules/internal/mud.config": ["./packages/world-modules/mud.config.ts"], "@latticexyz/world-modules/out/*": ["./packages/world-modules/out/*"] }