diff --git a/.changeset/long-dogs-wash.md b/.changeset/long-dogs-wash.md new file mode 100644 index 0000000000..005fc45e35 --- /dev/null +++ b/.changeset/long-dogs-wash.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/cli": patch +--- + +Deployer now waits for prerequisite transactions before continuing. diff --git a/packages/cli/src/deploy/configToModules.ts b/packages/cli/src/deploy/configToModules.ts index 1dd7140b88..73f4387f1d 100644 --- a/packages/cli/src/deploy/configToModules.ts +++ b/packages/cli/src/deploy/configToModules.ts @@ -5,10 +5,8 @@ import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema- import { bytesToHex } from "viem"; import { createPrepareDeploy } from "./createPrepareDeploy"; import { World } from "@latticexyz/world"; -import { getContractArtifact } from "../utils/getContractArtifact"; import { importContractArtifact } from "../utils/importContractArtifact"; import { resolveWithContext } from "@latticexyz/world/internal"; -import metadataModule from "@latticexyz/world-module-metadata/out/MetadataModule.sol/MetadataModule.json" assert { type: "json" }; /** 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 = { @@ -19,28 +17,11 @@ const knownModuleArtifacts = { "@latticexyz/world-modules/out/Unstable_CallWithSignatureModule.sol/Unstable_CallWithSignatureModule.json", }; -const metadataModuleArtifact = getContractArtifact(metadataModule); - export async function configToModules( config: config, // TODO: remove/replace `forgeOutDir` forgeOutDir: string, ): Promise { - const defaultModules: Module[] = [ - // TODO: replace metadata install here with custom logic inside `ensureModules` or an `ensureDefaultModules` to check - // if metadata namespace exists, if we own it, and if so transfer ownership to the module before reinstalling - // (https://github.com/latticexyz/mud/issues/3035) - { - optional: true, - name: "MetadataModule", - installAsRoot: false, - installData: "0x", - prepareDeploy: createPrepareDeploy(metadataModuleArtifact.bytecode, metadataModuleArtifact.placeholders), - deployedBytecodeSize: metadataModuleArtifact.deployedBytecodeSize, - abi: metadataModuleArtifact.abi, - }, - ]; - const modules = await Promise.all( config.modules.map(async (mod): Promise => { let artifactPath = mod.artifactPath; @@ -98,5 +79,5 @@ export async function configToModules( }), ); - return [...defaultModules, ...modules]; + return modules; } diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index e881e3d1c7..70efef50c5 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -4,7 +4,6 @@ import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; import { Library, Module, System, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; import { ensureSystems } from "./ensureSystems"; -import { waitForTransactionReceipt } from "viem/actions"; import { getWorldDeploy } from "./getWorldDeploy"; import { ensureFunctions } from "./ensureFunctions"; import { ensureModules } from "./ensureModules"; @@ -16,6 +15,7 @@ import { randomBytes } from "crypto"; import { ensureWorldFactory } from "./ensureWorldFactory"; import { Table } from "@latticexyz/config"; import { ensureResourceTags } from "./ensureResourceTags"; +import { waitForTransactions } from "./waitForTransactions"; type DeployOptions = { client: Client; @@ -95,11 +95,9 @@ export async function deploy({ worldDeploy, resourceIds: [...tables.map(({ tableId }) => tableId), ...systems.map(({ systemId }) => systemId)], }); - - debug("waiting for all namespace registration transactions to confirm"); - for (const tx of namespaceTxs) { - await waitForTransactionReceipt(client, { hash: tx }); - } + // Wait for namespaces to be available, otherwise referencing them below may fail. + // This is only here because OPStack chains don't let us estimate gas with pending block tag. + await waitForTransactions({ client, hashes: namespaceTxs, debugLabel: "namespace registrations" }); const tableTxs = await ensureTables({ client, @@ -113,6 +111,14 @@ export async function deploy({ worldDeploy, systems, }); + // Wait for tables and systems to be available, otherwise referencing their resource IDs below may fail. + // This is only here because OPStack chains don't let us estimate gas with pending block tag. + await waitForTransactions({ + client, + hashes: [...tableTxs, ...systemTxs], + debugLabel: "table and system registrations", + }); + const functionTxs = await ensureFunctions({ client, worldDeploy, @@ -135,19 +141,18 @@ export async function deploy({ const tagTxs = await ensureResourceTags({ client, + deployerAddress, + libraries, worldDeploy, tags: [...tableTags, ...systemTags], valueToHex: stringToHex, }); - const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs, ...tagTxs]; - - // wait for each tx separately/serially, because parallelizing results in RPC errors - debug("waiting for all transactions to confirm"); - for (const tx of txs) { - await waitForTransactionReceipt(client, { hash: tx }); - // TODO: throw if there was a revert? - } + await waitForTransactions({ + client, + hashes: [...functionTxs, ...moduleTxs, ...tagTxs], + debugLabel: "remaining transactions", + }); debug("deploy complete"); return worldDeploy; diff --git a/packages/cli/src/deploy/ensureContract.ts b/packages/cli/src/deploy/ensureContract.ts index e2e8259d5c..5a4cfd4925 100644 --- a/packages/cli/src/deploy/ensureContract.ts +++ b/packages/cli/src/deploy/ensureContract.ts @@ -4,7 +4,6 @@ import { contractSizeLimit, salt } from "./common"; import { sendTransaction } from "@latticexyz/common"; import { debug } from "./debug"; import pRetry from "p-retry"; -import { wait } from "@latticexyz/common/utils"; export type Contract = { bytecode: Hex; @@ -56,11 +55,7 @@ export async function ensureContract({ }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to deploy ${debugLabel}, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug(`failed to deploy ${debugLabel}, retrying...`), }, ), ]; diff --git a/packages/cli/src/deploy/ensureContractsDeployed.ts b/packages/cli/src/deploy/ensureContractsDeployed.ts index 789fd0928c..6724641799 100644 --- a/packages/cli/src/deploy/ensureContractsDeployed.ts +++ b/packages/cli/src/deploy/ensureContractsDeployed.ts @@ -1,8 +1,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"; +import { waitForTransactions } from "./waitForTransactions"; export async function ensureContractsDeployed({ client, @@ -20,14 +19,11 @@ export async function ensureContractsDeployed({ await Promise.all(uniqueContracts.map((contract) => ensureContract({ client, deployerAddress, ...contract }))) ).flat(); - if (txs.length) { - debug("waiting for contracts"); - // wait for each tx separately/serially, because parallelizing results in RPC errors - for (const tx of txs) { - await waitForTransactionReceipt(client, { hash: tx }); - // TODO: throw if there was a revert? - } - } + await waitForTransactions({ + client, + hashes: txs, + debugLabel: "contract deploys", + }); return txs; } diff --git a/packages/cli/src/deploy/ensureFunctions.ts b/packages/cli/src/deploy/ensureFunctions.ts index 3f29bd65ce..49c9edf102 100644 --- a/packages/cli/src/deploy/ensureFunctions.ts +++ b/packages/cli/src/deploy/ensureFunctions.ts @@ -4,7 +4,6 @@ import { getFunctions } from "@latticexyz/world/internal"; import { WorldDeploy, WorldFunction, worldAbi } from "./common"; import { debug } from "./debug"; import pRetry from "p-retry"; -import { wait } from "@latticexyz/common/utils"; export async function ensureFunctions({ client, @@ -46,44 +45,35 @@ export async function ensureFunctions({ return Promise.all( toAdd.map((func) => { const { namespace } = hexToResource(func.systemId); - if (namespace === "") { - return pRetry( - () => - writeContract(client, { - chain: client.chain ?? null, - address: worldDeploy.address, - abi: worldAbi, - // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) + + // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) + const params = + namespace === "" + ? ({ functionName: "registerRootFunctionSelector", - args: [func.systemId, func.systemFunctionSignature, func.systemFunctionSignature], - }), - { - retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to register function ${func.signature}, retrying in ${delay}ms...`); - await wait(delay); - }, - }, - ); - } + args: [ + func.systemId, + // use system function signature as world signature + func.systemFunctionSignature, + func.systemFunctionSignature, + ], + } as const) + : ({ + functionName: "registerFunctionSelector", + args: [func.systemId, func.systemFunctionSignature], + } as const); + return pRetry( () => writeContract(client, { chain: client.chain ?? null, address: worldDeploy.address, abi: worldAbi, - // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) - functionName: "registerFunctionSelector", - args: [func.systemId, func.systemFunctionSignature], + ...params, }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to register function ${func.signature}, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug(`failed to register function ${func.signature}, retrying...`), }, ); }), diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 7b3fcc6dd0..49ed384147 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -2,7 +2,7 @@ import { Client, Transport, Chain, Account, Hex, BaseError } from "viem"; import { writeContract } from "@latticexyz/common"; import { Library, Module, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; -import { isDefined, wait } from "@latticexyz/common/utils"; +import { isDefined } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; @@ -41,23 +41,16 @@ export async function ensureModules({ // append module's ABI so that we can decode any custom errors const abi = [...worldAbi, ...mod.abi]; const moduleAddress = mod.prepareDeploy(deployerAddress, libraries).address; - return mod.installAsRoot - ? await writeContract(client, { - chain: client.chain ?? null, - address: worldDeploy.address, - abi, - // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) - functionName: "installRootModule", - args: [moduleAddress, mod.installData], - }) - : await writeContract(client, { - chain: client.chain ?? null, - address: worldDeploy.address, - abi, - // TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645) - functionName: "installModule", - args: [moduleAddress, mod.installData], - }); + // 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); + return writeContract(client, { + chain: client.chain ?? null, + address: worldDeploy.address, + abi, + ...params, + }); } catch (error) { if (error instanceof BaseError && error.message.includes("Module_AlreadyInstalled")) { debug(`module ${mod.name} already installed`); @@ -73,11 +66,7 @@ export async function ensureModules({ }, { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to install module ${mod.name}, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug(`failed to install module ${mod.name}, retrying...`), }, ), ), diff --git a/packages/cli/src/deploy/ensureResourceTags.ts b/packages/cli/src/deploy/ensureResourceTags.ts index 56e83372d9..c279649132 100644 --- a/packages/cli/src/deploy/ensureResourceTags.ts +++ b/packages/cli/src/deploy/ensureResourceTags.ts @@ -1,11 +1,18 @@ import { Hex, Client, Transport, Chain, Account, stringToHex, BaseError } from "viem"; -import { WorldDeploy } from "./common"; +import { Library, WorldDeploy } from "./common"; import { debug } from "./debug"; import { hexToResource, writeContract } from "@latticexyz/common"; import { identity, isDefined } from "@latticexyz/common/utils"; import metadataConfig from "@latticexyz/world-module-metadata/mud.config"; import metadataAbi from "@latticexyz/world-module-metadata/out/IMetadataSystem.sol/IMetadataSystem.abi.json" assert { type: "json" }; import { getRecord } from "./getRecord"; +import { ensureModules } from "./ensureModules"; +import metadataModule from "@latticexyz/world-module-metadata/out/MetadataModule.sol/MetadataModule.json" assert { type: "json" }; +import { getContractArtifact } from "../utils/getContractArtifact"; +import { createPrepareDeploy } from "./createPrepareDeploy"; +import { waitForTransactions } from "./waitForTransactions"; + +const metadataModuleArtifact = getContractArtifact(metadataModule); export type ResourceTag = { resourceId: Hex; @@ -15,11 +22,15 @@ export type ResourceTag = { export async function ensureResourceTags({ client, + deployerAddress, + libraries, worldDeploy, tags, valueToHex = identity, }: { readonly client: Client; + readonly deployerAddress: Hex; + readonly libraries: readonly Library[]; readonly worldDeploy: WorldDeploy; readonly tags: readonly ResourceTag[]; } & (value extends Hex @@ -42,6 +53,34 @@ export async function ensureResourceTags({ if (pendingTags.length === 0) return []; + // TODO: check if metadata namespace exists, if we own it, and if so transfer ownership to the module before reinstalling + // (https://github.com/latticexyz/mud/issues/3035) + const moduleTxs = await ensureModules({ + client, + deployerAddress, + worldDeploy, + libraries, + modules: [ + { + optional: true, + name: "MetadataModule", + installAsRoot: false, + installData: "0x", + prepareDeploy: createPrepareDeploy(metadataModuleArtifact.bytecode, metadataModuleArtifact.placeholders), + deployedBytecodeSize: metadataModuleArtifact.deployedBytecodeSize, + abi: metadataModuleArtifact.abi, + }, + ], + }); + + // Wait for metadata module to be available, otherwise calling the metadata system below may fail. + // This is only here because OPStack chains don't let us estimate gas with pending block tag. + await waitForTransactions({ + client, + hashes: moduleTxs, + debugLabel: "metadata module installation", + }); + debug("setting", pendingTags.length, "resource tags"); return ( await Promise.all( diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index 356af81b35..c018db1043 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -4,7 +4,6 @@ import { Library, System, WorldDeploy, worldAbi } from "./common"; import { debug } from "./debug"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; -import { wait } from "@latticexyz/common/utils"; import pRetry from "p-retry"; import { ensureContractsDeployed } from "./ensureContractsDeployed"; @@ -87,11 +86,7 @@ export async function ensureSystems({ }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to register system ${resourceToLabel(system)}, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug(`failed to register system ${resourceToLabel(system)}, retrying...`), }, ), ), @@ -153,11 +148,7 @@ export async function ensureSystems({ }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to revoke access, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug("failed to revoke access, retrying..."), }, ), ), @@ -173,11 +164,7 @@ export async function ensureSystems({ }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to grant access, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug("failed to grant access, retrying..."), }, ), ), diff --git a/packages/cli/src/deploy/ensureTables.ts b/packages/cli/src/deploy/ensureTables.ts index 2a2e1d2153..6f2deac46b 100644 --- a/packages/cli/src/deploy/ensureTables.ts +++ b/packages/cli/src/deploy/ensureTables.ts @@ -13,7 +13,6 @@ import { import { debug } from "./debug"; import { getTables } from "./getTables"; import pRetry from "p-retry"; -import { wait } from "@latticexyz/common/utils"; import { Table } from "@latticexyz/config"; export async function ensureTables({ @@ -59,11 +58,7 @@ export async function ensureTables({ }), { retries: 3, - onFailedAttempt: async (error) => { - const delay = error.attemptNumber * 500; - debug(`failed to register table ${resourceToLabel(table)}, retrying in ${delay}ms...`); - await wait(delay); - }, + onFailedAttempt: () => debug(`failed to register table ${resourceToLabel(table)}, retrying...`), }, ); }), diff --git a/packages/cli/src/deploy/waitForTransactions.ts b/packages/cli/src/deploy/waitForTransactions.ts new file mode 100644 index 0000000000..934233de22 --- /dev/null +++ b/packages/cli/src/deploy/waitForTransactions.ts @@ -0,0 +1,24 @@ +import { Client, Transport, Chain, Account, Hex } from "viem"; +import { debug } from "./debug"; +import { waitForTransactionReceipt } from "viem/actions"; + +export async function waitForTransactions({ + client, + hashes, + debugLabel = "transactions", +}: { + readonly client: Client; + readonly hashes: readonly Hex[]; + readonly debugLabel: string; +}): Promise { + if (!hashes.length) return; + + debug(`waiting for ${debugLabel} to confirm`); + // wait for each tx separately/serially, because parallelizing results in RPC errors + for (const hash of hashes) { + const receipt = await waitForTransactionReceipt(client, { hash }); + if (receipt.status === "reverted") { + throw new Error(`Transaction reverted: ${hash}`); + } + } +}