Skip to content

feat: install module with delegation #3586

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

Merged
merged 29 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
832ac42
add config
frolic Feb 6, 2025
7997c41
wrap install in a delegation
frolic Feb 6, 2025
087f6c3
fix types
frolic Feb 6, 2025
ea9add5
install metadata module with delegation
frolic Feb 6, 2025
0c19288
todos
frolic Feb 6, 2025
5637263
Merge remote-tracking branch 'origin/main' into holic/module-delegation
frolic Feb 6, 2025
a5044e6
migrate module calls to callFrom
frolic Feb 7, 2025
ea91731
update snapshots
frolic Feb 7, 2025
748f1e7
update metadata module tests
frolic Feb 7, 2025
2644311
nevermind
frolic Feb 7, 2025
426d06d
update snapshots again
frolic Feb 7, 2025
0648744
gas report
frolic Feb 7, 2025
cf53d68
rename
frolic Feb 7, 2025
b7ee1f7
fix conflict between IStore and IStoreRegistrationSystem
frolic Feb 7, 2025
7001c51
generate world system libs
frolic Feb 7, 2025
35c5ac5
calldata -> memory
frolic Feb 7, 2025
a681215
use system libs
frolic Feb 7, 2025
64301f5
gas report
frolic Feb 7, 2025
f38cd84
separate configs
frolic Feb 7, 2025
f56fd3c
Merge remote-tracking branch 'origin/main' into holic/module-delegation
frolic Feb 11, 2025
29947e2
fixes after merge
frolic Feb 11, 2025
f7978f7
import registration system ID from one place
frolic Feb 11, 2025
293f652
update snapshots and gas reports
frolic Feb 11, 2025
6a19df0
update import path
frolic Feb 11, 2025
0186b23
Create dirty-pumas-dream.md
frolic Feb 11, 2025
75d2ce8
Create hot-pans-love.md
frolic Feb 11, 2025
9ba3d21
Create purple-houses-sell.md
frolic Feb 11, 2025
447bb99
Update purple-houses-sell.md
frolic Feb 11, 2025
b402910
Update purple-houses-sell.md
frolic Feb 11, 2025
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
2 changes: 1 addition & 1 deletion packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export type DeployedSystem = Omit<

