Skip to content

Commit

Permalink
feat(cli,world): register system ABI onchain (#3050)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Aug 27, 2024
1 parent e583fc9 commit 31caecc
Show file tree
Hide file tree
Showing 27 changed files with 362 additions and 45 deletions.
5 changes: 5 additions & 0 deletions .changeset/ten-foxes-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/cli": patch
---

In addition to table labels, system labels and ABIs are now registered onchain during deploy.
15 changes: 11 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ yarn.lock
lerna-debug.log
yarn-error.log
.turbo
.attest
.tstrace

.env*

# We don't want projects created from templates to ignore their lockfiles, but we don't
# want to check them in here, so we'll ignore them from the root.
templates/*/pnpm-lock.yaml

.env

# mud test data
test-data/world-logs-bulk-*.json
test-data/world-logs-query.json

.attest
.tstrace
# mud artifacts
.mud

# sqlite indexer data
*.db
*.db-journal
2 changes: 0 additions & 2 deletions e2e/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
node_modules
*.db
*.db-journal
7 changes: 6 additions & 1 deletion examples/local-explorer/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
indexer.db
node_modules

# mud artifacts
.mud
# sqlite indexer data
*.db
*.db-journal
4 changes: 2 additions & 2 deletions packages/cli/src/build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { tablegen } from "@latticexyz/store/codegen";
import { worldgen } from "@latticexyz/world/node";
import { buildSystemsManifest, worldgen } from "@latticexyz/world/node";
import { World as WorldConfig } from "@latticexyz/world";
import { forge } from "@latticexyz/common/foundry";
import { execa } from "execa";
Expand All @@ -21,7 +21,7 @@ export async function build({
foundryProfile = process.env.FOUNDRY_PROFILE,
}: BuildOptions): Promise<void> {
await Promise.all([tablegen({ rootDir, config }), worldgen({ rootDir, config })]);

await forge(["build"], { profile: foundryProfile });
await buildSystemsManifest({ rootDir, config });
await execa("mud", ["abi-ts"], { stdio: "inherit" });
}
6 changes: 5 additions & 1 deletion packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ export type System = DeterministicContract & {
readonly allowedAddresses: readonly Hex[];
readonly allowedSystemIds: readonly Hex[];
// world registration
// TODO: replace this with system manifest data
readonly worldFunctions: readonly WorldFunction[];
// human readable ABIs to register onchain
readonly abi: readonly string[];
readonly worldAbi: readonly string[];
};

export type DeployedSystem = Omit<
System,
"label" | "namespaceLabel" | "abi" | "prepareDeploy" | "deployedBytecodeSize" | "allowedSystemIds"
"label" | "namespaceLabel" | "abi" | "worldAbi" | "prepareDeploy" | "deployedBytecodeSize" | "allowedSystemIds"
> & {
address: Address;
};
Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,16 @@ export async function deploy({
});

const tableTags = tables.map(({ tableId: resourceId, label }) => ({ resourceId, tag: "label", value: label }));
const systemTags = systems.flatMap(({ systemId: resourceId, label, abi, worldAbi }) => [
{ resourceId, tag: "label", value: label },
{ resourceId, tag: "abi", value: abi.join("\n") },
{ resourceId, tag: "worldAbi", value: worldAbi.join("\n") },
]);

const tagTxs = await ensureResourceTags({
client,
worldDeploy,
tags: tableTags,
tags: [...tableTags, ...systemTags],
valueToHex: stringToHex,
});

Expand Down
16 changes: 13 additions & 3 deletions packages/cli/src/deploy/resolveConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path";
import { resolveSystems } from "@latticexyz/world/node";
import { loadSystemsManifest, resolveSystems } from "@latticexyz/world/node";
import { Library, System, WorldFunction } from "./common";
import { Hex, isHex, toFunctionSelector, toFunctionSignature } from "viem";
import { getContractData } from "../utils/getContractData";
Expand Down Expand Up @@ -40,12 +40,21 @@ export async function resolveConfig({
.map(toFunctionSignature);

const configSystems = await resolveSystems({ rootDir, config });
const systemsManifest = await loadSystemsManifest({ rootDir, config });

const systems = configSystems
.filter((system) => !system.deploy.disabled)
.map((system): System => {
const manifest = systemsManifest.systems.find(({ systemId }) => systemId === system.systemId);
if (!manifest) {
throw new Error(
`System "${system.label}" not found in systems manifest. Run \`mud build\` before trying again.`,
);
}

const contractData = getContractData(`${system.label}.sol`, system.label, forgeOutDir);

// TODO: replace this with manifest
const worldFunctions = system.deploy.registerWorldFunctions
? contractData.abi
.filter((item): item is typeof item & { type: "function" } => item.type === "function")
Expand All @@ -64,7 +73,7 @@ export async function resolveConfig({
})
: [];

// TODO: move to resolveSystems?
// TODO: move to resolveSystems? or system manifest?
const allowedAddresses = system.accessList.filter((target): target is Hex => isHex(target));
const allowedSystemIds = system.accessList
.filter((target) => !isHex(target))
Expand All @@ -80,8 +89,9 @@ export async function resolveConfig({
allowedSystemIds,
prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders),
deployedBytecodeSize: contractData.deployedBytecodeSize,
abi: contractData.abi,
worldFunctions,
abi: manifest.abi,
worldAbi: manifest.worldAbi,
};
});

Expand Down
13 changes: 6 additions & 7 deletions packages/common/src/codegen/utils/contractToInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,20 @@ interface SymbolImport {
/**
* Parse the contract data to get the functions necessary to generate an interface,
* and symbols to import from the original contract.
* @param data contents of a file with the solidity contract
* @param source contents of a file with the solidity contract
* @param contractName name of the contract
* @returns interface data
*/
export function contractToInterface(
data: string,
source: string,
contractName: string,
): {
functions: ContractInterfaceFunction[];
errors: ContractInterfaceError[];
symbolImports: SymbolImport[];
} {
const ast = parse(data);

const contractNode = findContractNode(parse(data), contractName);
const ast = parse(source);
const contractNode = findContractNode(ast, contractName);
let symbolImports: SymbolImport[] = [];
const functions: ContractInterfaceFunction[] = [];
const errors: ContractInterfaceError[] = [];
Expand Down Expand Up @@ -107,8 +106,8 @@ export function contractToInterface(
};
}

function findContractNode(ast: SourceUnit, contractName: string): ContractDefinition | undefined {
let contract = undefined;
export function findContractNode(ast: SourceUnit, contractName: string): ContractDefinition | undefined {
let contract: ContractDefinition | undefined = undefined;

visit(ast, {
ContractDefinition(node) {
Expand Down
3 changes: 3 additions & 0 deletions packages/common/src/utils/indent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function indent(message: string, indentation = " "): string {
return message.replaceAll(/(^|\n)/g, `$1${indentation}`);
}
1 change: 1 addition & 0 deletions packages/common/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from "./chunk";
export * from "./groupBy";
export * from "./identity";
export * from "./includes";
export * from "./indent";
export * from "./isDefined";
export * from "./isNotNull";
export * from "./iteratorToArray";
Expand Down
5 changes: 1 addition & 4 deletions packages/world/ts/debug.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import createDebug from "debug";

export const debug = createDebug("mud:world");
export const error = createDebug("mud:world");

// Pipe debug output to stdout instead of stderr
debug.log = console.debug.bind(console);

// Pipe error output to stderr
export const error = createDebug("mud:world");
error.log = console.error.bind(console);
84 changes: 84 additions & 0 deletions packages/world/ts/node/buildSystemsManifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { mkdir, writeFile } from "node:fs/promises";
import { ResolvedSystem, resolveSystems } from "./resolveSystems";
import { World } from "../config/v2";
import { ContractArtifact, systemsManifestFilename } from "./common";
import { findContractArtifacts } from "./findContractArtifacts";
import { getOutDirectory as getForgeOutDirectory } from "@latticexyz/common/foundry";
import path from "node:path";
import { Abi, Hex, isHex } from "viem";
import { formatAbi, formatAbiItem } from "abitype";
import { debug } from "./debug";
import { type } from "arktype";

export const SystemsManifest = type({
systems: [
{
// labels
namespaceLabel: "string",
label: "string",
// resource ID
namespace: "string",
name: "string",
systemId: ["string", ":", (s): s is Hex => isHex(s, { strict: false })],
// abi
abi: "string[]",
worldAbi: "string[]",
},
"[]",
],
createdAt: "number",
});

export async function buildSystemsManifest(opts: { rootDir: string; config: World }): Promise<void> {
// we have to import these at runtime because they may not yet exist at build time
const { default: IBaseWorldAbi } = await import("../../out/IBaseWorld.sol/IBaseWorld.abi.json");
const { default: SystemAbi } = await import("../../out/System.sol/System.abi.json");
const excludedAbi = formatAbi([
...IBaseWorldAbi.filter((item) => item.type === "event" || item.type === "error"),
...SystemAbi,
] as Abi);

const systems = await resolveSystems(opts);

// TODO: expose a `cwd` option to make sure this runs relative to `rootDir`
const forgeOutDir = await getForgeOutDirectory();
const contractArtifacts = await findContractArtifacts({ forgeOutDir });

function getSystemArtifact(system: ResolvedSystem): ContractArtifact {
const artifact = contractArtifacts.find((a) => a.sourcePath === system.sourcePath && a.name === system.label);
if (!artifact) {
throw new Error(
`Could not find build artifact for system \`${system.label}\` at \`${system.sourcePath}\`. Did \`forge build\` run successfully?`,
);
}
return artifact;
}

const manifest = {
systems: systems.map((system): (typeof SystemsManifest)["infer"]["systems"][number] => {
const artifact = getSystemArtifact(system);
const abi = artifact.abi.filter((item) => !excludedAbi.includes(formatAbiItem(item)));
const worldAbi = system.deploy.registerWorldFunctions
? abi.map((item) => (item.type === "function" ? { ...item, name: `${system.namespace}__${item.name}` } : item))
: [];
return {
// labels
namespaceLabel: system.namespaceLabel,
label: system.label,
// resource ID
namespace: system.namespace,
name: system.name,
systemId: system.systemId,
// abi
abi: formatAbi(abi).sort((a, b) => a.localeCompare(b)),
worldAbi: formatAbi(worldAbi).sort((a, b) => a.localeCompare(b)),
};
}),
createdAt: Date.now(),
} satisfies typeof SystemsManifest.infer;

const outFile = path.join(opts.rootDir, systemsManifestFilename);
await mkdir(path.dirname(outFile), { recursive: true });
await writeFile(outFile, JSON.stringify(manifest, null, 2) + "\n");
debug("Wrote systems manifest to", systemsManifestFilename);
}
28 changes: 28 additions & 0 deletions packages/world/ts/node/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Abi, Hex } from "viem";

// https://eips.ethereum.org/EIPS/eip-170
export const contractSizeLimit = parseInt("6000", 16);

// relative to project root dir (`rootDir`)
export const systemsManifestFilename = ".mud/local/systems.json";

export type ReferenceIdentifier = {
/**
* Path to source file, e.g. `src/SomeLib.sol`
*/
sourcePath: string;
/**
* Reference name, e.g. `SomeLib`
*/
name: string;
};

export type PendingBytecode = readonly (Hex | ReferenceIdentifier)[];

export type ContractArtifact = {
readonly sourcePath: string;
readonly name: string;
// TODO: rename `createCode` or `creationBytecode` to better differentiate from deployed bytecode?
readonly bytecode: PendingBytecode;
readonly abi: Abi;
};
3 changes: 3 additions & 0 deletions packages/world/ts/node/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { debug as parentDebug } from "../debug";

export const debug = parentDebug.extend("codegen");
Loading

0 comments on commit 31caecc

Please sign in to comment.