Skip to content

Commit

Permalink
feat(cli): ensure contracts deployed before using (#1780)
Browse files Browse the repository at this point in the history
  • Loading branch information
holic authored Oct 16, 2023
1 parent aa5b227 commit 2020635
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 45 deletions.
8 changes: 4 additions & 4 deletions packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const worldDeployEvents = [helloStoreEvent, helloWorldEvent] as const;

export const worldAbi = [...IBaseWorldAbi, ...IModuleAbi] as const;

// Ideally, this should be an append-only list. Before adding more versions here, be sure to add backwards-compatible support for old Store/World versions.
export const supportedStoreVersions = ["1.0.0-unaudited"];
export const supportedWorldVersions = ["1.0.0-unaudited"];

export type WorldDeploy = {
readonly address: Address;
readonly worldVersion: string;
Expand Down Expand Up @@ -66,7 +70,3 @@ export type Config<config extends ConfigInput> = {
readonly systems: readonly System[];
readonly modules: readonly Module[];
};

// Ideally, this should be an append-only list. Before adding more versions here, be sure to add backwards-compatible support for old Store/World versions.
export const supportedStoreVersions = ["1.0.0-unaudited"];
export const supportedWorldVersions = ["1.0.0-unaudited"];
48 changes: 23 additions & 25 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Account, Address, Chain, Client, Transport } from "viem";
import { Account, Address, Chain, Client, Transport, getAddress } from "viem";
import { ensureDeployer } from "./ensureDeployer";
import { deployWorld } from "./deployWorld";
import { ensureTables } from "./ensureTables";
Expand All @@ -14,6 +14,8 @@ import { debug } from "./debug";
import { resourceLabel } from "./resourceLabel";
import { ensureContract } from "./ensureContract";
import { uniqueBy } from "@latticexyz/common/utils";
import { ensureContractsDeployed } from "./ensureContractsDeployed";
import { coreModuleBytecode, worldFactoryBytecode } from "./ensureWorldFactory";

type DeployOptions<configInput extends ConfigInput> = {
client: Client<Transport, Chain | undefined, Account>;
Expand All @@ -32,8 +34,28 @@ export async function deploy<configInput extends ConfigInput>({
config,
worldAddress: existingWorldAddress,
}: DeployOptions<configInput>): Promise<WorldDeploy> {
const tables = Object.values(config.tables) as Table[];
const systems = Object.values(config.systems);

await ensureDeployer(client);

// deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
await ensureContractsDeployed({
client,
contracts: [
{ bytecode: coreModuleBytecode, label: "core module" },
{ bytecode: worldFactoryBytecode, label: "world factory" },
...uniqueBy(systems, (system) => getAddress(system.address)).map((system) => ({
bytecode: system.bytecode,
label: `${resourceLabel(system)} system`,
})),
...uniqueBy(config.modules, (mod) => getAddress(mod.address)).map((mod) => ({
bytecode: mod.bytecode,
label: `${mod.name} module`,
})),
],
});

const worldDeploy = existingWorldAddress
? await getWorldDeploy(client, existingWorldAddress)
: await deployWorld(client);
Expand All @@ -45,36 +67,12 @@ export async function deploy<configInput extends ConfigInput>({
throw new Error(`Unsupported World version: ${worldDeploy.worldVersion}`);
}

const tables = Object.values(config.tables) as Table[];
const systems = Object.values(config.systems);

await assertNamespaceOwner({
client,
worldDeploy,
resourceIds: [...tables.map((table) => table.tableId), ...systems.map((system) => system.systemId)],
});

// deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
const contractTxs = (
await Promise.all([
...uniqueBy(systems, (system) => system.address).map((system) =>
ensureContract({ client, bytecode: system.bytecode, label: `${resourceLabel(system)} system` })
),
...uniqueBy(config.modules, (mod) => mod.address).map((mod) =>
ensureContract({ client, bytecode: mod.bytecode, label: `${mod.name} module` })
),
])
).flat();

if (contractTxs.length) {
debug("waiting for contracts");
// wait for each tx separately/serially, because parallelizing results in RPC errors
for (const tx of contractTxs) {
await waitForTransactionReceipt(client, { hash: tx });
// TODO: throw if there was a revert?
}
}

const tableTxs = await ensureTables({
client,
worldDeploy,
Expand Down
9 changes: 6 additions & 3 deletions packages/cli/src/deploy/ensureContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@ import { debug } from "./debug";
import pRetry from "p-retry";
import { wait } from "@latticexyz/common/utils";

export type Contract = {
bytecode: Hex;
label?: string;
};

export async function ensureContract({
client,
bytecode,
label = "contract",
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
readonly bytecode: Hex;
readonly label?: string;
}): Promise<readonly Hex[]> {
} & Contract): Promise<readonly Hex[]> {
const address = getCreate2Address({ from: deployer, salt, bytecode });

const contractCode = await getBytecode(client, { address, blockTag: "pending" });
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/deploy/ensureContractsDeployed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Client, Transport, Chain, Account, Hex } from "viem";
import { waitForTransactionReceipt } from "viem/actions";
import { debug } from "./debug";
import { Contract, ensureContract } from "./ensureContract";

export async function ensureContractsDeployed({
client,
contracts,
}: {
readonly client: Client<Transport, Chain | undefined, Account>;
readonly contracts: readonly Contract[];
}): Promise<readonly Hex[]> {
const txs = (await Promise.all(contracts.map((contract) => ensureContract({ client, ...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?
}
}

return txs;
}
13 changes: 11 additions & 2 deletions packages/cli/src/deploy/ensureModules.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Client, Transport, Chain, Account, Hex, BaseError } from "viem";
import { Client, Transport, Chain, Account, Hex, BaseError, getAddress } from "viem";
import { writeContract } from "@latticexyz/common";
import { Module, WorldDeploy, worldAbi } from "./common";
import { debug } from "./debug";
import { isDefined, wait } from "@latticexyz/common/utils";
import { isDefined, uniqueBy, wait } from "@latticexyz/common/utils";
import pRetry from "p-retry";
import { ensureContractsDeployed } from "./ensureContractsDeployed";

export async function ensureModules({
client,
Expand All @@ -16,6 +17,14 @@ export async function ensureModules({
}): Promise<readonly Hex[]> {
if (!modules.length) return [];

await ensureContractsDeployed({
client,
contracts: uniqueBy(modules, (mod) => getAddress(mod.address)).map((mod) => ({
bytecode: mod.bytecode,
label: `${mod.name} module`,
})),
});

debug("installing modules:", modules.map((mod) => mod.name).join(", "));
return (
await Promise.all(
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/deploy/ensureSystems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import { debug } from "./debug";
import { resourceLabel } from "./resourceLabel";
import { getSystems } from "./getSystems";
import { getResourceAccess } from "./getResourceAccess";
import { wait } from "@latticexyz/common/utils";
import { uniqueBy, wait } from "@latticexyz/common/utils";
import pRetry from "p-retry";
import { ensureContractsDeployed } from "./ensureContractsDeployed";

export async function ensureSystems({
client,
Expand Down Expand Up @@ -126,6 +127,14 @@ export async function ensureSystems({
debug("registering new systems", systemsToAdd.map(resourceLabel).join(", "));
}

await ensureContractsDeployed({
client,
contracts: uniqueBy(missingSystems, (system) => getAddress(system.address)).map((system) => ({
bytecode: system.bytecode,
label: `${resourceLabel(system)} system`,
})),
});

const registerTxs = missingSystems.map((system) =>
pRetry(
() =>
Expand Down
21 changes: 11 additions & 10 deletions packages/cli/src/deploy/ensureWorldFactory.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import coreModuleBuild from "@latticexyz/world/out/CoreModule.sol/CoreModule.json" assert { type: "json" };
import worldFactoryBuild from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.json" assert { type: "json" };
import { Client, Transport, Chain, Account, Hex, parseAbi, getCreate2Address, encodeDeployData } from "viem";
import { ensureContract } from "./ensureContract";
import { deployer } from "./ensureDeployer";
import { salt } from "./common";
import { ensureContractsDeployed } from "./ensureContractsDeployed";

const coreModuleBytecode = encodeDeployData({
export const coreModuleBytecode = encodeDeployData({
bytecode: coreModuleBuild.bytecode.object as Hex,
abi: [],
});

const coreModule = getCreate2Address({ from: deployer, bytecode: coreModuleBytecode, salt });
export const coreModule = getCreate2Address({ from: deployer, bytecode: coreModuleBytecode, salt });

const worldFactoryBytecode = encodeDeployData({
export const worldFactoryBytecode = encodeDeployData({
bytecode: worldFactoryBuild.bytecode.object as Hex,
abi: parseAbi(["constructor(address)"]),
args: [coreModule],
Expand All @@ -24,10 +24,11 @@ export async function ensureWorldFactory(
client: Client<Transport, Chain | undefined, Account>
): Promise<readonly Hex[]> {
// WorldFactory constructor doesn't call CoreModule, only sets its address, so we can do these in parallel since the address is deterministic
return (
await Promise.all([
ensureContract({ client, bytecode: coreModuleBytecode, label: "core module" }),
ensureContract({ client, bytecode: worldFactoryBytecode, label: "world factory" }),
])
).flat();
return await ensureContractsDeployed({
client,
contracts: [
{ bytecode: coreModuleBytecode, label: "core module" },
{ bytecode: worldFactoryBytecode, label: "world factory" },
],
});
}

0 comments on commit 2020635

Please sign in to comment.