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(cli,world): register system ABI onchain #3050

Merged
merged 26 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
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
Loading