@latticexyz/cli 2.0.0-skystrife-playtest-9e9511d4 → 2.0.0-transaction-context-324984c5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-22IIKR4S.js +4 -0
- package/dist/chunk-22IIKR4S.js.map +1 -0
- package/dist/commands-3JV3U43E.js +27 -0
- package/dist/commands-3JV3U43E.js.map +1 -0
- package/dist/errors-XGN6V2Y3.js +2 -0
- package/dist/errors-XGN6V2Y3.js.map +1 -0
- package/dist/index.js +0 -1
- package/dist/mud.js +1 -14
- package/dist/mud.js.map +1 -1
- package/package.json +20 -13
- package/src/build.ts +44 -0
- package/src/commands/build.ts +36 -0
- package/src/commands/deploy.ts +7 -30
- package/src/commands/dev-contracts.ts +76 -128
- package/src/commands/index.ts +2 -0
- package/src/commands/set-version.ts +23 -61
- package/src/commands/tablegen.ts +3 -2
- package/src/commands/test.ts +30 -36
- package/src/commands/trace.ts +13 -6
- package/src/commands/worldgen.ts +1 -1
- package/src/common.ts +1 -0
- package/src/debug.ts +10 -0
- package/src/deploy/common.ts +76 -0
- package/src/deploy/configToTables.ts +68 -0
- package/src/deploy/create2/README.md +9 -0
- package/src/deploy/create2/deployment.json +7 -0
- package/src/deploy/debug.ts +10 -0
- package/src/deploy/deploy.ts +116 -0
- package/src/deploy/deployWorld.ts +37 -0
- package/src/deploy/ensureContract.ts +61 -0
- package/src/deploy/ensureContractsDeployed.ts +25 -0
- package/src/deploy/ensureDeployer.ts +36 -0
- package/src/deploy/ensureFunctions.ts +86 -0
- package/src/deploy/ensureModules.ts +73 -0
- package/src/deploy/ensureNamespaceOwner.ts +71 -0
- package/src/deploy/ensureSystems.ts +162 -0
- package/src/deploy/ensureTables.ts +65 -0
- package/src/deploy/ensureWorldFactory.ts +118 -0
- package/src/deploy/getFunctions.ts +58 -0
- package/src/deploy/getResourceAccess.ts +51 -0
- package/src/deploy/getResourceIds.ts +31 -0
- package/src/deploy/getSystems.ts +48 -0
- package/src/deploy/getTableValue.ts +30 -0
- package/src/deploy/getTables.ts +59 -0
- package/src/deploy/getWorldDeploy.ts +39 -0
- package/src/deploy/logsToWorldDeploy.ts +49 -0
- package/src/deploy/resolveConfig.ts +151 -0
- package/src/deploy/resourceLabel.ts +3 -0
- package/src/index.ts +1 -1
- package/src/mud.ts +37 -31
- package/src/mudPackages.ts +24 -0
- package/src/runDeploy.ts +131 -0
- package/src/utils/modules/constants.ts +26 -0
- package/src/utils/utils/getContractData.ts +32 -0
- package/src/utils/utils/postDeploy.ts +25 -0
- package/dist/chunk-OJAPOMSC.js +0 -11
- package/dist/chunk-OJAPOMSC.js.map +0 -1
- package/src/utils/deploy.ts +0 -620
- package/src/utils/deployHandler.ts +0 -93
- package/src/utils/getChainId.ts +0 -10
- package/src/utils/index.ts +0 -6
@@ -0,0 +1,59 @@
|
|
1
|
+
import { Client, parseAbiItem, decodeAbiParameters, parseAbiParameters } from "viem";
|
2
|
+
import { Table } from "./configToTables";
|
3
|
+
import { hexToResource } from "@latticexyz/common";
|
4
|
+
import { WorldDeploy, storeTables } from "./common";
|
5
|
+
import { debug } from "./debug";
|
6
|
+
import { storeSetRecordEvent } from "@latticexyz/store";
|
7
|
+
import { getLogs } from "viem/actions";
|
8
|
+
import { KeySchema, ValueSchema, decodeKey, decodeValueArgs, hexToSchema } from "@latticexyz/protocol-parser";
|
9
|
+
|
10
|
+
export async function getTables({
|
11
|
+
client,
|
12
|
+
worldDeploy,
|
13
|
+
}: {
|
14
|
+
readonly client: Client;
|
15
|
+
readonly worldDeploy: WorldDeploy;
|
16
|
+
}): Promise<readonly Table[]> {
|
17
|
+
// This assumes we only use `Tables._set(...)`, which is true as of this writing.
|
18
|
+
// TODO: PR to viem's getLogs to accept topics array so we can filter on all store events and quickly recreate this table's current state
|
19
|
+
// TODO: consider moving this to a batched getRecord for Tables table
|
20
|
+
|
21
|
+
debug("looking up tables for", worldDeploy.address);
|
22
|
+
const logs = await getLogs(client, {
|
23
|
+
strict: true,
|
24
|
+
// this may fail for certain RPC providers with block range limits
|
25
|
+
// if so, could potentially use our fetchLogs helper (which does pagination)
|
26
|
+
fromBlock: worldDeploy.deployBlock,
|
27
|
+
toBlock: worldDeploy.stateBlock,
|
28
|
+
address: worldDeploy.address,
|
29
|
+
event: parseAbiItem(storeSetRecordEvent),
|
30
|
+
args: { tableId: storeTables.store_Tables.tableId },
|
31
|
+
});
|
32
|
+
|
33
|
+
// TODO: combine with store-sync logToTable and export from somewhere
|
34
|
+
const tables = logs.map((log) => {
|
35
|
+
const { tableId } = decodeKey(storeTables.store_Tables.keySchema, log.args.keyTuple);
|
36
|
+
const { namespace, name } = hexToResource(tableId);
|
37
|
+
const value = decodeValueArgs(storeTables.store_Tables.valueSchema, log.args);
|
38
|
+
|
39
|
+
// TODO: migrate to better helper
|
40
|
+
const keySchemaFields = hexToSchema(value.keySchema);
|
41
|
+
const valueSchemaFields = hexToSchema(value.valueSchema);
|
42
|
+
const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0];
|
43
|
+
const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0];
|
44
|
+
|
45
|
+
const valueAbiTypes = [...valueSchemaFields.staticFields, ...valueSchemaFields.dynamicFields];
|
46
|
+
|
47
|
+
const keySchema = Object.fromEntries(
|
48
|
+
keySchemaFields.staticFields.map((abiType, i) => [keyNames[i], abiType])
|
49
|
+
) as KeySchema;
|
50
|
+
const valueSchema = Object.fromEntries(valueAbiTypes.map((abiType, i) => [fieldNames[i], abiType])) as ValueSchema;
|
51
|
+
|
52
|
+
return { namespace, name, tableId, keySchema, valueSchema } as const;
|
53
|
+
});
|
54
|
+
// TODO: filter/detect duplicates?
|
55
|
+
|
56
|
+
debug("found", tables.length, "tables for", worldDeploy.address);
|
57
|
+
|
58
|
+
return tables;
|
59
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import { Client, Address, getAddress, parseAbi } from "viem";
|
2
|
+
import { getBlockNumber, getLogs } from "viem/actions";
|
3
|
+
import { WorldDeploy, worldDeployEvents } from "./common";
|
4
|
+
import { debug } from "./debug";
|
5
|
+
import { logsToWorldDeploy } from "./logsToWorldDeploy";
|
6
|
+
|
7
|
+
const deploys = new Map<Address, WorldDeploy>();
|
8
|
+
|
9
|
+
export async function getWorldDeploy(client: Client, worldAddress: Address): Promise<WorldDeploy> {
|
10
|
+
const address = getAddress(worldAddress);
|
11
|
+
|
12
|
+
let deploy = deploys.get(address);
|
13
|
+
if (deploy != null) {
|
14
|
+
return deploy;
|
15
|
+
}
|
16
|
+
|
17
|
+
debug("looking up world deploy for", address);
|
18
|
+
|
19
|
+
const stateBlock = await getBlockNumber(client);
|
20
|
+
const logs = await getLogs(client, {
|
21
|
+
strict: true,
|
22
|
+
address,
|
23
|
+
events: parseAbi(worldDeployEvents),
|
24
|
+
// this may fail for certain RPC providers with block range limits
|
25
|
+
// if so, could potentially use our fetchLogs helper (which does pagination)
|
26
|
+
fromBlock: "earliest",
|
27
|
+
toBlock: stateBlock,
|
28
|
+
});
|
29
|
+
|
30
|
+
deploy = {
|
31
|
+
...logsToWorldDeploy(logs),
|
32
|
+
stateBlock,
|
33
|
+
};
|
34
|
+
deploys.set(address, deploy);
|
35
|
+
|
36
|
+
debug("found world deploy for", address, "at block", deploy.deployBlock);
|
37
|
+
|
38
|
+
return deploy;
|
39
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { AbiEventSignatureNotFoundError, Log, decodeEventLog, hexToString, parseAbi, trim } from "viem";
|
2
|
+
import { WorldDeploy, worldDeployEvents } from "./common";
|
3
|
+
import { isDefined } from "@latticexyz/common/utils";
|
4
|
+
|
5
|
+
export function logsToWorldDeploy(logs: readonly Log<bigint, number, false>[]): Omit<WorldDeploy, "stateBlock"> {
|
6
|
+
const deployLogs = logs
|
7
|
+
.map((log) => {
|
8
|
+
try {
|
9
|
+
return {
|
10
|
+
...log,
|
11
|
+
...decodeEventLog({
|
12
|
+
strict: true,
|
13
|
+
abi: parseAbi(worldDeployEvents),
|
14
|
+
topics: log.topics,
|
15
|
+
data: log.data,
|
16
|
+
}),
|
17
|
+
};
|
18
|
+
} catch (error: unknown) {
|
19
|
+
if (error instanceof AbiEventSignatureNotFoundError) {
|
20
|
+
return;
|
21
|
+
}
|
22
|
+
throw error;
|
23
|
+
}
|
24
|
+
})
|
25
|
+
.filter(isDefined);
|
26
|
+
|
27
|
+
// TODO: should this test for/validate that only one of each of these events is present? and that the address/block number don't change between each?
|
28
|
+
const { address, deployBlock, worldVersion, storeVersion } = deployLogs.reduce<Partial<WorldDeploy>>(
|
29
|
+
(deploy, log) => ({
|
30
|
+
...deploy,
|
31
|
+
address: log.address,
|
32
|
+
deployBlock: log.blockNumber,
|
33
|
+
...(log.eventName === "HelloWorld"
|
34
|
+
? { worldVersion: hexToString(trim(log.args.worldVersion, { dir: "right" })) }
|
35
|
+
: null),
|
36
|
+
...(log.eventName === "HelloStore"
|
37
|
+
? { storeVersion: hexToString(trim(log.args.storeVersion, { dir: "right" })) }
|
38
|
+
: null),
|
39
|
+
}),
|
40
|
+
{}
|
41
|
+
);
|
42
|
+
|
43
|
+
if (address == null) throw new Error("could not find world address");
|
44
|
+
if (deployBlock == null) throw new Error("could not find world deploy block number");
|
45
|
+
if (worldVersion == null) throw new Error("could not find world version");
|
46
|
+
if (storeVersion == null) throw new Error("could not find store version");
|
47
|
+
|
48
|
+
return { address, deployBlock, worldVersion, storeVersion };
|
49
|
+
}
|
@@ -0,0 +1,151 @@
|
|
1
|
+
import { resolveWorldConfig } from "@latticexyz/world";
|
2
|
+
import { Config, ConfigInput, WorldFunction, salt } from "./common";
|
3
|
+
import { resourceToHex, hexToResource } from "@latticexyz/common";
|
4
|
+
import { resolveWithContext } from "@latticexyz/config";
|
5
|
+
import { encodeField } from "@latticexyz/protocol-parser";
|
6
|
+
import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type";
|
7
|
+
import {
|
8
|
+
getFunctionSelector,
|
9
|
+
Hex,
|
10
|
+
getCreate2Address,
|
11
|
+
getAddress,
|
12
|
+
hexToBytes,
|
13
|
+
Abi,
|
14
|
+
bytesToHex,
|
15
|
+
getFunctionSignature,
|
16
|
+
} from "viem";
|
17
|
+
import { getExistingContracts } from "../utils/getExistingContracts";
|
18
|
+
import { defaultModuleContracts } from "../utils/modules/constants";
|
19
|
+
import { getContractData } from "../utils/utils/getContractData";
|
20
|
+
import { configToTables } from "./configToTables";
|
21
|
+
import { deployer } from "./ensureDeployer";
|
22
|
+
import { resourceLabel } from "./resourceLabel";
|
23
|
+
|
24
|
+
// TODO: this should be replaced by https://github.com/latticexyz/mud/issues/1668
|
25
|
+
|
26
|
+
export function resolveConfig<config extends ConfigInput>({
|
27
|
+
config,
|
28
|
+
forgeSourceDir,
|
29
|
+
forgeOutDir,
|
30
|
+
}: {
|
31
|
+
config: config;
|
32
|
+
forgeSourceDir: string;
|
33
|
+
forgeOutDir: string;
|
34
|
+
}): Config<config> {
|
35
|
+
const tables = configToTables(config);
|
36
|
+
|
37
|
+
// TODO: should the config parser/loader help with resolving systems?
|
38
|
+
const contractNames = getExistingContracts(forgeSourceDir).map(({ basename }) => basename);
|
39
|
+
const resolvedConfig = resolveWorldConfig(config, contractNames);
|
40
|
+
const baseSystemContractData = getContractData("System", forgeOutDir);
|
41
|
+
const baseSystemFunctions = baseSystemContractData.abi
|
42
|
+
.filter((item): item is typeof item & { type: "function" } => item.type === "function")
|
43
|
+
.map(getFunctionSignature);
|
44
|
+
|
45
|
+
const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]) => {
|
46
|
+
const namespace = config.namespace;
|
47
|
+
const name = system.name;
|
48
|
+
const systemId = resourceToHex({ type: "system", namespace, name });
|
49
|
+
const contractData = getContractData(systemName, forgeOutDir);
|
50
|
+
|
51
|
+
const systemFunctions = contractData.abi
|
52
|
+
.filter((item): item is typeof item & { type: "function" } => item.type === "function")
|
53
|
+
.map(getFunctionSignature)
|
54
|
+
.filter((sig) => !baseSystemFunctions.includes(sig))
|
55
|
+
.map((sig): WorldFunction => {
|
56
|
+
// TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
|
57
|
+
const worldSignature = namespace === "" ? sig : `${namespace}__${sig}`;
|
58
|
+
return {
|
59
|
+
signature: worldSignature,
|
60
|
+
selector: getFunctionSelector(worldSignature),
|
61
|
+
systemId,
|
62
|
+
systemFunctionSignature: sig,
|
63
|
+
systemFunctionSelector: getFunctionSelector(sig),
|
64
|
+
};
|
65
|
+
});
|
66
|
+
|
67
|
+
return {
|
68
|
+
namespace,
|
69
|
+
name,
|
70
|
+
systemId,
|
71
|
+
allowAll: system.openAccess,
|
72
|
+
allowedAddresses: system.accessListAddresses as Hex[],
|
73
|
+
allowedSystemIds: system.accessListSystems.map((name) =>
|
74
|
+
resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name })
|
75
|
+
),
|
76
|
+
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
|
77
|
+
bytecode: contractData.bytecode,
|
78
|
+
deployedBytecodeSize: contractData.deployedBytecodeSize,
|
79
|
+
abi: contractData.abi,
|
80
|
+
functions: systemFunctions,
|
81
|
+
};
|
82
|
+
});
|
83
|
+
|
84
|
+
// resolve allowedSystemIds
|
85
|
+
// TODO: resolve this at deploy time so we can allow for arbitrary system IDs registered in the world as the source-of-truth rather than config
|
86
|
+
const systemsWithAccess = systems.map(({ allowedAddresses, allowedSystemIds, ...system }) => {
|
87
|
+
const allowedSystemAddresses = allowedSystemIds.map((systemId) => {
|
88
|
+
const targetSystem = systems.find((s) => s.systemId === systemId);
|
89
|
+
if (!targetSystem) {
|
90
|
+
throw new Error(
|
91
|
+
`System ${resourceLabel(system)} wanted access to ${resourceLabel(
|
92
|
+
hexToResource(systemId)
|
93
|
+
)}, but it wasn't found in the config.`
|
94
|
+
);
|
95
|
+
}
|
96
|
+
return targetSystem.address;
|
97
|
+
});
|
98
|
+
return {
|
99
|
+
...system,
|
100
|
+
allowedAddresses: Array.from(
|
101
|
+
new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr)))
|
102
|
+
),
|
103
|
+
};
|
104
|
+
});
|
105
|
+
|
106
|
+
// ugh (https://github.com/latticexyz/mud/issues/1668)
|
107
|
+
const resolveContext = {
|
108
|
+
tableIds: Object.fromEntries(
|
109
|
+
Object.entries(config.tables).map(([tableName, table]) => [
|
110
|
+
tableName,
|
111
|
+
hexToBytes(
|
112
|
+
resourceToHex({
|
113
|
+
type: table.offchainOnly ? "offchainTable" : "table",
|
114
|
+
namespace: config.namespace,
|
115
|
+
name: table.name,
|
116
|
+
})
|
117
|
+
),
|
118
|
+
])
|
119
|
+
),
|
120
|
+
};
|
121
|
+
|
122
|
+
const modules = config.modules.map((mod) => {
|
123
|
+
const contractData =
|
124
|
+
defaultModuleContracts.find((defaultMod) => defaultMod.name === mod.name) ??
|
125
|
+
getContractData(mod.name, forgeOutDir);
|
126
|
+
const installArgs = mod.args
|
127
|
+
.map((arg) => resolveWithContext(arg, resolveContext))
|
128
|
+
.map((arg) => {
|
129
|
+
const value = arg.value instanceof Uint8Array ? bytesToHex(arg.value) : arg.value;
|
130
|
+
return encodeField(arg.type as SchemaAbiType, value as SchemaAbiTypeToPrimitiveType<SchemaAbiType>);
|
131
|
+
});
|
132
|
+
if (installArgs.length > 1) {
|
133
|
+
throw new Error(`${mod.name} module should only have 0-1 args, but had ${installArgs.length} args.`);
|
134
|
+
}
|
135
|
+
return {
|
136
|
+
name: mod.name,
|
137
|
+
installAsRoot: mod.root,
|
138
|
+
installData: installArgs.length === 0 ? "0x" : installArgs[0],
|
139
|
+
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
|
140
|
+
bytecode: contractData.bytecode,
|
141
|
+
deployedBytecodeSize: contractData.deployedBytecodeSize,
|
142
|
+
abi: contractData.abi,
|
143
|
+
};
|
144
|
+
});
|
145
|
+
|
146
|
+
return {
|
147
|
+
tables,
|
148
|
+
systems: systemsWithAccess,
|
149
|
+
modules,
|
150
|
+
};
|
151
|
+
}
|
package/src/index.ts
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
// nothing to export
|
package/src/mud.ts
CHANGED
@@ -1,39 +1,45 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
2
|
|
3
|
-
import yargs from "yargs";
|
4
|
-
import { hideBin } from "yargs/helpers";
|
5
|
-
import { commands } from "./commands";
|
6
|
-
import { logError } from "./utils/errors";
|
7
|
-
|
8
3
|
// Load .env file into process.env
|
9
4
|
import * as dotenv from "dotenv";
|
10
|
-
import chalk from "chalk";
|
11
5
|
dotenv.config();
|
12
6
|
|
13
|
-
|
14
|
-
//
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
//
|
31
|
-
|
32
|
-
|
7
|
+
async function run() {
|
8
|
+
// Import everything else async so they can pick up env vars in .env
|
9
|
+
const { default: yargs } = await import("yargs");
|
10
|
+
const { default: chalk } = await import("chalk");
|
11
|
+
const { hideBin } = await import("yargs/helpers");
|
12
|
+
const { logError } = await import("./utils/errors");
|
13
|
+
const { commands } = await import("./commands");
|
14
|
+
|
15
|
+
yargs(hideBin(process.argv))
|
16
|
+
// Explicit name to display in help (by default it's the entry file, which may not be "mud" for e.g. ts-node)
|
17
|
+
.scriptName("mud")
|
18
|
+
// Use the commands directory to scaffold
|
19
|
+
// command array overload isn't typed, see https://github.com/yargs/yargs/blob/main/docs/advanced.md#esm-hierarchy
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
21
|
+
.command(commands as any)
|
22
|
+
// Enable strict mode.
|
23
|
+
.strict()
|
24
|
+
// Custom error handler
|
25
|
+
.fail((msg, err) => {
|
26
|
+
console.error(chalk.red(msg));
|
27
|
+
if (msg.includes("Missing required argument")) {
|
28
|
+
console.log(
|
29
|
+
chalk.yellow(`Run 'pnpm mud ${process.argv[2]} --help' for a list of available and required arguments.`)
|
30
|
+
);
|
31
|
+
}
|
33
32
|
console.log("");
|
34
|
-
|
33
|
+
// Even though `.fail` type says we should get an `Error`, this can sometimes be undefined
|
34
|
+
if (err != null) {
|
35
|
+
logError(err);
|
36
|
+
console.log("");
|
37
|
+
}
|
38
|
+
|
39
|
+
process.exit(1);
|
40
|
+
})
|
41
|
+
// Useful aliases.
|
42
|
+
.alias({ h: "help" }).argv;
|
43
|
+
}
|
35
44
|
|
36
|
-
|
37
|
-
})
|
38
|
-
// Useful aliases.
|
39
|
-
.alias({ h: "help" }).argv;
|
45
|
+
run();
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { ZodError, z } from "zod";
|
2
|
+
import { MudPackages } from "./common";
|
3
|
+
|
4
|
+
const envSchema = z.object({
|
5
|
+
MUD_PACKAGES: z.string().transform((value) => JSON.parse(value) as MudPackages),
|
6
|
+
});
|
7
|
+
|
8
|
+
function parseEnv(): z.infer<typeof envSchema> {
|
9
|
+
try {
|
10
|
+
return envSchema.parse({
|
11
|
+
// tsup replaces the env vars with their values at compile time
|
12
|
+
MUD_PACKAGES: process.env.MUD_PACKAGES,
|
13
|
+
});
|
14
|
+
} catch (error) {
|
15
|
+
if (error instanceof ZodError) {
|
16
|
+
const { _errors, ...invalidEnvVars } = error.format();
|
17
|
+
console.error(`\nMissing or invalid environment variables:\n\n ${Object.keys(invalidEnvVars).join("\n ")}\n`);
|
18
|
+
process.exit(1);
|
19
|
+
}
|
20
|
+
throw error;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
export const mudPackages = parseEnv().MUD_PACKAGES;
|
package/src/runDeploy.ts
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
import path from "node:path";
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
3
|
+
import { InferredOptionTypes, Options } from "yargs";
|
4
|
+
import { deploy } from "./deploy/deploy";
|
5
|
+
import { createWalletClient, http, Hex, isHex } from "viem";
|
6
|
+
import { privateKeyToAccount } from "viem/accounts";
|
7
|
+
import { loadConfig } from "@latticexyz/config/node";
|
8
|
+
import { StoreConfig } from "@latticexyz/store";
|
9
|
+
import { WorldConfig } from "@latticexyz/world";
|
10
|
+
import { getOutDirectory, getRpcUrl, getSrcDirectory } from "@latticexyz/common/foundry";
|
11
|
+
import chalk from "chalk";
|
12
|
+
import { MUDError } from "@latticexyz/common/errors";
|
13
|
+
import { resolveConfig } from "./deploy/resolveConfig";
|
14
|
+
import { getChainId } from "viem/actions";
|
15
|
+
import { postDeploy } from "./utils/utils/postDeploy";
|
16
|
+
import { WorldDeploy } from "./deploy/common";
|
17
|
+
import { build } from "./build";
|
18
|
+
|
19
|
+
export const deployOptions = {
|
20
|
+
configPath: { type: "string", desc: "Path to the config file" },
|
21
|
+
printConfig: { type: "boolean", desc: "Print the resolved config" },
|
22
|
+
profile: { type: "string", desc: "The foundry profile to use" },
|
23
|
+
saveDeployment: { type: "boolean", desc: "Save the deployment info to a file", default: true },
|
24
|
+
rpc: { type: "string", desc: "The RPC URL to use. Defaults to the RPC url from the local foundry.toml" },
|
25
|
+
worldAddress: { type: "string", desc: "Deploy to an existing World at the given address" },
|
26
|
+
srcDir: { type: "string", desc: "Source directory. Defaults to foundry src directory." },
|
27
|
+
skipBuild: { type: "boolean", desc: "Skip rebuilding the contracts before deploying" },
|
28
|
+
alwaysRunPostDeploy: {
|
29
|
+
type: "boolean",
|
30
|
+
desc: "Always run PostDeploy.s.sol after each deploy (including during upgrades). By default, PostDeploy.s.sol is only run once after a new world is deployed.",
|
31
|
+
},
|
32
|
+
salt: {
|
33
|
+
type: "string",
|
34
|
+
desc: "The deployment salt to use. Defaults to a random salt.",
|
35
|
+
},
|
36
|
+
} as const satisfies Record<string, Options>;
|
37
|
+
|
38
|
+
export type DeployOptions = InferredOptionTypes<typeof deployOptions>;
|
39
|
+
|
40
|
+
/**
|
41
|
+
* Given some CLI arguments, finds and resolves a MUD config, foundry profile, and runs a deploy.
|
42
|
+
* This is used by the deploy, test, and dev-contracts CLI commands.
|
43
|
+
*/
|
44
|
+
export async function runDeploy(opts: DeployOptions): Promise<WorldDeploy> {
|
45
|
+
const salt = opts.salt;
|
46
|
+
if (salt != null && !isHex(salt)) {
|
47
|
+
throw new MUDError("Expected hex string for salt");
|
48
|
+
}
|
49
|
+
|
50
|
+
const profile = opts.profile ?? process.env.FOUNDRY_PROFILE;
|
51
|
+
|
52
|
+
const config = (await loadConfig(opts.configPath)) as StoreConfig & WorldConfig;
|
53
|
+
if (opts.printConfig) {
|
54
|
+
console.log(chalk.green("\nResolved config:\n"), JSON.stringify(config, null, 2));
|
55
|
+
}
|
56
|
+
|
57
|
+
const srcDir = opts.srcDir ?? (await getSrcDirectory(profile));
|
58
|
+
const outDir = await getOutDirectory(profile);
|
59
|
+
|
60
|
+
const rpc = opts.rpc ?? (await getRpcUrl(profile));
|
61
|
+
console.log(
|
62
|
+
chalk.bgBlue(
|
63
|
+
chalk.whiteBright(`\n Deploying MUD contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
|
64
|
+
)
|
65
|
+
);
|
66
|
+
|
67
|
+
// Run build
|
68
|
+
if (!opts.skipBuild) {
|
69
|
+
await build({ config, srcDir, foundryProfile: profile });
|
70
|
+
}
|
71
|
+
|
72
|
+
const privateKey = process.env.PRIVATE_KEY as Hex;
|
73
|
+
if (!privateKey) {
|
74
|
+
throw new MUDError(
|
75
|
+
`Missing PRIVATE_KEY environment variable.
|
76
|
+
Run 'echo "PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" > .env'
|
77
|
+
in your contracts directory to use the default anvil private key.`
|
78
|
+
);
|
79
|
+
}
|
80
|
+
|
81
|
+
const resolvedConfig = resolveConfig({ config, forgeSourceDir: srcDir, forgeOutDir: outDir });
|
82
|
+
|
83
|
+
const client = createWalletClient({
|
84
|
+
transport: http(rpc),
|
85
|
+
account: privateKeyToAccount(privateKey),
|
86
|
+
});
|
87
|
+
console.log("Deploying from", client.account.address);
|
88
|
+
|
89
|
+
const startTime = Date.now();
|
90
|
+
const worldDeploy = await deploy({
|
91
|
+
salt,
|
92
|
+
worldAddress: opts.worldAddress as Hex | undefined,
|
93
|
+
client,
|
94
|
+
config: resolvedConfig,
|
95
|
+
});
|
96
|
+
if (opts.worldAddress == null || opts.alwaysRunPostDeploy) {
|
97
|
+
await postDeploy(config.postDeployScript, worldDeploy.address, rpc, profile);
|
98
|
+
}
|
99
|
+
console.log(chalk.green("Deployment completed in", (Date.now() - startTime) / 1000, "seconds"));
|
100
|
+
|
101
|
+
const deploymentInfo = {
|
102
|
+
worldAddress: worldDeploy.address,
|
103
|
+
blockNumber: Number(worldDeploy.deployBlock),
|
104
|
+
};
|
105
|
+
|
106
|
+
if (opts.saveDeployment) {
|
107
|
+
const chainId = await getChainId(client);
|
108
|
+
const deploysDir = path.join(config.deploysDirectory, chainId.toString());
|
109
|
+
mkdirSync(deploysDir, { recursive: true });
|
110
|
+
writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2));
|
111
|
+
writeFileSync(path.join(deploysDir, Date.now() + ".json"), JSON.stringify(deploymentInfo, null, 2));
|
112
|
+
|
113
|
+
const localChains = [1337, 31337];
|
114
|
+
const deploys = existsSync(config.worldsFile) ? JSON.parse(readFileSync(config.worldsFile, "utf-8")) : {};
|
115
|
+
deploys[chainId] = {
|
116
|
+
address: deploymentInfo.worldAddress,
|
117
|
+
// We expect the worlds file to be committed and since local deployments are often
|
118
|
+
// a consistent address but different block number, we'll ignore the block number.
|
119
|
+
blockNumber: localChains.includes(chainId) ? undefined : deploymentInfo.blockNumber,
|
120
|
+
};
|
121
|
+
writeFileSync(config.worldsFile, JSON.stringify(deploys, null, 2));
|
122
|
+
|
123
|
+
console.log(
|
124
|
+
chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${config.worldsFile} and ${deploysDir}): \n`))
|
125
|
+
);
|
126
|
+
}
|
127
|
+
|
128
|
+
console.log(deploymentInfo);
|
129
|
+
|
130
|
+
return worldDeploy;
|
131
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import KeysWithValueModuleData from "@latticexyz/world-modules/out/KeysWithValueModule.sol/KeysWithValueModule.json" assert { type: "json" };
|
2
|
+
import KeysInTableModuleData from "@latticexyz/world-modules/out/KeysInTableModule.sol/KeysInTableModule.json" assert { type: "json" };
|
3
|
+
import UniqueEntityModuleData from "@latticexyz/world-modules/out/UniqueEntityModule.sol/UniqueEntityModule.json" assert { type: "json" };
|
4
|
+
import { Abi, Hex, size } from "viem";
|
5
|
+
|
6
|
+
// These modules are always deployed
|
7
|
+
export const defaultModuleContracts = [
|
8
|
+
{
|
9
|
+
name: "KeysWithValueModule",
|
10
|
+
abi: KeysWithValueModuleData.abi as Abi,
|
11
|
+
bytecode: KeysWithValueModuleData.bytecode.object as Hex,
|
12
|
+
deployedBytecodeSize: size(KeysWithValueModuleData.deployedBytecode.object as Hex),
|
13
|
+
},
|
14
|
+
{
|
15
|
+
name: "KeysInTableModule",
|
16
|
+
abi: KeysInTableModuleData.abi as Abi,
|
17
|
+
bytecode: KeysInTableModuleData.bytecode.object as Hex,
|
18
|
+
deployedBytecodeSize: size(KeysInTableModuleData.deployedBytecode.object as Hex),
|
19
|
+
},
|
20
|
+
{
|
21
|
+
name: "UniqueEntityModule",
|
22
|
+
abi: UniqueEntityModuleData.abi as Abi,
|
23
|
+
bytecode: UniqueEntityModuleData.bytecode.object as Hex,
|
24
|
+
deployedBytecodeSize: size(UniqueEntityModuleData.deployedBytecode.object as Hex),
|
25
|
+
},
|
26
|
+
];
|
@@ -0,0 +1,32 @@
|
|
1
|
+
import { readFileSync } from "fs";
|
2
|
+
import path from "path";
|
3
|
+
import { MUDError } from "@latticexyz/common/errors";
|
4
|
+
import { Abi, Hex, size } from "viem";
|
5
|
+
|
6
|
+
/**
|
7
|
+
* Load the contract's abi and bytecode from the file system
|
8
|
+
* @param contractName: Name of the contract to load
|
9
|
+
*/
|
10
|
+
export function getContractData(
|
11
|
+
contractName: string,
|
12
|
+
forgeOutDirectory: string
|
13
|
+
): { bytecode: Hex; abi: Abi; deployedBytecodeSize: number } {
|
14
|
+
let data: any;
|
15
|
+
const contractDataPath = path.join(forgeOutDirectory, contractName + ".sol", contractName + ".json");
|
16
|
+
try {
|
17
|
+
data = JSON.parse(readFileSync(contractDataPath, "utf8"));
|
18
|
+
} catch (error: any) {
|
19
|
+
throw new MUDError(`Error reading file at ${contractDataPath}`);
|
20
|
+
}
|
21
|
+
|
22
|
+
const bytecode = data?.bytecode?.object;
|
23
|
+
if (!bytecode) throw new MUDError(`No bytecode found in ${contractDataPath}`);
|
24
|
+
|
25
|
+
const deployedBytecode = data?.deployedBytecode?.object;
|
26
|
+
if (!deployedBytecode) throw new MUDError(`No deployed bytecode found in ${contractDataPath}`);
|
27
|
+
|
28
|
+
const abi = data?.abi;
|
29
|
+
if (!abi) throw new MUDError(`No ABI found in ${contractDataPath}`);
|
30
|
+
|
31
|
+
return { abi, bytecode, deployedBytecodeSize: size(deployedBytecode as Hex) };
|
32
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { existsSync } from "fs";
|
2
|
+
import path from "path";
|
3
|
+
import chalk from "chalk";
|
4
|
+
import { getScriptDirectory, forge } from "@latticexyz/common/foundry";
|
5
|
+
|
6
|
+
export async function postDeploy(
|
7
|
+
postDeployScript: string,
|
8
|
+
worldAddress: string,
|
9
|
+
rpc: string,
|
10
|
+
profile: string | undefined
|
11
|
+
): Promise<void> {
|
12
|
+
// Execute postDeploy forge script
|
13
|
+
const postDeployPath = path.join(await getScriptDirectory(), postDeployScript + ".s.sol");
|
14
|
+
if (existsSync(postDeployPath)) {
|
15
|
+
console.log(chalk.blue(`Executing post deploy script at ${postDeployPath}`));
|
16
|
+
await forge(
|
17
|
+
["script", postDeployScript, "--sig", "run(address)", worldAddress, "--broadcast", "--rpc-url", rpc, "-vvv"],
|
18
|
+
{
|
19
|
+
profile: profile,
|
20
|
+
}
|
21
|
+
);
|
22
|
+
} else {
|
23
|
+
console.log(`No script at ${postDeployPath}, skipping post deploy hook`);
|
24
|
+
}
|
25
|
+
}
|