export type Module = DeterministicContract & {
readonly name: string;
readonly installAsRoot: boolean;
readonly installStrategy: "root" | "delegation" | "default";
readonly installData: Hex; // TODO: figure out better naming for this
/**
* @internal
Expand Down
43 changes: 43 additions & 0 deletions packages/cli/src/deploy/compat/moduleArtifactPathFromName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Module } from "@latticexyz/world/internal";
import path from "node:path";

// 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",
};

/** @internal For use with `config.modules.map(...)` */
export function moduleArtifactPathFromName(
forgeOutDir: string,
): (mod: Module) => Module & { readonly artifactPath: string } {
return (mod) => {
if (mod.artifactPath) return mod as never;
if (!mod.name) throw new Error("No `artifactPath` provided for module.");

const artifactPath =
knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ??
path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`);

console.warn(
[
"",
`⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`,
"",
"To resolve this, you can replace this:",
"",
` name: ${JSON.stringify(mod.name)}`,
"",
"with this:",
"",
` artifactPath: ${JSON.stringify(artifactPath)}`,
"",
].join("\n"),
);

return { ...mod, artifactPath };
};
}
86 changes: 27 additions & 59 deletions packages/cli/src/deploy/configToModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,10 @@ import { resolveWithContext } from "@latticexyz/world/internal";
import callWithSignatureModule from "@latticexyz/world-module-callwithsignature/out/CallWithSignatureModule.sol/CallWithSignatureModule.json" assert { type: "json" };
import { getContractArtifact } from "../utils/getContractArtifact";
import { excludeCallWithSignatureModule } from "./compat/excludeUnstableCallWithSignatureModule";
import { moduleArtifactPathFromName } from "./compat/moduleArtifactPathFromName";

const callWithSignatureModuleArtifact = getContractArtifact(callWithSignatureModule);

/** 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",
};

export async function configToModules<config extends World>(
config: config,
// TODO: remove/replace `forgeOutDir`
Expand All @@ -32,7 +26,7 @@ export async function configToModules<config extends World>(
// TODO: figure out approach to install on existing worlds where deployer may not own root namespace
optional: true,
name: "CallWithSignatureModule",
installAsRoot: true,
installStrategy: "root",
installData: "0x",
prepareDeploy: createPrepareDeploy(
callWithSignatureModuleArtifact.bytecode,
Expand All @@ -44,60 +38,34 @@ export async function configToModules<config extends World>(
];

const modules = await Promise.all(
config.modules.filter(excludeCallWithSignatureModule).map(async (mod): Promise<Module> => {
let artifactPath = mod.artifactPath;

// Backwards compatibility
// TODO: move this up a level so we don't need `forgeOutDir` in here?
if (!artifactPath) {
if (mod.name) {
artifactPath =
knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ??
path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`);
console.warn(
[
"",
`⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`,
"",
"To resolve this, you can replace this:",
"",
` name: ${JSON.stringify(mod.name)}`,
"",
"with this:",
"",
` artifactPath: ${JSON.stringify(artifactPath)}`,
"",
].join("\n"),
);
} else {
throw new Error("No `artifactPath` provided for module.");
}
}
config.modules
.filter(excludeCallWithSignatureModule)
.map(moduleArtifactPathFromName(forgeOutDir))
.map(async (mod): Promise<Module> => {
const name = path.basename(mod.artifactPath, ".json");
const artifact = await importContractArtifact({ artifactPath: mod.artifactPath });

const name = path.basename(artifactPath, ".json");
const artifact = await importContractArtifact({ artifactPath });
// TODO: replace args with something more strongly typed
const installArgs = mod.args
.map((arg) => resolveWithContext(arg, { config }))
.map((arg) => {
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
});

// TODO: replace args with something more strongly typed
const installArgs = mod.args
.map((arg) => resolveWithContext(arg, { config }))
.map((arg) => {
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
});

if (installArgs.length > 1) {
throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`);
}
if (installArgs.length > 1) {
throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`);
}

return {
name,
installAsRoot: mod.root,
installData: installArgs.length === 0 ? "0x" : installArgs[0],
prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders),
deployedBytecodeSize: artifact.deployedBytecodeSize,
abi: artifact.abi,
};
}),
return {
name,
installStrategy: mod.root ? "root" : mod.useDelegation ? "delegation" : "default",
installData: installArgs.length === 0 ? "0x" : installArgs[0],
prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders),
deployedBytecodeSize: artifact.deployedBytecodeSize,
abi: artifact.abi,
};
}),
);

return [...defaultModules, ...modules];
Expand Down
120 changes: 112 additions & 8 deletions packages/cli/src/deploy/ensureModules.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Client, Transport, Chain, Account, Hex, BaseError } from "viem";
import { writeContract } from "@latticexyz/common";
import { resourceToHex, writeContract } from "@latticexyz/common";
import { Module, WorldDeploy, worldAbi } from "./common";
import { debug } from "./debug";
import { isDefined } from "@latticexyz/common/utils";
import pRetry from "p-retry";
import { LibraryMap } from "./getLibraryMap";
import { ensureContractsDeployed } from "@latticexyz/common/internal";
import { encodeSystemCalls } from "@latticexyz/world/internal";

