diff --git a/.changeset/thin-boxes-sparkle.md b/.changeset/thin-boxes-sparkle.md new file mode 100644 index 0000000000..c03d8c9227 --- /dev/null +++ b/.changeset/thin-boxes-sparkle.md @@ -0,0 +1,9 @@ +--- +"@latticexyz/cli": minor +--- + +`mud deploy` now supports public/linked libraries. + +This helps with cases where system contracts would exceed the EVM bytecode size limit and logic would need to be split into many smaller systems. + +Instead of the overhead and complexity of system-to-system calls, this logic can now be moved into public libraries that will be deployed alongside your systems and automatically `delegatecall`ed. diff --git a/e2e/packages/contracts/src/codegen/world/ILibWrapperSystem.sol b/e2e/packages/contracts/src/codegen/world/ILibWrapperSystem.sol new file mode 100644 index 0000000000..fffc229af7 --- /dev/null +++ b/e2e/packages/contracts/src/codegen/world/ILibWrapperSystem.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/* Autogenerated file. Do not edit manually. */ + +/** + * @title ILibWrapperSystem + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This interface is automatically generated from the corresponding system contract. Do not edit manually. + */ +interface ILibWrapperSystem { + function callLib() external pure returns (string memory); + + function callFreeFunc() external pure returns (string memory); +} diff --git a/e2e/packages/contracts/src/codegen/world/IWorld.sol b/e2e/packages/contracts/src/codegen/world/IWorld.sol index 62448e2b51..da4671b1e1 100644 --- a/e2e/packages/contracts/src/codegen/world/IWorld.sol +++ b/e2e/packages/contracts/src/codegen/world/IWorld.sol @@ -6,6 +6,7 @@ pragma solidity >=0.8.24; import { IBaseWorld } from "@latticexyz/world/src/codegen/interfaces/IBaseWorld.sol"; import { ICustomErrorsSystem } from "./ICustomErrorsSystem.sol"; +import { ILibWrapperSystem } from "./ILibWrapperSystem.sol"; import { INumberListSystem } from "./INumberListSystem.sol"; import { INumberSystem } from "./INumberSystem.sol"; import { IVectorSystem } from "./IVectorSystem.sol"; @@ -17,4 +18,11 @@ import { IVectorSystem } from "./IVectorSystem.sol"; * that are dynamically registered in the World during deployment. * @dev This is an autogenerated file; do not edit manually. */ -interface IWorld is IBaseWorld, ICustomErrorsSystem, INumberListSystem, INumberSystem, IVectorSystem {} +interface IWorld is + IBaseWorld, + ICustomErrorsSystem, + ILibWrapperSystem, + INumberListSystem, + INumberSystem, + IVectorSystem +{} diff --git a/e2e/packages/contracts/src/libraries/Lib2.sol b/e2e/packages/contracts/src/libraries/Lib2.sol new file mode 100644 index 0000000000..454fb35188 --- /dev/null +++ b/e2e/packages/contracts/src/libraries/Lib2.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { Lib3 } from "../systems/LibWrapperSystem.sol"; + +/** + * @title Library 2 + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Testing that the deployer can handle nesting of 2 libraries + * Included in a separate file to test handling libraries in different files + */ +library Lib2 { + function call() public pure returns (string memory) { + return Lib3.call(); + } +} diff --git a/e2e/packages/contracts/src/libraries/Lib4and5.sol b/e2e/packages/contracts/src/libraries/Lib4and5.sol new file mode 100644 index 0000000000..cc0371312f --- /dev/null +++ b/e2e/packages/contracts/src/libraries/Lib4and5.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +/** + * @title Library 4 + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Testing that the deployer can handle nesting of 4 libraries + * Included in a separate file to test handling libraries in different files + */ +library Lib4 { + function call() public pure returns (string memory) { + return Lib5.call(); + } +} + +/** + * @title Library 5 + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Testing that the deployer can handle nesting of 4 libraries + * Included in a separate file to test handling libraries in different files + */ +library Lib5 { + function call() public pure returns (string memory) { + return "success"; + } +} diff --git a/e2e/packages/contracts/src/systems/LibWrapperSystem.sol b/e2e/packages/contracts/src/systems/LibWrapperSystem.sol new file mode 100644 index 0000000000..aa3b8392fb --- /dev/null +++ b/e2e/packages/contracts/src/systems/LibWrapperSystem.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { System } from "@latticexyz/world/src/System.sol"; +import { Lib2 } from "../libraries/Lib2.sol"; +import { Lib4 } from "../libraries/Lib4and5.sol"; + +/** + * @dev For calling a library using a free function. + */ +function freeLibWrapper() pure returns (string memory) { + return Lib1.call(); +} + +/** + * @title Library 1 + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Used for testing that the deployer can handle a single library call + */ +library Lib1 { + function call() public pure returns (string memory) { + return Lib2.call(); + } +} + +/** + * @title Library 3 + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev Testing that the deployer can handle nesting of 3 libraries + */ +library Lib3 { + function call() public pure returns (string memory) { + return Lib4.call(); + } +} + +/** + * @title Library Wrapper System + * @author MUD (https://mud.dev) by Lattice (https://lattice.xyz) + * @dev This contract is used for testing that the deployer can handle deeply nested public libraries + */ +contract LibWrapperSystem is System { + function callLib() public pure returns (string memory) { + return Lib1.call(); + } + + function callFreeFunc() public pure returns (string memory) { + return freeLibWrapper(); + } +} diff --git a/e2e/packages/contracts/test/PublicLibrary.t.sol b/e2e/packages/contracts/test/PublicLibrary.t.sol new file mode 100644 index 0000000000..d159a038a0 --- /dev/null +++ b/e2e/packages/contracts/test/PublicLibrary.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.24; + +import { MudTest } from "@latticexyz/world/test/MudTest.t.sol"; + +import { IWorld } from "../src/codegen/world/IWorld.sol"; + +contract PublicLibraryTest is MudTest { + /** + * @dev Test that the deployer can handle deeply nested public libraries. + */ + function testNesting() public { + assertEq(IWorld(worldAddress).callLib(), "success"); + assertEq(IWorld(worldAddress).callFreeFunc(), "success"); + } +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 2c429e3b87..1e02298487 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,6 +59,7 @@ "path": "^0.12.7", "rxjs": "7.5.5", "throttle-debounce": "^5.0.0", + "toposort": "^2.0.2", "typescript": "5.1.6", "viem": "2.7.12", "yargs": "^17.7.1", @@ -72,6 +73,7 @@ "@types/node": "^18.15.11", "@types/openurl": "^1.0.0", "@types/throttle-debounce": "^5.0.0", + "@types/toposort": "^2.0.6", "@types/yargs": "^17.0.10", "ds-test": "https://github.com/dapphub/ds-test.git#e282159d5170298eb2455a6c05280ab5a73a4ef0", "forge-std": "https://github.com/foundry-rs/forge-std.git#74cfb77e308dd188d2f58864aaf44963ae6b88b1", diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index 298ec652f4..2fba024595 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -47,13 +47,48 @@ export type WorldFunction = { readonly systemFunctionSelector: Hex; }; +export type LibraryPlaceholder = { + /** + * Path to library source file, e.g. `src/libraries/SomeLib.sol` + */ + path: string; + /** + * Library name, e.g. `SomeLib` + */ + name: string; + /** + * Byte offset of placeholder in bytecode + */ + start: number; + /** + * Size of placeholder to replace in bytes + */ + length: number; +}; + export type DeterministicContract = { - readonly getAddress: (deployer: Address) => Address; - readonly bytecode: Hex; + readonly prepareDeploy: ( + deployer: Address, + libraries: readonly Library[], + ) => { + readonly address: Address; + readonly bytecode: Hex; + }; readonly deployedBytecodeSize: number; readonly abi: Abi; }; +export type Library = DeterministicContract & { + /** + * Path to library source file, e.g. `src/libraries/SomeLib.sol` + */ + path: string; + /** + * Library name, e.g. `SomeLib` + */ + name: string; +}; + export type System = DeterministicContract & { readonly namespace: string; readonly name: string; @@ -64,10 +99,7 @@ export type System = DeterministicContract & { readonly functions: readonly WorldFunction[]; }; -export type DeployedSystem = Omit< - System, - "getAddress" | "abi" | "bytecode" | "deployedBytecodeSize" | "allowedSystemIds" -> & { +export type DeployedSystem = Omit & { address: Address; }; @@ -82,4 +114,5 @@ export type Config = { readonly tables: Tables; readonly systems: readonly System[]; readonly modules: readonly Module[]; + readonly libraries: readonly Library[]; }; diff --git a/packages/cli/src/deploy/createPrepareDeploy.ts b/packages/cli/src/deploy/createPrepareDeploy.ts new file mode 100644 index 0000000000..a1846de350 --- /dev/null +++ b/packages/cli/src/deploy/createPrepareDeploy.ts @@ -0,0 +1,28 @@ +import { DeterministicContract, Library, LibraryPlaceholder, salt } from "./common"; +import { spliceHex } from "@latticexyz/common"; +import { Hex, getCreate2Address, Address } from "viem"; + +export function createPrepareDeploy( + bytecodeWithPlaceholders: Hex, + placeholders: readonly LibraryPlaceholder[], +): DeterministicContract["prepareDeploy"] { + return function prepareDeploy(deployer: Address, libraries: readonly Library[]) { + let bytecode = bytecodeWithPlaceholders; + for (const placeholder of placeholders) { + const library = libraries.find((lib) => lib.path === placeholder.path && lib.name === placeholder.name); + if (!library) { + throw new Error(`Could not find library for bytecode placeholder ${placeholder.path}:${placeholder.name}`); + } + bytecode = spliceHex( + bytecode, + placeholder.start, + placeholder.length, + library.prepareDeploy(deployer, libraries).address, + ); + } + return { + bytecode, + address: getCreate2Address({ from: deployer, bytecode, salt }), + }; + }; +} diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index febcca2963..27858fc968 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -1,4 +1,4 @@ -import { Account, Address, Chain, Client, Hex, Transport, getAddress } from "viem"; +import { Account, Address, Chain, Client, Hex, Transport } from "viem"; import { ensureDeployer } from "./ensureDeployer"; import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; @@ -12,7 +12,6 @@ import { Table } from "./configToTables"; import { ensureNamespaceOwner } from "./ensureNamespaceOwner"; import { debug } from "./debug"; import { resourceToLabel } from "@latticexyz/common"; -import { uniqueBy } from "@latticexyz/common/utils"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; import { randomBytes } from "crypto"; import { ensureWorldFactory } from "./ensureWorldFactory"; @@ -55,13 +54,18 @@ export async function deploy({ client, deployerAddress, contracts: [ - ...uniqueBy(config.systems, (system) => getAddress(system.getAddress(deployerAddress))).map((system) => ({ - bytecode: system.bytecode, + ...config.libraries.map((library) => ({ + bytecode: library.prepareDeploy(deployerAddress, config.libraries).bytecode, + deployedBytecodeSize: library.deployedBytecodeSize, + label: `${library.path}:${library.name} library`, + })), + ...config.systems.map((system) => ({ + bytecode: system.prepareDeploy(deployerAddress, config.libraries).bytecode, deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceToLabel(system)} system`, })), - ...uniqueBy(config.modules, (mod) => getAddress(mod.getAddress(deployerAddress))).map((mod) => ({ - bytecode: mod.bytecode, + ...config.modules.map((mod) => ({ + bytecode: mod.prepareDeploy(deployerAddress, config.libraries).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, })), @@ -98,6 +102,7 @@ export async function deploy({ const systemTxs = await ensureSystems({ client, deployerAddress, + libraries: config.libraries, worldDeploy, systems: config.systems, }); @@ -109,6 +114,7 @@ export async function deploy({ const moduleTxs = await ensureModules({ client, deployerAddress, + libraries: config.libraries, worldDeploy, modules: config.modules, }); diff --git a/packages/cli/src/deploy/ensureContract.ts b/packages/cli/src/deploy/ensureContract.ts index d86269ee54..b9722d9a9e 100644 --- a/packages/cli/src/deploy/ensureContract.ts +++ b/packages/cli/src/deploy/ensureContract.ts @@ -22,6 +22,10 @@ export async function ensureContract({ readonly client: Client; readonly deployerAddress: Hex; } & Contract): Promise { + if (bytecode.includes("__$")) { + throw new Error(`Found unlinked public library in ${label} bytecode`); + } + const address = getCreate2Address({ from: deployerAddress, salt, bytecode }); const contractCode = await getBytecode(client, { address, blockTag: "pending" }); diff --git a/packages/cli/src/deploy/ensureContractsDeployed.ts b/packages/cli/src/deploy/ensureContractsDeployed.ts index ca9d304850..789fd0928c 100644 --- a/packages/cli/src/deploy/ensureContractsDeployed.ts +++ b/packages/cli/src/deploy/ensureContractsDeployed.ts @@ -2,6 +2,7 @@ import { Client, Transport, Chain, Account, Hex } from "viem"; import { waitForTransactionReceipt } from "viem/actions"; import { debug } from "./debug"; import { Contract, ensureContract } from "./ensureContract"; +import { uniqueBy } from "@latticexyz/common/utils"; export async function ensureContractsDeployed({ client, @@ -12,8 +13,11 @@ export async function ensureContractsDeployed({ readonly deployerAddress: Hex; readonly contracts: readonly Contract[]; }): Promise { + // Deployments assume a deterministic deployer, so we only need to deploy the unique bytecode + const uniqueContracts = uniqueBy(contracts, (contract) => contract.bytecode); + const txs = ( - await Promise.all(contracts.map((contract) => ensureContract({ client, deployerAddress, ...contract }))) + await Promise.all(uniqueContracts.map((contract) => ensureContract({ client, deployerAddress, ...contract }))) ).flat(); if (txs.length) { diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 5a658a85c2..31d689d489 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -1,19 +1,21 @@ -import { Client, Transport, Chain, Account, Hex, BaseError, getAddress } from "viem"; +import { Client, Transport, Chain, Account, Hex, BaseError } from "viem"; import { writeContract } from "@latticexyz/common"; -import { Module, WorldDeploy, worldAbi } from "./common"; +import { Library, Module, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; -import { isDefined, uniqueBy, wait } from "@latticexyz/common/utils"; +import { isDefined, wait } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; export async function ensureModules({ client, deployerAddress, + libraries, worldDeploy, modules, }: { readonly client: Client; - readonly deployerAddress: Hex; // TODO: move this into WorldDeploy to reuse a world's deployer? + readonly deployerAddress: Hex; + readonly libraries: readonly Library[]; readonly worldDeploy: WorldDeploy; readonly modules: readonly Module[]; }): Promise { @@ -22,8 +24,8 @@ export async function ensureModules({ await ensureContractsDeployed({ client, deployerAddress, - contracts: uniqueBy(modules, (mod) => getAddress(mod.getAddress(deployerAddress))).map((mod) => ({ - bytecode: mod.bytecode, + contracts: modules.map((mod) => ({ + bytecode: mod.prepareDeploy(deployerAddress, libraries).bytecode, deployedBytecodeSize: mod.deployedBytecodeSize, label: `${mod.name} module`, })), @@ -36,6 +38,7 @@ export async function ensureModules({ pRetry( async () => { try { + const moduleAddress = mod.prepareDeploy(deployerAddress, libraries).address; return mod.installAsRoot ? await writeContract(client, { chain: client.chain ?? null, @@ -43,7 +46,7 @@ export async function ensureModules({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installRootModule", - args: [mod.getAddress(deployerAddress), mod.installData], + args: [moduleAddress, mod.installData], }) : await writeContract(client, { chain: client.chain ?? null, @@ -51,7 +54,7 @@ export async function ensureModules({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "installModule", - args: [mod.getAddress(deployerAddress), mod.installData], + args: [moduleAddress, mod.installData], }); } catch (error) { if (error instanceof BaseError && error.message.includes("Module_AlreadyInstalled")) { diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index dc1661a8c2..4d18dc1892 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -1,10 +1,10 @@ import { Client, Transport, Chain, Account, Hex, getAddress, Address } from "viem"; import { writeContract, resourceToLabel } from "@latticexyz/common"; -import { System, WorldDeploy, worldAbi } from "./common"; +import { Library, System, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; -import { uniqueBy, wait } from "@latticexyz/common/utils"; +import { wait } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; @@ -13,11 +13,13 @@ import { ensureContractsDeployed } from "./ensureContractsDeployed"; export async function ensureSystems({ client, deployerAddress, + libraries, worldDeploy, systems, }: { readonly client: Client; - readonly deployerAddress: Hex; // TODO: move this into WorldDeploy to reuse a world's deployer? + readonly deployerAddress: Hex; + readonly libraries: readonly Library[]; readonly worldDeploy: WorldDeploy; readonly systems: readonly System[]; }): Promise { @@ -32,7 +34,7 @@ export async function ensureSystems({ worldSystems.some( (worldSystem) => worldSystem.systemId === system.systemId && - getAddress(worldSystem.address) === getAddress(system.getAddress(deployerAddress)), + getAddress(worldSystem.address) === getAddress(system.prepareDeploy(deployerAddress, libraries).address), ), ); if (existingSystems.length) { @@ -47,7 +49,7 @@ export async function ensureSystems({ worldSystems.some( (worldSystem) => worldSystem.systemId === system.systemId && - getAddress(worldSystem.address) !== getAddress(system.getAddress(deployerAddress)), + getAddress(worldSystem.address) !== getAddress(system.prepareDeploy(deployerAddress, libraries).address), ), ); if (systemsToUpgrade.length) { @@ -64,8 +66,8 @@ export async function ensureSystems({ await ensureContractsDeployed({ client, deployerAddress, - contracts: uniqueBy(missingSystems, (system) => getAddress(system.getAddress(deployerAddress))).map((system) => ({ - bytecode: system.bytecode, + contracts: missingSystems.map((system) => ({ + bytecode: system.prepareDeploy(deployerAddress, libraries).bytecode, deployedBytecodeSize: system.deployedBytecodeSize, label: `${resourceToLabel(system)} system`, })), @@ -81,7 +83,7 @@ export async function ensureSystems({ abi: worldAbi, // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) functionName: "registerSystem", - args: [system.systemId, system.getAddress(deployerAddress), system.allowAll], + args: [system.systemId, system.prepareDeploy(deployerAddress, libraries).address, system.allowAll], }), { retries: 3, @@ -109,7 +111,7 @@ export async function ensureSystems({ resourceId: system.systemId, address: worldSystems.find((s) => s.systemId === systemId)?.address ?? - systems.find((s) => s.systemId === systemId)?.getAddress(deployerAddress), + systems.find((s) => s.systemId === systemId)?.prepareDeploy(deployerAddress, libraries).address, })) .filter((access): access is typeof access & { address: Address } => access.address != null), ), diff --git a/packages/cli/src/deploy/findLibraries.ts b/packages/cli/src/deploy/findLibraries.ts new file mode 100644 index 0000000000..c2a87830cd --- /dev/null +++ b/packages/cli/src/deploy/findLibraries.ts @@ -0,0 +1,36 @@ +import { readFileSync } from "fs"; +import glob from "glob"; +import { orderByDependencies } from "./orderByDependencies"; +import { LinkReferences } from "../utils/findPlaceholders"; + +export function findLibraries(forgeOutDir: string): readonly { + readonly path: string; + readonly name: string; +}[] { + const artifacts = glob + .sync(`${forgeOutDir}/**/*.json`, { ignore: "**/*.abi.json" }) + .map((path) => JSON.parse(readFileSync(path, "utf8"))); + + const libraries = artifacts.flatMap((artifact) => { + if (!artifact.metadata) return []; + + const contractPath = Object.keys(artifact.metadata.settings.compilationTarget)[0]; + const contractName = artifact.metadata.settings.compilationTarget[contractPath]; + const linkReferences = artifact.bytecode.linkReferences as LinkReferences; + + return Object.entries(linkReferences).flatMap(([libraryPath, reference]) => + Object.keys(reference).map((libraryName) => ({ + path: libraryPath, + name: libraryName, + dependentPath: contractPath, + dependentName: contractName, + })), + ); + }); + + return orderByDependencies( + libraries, + (lib) => `${lib.path}:${lib.name}`, + (lib) => [`${lib.dependentPath}:${lib.dependentName}`], + ); +} diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index b3cfa98eb6..1c2831e26e 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -1,5 +1,5 @@ -import { DeployedSystem, System, WorldDeploy, worldTables } from "./common"; -import { Address, Client } from "viem"; +import { DeployedSystem, WorldDeploy, worldTables } from "./common"; +import { Client } from "viem"; import { getResourceIds } from "./getResourceIds"; import { hexToResource, resourceToLabel } from "@latticexyz/common"; import { getTableValue } from "./getTableValue"; @@ -23,7 +23,7 @@ export async function getSystems({ debug("looking up systems", systems.map(resourceToLabel).join(", ")); return await Promise.all( - systems.map(async (system) => { + systems.map(async (system): Promise => { const { system: address, publicAccess } = await getTableValue({ client, worldDeploy, diff --git a/packages/cli/src/deploy/orderByDependencies.ts b/packages/cli/src/deploy/orderByDependencies.ts new file mode 100644 index 0000000000..ec54e6724e --- /dev/null +++ b/packages/cli/src/deploy/orderByDependencies.ts @@ -0,0 +1,12 @@ +import toposort from "toposort"; + +export function orderByDependencies( + items: readonly T[], + itemKey: (item: T) => string, + dependencyKeys: (item: T) => string[], +): readonly T[] { + const dependencyOrder = toposort( + items.flatMap((item) => dependencyKeys(item).map((dependency) => [itemKey(item), dependency] as [string, string])), + ); + return [...items].sort((a, b) => dependencyOrder.indexOf(itemKey(a)) - dependencyOrder.indexOf(itemKey(b))); +} diff --git a/packages/cli/src/deploy/resolveConfig.ts b/packages/cli/src/deploy/resolveConfig.ts index 2cde8cfc1f..2c4171a943 100644 --- a/packages/cli/src/deploy/resolveConfig.ts +++ b/packages/cli/src/deploy/resolveConfig.ts @@ -1,15 +1,18 @@ +import path from "path"; import { resolveWorldConfig } from "@latticexyz/world"; -import { Config, ConfigInput, WorldFunction, salt } from "./common"; +import { Config, ConfigInput, Library, Module, System, WorldFunction } from "./common"; import { resourceToHex } from "@latticexyz/common"; import { resolveWithContext } from "@latticexyz/config"; import { encodeField } from "@latticexyz/protocol-parser"; import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type"; -import { Hex, getCreate2Address, hexToBytes, bytesToHex, Address, toFunctionSelector, toFunctionSignature } from "viem"; +import { Hex, hexToBytes, bytesToHex, toFunctionSelector, toFunctionSignature } from "viem"; import { getExistingContracts } from "../utils/getExistingContracts"; -import { defaultModuleContracts } from "../utils/modules/constants"; -import { getContractData } from "../utils/utils/getContractData"; +import { defaultModuleContracts } from "../utils/defaultModuleContracts"; +import { getContractData } from "../utils/getContractData"; import { configToTables } from "./configToTables"; import { groupBy } from "@latticexyz/common/utils"; +import { findLibraries } from "./findLibraries"; +import { createPrepareDeploy } from "./createPrepareDeploy"; // TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668 @@ -22,21 +25,33 @@ export function resolveConfig({ forgeSourceDir: string; forgeOutDir: string; }): Config { + const libraries = findLibraries(forgeOutDir).map((library): Library => { + // foundry/solc flattens artifacts, so we just use the path basename + const contractData = getContractData(path.basename(library.path), library.name, forgeOutDir); + return { + path: library.path, + name: library.name, + abi: contractData.abi, + prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), + deployedBytecodeSize: contractData.deployedBytecodeSize, + }; + }); + const tables = configToTables(config); // TODO: should the config parser/loader help with resolving systems? const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename); const resolvedConfig = resolveWorldConfig(config, contractNames); - const baseSystemContractData = getContractData("System", forgeOutDir); + const baseSystemContractData = getContractData("System.sol", "System", forgeOutDir); const baseSystemFunctions = baseSystemContractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") .map(toFunctionSignature); - const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]) => { + const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]): System => { const namespace = config.namespace; const name = system.name; const systemId = resourceToHex({ type: "system", namespace, name }); - const contractData = getContractData(systemName, forgeOutDir); + const contractData = getContractData(`${systemName}.sol`, systemName, forgeOutDir); const systemFunctions = contractData.abi .filter((item): item is typeof item & { type: "function" } => item.type === "function") @@ -63,8 +78,7 @@ export function resolveConfig({ allowedSystemIds: system.accessListSystems.map((name) => resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name }), ), - getAddress: (deployer: Address) => getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), - bytecode: contractData.bytecode, + prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, functions: systemFunctions, @@ -102,10 +116,10 @@ export function resolveConfig({ ), }; - const modules = config.modules.map((mod) => { + const modules = config.modules.map((mod): Module => { const contractData = defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ?? - getContractData(mod.name, forgeOutDir); + getContractData(`${mod.name}.sol`, mod.name, forgeOutDir); const installArgs = mod.args .map((arg) => resolveWithContext(arg, resolveContext)) .map((arg) => { @@ -119,8 +133,7 @@ export function resolveConfig({ name: mod.name, installAsRoot: mod.root, installData: installArgs.length === 0 ? "0x" : installArgs[0], - getAddress: (deployer: Address) => getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }), - bytecode: contractData.bytecode, + prepareDeploy: createPrepareDeploy(contractData.bytecode, contractData.placeholders), deployedBytecodeSize: contractData.deployedBytecodeSize, abi: contractData.abi, }; @@ -130,5 +143,6 @@ export function resolveConfig({ tables, systems, modules, + libraries, }; } diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index cc9804db7b..837026c9da 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -12,7 +12,7 @@ import chalk from "chalk"; import { MUDError } from "@latticexyz/common/errors"; import { resolveConfig } from "./deploy/resolveConfig"; import { getChainId } from "viem/actions"; -import { postDeploy } from "./utils/utils/postDeploy"; +import { postDeploy } from "./utils/postDeploy"; import { WorldDeploy } from "./deploy/common"; import { build } from "./build"; @@ -99,6 +99,7 @@ in your contracts directory to use the default anvil private key.`, }), account: privateKeyToAccount(privateKey), }); + console.log("Deploying from", client.account.address); const startTime = Date.now(); diff --git a/packages/cli/src/utils/modules/constants.ts b/packages/cli/src/utils/defaultModuleContracts.ts similarity index 80% rename from packages/cli/src/utils/modules/constants.ts rename to packages/cli/src/utils/defaultModuleContracts.ts index 239860ba6f..debb7b6828 100644 --- a/packages/cli/src/utils/modules/constants.ts +++ b/packages/cli/src/utils/defaultModuleContracts.ts @@ -2,6 +2,7 @@ import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValue import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" }; import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" }; import { Abi, Hex, size } from "viem"; +import { findPlaceholders } from "./findPlaceholders"; // These modules are always deployed export const defaultModuleContracts = [ @@ -9,18 +10,21 @@ export const defaultModuleContracts = [ name: "KeysWithValueModule", abi: KeysWithValueModuleData.abi as Abi, bytecode: KeysWithValueModuleData.bytecode.object as Hex, + placeholders: findPlaceholders(KeysWithValueModuleData.bytecode.linkReferences), deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex), }, { name: "KeysInTableModule", abi: KeysInTableModuleData.abi as Abi, bytecode: KeysInTableModuleData.bytecode.object as Hex, + placeholders: findPlaceholders(KeysInTableModuleData.bytecode.linkReferences), deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex), }, { name: "UniqueEntityModule", abi: UniqueEntityModuleData.abi as Abi, bytecode: UniqueEntityModuleData.bytecode.object as Hex, + placeholders: findPlaceholders(UniqueEntityModuleData.bytecode.linkReferences), deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex), }, ]; diff --git a/packages/cli/src/utils/findPlaceholders.ts b/packages/cli/src/utils/findPlaceholders.ts new file mode 100644 index 0000000000..5006624774 --- /dev/null +++ b/packages/cli/src/utils/findPlaceholders.ts @@ -0,0 +1,27 @@ +import { LibraryPlaceholder } from "../deploy/common"; + +// TODO: move this to a broader solc artifact type +/** From `artifact.bytecode.linkReferences` where `artifact` is the solc JSON output of a compiled Solidity contract */ +export type LinkReferences = { + [filename: string]: { + [name: string]: { + start: number; + length: number; + }[]; + }; +}; + +export function findPlaceholders(linkReferences: LinkReferences): readonly LibraryPlaceholder[] { + return Object.entries(linkReferences).flatMap(([path, contracts]) => + Object.entries(contracts).flatMap(([contractName, locations]) => + locations.map( + (location): LibraryPlaceholder => ({ + path, + name: contractName, + start: location.start, + length: location.length, + }), + ), + ), + ); +} diff --git a/packages/cli/src/utils/utils/getContractData.ts b/packages/cli/src/utils/getContractData.ts similarity index 64% rename from packages/cli/src/utils/utils/getContractData.ts rename to packages/cli/src/utils/getContractData.ts index c527d2b873..c55e2c9a11 100644 --- a/packages/cli/src/utils/utils/getContractData.ts +++ b/packages/cli/src/utils/getContractData.ts @@ -2,17 +2,20 @@ import { readFileSync } from "fs"; import path from "path"; import { MUDError } from "@latticexyz/common/errors"; import { Abi, Hex, size } from "viem"; +import { LibraryPlaceholder } from "../deploy/common"; +import { findPlaceholders } from "./findPlaceholders"; /** * Load the contract's abi and bytecode from the file system * @param contractName: Name of the contract to load */ export function getContractData( + filename: string, contractName: string, forgeOutDirectory: string, -): { bytecode: Hex; abi: Abi; deployedBytecodeSize: number } { +): { bytecode: Hex; placeholders: readonly LibraryPlaceholder[]; abi: Abi; deployedBytecodeSize: number } { let data: any; - const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json"); + const contractDataPath = path.join(forgeOutDirectory, filename, contractName + ".json"); try { data = JSON.parse(readFileSync(contractDataPath, "utf8")); } catch (error: any) { @@ -28,5 +31,7 @@ export function getContractData( const abi = data?.abi; if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`); - return { abi, bytecode, deployedBytecodeSize: size(deployedBytecode as Hex) }; + const placeholders = findPlaceholders(data?.bytecode?.linkReferences ?? {}); + + return { abi, bytecode, placeholders, deployedBytecodeSize: size(deployedBytecode as Hex) }; } diff --git a/packages/cli/src/utils/utils/postDeploy.ts b/packages/cli/src/utils/postDeploy.ts similarity index 100% rename from packages/cli/src/utils/utils/postDeploy.ts rename to packages/cli/src/utils/postDeploy.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 98e9d8303f..fb0b6f3855 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,6 +211,9 @@ importers: throttle-debounce: specifier: ^5.0.0 version: 5.0.0 + toposort: + specifier: ^2.0.2 + version: 2.0.2 typescript: specifier: 5.1.6 version: 5.1.6 @@ -245,6 +248,9 @@ importers: '@types/throttle-debounce': specifier: ^5.0.0 version: 5.0.0 + '@types/toposort': + specifier: ^2.0.6 + version: 2.0.6 '@types/yargs': specifier: ^17.0.10 version: 17.0.23 @@ -3525,6 +3531,10 @@ packages: resolution: {integrity: sha512-Pb7k35iCGFcGPECoNE4DYp3Oyf2xcTd3FbFQxXUI9hEYKUl6YX+KLf7HrBmgVcD05nl50LIH6i+80js4iYmWbw==} dev: true + /@types/toposort@2.0.6: + resolution: {integrity: sha512-9OMv8NIjiJclYZ+FIqNCs6B6/7HSIccWCpgeVmp2TurRXllEIYNxAwGBBFEataTsHwLt8oeVmMCmdld8dVq/hw==} + dev: true + /@types/uuid@8.3.4: resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} dev: true @@ -10420,6 +10430,10 @@ packages: engines: {node: '>=0.6'} dev: false + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /tough-cookie@4.1.3: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'}