From 4fe079309fae04ffd2e611311937906f65bf91e6 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 12 Oct 2023 11:45:02 +0100 Subject: [PATCH] fix(cli): support enums in deploy, only deploy modules/systems once (#1749) --- .changeset/red-sheep-confess.md | 9 ++++++++ packages/cli/src/deploy/configToTables.ts | 28 +++++++++++++++-------- packages/cli/src/deploy/deploy.ts | 18 ++++++++------- packages/cli/src/deploy/ensureModules.ts | 5 +++- packages/cli/src/deploy/ensureSystems.ts | 3 ++- packages/common/src/utils/index.ts | 1 + packages/common/src/utils/uniqueBy.ts | 7 ++++++ packages/store/ts/config/storeConfig.ts | 17 +++++++------- 8 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 .changeset/red-sheep-confess.md create mode 100644 packages/common/src/utils/uniqueBy.ts diff --git a/.changeset/red-sheep-confess.md b/.changeset/red-sheep-confess.md new file mode 100644 index 0000000000..fdf59148f8 --- /dev/null +++ b/.changeset/red-sheep-confess.md @@ -0,0 +1,9 @@ +--- +"@latticexyz/cli": patch +--- + +Fixed a few issues with deploys: + +- properly handle enums in MUD config +- only deploy each unique module/system once +- waits for transactions serially instead of in parallel, to avoid RPC errors diff --git a/packages/cli/src/deploy/configToTables.ts b/packages/cli/src/deploy/configToTables.ts index 79fe8343e3..4c07a81bb3 100644 --- a/packages/cli/src/deploy/configToTables.ts +++ b/packages/cli/src/deploy/configToTables.ts @@ -6,6 +6,12 @@ import { Hex } from "viem"; // TODO: we shouldn't need this file once our config parsing returns nicely formed tables +type UserTypes = config["userTypes"]; +// TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }` +// type UserTypes = config["userTypes"] & { +// [k in keyof config["enums"]]: { internalType: "uint8" }; +// }; + export type TableKey< config extends StoreConfig = StoreConfig, table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]] @@ -18,19 +24,17 @@ export type Table< readonly namespace: config["namespace"]; readonly name: table["name"]; readonly tableId: Hex; - // TODO: support enums - readonly keySchema: table["keySchema"] extends KeySchema + readonly keySchema: table["keySchema"] extends KeySchema> ? KeySchema & { - readonly [k in keyof table["keySchema"]]: config["userTypes"][table["keySchema"][k]]["internalType"] extends StaticAbiType - ? config["userTypes"][table["keySchema"][k]]["internalType"] + readonly [k in keyof table["keySchema"]]: UserTypes[table["keySchema"][k]]["internalType"] extends StaticAbiType + ? UserTypes[table["keySchema"][k]]["internalType"] : table["keySchema"][k]; } : KeySchema; - // TODO: support enums - readonly valueSchema: table["valueSchema"] extends ValueSchema + readonly valueSchema: table["valueSchema"] extends ValueSchema> ? { - readonly [k in keyof table["valueSchema"]]: config["userTypes"][table["valueSchema"][k]]["internalType"] extends SchemaAbiType - ? config["userTypes"][table["valueSchema"][k]]["internalType"] + readonly [k in keyof table["valueSchema"]]: UserTypes[table["valueSchema"][k]]["internalType"] extends SchemaAbiType + ? UserTypes[table["valueSchema"][k]]["internalType"] : table["valueSchema"][k]; } : ValueSchema; @@ -41,6 +45,10 @@ export type Tables = { }; export function configToTables(config: config): Tables { + const userTypes = { + ...config.userTypes, + ...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)), + }; return Object.fromEntries( Object.entries(config.tables).map(([tableName, table]) => [ `${config.namespace}_${tableName}` satisfies TableKey, @@ -52,8 +60,8 @@ export function configToTables(config: config): Tabl namespace: config.namespace, name: table.name, }), - keySchema: resolveUserTypes(table.keySchema, config.userTypes) as any, - valueSchema: resolveUserTypes(table.valueSchema, config.userTypes) as any, + keySchema: resolveUserTypes(table.keySchema, userTypes) as any, + valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any, } satisfies Table, ]) ) as Tables; diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index 885cd47987..6fa494dfd6 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -4,13 +4,13 @@ import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; import { Config, ConfigInput, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; import { ensureSystems } from "./ensureSystems"; -import { waitForTransactionReceipt } from "viem/actions"; +import { getTransactionReceipt, waitForTransactionReceipt } from "viem/actions"; import { getWorldDeploy } from "./getWorldDeploy"; import { ensureFunctions } from "./ensureFunctions"; import { ensureModules } from "./ensureModules"; import { Table } from "./configToTables"; -import { getResourceIds } from "./getResourceIds"; import { assertNamespaceOwner } from "./assertNamespaceOwner"; +import { debug } from "./debug"; type DeployOptions = { client: Client; @@ -72,13 +72,15 @@ export async function deploy({ modules: config.modules, }); - const receipts = await Promise.all( - [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs].map((tx) => - waitForTransactionReceipt(client, { hash: tx }) - ) - ); + const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs]; - // TODO: throw if there was a revert? or attempt to re-run deploy? + // wait for each tx separately/serially, because parallelizing results in RPC errors + debug("waiting for transactions to confirm"); + for (const tx of txs) { + await waitForTransactionReceipt(client, { hash: tx }); + // TODO: throw if there was a revert? + } + debug("deploy complete"); return worldDeploy; } diff --git a/packages/cli/src/deploy/ensureModules.ts b/packages/cli/src/deploy/ensureModules.ts index 260700a173..9efc1ac3b3 100644 --- a/packages/cli/src/deploy/ensureModules.ts +++ b/packages/cli/src/deploy/ensureModules.ts @@ -3,6 +3,7 @@ import { writeContract } from "@latticexyz/common"; import { Module, WorldDeploy, worldAbi } from "./common"; import { ensureContract } from "./ensureContract"; import { debug } from "./debug"; +import { uniqueBy } from "@latticexyz/common/utils"; export async function ensureModules({ client, @@ -17,7 +18,9 @@ export async function ensureModules({ // kick off contract deployments first, otherwise installing modules can fail const contractTxs = await Promise.all( - modules.map((mod) => ensureContract({ client, bytecode: mod.bytecode, label: `${mod.name} module` })) + uniqueBy(modules, (mod) => mod.address).map((mod) => + ensureContract({ client, bytecode: mod.bytecode, label: `${mod.name} module` }) + ) ); // then start installing modules diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index ef6201ba0c..2a40d81d92 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -6,6 +6,7 @@ import { debug } from "./debug"; import { resourceLabel } from "./resourceLabel"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; +import { uniqueBy } from "@latticexyz/common/utils"; export async function ensureSystems({ client, @@ -105,7 +106,7 @@ export async function ensureSystems({ // kick off contract deployments first, otherwise registering systems can fail const contractTxs = await Promise.all( - missingSystems.map((system) => + uniqueBy(missingSystems, (system) => system.address).map((system) => ensureContract({ client, bytecode: system.bytecode, label: `${resourceLabel(system)} system` }) ) ); diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts index bd68ad8e2d..1ccd90fe05 100644 --- a/packages/common/src/utils/index.ts +++ b/packages/common/src/utils/index.ts @@ -8,5 +8,6 @@ export * from "./identity"; export * from "./isDefined"; export * from "./isNotNull"; export * from "./iteratorToArray"; +export * from "./uniqueBy"; export * from "./wait"; export * from "./waitForIdle"; diff --git a/packages/common/src/utils/uniqueBy.ts b/packages/common/src/utils/uniqueBy.ts new file mode 100644 index 0000000000..a0d884ccfd --- /dev/null +++ b/packages/common/src/utils/uniqueBy.ts @@ -0,0 +1,7 @@ +export function uniqueBy(values: readonly value[], getKey: (value: value) => key): readonly value[] { + const map = new Map(); + for (const value of values) { + map.set(getKey(value), value); + } + return Array.from(map.values()); +} diff --git a/packages/store/ts/config/storeConfig.ts b/packages/store/ts/config/storeConfig.ts index fb160a9f2a..a9f9071438 100644 --- a/packages/store/ts/config/storeConfig.ts +++ b/packages/store/ts/config/storeConfig.ts @@ -71,7 +71,10 @@ const zShorthandSchemaConfig = zFieldData.transform((fieldData) => { export const zSchemaConfig = zFullSchemaConfig.or(zShorthandSchemaConfig); -export type ResolvedSchema, TUserTypes extends Record> = { +export type ResolvedSchema< + TSchema extends Record, + TUserTypes extends Record> +> = { [key in keyof TSchema]: TSchema[key] extends keyof TUserTypes ? TUserTypes[TSchema[key]]["internalType"] : TSchema[key]; @@ -79,10 +82,10 @@ export type ResolvedSchema, TUserTypes ex // TODO: add strong types to UserTypes config and use them here // (see https://github.com/latticexyz/mud/pull/1588) -export function resolveUserTypes, TUserTypes extends Record>( - schema: TSchema, - userTypes: TUserTypes -): ResolvedSchema { +export function resolveUserTypes< + TSchema extends Record, + TUserTypes extends Record> +>(schema: TSchema, userTypes: TUserTypes): ResolvedSchema { const resolvedSchema: Record = {}; for (const [key, value] of Object.entries(schema)) { resolvedSchema[key] = (userTypes[value]?.internalType as SchemaAbiType) ?? value; @@ -295,10 +298,6 @@ export type UserTypesConfig; }; -export type FullUserTypesConfig = { - userTypes: Record; -}; - const zUserTypeConfig = z.object({ filePath: z.string(), internalType: z.enum(schemaAbiTypes),