Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(world-module-metadata): add metadata module #3026

Merged
merged 24 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/spotty-camels-occur.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
30 changes: 27 additions & 3 deletions packages/cli/src/deploy/configToModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 extends World>(
config: config,
// TODO: remove/replace `forgeOutDir`
forgeOutDir: string,
): Promise<readonly Module[]> {
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<Module> => {
let artifactPath = mod.artifactPath;
Expand Down Expand Up @@ -46,7 +70,7 @@ export async function configToModules<config extends World>(
}

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
Expand All @@ -71,5 +95,5 @@ export async function configToModules<config extends World>(
}),
);

return modules;
return [...defaultModules, ...modules];
}
6 changes: 6 additions & 0 deletions packages/cli/src/deploy/ensureModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
39 changes: 2 additions & 37 deletions packages/cli/src/utils/getContractArtifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,26 +31,9 @@ const artifactSchema = z.object({
abi: abiSchema,
});

export async function getContractArtifact({
packageJsonPath,
artifactPath,
}: GetContractArtifactOptions): Promise<GetContractArtifactResult> {
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 {
Expand Down
40 changes: 40 additions & 0 deletions packages/cli/src/utils/importContractArtifact.ts
Original file line number Diff line number Diff line change
@@ -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<GetContractArtifactResult> {
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);
}
8 changes: 0 additions & 8 deletions packages/cli/src/utils/knownModuleArtifacts.ts

This file was deleted.

2 changes: 2 additions & 0 deletions packages/world-module-metadata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cache
out
8 changes: 8 additions & 0 deletions packages/world-module-metadata/.solhint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "solhint:recommended",
"rules": {
"compiler-version": ["error", ">=0.8.0"],
"avoid-low-level-calls": "off",
"func-visibility": ["warn", { "ignoreConstructors": true }]
}
}
1 change: 1 addition & 0 deletions packages/world-module-metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Metadata world module
15 changes: 15 additions & 0 deletions packages/world-module-metadata/foundry.toml
Original file line number Diff line number Diff line change
@@ -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"
]
20 changes: 20 additions & 0 deletions packages/world-module-metadata/gas-report.json
Original file line number Diff line number Diff line change
@@ -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
}
]
18 changes: 18 additions & 0 deletions packages/world-module-metadata/mud.config.ts
Original file line number Diff line number Diff line change
@@ -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"],
},
},
});
63 changes: 63 additions & 0 deletions packages/world-module-metadata/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
3 changes: 3 additions & 0 deletions packages/world-module-metadata/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ds-test/=node_modules/ds-test/src/
forge-std/=node_modules/forge-std/src/
@latticexyz/=node_modules/@latticexyz/
57 changes: 57 additions & 0 deletions packages/world-module-metadata/src/MetadataModule.sol
Original file line number Diff line number Diff line change
@@ -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());
Copy link
Member Author

@holic holic Aug 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this pattern feels a little weird to me, but curious to hear what you think:

on first install, metadata module creates metadata namespace, registers tables/systems, hands ownership back to original caller

on second install (idempotent, would upgrade systems or add tables), you'd first need to hand ownership of the namespace to the module, then it hands it back after install

should the world or something else own the metadata namespace? if so, how would you perform an upgrade to the metadata module?

or maybe we should consider the metadata module more of a "locked in" thing, rename it to something more specific like "resource tag module" and if we want more metadata, we add as another module and namespace rather than trying to figure out how to upgrade this one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this pattern is fine! makes it possible to upgrade, which is nice

}
}
Loading
Loading