export async function ensureModules({
client,
Expand Down Expand Up @@ -39,17 +40,55 @@ export async function ensureModules({
pRetry(
async () => {
try {
// append module's ABI so that we can decode any custom errors
const abi = [...worldAbi, ...mod.abi];
const moduleAddress = mod.prepareDeploy(deployerAddress, libraryMap).address;
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
const params = mod.installAsRoot
? ({ functionName: "installRootModule", args: [moduleAddress, mod.installData] } as const)
: ({ functionName: "installModule", args: [moduleAddress, mod.installData] } as const);

// TODO: fix strong types for world ABI etc
// TODO: add return types to get better type safety
const params = (() => {
if (mod.installStrategy === "root") {
return {
functionName: "installRootModule",
args: [moduleAddress, mod.installData],
} as const;
}

if (mod.installStrategy === "delegation") {
return {
functionName: "batchCall",
args: encodeSystemCalls([
{
abi: registrationSystemAbi,
systemId: registrationSystemId,
functionName: "registerDelegation",
args: [moduleAddress, unlimitedDelegationControlId, "0x"],
},
{
abi: registrationSystemAbi,
systemId: registrationSystemId,
functionName: "installModule",
args: [moduleAddress, mod.installData],
},
{
abi: registrationSystemAbi,
systemId: registrationSystemId,
functionName: "unregisterDelegation",
args: [moduleAddress],
},
]),
} as const;
}

return {
functionName: "installModule",
args: [moduleAddress, mod.installData],
} as const;
})();

return await writeContract(client, {
chain: client.chain ?? null,
address: worldDeploy.address,
abi,
// append module's ABI so that we can decode any custom errors
abi: [...worldAbi, ...mod.abi],
...params,
});
} catch (error) {
Expand All @@ -74,3 +113,68 @@ export async function ensureModules({
)
).filter(isDefined);
}

// TODO: export from world
const unlimitedDelegationControlId = resourceToHex({ type: "system", namespace: "", name: "unlimited" });

// TODO: export from world
// world/src/modules/init/constants.sol
const registrationSystemId = resourceToHex({ type: "system", namespace: "", name: "Registration" });

// world/src/modules/init/RegistrationSystem.sol
const registrationSystemAbi = [
{
type: "function",
name: "installModule",
inputs: [
{
name: "module",
type: "address",
internalType: "contract IModule",
},
{
name: "encodedArgs",
type: "bytes",
internalType: "bytes",
},
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "registerDelegation",
inputs: [
{
name: "delegatee",
type: "address",
internalType: "address",
},
{
name: "delegationControlId",
type: "bytes32",
internalType: "ResourceId",
},
{
name: "initCallData",
type: "bytes",
internalType: "bytes",
},
],
outputs: [],
stateMutability: "nonpayable",
},
{
type: "function",
name: "unregisterDelegation",
inputs: [
{
name: "delegatee",
type: "address",
internalType: "address",
},
],
outputs: [],
stateMutability: "nonpayable",
},
] as const;
4 changes: 1 addition & 3 deletions packages/cli/src/deploy/ensureResourceTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@ export async function ensureResourceTags<const value>({
});
if (pendingTags.length === 0) return [];

// TODO: check if metadata namespace exists, if we own it, and if so transfer ownership to the module before reinstalling
// (https://github.com/latticexyz/mud/issues/3035)
const moduleTxs = await ensureModules({
client,
deployerAddress,
Expand All @@ -85,7 +83,7 @@ export async function ensureResourceTags<const value>({
{
optional: true,
name: "MetadataModule",
installAsRoot: false,
installStrategy: "delegation",
Comment on lines -88 to +86
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to install the metadata module via a delegation? (Do we want to give the installer ownership over the metadata tables or keep it like anyone can install it but it's immutable after it's been installed?)

Copy link
Member Author

@frolic frolic Feb 11, 2025

Choose a reason for hiding this comment

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

chatted in discord:

this doesn't change the current behavior, which passed namespace ownership back to the deployer: https://github.com/latticexyz/mud/pull/3586/files#diff-4b5b7ac0dc35d7914ce67b4dfcee8d54a4814b759a87d0ddce84efe17d3ade08L55

which is ~almost always going to be the world creator, since this is a default module installed with the world deploy, and only in cases where you're deploying to an old world with a new MUD version would this be a potential issue

can separately decide what we wanna do going forward

installData: "0x",
prepareDeploy: createPrepareDeploy(metadataModuleArtifact.bytecode, metadataModuleArtifact.placeholders),
deployedBytecodeSize: metadataModuleArtifact.deployedBytecodeSize,
Expand Down
1 change: 1 addition & 0 deletions packages/entrykit/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const defaultClientConfig = {
pollingInterval: 250,
} as const satisfies Pick<ClientConfig, "pollingInterval">;

// TODO: move to world
export const unlimitedDelegationControlId = resourceToHex({ type: "system", namespace: "", name: "unlimited" });

export const worldTables = worldConfig.namespaces.world.tables;
Expand Down
1 change: 1 addition & 0 deletions packages/entrykit/src/onboarding/useSetupSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export function useSetupSession({ userClient }: { userClient: ConnectedClient })
userClient,
sessionClient,
worldAddress,
// TODO: export from world
systemId: resourceToHex({ type: "system", namespace: "", name: "Registration" }),
callData: encodeFunctionData({
abi: IBaseWorldAbi,
Expand Down
10 changes: 5 additions & 5 deletions packages/store/ts/flattenStoreLogs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,14 @@ describe("flattenStoreLogs", async () => {
"Store_SetRecord world__FunctionSignatur (0x1fae630800000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__InstalledModules (0x00000000000000000000000051bd8d2de7017c23ee5bdc885e70dfdd0862b837,0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)",
"Store_SetRecord store__ResourceIds (0x6e736d6574616461746100000000000000000000000000000000000000000000)",
"Store_SetRecord world__NamespaceOwner (0x6e736d6574616461746100000000000000000000000000000000000000000000)",
"Store_SetRecord world__ResourceAccess (0x6e736d6574616461746100000000000000000000000000000000000000000000,0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266)",
"Store_SetRecord store__Tables (0x74626d657461646174610000000000005265736f757263655461670000000000)",
"Store_SetRecord store__ResourceIds (0x74626d657461646174610000000000005265736f757263655461670000000000)",
"Store_SetRecord store__ResourceIds (0x73796d657461646174610000000000004d6574616461746153797374656d0000)",
"Store_SetRecord world__Systems (0x73796d657461646174610000000000004d6574616461746153797374656d0000)",
"Store_SetRecord world__SystemRegistry (0x00000000000000000000000053e501d8e4c977ff5b27446ec6a60e57c7ef1050)",
"Store_SetRecord world__ResourceAccess (0x6e736d6574616461746100000000000000000000000000000000000000000000,0x00000000000000000000000053e501d8e4c977ff5b27446ec6a60e57c7ef1050)",
"Store_SetRecord world__SystemRegistry (0x0000000000000000000000009547134364d2890dbb7bb0148fe339b9c4253e3e)",
"Store_SetRecord world__ResourceAccess (0x6e736d6574616461746100000000000000000000000000000000000000000000,0x0000000000000000000000009547134364d2890dbb7bb0148fe339b9c4253e3e)",
"Store_SetRecord world__FunctionSelector (0xff66f05f00000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__FunctionSignatur (0xc6972e9300000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__FunctionSignatur (0xff66f05f00000000000000000000000000000000000000000000000000000000)",
Expand All @@ -163,9 +165,7 @@ describe("flattenStoreLogs", async () => {
"Store_SetRecord world__FunctionSelector (0x5ce7ca1a00000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__FunctionSignatur (0xf128760200000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__FunctionSignatur (0x5ce7ca1a00000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord world__NamespaceOwner (0x6e736d6574616461746100000000000000000000000000000000000000000000)",
"Store_SetRecord world__ResourceAccess (0x6e736d6574616461746100000000000000000000000000000000000000000000,0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266)",
"Store_SetRecord world__InstalledModules (0x0000000000000000000000000f8d2ae4af9b5c4677766030338a8720e30073cc,0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)",
"Store_SetRecord world__InstalledModules (0x00000000000000000000000023a2972dd28eea3d545e71403775436115380f1c,0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470)",
"Store_SetRecord metadata__ResourceTag (0x737900000000000000000000000000004d6f766553797374656d000000000000,0x6162690000000000000000000000000000000000000000000000000000000000)",
"Store_SetRecord metadata__ResourceTag (0x737900000000000000000000000000004d6f766553797374656d000000000000,0x776f726c64416269000000000000000000000000000000000000000000000000)",
"Store_SetRecord Position (0x0000000000000000000000001d96f2f6bef1202e4ce1ff6dad0c2cb002861d3e)",
Expand Down
Loading
Loading