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: install module with delegation #3586

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

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.
6 changes: 6 additions & 0 deletions .changeset/hot-pans-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@latticexyz/cli": patch
"@latticexyz/world-module-metadata": patch
---

Metadata module has been updated to install via delegation, making it easier for later module upgrades and to demonstrate modules installed via delegation.
25 changes: 25 additions & 0 deletions .changeset/purple-houses-sell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@latticexyz/world": patch
---

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.

```diff
-encodeSystemCalls(abi, [{
+encodeSystemCalls([{
+ abi,
systemId: '0x...',
functionName: '...',
args: [...],
}]);
```

```diff
-encodeSystemCallsFrom(from, abi, [{
+encodeSystemCallsFrom(from, [{
+ abi,
systemId: '0x...',
functionName: '...',
args: [...],
}]);
```
11 changes: 6 additions & 5 deletions e2e/packages/sync-test/registerDelegationWithSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import { createAsyncErrorHandler } from "./asyncErrors";
import { deployContracts, startViteServer, startBrowserAndPage, openClientWithRootAccount } from "./setup";
import { rpcHttpUrl } from "./setup/constants";
import { waitForInitialSync } from "./data/waitForInitialSync";
import { createBurnerAccount, resourceToHex, transportObserver } from "@latticexyz/common";
import { createBurnerAccount, hexToResource, resourceToHex, transportObserver } from "@latticexyz/common";
import { http, createWalletClient, ClientConfig, encodeFunctionData, toHex } from "viem";
import { mudFoundry } from "@latticexyz/common/chains";
import { encodeEntity } from "@latticexyz/store-sync/recs";
import { callPageFunction } from "./data/callPageFunction";
import worldConfig from "@latticexyz/world/mud.config";
import worldConfig, { systemsConfig as worldSystemsConfig } from "@latticexyz/world/mud.config";
import { callWithSignatureTypes } from "@latticexyz/world-module-callwithsignature/internal";
import { getWorld } from "./data/getWorld";
import { callWithSignature } from "./data/callWithSignature";
Expand Down Expand Up @@ -59,7 +59,8 @@ describe("callWithSignature", async () => {
});

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

// Declare delegation parameters
const delegatee = "0x7203e7ADfDF38519e1ff4f8Da7DCdC969371f377";
Expand All @@ -84,8 +85,8 @@ describe("callWithSignature", async () => {
primaryType: "Call",
message: {
signer: delegator.address,
systemNamespace: "",
systemName: "Registration",
systemNamespace: systemResource.namespace,
systemName: systemResource.name,
callData,
nonce,
},
Expand Down
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,13 @@
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";
import { systemsConfig as worldSystemsConfig } from "@latticexyz/world/mud.config";

export async function ensureModules({
client,
Expand Down Expand Up @@ -39,17 +41,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 +114,67 @@ export async function ensureModules({
)
).filter(isDefined);
}

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

const registrationSystemId = worldSystemsConfig.systems.RegistrationSystem.systemId;

// world/src/modules/init/RegistrationSystem.sol
// TODO: import from world once we fix strongly typed JSON imports
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;
Loading
Loading