Skip to content

Commit 3187081

Browse files
authored
feat: install module with delegation (#3586)
1 parent 06e48e0 commit 3187081

24 files changed

Lines changed: 349 additions & 150 deletions

.changeset/dirty-pumas-dream.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@latticexyz/cli": patch
3+
"@latticexyz/world": patch
4+
---
5+
6+
Added `useDelegation` module config option to install modules using a temporary, unlimited delegation. This allows modules to install or upgrade systems and tables on your behalf.

.changeset/hot-pans-love.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@latticexyz/cli": patch
3+
"@latticexyz/world-module-metadata": patch
4+
---
5+
6+
Metadata module has been updated to install via delegation, making it easier for later module upgrades and to demonstrate modules installed via delegation.

.changeset/purple-houses-sell.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@latticexyz/world": patch
3+
---
4+
5+
Updated `encodeSystemCalls` and `encodeSystemCallsFrom` to include the `abi` in each call so that different systems/ABIs can be called in batch. Types have been improved to properly hint/narrow the expected arguments for each call.
6+
7+
```diff
8+
-encodeSystemCalls(abi, [{
9+
+encodeSystemCalls([{
10+
+ abi,
11+
systemId: '0x...',
12+
functionName: '...',
13+
args: [...],
14+
}]);
15+
```
16+
17+
```diff
18+
-encodeSystemCallsFrom(from, abi, [{
19+
+encodeSystemCallsFrom(from, [{
20+
+ abi,
21+
systemId: '0x...',
22+
functionName: '...',
23+
args: [...],
24+
}]);
25+
```

e2e/packages/sync-test/registerDelegationWithSignature.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import { createAsyncErrorHandler } from "./asyncErrors";
55
import { deployContracts, startViteServer, startBrowserAndPage, openClientWithRootAccount } from "./setup";
66
import { rpcHttpUrl } from "./setup/constants";
77
import { waitForInitialSync } from "./data/waitForInitialSync";
8-
import { createBurnerAccount, resourceToHex, transportObserver } from "@latticexyz/common";
8+
import { createBurnerAccount, hexToResource, resourceToHex, transportObserver } from "@latticexyz/common";
99
import { http, createWalletClient, ClientConfig, encodeFunctionData, toHex } from "viem";
1010
import { mudFoundry } from "@latticexyz/common/chains";
1111
import { encodeEntity } from "@latticexyz/store-sync/recs";
1212
import { callPageFunction } from "./data/callPageFunction";
13-
import worldConfig from "@latticexyz/world/mud.config";
13+
import worldConfig, { systemsConfig as worldSystemsConfig } from "@latticexyz/world/mud.config";
1414
import { callWithSignatureTypes } from "@latticexyz/world-module-callwithsignature/internal";
1515
import { getWorld } from "./data/getWorld";
1616
import { callWithSignature } from "./data/callWithSignature";
@@ -59,7 +59,8 @@ describe("callWithSignature", async () => {
5959
});
6060

6161
const worldContract = await getWorld(page);
62-
const systemId = resourceToHex({ type: "system", namespace: "", name: "Registration" });
62+
const systemId = worldSystemsConfig.systems.RegistrationSystem.systemId;
63+
const systemResource = hexToResource(systemId);
6364

6465
// Declare delegation parameters
6566
const delegatee = "0x7203e7ADfDF38519e1ff4f8Da7DCdC969371f377";
@@ -84,8 +85,8 @@ describe("callWithSignature", async () => {
8485
primaryType: "Call",
8586
message: {
8687
signer: delegator.address,
87-
systemNamespace: "",
88-
systemName: "Registration",
88+
systemNamespace: systemResource.namespace,
89+
systemName: systemResource.name,
8990
callData,
9091
nonce,
9192
},

packages/cli/src/deploy/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export type DeployedSystem = Omit<
109109

110110
export type Module = DeterministicContract & {
111111
readonly name: string;
112-
readonly installAsRoot: boolean;
112+
readonly installStrategy: "root" | "delegation" | "default";
113113
readonly installData: Hex; // TODO: figure out better naming for this
114114
/**
115115
* @internal
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Module } from "@latticexyz/world/internal";
2+
import path from "node:path";
3+
4+
// Please don't add to this list!
5+
//
6+
// These are kept for backwards compatibility and assumes the downstream project has this module installed as a dependency.
7+
const knownModuleArtifacts = {
8+
KeysWithValueModule: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json",
9+
KeysInTableModule: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json",
10+
UniqueEntityModule: "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json",
11+
};
12+
13+
/** @internal For use with `config.modules.map(...)` */
14+
export function moduleArtifactPathFromName(
15+
forgeOutDir: string,
16+
): (mod: Module) => Module & { readonly artifactPath: string } {
17+
return (mod) => {
18+
if (mod.artifactPath) return mod as never;
19+
if (!mod.name) throw new Error("No `artifactPath` provided for module.");
20+
21+
const artifactPath =
22+
knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ??
23+
path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`);
24+
25+
console.warn(
26+
[
27+
"",
28+
`⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`,
29+
"",
30+
"To resolve this, you can replace this:",
31+
"",
32+
` name: ${JSON.stringify(mod.name)}`,
33+
"",
34+
"with this:",
35+
"",
36+
` artifactPath: ${JSON.stringify(artifactPath)}`,
37+
"",
38+
].join("\n"),
39+
);
40+
41+
return { ...mod, artifactPath };
42+
};
43+
}

packages/cli/src/deploy/configToModules.ts

Lines changed: 27 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,10 @@ import { resolveWithContext } from "@latticexyz/world/internal";
1010
import callWithSignatureModule from "@latticexyz/world-module-callwithsignature/out/CallWithSignatureModule.sol/CallWithSignatureModule.json" assert { type: "json" };
1111
import { getContractArtifact } from "../utils/getContractArtifact";
1212
import { excludeCallWithSignatureModule } from "./compat/excludeUnstableCallWithSignatureModule";
13+
import { moduleArtifactPathFromName } from "./compat/moduleArtifactPathFromName";
1314

1415
const callWithSignatureModuleArtifact = getContractArtifact(callWithSignatureModule);
1516

16-
/** 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. */
17-
const knownModuleArtifacts = {
18-
KeysWithValueModule: "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json",
19-
KeysInTableModule: "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json",
20-
UniqueEntityModule: "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json",
21-
};
22-
2317
export async function configToModules<config extends World>(
2418
config: config,
2519
// TODO: remove/replace `forgeOutDir`
@@ -32,7 +26,7 @@ export async function configToModules<config extends World>(
3226
// TODO: figure out approach to install on existing worlds where deployer may not own root namespace
3327
optional: true,
3428
name: "CallWithSignatureModule",
35-
installAsRoot: true,
29+
installStrategy: "root",
3630
installData: "0x",
3731
prepareDeploy: createPrepareDeploy(
3832
callWithSignatureModuleArtifact.bytecode,
@@ -44,60 +38,34 @@ export async function configToModules<config extends World>(
4438
];
4539

4640
const modules = await Promise.all(
47-
config.modules.filter(excludeCallWithSignatureModule).map(async (mod): Promise<Module> => {
48-
let artifactPath = mod.artifactPath;
49-
50-
// Backwards compatibility
51-
// TODO: move this up a level so we don't need `forgeOutDir` in here?
52-
if (!artifactPath) {
53-
if (mod.name) {
54-
artifactPath =
55-
knownModuleArtifacts[mod.name as keyof typeof knownModuleArtifacts] ??
56-
path.join(forgeOutDir, `${mod.name}.sol`, `${mod.name}.json`);
57-
console.warn(
58-
[
59-
"",
60-
`⚠️ Your \`mud.config.ts\` is using a module with a \`name\`, but this option is deprecated.`,
61-
"",
62-
"To resolve this, you can replace this:",
63-
"",
64-
` name: ${JSON.stringify(mod.name)}`,
65-
"",
66-
"with this:",
67-
"",
68-
` artifactPath: ${JSON.stringify(artifactPath)}`,
69-
"",
70-
].join("\n"),
71-
);
72-
} else {
73-
throw new Error("No `artifactPath` provided for module.");
74-
}
75-
}
41+
config.modules
42+
.filter(excludeCallWithSignatureModule)
43+
.map(moduleArtifactPathFromName(forgeOutDir))
44+
.map(async (mod): Promise<Module> => {
45+
const name = path.basename(mod.artifactPath, ".json");
46+
const artifact = await importContractArtifact({ artifactPath: mod.artifactPath });
7647

77-
const name = path.basename(artifactPath, ".json");
78-
const artifact = await importContractArtifact({ artifactPath });
48+
// TODO: replace args with something more strongly typed
49+
const installArgs = mod.args
50+
.map((arg) => resolveWithContext(arg, { config }))
51+
.map((arg) => {
52+
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
53+
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
54+
});
7955

80-
// TODO: replace args with something more strongly typed
81-
const installArgs = mod.args
82-
.map((arg) => resolveWithContext(arg, { config }))
83-
.map((arg) => {
84-
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
85-
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
86-
});
87-
88-
if (installArgs.length > 1) {
89-
throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`);
90-
}
56+
if (installArgs.length > 1) {
57+
throw new Error(`${name} module should only have 0-1 args, but had ${installArgs.length} args.`);
58+
}
9159

92-
return {
93-
name,
94-
installAsRoot: mod.root,
95-
installData: installArgs.length === 0 ? "0x" : installArgs[0],
96-
prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders),
97-
deployedBytecodeSize: artifact.deployedBytecodeSize,
98-
abi: artifact.abi,
99-
};
100-
}),
60+
return {
61+
name,
62+
installStrategy: mod.root ? "root" : mod.useDelegation ? "delegation" : "default",
63+
installData: installArgs.length === 0 ? "0x" : installArgs[0],
64+
prepareDeploy: createPrepareDeploy(artifact.bytecode, artifact.placeholders),
65+
deployedBytecodeSize: artifact.deployedBytecodeSize,
66+
abi: artifact.abi,
67+
};
68+
}),
10169
);
10270

10371
return [...defaultModules, ...modules];

packages/cli/src/deploy/ensureModules.ts

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { Client, Transport, Chain, Account, Hex, BaseError } from "viem";
2-
import { writeContract } from "@latticexyz/common";
2+
import { resourceToHex, writeContract } from "@latticexyz/common";
33
import { Module, WorldDeploy, worldAbi } from "./common";
44
import { debug } from "./debug";
55
import { isDefined } from "@latticexyz/common/utils";
66
import pRetry from "p-retry";
77
import { LibraryMap } from "./getLibraryMap";
88
import { ensureContractsDeployed } from "@latticexyz/common/internal";
9+
import { encodeSystemCalls } from "@latticexyz/world/internal";
10+
import { systemsConfig as worldSystemsConfig } from "@latticexyz/world/mud.config";
911

1012
export async function ensureModules({
1113
client,
@@ -39,17 +41,55 @@ export async function ensureModules({
3941
pRetry(
4042
async () => {
4143
try {
42-
// append module's ABI so that we can decode any custom errors
43-
const abi = [...worldAbi, ...mod.abi];
4444
const moduleAddress = mod.prepareDeploy(deployerAddress, libraryMap).address;
45-
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
46-
const params = mod.installAsRoot
47-
? ({ functionName: "installRootModule", args: [moduleAddress, mod.installData] } as const)
48-
: ({ functionName: "installModule", args: [moduleAddress, mod.installData] } as const);
45+
46+
// TODO: fix strong types for world ABI etc
47+
// TODO: add return types to get better type safety
48+
const params = (() => {
49+
if (mod.installStrategy === "root") {
50+
return {
51+
functionName: "installRootModule",
52+
args: [moduleAddress, mod.installData],
53+
} as const;
54+
}
55+
56+
if (mod.installStrategy === "delegation") {
57+
return {
58+
functionName: "batchCall",
59+
args: encodeSystemCalls([
60+
{
61+
abi: registrationSystemAbi,
62+
systemId: registrationSystemId,
63+
functionName: "registerDelegation",
64+
args: [moduleAddress, unlimitedDelegationControlId, "0x"],
65+
},
66+
{
67+
abi: registrationSystemAbi,
68+
systemId: registrationSystemId,
69+
functionName: "installModule",
70+
args: [moduleAddress, mod.installData],
71+
},
72+
{
73+
abi: registrationSystemAbi,
74+
systemId: registrationSystemId,
75+
functionName: "unregisterDelegation",
76+
args: [moduleAddress],
77+
},
78+
]),
79+
} as const;
80+
}
81+
82+
return {
83+
functionName: "installModule",
84+
args: [moduleAddress, mod.installData],
85+
} as const;
86+
})();
87+
4988
return await writeContract(client, {
5089
chain: client.chain ?? null,
5190
address: worldDeploy.address,
52-
abi,
91+
// append module's ABI so that we can decode any custom errors
92+
abi: [...worldAbi, ...mod.abi],
5393
...params,
5494
});
5595
} catch (error) {
@@ -74,3 +114,67 @@ export async function ensureModules({
74114
)
75115
).filter(isDefined);
76116
}
117+
118+
// TODO: export from world
119+
const unlimitedDelegationControlId = resourceToHex({ type: "system", namespace: "", name: "unlimited" });
120+
121+
const registrationSystemId = worldSystemsConfig.systems.RegistrationSystem.systemId;
122+
123+
// world/src/modules/init/RegistrationSystem.sol
124+
// TODO: import from world once we fix strongly typed JSON imports
125+
const registrationSystemAbi = [
126+
{
127+
type: "function",
128+
name: "installModule",
129+
inputs: [
130+
{
131+
name: "module",
132+
type: "address",
133+
internalType: "contract IModule",
134+
},
135+
{
136+
name: "encodedArgs",
137+
type: "bytes",
138+
internalType: "bytes",
139+
},
140+
],
141+
outputs: [],
142+
stateMutability: "nonpayable",
143+
},
144+
{
145+
type: "function",
146+
name: "registerDelegation",
147+
inputs: [
148+
{
149+
name: "delegatee",
150+
type: "address",
151+
internalType: "address",
152+
},
153+
{
154+
name: "delegationControlId",
155+
type: "bytes32",
156+
internalType: "ResourceId",
157+
},
158+
{
159+
name: "initCallData",
160+
type: "bytes",
161+
internalType: "bytes",
162+
},
163+
],
164+
outputs: [],
165+
stateMutability: "nonpayable",
166+
},
167+
{
168+
type: "function",
169+
name: "unregisterDelegation",
170+
inputs: [
171+
{
172+
name: "delegatee",
173+
type: "address",
174+
internalType: "address",
175+
},
176+
],
177+
outputs: [],
178+
stateMutability: "nonpayable",
179+
},
180+
] as const;

0 commit comments

Comments
 (0)