@latticexyz/cli 1.40.0 → 1.41.0
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/LICENSE +21 -0
- package/dist/chunk-AER7UDD4.js +0 -0
- package/dist/chunk-ATAWDHWC.js +67 -0
- package/dist/{chunk-6AQ6LFVZ.js → chunk-GR245KYP.js} +95 -1
- package/dist/{chunk-S3V3XX7N.js → chunk-SLIMIO4Z.js} +1 -1
- package/dist/{chunk-JNGSW4AP.js → chunk-XRS7KWBZ.js} +139 -85
- package/dist/chunk-YZATC2M3.js +397 -0
- package/dist/chunk-ZYDMYSTH.js +1178 -0
- package/dist/config/index.d.ts +427 -3
- package/dist/config/index.js +26 -3
- package/dist/deploy-v2-b7b3207d.d.ts +92 -0
- package/dist/index.d.ts +110 -3
- package/dist/index.js +53 -8
- package/dist/mud.js +172 -46
- package/dist/utils/deprecated/index.js +2 -2
- package/dist/utils/index.d.ts +5 -44
- package/dist/utils/index.js +18 -3
- package/package.json +13 -9
- package/src/commands/deploy-v2.ts +100 -0
- package/src/commands/deprecated/call-system.ts +1 -1
- package/src/commands/deprecated/deploy-contracts.ts +1 -1
- package/src/commands/deprecated/test.ts +9 -6
- package/src/commands/deprecated/trace.ts +1 -1
- package/src/commands/gas-report.ts +1 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/tablegen.ts +4 -18
- package/src/config/commonSchemas.ts +16 -0
- package/src/config/index.ts +8 -0
- package/src/config/loadStoreConfig.ts +2 -88
- package/src/config/loadWorldConfig.test-d.ts +11 -0
- package/src/config/loadWorldConfig.ts +178 -0
- package/src/config/{loadStoreConfig.test-d.ts → parseStoreConfig.test-d.ts} +5 -2
- package/src/config/parseStoreConfig.ts +174 -0
- package/src/config/validation.ts +46 -0
- package/src/constants.ts +1 -0
- package/src/index.ts +15 -5
- package/src/mud.ts +4 -0
- package/src/{render-table → render-solidity}/common.ts +37 -11
- package/src/{render-table → render-solidity}/field.ts +29 -20
- package/src/{render-table → render-solidity}/record.ts +3 -8
- package/src/{render-table → render-solidity}/renderTable.ts +38 -11
- package/src/{render-table → render-solidity}/renderTablesFromConfig.ts +37 -27
- package/src/render-solidity/renderTypes.ts +19 -0
- package/src/render-solidity/renderTypesFromConfig.ts +13 -0
- package/src/render-solidity/tablegen.ts +35 -0
- package/src/{render-table → render-solidity}/types.ts +25 -0
- package/src/render-solidity/userType.ts +100 -0
- package/src/utils/deploy-v2.ts +345 -0
- package/src/utils/deprecated/build.ts +1 -1
- package/src/utils/deprecated/typegen.ts +1 -1
- package/src/utils/errors.ts +12 -2
- package/src/utils/execLog.ts +22 -0
- package/src/utils/foundry.ts +94 -0
- package/src/utils/index.ts +2 -1
- package/dist/chunk-B6VWCGHZ.js +0 -199
- package/dist/chunk-JKAA3WMC.js +0 -55
- package/dist/chunk-PJ6GS2R4.js +0 -22
- package/dist/chunk-UC3QPOON.js +0 -35
- package/dist/loadStoreConfig-37f99136.d.ts +0 -164
- package/dist/render-table/index.d.ts +0 -29
- package/dist/render-table/index.js +0 -24
- package/dist/renderTable-9e6410c5.d.ts +0 -72
- package/src/utils/forgeConfig.ts +0 -45
- /package/src/{render-table → render-solidity}/index.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@latticexyz/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Command line interface for mud",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,15 +16,18 @@
|
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"prepare": "yarn build && chmod u+x git-install.sh",
|
|
19
|
+
"codegen": "ts-node --esm --files ./scripts/codegen.ts",
|
|
19
20
|
"lint": "eslint . --ext .ts",
|
|
20
21
|
"dev": "tsup --watch",
|
|
21
22
|
"build": "tsup",
|
|
22
23
|
"link": "yarn link",
|
|
23
|
-
"test": "vitest typecheck --run &&
|
|
24
|
+
"test": "vitest typecheck --run && yarn test:contracts",
|
|
25
|
+
"test:contracts": "yarn codegen && forge test",
|
|
24
26
|
"git:install": "bash git-install.sh",
|
|
25
|
-
"release": "npm publish
|
|
27
|
+
"release": "npm publish --access=public"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
30
|
+
"@latticexyz/store": "^1.41.0",
|
|
28
31
|
"@types/ejs": "^3.1.1",
|
|
29
32
|
"@types/glob": "^7.2.0",
|
|
30
33
|
"@types/node": "^17.0.34",
|
|
@@ -37,17 +40,18 @@
|
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@improbable-eng/grpc-web": "^0.15.0",
|
|
39
42
|
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
|
|
40
|
-
"@latticexyz/schema-type": "^1.
|
|
41
|
-
"@latticexyz/services": "^1.
|
|
42
|
-
"@latticexyz/solecs": "^1.
|
|
43
|
-
"@latticexyz/std-contracts": "^1.
|
|
43
|
+
"@latticexyz/schema-type": "^1.41.0",
|
|
44
|
+
"@latticexyz/services": "^1.41.0",
|
|
45
|
+
"@latticexyz/solecs": "^1.41.0",
|
|
46
|
+
"@latticexyz/std-contracts": "^1.41.0",
|
|
44
47
|
"@typechain/ethers-v5": "^10.1.1",
|
|
45
48
|
"chalk": "^5.0.1",
|
|
46
49
|
"chokidar": "^3.5.3",
|
|
50
|
+
"dotenv": "^16.0.3",
|
|
47
51
|
"ds-test": "https://github.com/dapphub/ds-test.git#c9ce3f25bde29fc5eb9901842bf02850dfd2d084",
|
|
48
52
|
"ejs": "^3.1.8",
|
|
49
53
|
"ethers": "^5.7.2",
|
|
50
|
-
"execa": "^
|
|
54
|
+
"execa": "^7.0.0",
|
|
51
55
|
"find-up": "^6.3.0",
|
|
52
56
|
"forge-std": "https://github.com/foundry-rs/forge-std.git#b4f121555729b3afb3c5ffccb62ff4b6e2818fd3",
|
|
53
57
|
"glob": "^8.0.3",
|
|
@@ -65,5 +69,5 @@
|
|
|
65
69
|
"zod": "^3.20.6",
|
|
66
70
|
"zod-validation-error": "^0.3.2"
|
|
67
71
|
},
|
|
68
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "ecac84146f0288e3a129d1d3dbfaf373db86b1ab"
|
|
69
73
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import glob from "glob";
|
|
3
|
+
import path, { basename } from "path";
|
|
4
|
+
import type { CommandModule } from "yargs";
|
|
5
|
+
import { loadWorldConfig } from "../config/loadWorldConfig.js";
|
|
6
|
+
import { deploy } from "../utils/deploy-v2.js";
|
|
7
|
+
import { logError, MUDError } from "../utils/errors.js";
|
|
8
|
+
import { forge, getRpcUrl } from "../utils/foundry.js";
|
|
9
|
+
import { getOutDirectory } from "../utils/foundry.js";
|
|
10
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
11
|
+
import { loadStoreConfig } from "../config/loadStoreConfig.js";
|
|
12
|
+
import { deploymentInfoFilenamePrefix } from "../constants.js";
|
|
13
|
+
|
|
14
|
+
type Options = {
|
|
15
|
+
configPath?: string;
|
|
16
|
+
printConfig?: boolean;
|
|
17
|
+
profile?: string;
|
|
18
|
+
privateKey: string;
|
|
19
|
+
priorityFeeMultiplier: number;
|
|
20
|
+
clean?: boolean;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const commandModule: CommandModule<Options, Options> = {
|
|
24
|
+
command: "deploy-v2",
|
|
25
|
+
|
|
26
|
+
describe: "Deploy MUD v2 contracts",
|
|
27
|
+
|
|
28
|
+
builder(yargs) {
|
|
29
|
+
return yargs.options({
|
|
30
|
+
configPath: { type: "string", desc: "Path to the config file" },
|
|
31
|
+
clean: { type: "boolean", desc: "Remove the build forge artifacts and cache directories before building" },
|
|
32
|
+
printConfig: { type: "boolean", desc: "Print the resolved config" },
|
|
33
|
+
profile: { type: "string", desc: "The foundry profile to use" },
|
|
34
|
+
priorityFeeMultiplier: {
|
|
35
|
+
type: "number",
|
|
36
|
+
desc: "Multiply the estimated priority fee by the provided factor",
|
|
37
|
+
default: 1,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async handler(args) {
|
|
43
|
+
args.profile = args.profile ?? process.env.FOUNDRY_PROFILE;
|
|
44
|
+
const { configPath, printConfig, profile, clean } = args;
|
|
45
|
+
|
|
46
|
+
const rpc = await getRpcUrl(profile);
|
|
47
|
+
console.log(
|
|
48
|
+
chalk.bgBlue(
|
|
49
|
+
chalk.whiteBright(`\n Deploying MUD v2 contracts${profile ? " with profile " + profile : ""} to RPC ${rpc} \n`)
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (clean) await forge(["clean"], { profile });
|
|
54
|
+
|
|
55
|
+
// Run forge build
|
|
56
|
+
await forge(["build"], { profile });
|
|
57
|
+
|
|
58
|
+
// Get a list of all contract names
|
|
59
|
+
const outDir = await getOutDirectory();
|
|
60
|
+
const existingContracts = glob
|
|
61
|
+
.sync(`${outDir}/*.sol`)
|
|
62
|
+
// Get the basename of the file
|
|
63
|
+
.map((path) => basename(path, ".sol"));
|
|
64
|
+
|
|
65
|
+
// Load and resolve the config
|
|
66
|
+
const worldConfig = await loadWorldConfig(configPath, existingContracts);
|
|
67
|
+
const storeConfig = await loadStoreConfig(configPath);
|
|
68
|
+
const mudConfig = { ...worldConfig, ...storeConfig };
|
|
69
|
+
|
|
70
|
+
if (printConfig) console.log(chalk.green("\nResolved config:\n"), JSON.stringify(mudConfig, null, 2));
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const privateKey = process.env.PRIVATE_KEY;
|
|
74
|
+
if (!privateKey) throw new MUDError("Missing PRIVATE_KEY environment variable");
|
|
75
|
+
const deploymentInfo = await deploy(mudConfig, { ...args, rpc, privateKey });
|
|
76
|
+
|
|
77
|
+
// Write deployment result to file (latest and timestamp)
|
|
78
|
+
const outputDir = mudConfig.deploymentInfoDirectory;
|
|
79
|
+
mkdirSync(outputDir, { recursive: true });
|
|
80
|
+
writeFileSync(
|
|
81
|
+
path.join(outputDir, deploymentInfoFilenamePrefix + "latest.json"),
|
|
82
|
+
JSON.stringify(deploymentInfo, null, 2)
|
|
83
|
+
);
|
|
84
|
+
writeFileSync(
|
|
85
|
+
path.join(outputDir, deploymentInfoFilenamePrefix + Date.now() + ".json"),
|
|
86
|
+
JSON.stringify(deploymentInfo, null, 2)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
console.log(chalk.bgGreen(chalk.whiteBright(`\n Deployment result (written to ${outputDir}): \n`)));
|
|
90
|
+
console.log(deploymentInfo);
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
logError(error);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.exit(0);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default commandModule;
|
|
@@ -2,7 +2,7 @@ import { defaultAbiCoder as abi } from "ethers/lib/utils.js";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import type { CommandModule } from "yargs";
|
|
4
4
|
import { execLog } from "../../utils/deprecated/index.js";
|
|
5
|
-
import { getTestDirectory } from "../../utils/
|
|
5
|
+
import { getTestDirectory } from "../../utils/foundry.js";
|
|
6
6
|
|
|
7
7
|
type Options = {
|
|
8
8
|
rpc?: string;
|
|
@@ -2,7 +2,7 @@ import type { CommandModule } from "yargs";
|
|
|
2
2
|
import { DeployOptions, generateAndDeploy, hsr } from "../../utils/deprecated/index.js";
|
|
3
3
|
import openurl from "openurl";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
import { getSrcDirectory } from "../../utils/
|
|
5
|
+
import { getSrcDirectory } from "../../utils/foundry.js";
|
|
6
6
|
|
|
7
7
|
type Options = DeployOptions & {
|
|
8
8
|
watch?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs";
|
|
2
2
|
import { execLog, generateLibDeploy, resetLibDeploy } from "../../utils/deprecated/index.js";
|
|
3
|
-
import { getTestDirectory } from "../../utils/
|
|
3
|
+
import { getTestDirectory } from "../../utils/foundry.js";
|
|
4
4
|
|
|
5
5
|
type Options = {
|
|
6
6
|
forgeOpts?: string;
|
|
@@ -35,15 +35,18 @@ const commandModule: CommandModule<Options, Options> = {
|
|
|
35
35
|
...(forgeOpts?.split(" ") || []),
|
|
36
36
|
]);
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
console.log("Reset LibDeploy.sol");
|
|
40
|
-
await resetLibDeploy(testDir);
|
|
41
|
-
|
|
42
|
-
process.on("SIGINT", () => {
|
|
38
|
+
process.on("SIGINT", async () => {
|
|
43
39
|
console.log("\ngracefully shutting down from SIGINT (Crtl-C)");
|
|
44
40
|
child.kill();
|
|
41
|
+
await resetLibDeploy(testDir);
|
|
45
42
|
process.exit();
|
|
46
43
|
});
|
|
44
|
+
|
|
45
|
+
await child;
|
|
46
|
+
|
|
47
|
+
// Reset LibDeploy.sol
|
|
48
|
+
console.log("Reset LibDeploy.sol");
|
|
49
|
+
await resetLibDeploy(testDir);
|
|
47
50
|
},
|
|
48
51
|
};
|
|
49
52
|
|
|
@@ -4,7 +4,7 @@ import { readFileSync } from "fs";
|
|
|
4
4
|
import { Contract } from "ethers";
|
|
5
5
|
import { JsonRpcProvider } from "@ethersproject/providers";
|
|
6
6
|
import WorldAbi from "@latticexyz/solecs/abi/World.json" assert { type: "json" };
|
|
7
|
-
import { getSrcDirectory } from "../../utils/
|
|
7
|
+
import { getSrcDirectory } from "../../utils/foundry.js";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { componentsDir, systemsDir } from "../../utils/deprecated/constants.js";
|
|
10
10
|
|
|
@@ -156,7 +156,7 @@ console.log("GAS REPORT: ${name} [${functionCall.replaceAll('"', '\\"')}]:", _ga
|
|
|
156
156
|
stdio: ["inherit", "pipe", "inherit"],
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
//
|
|
159
|
+
// Extract the logs from the child process
|
|
160
160
|
let logs = "";
|
|
161
161
|
try {
|
|
162
162
|
logs = (await child).stdout;
|
package/src/commands/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import faucet from "./faucet.js";
|
|
|
14
14
|
import gasReport from "./gas-report.js";
|
|
15
15
|
import hello from "./hello.js";
|
|
16
16
|
import tablegen from "./tablegen.js";
|
|
17
|
+
import deployV2 from "./deploy-v2.js";
|
|
17
18
|
|
|
18
19
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Each command has different options
|
|
19
20
|
export const commands: CommandModule<any, any>[] = [
|
|
@@ -21,6 +22,7 @@ export const commands: CommandModule<any, any>[] = [
|
|
|
21
22
|
callSystem,
|
|
22
23
|
codegenLibdeploy,
|
|
23
24
|
deployContracts,
|
|
25
|
+
deployV2,
|
|
24
26
|
devnode,
|
|
25
27
|
faucet,
|
|
26
28
|
gasReport,
|
package/src/commands/tablegen.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import type { CommandModule } from "yargs";
|
|
2
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
-
import path from "path";
|
|
4
2
|
import { loadStoreConfig } from "../config/loadStoreConfig.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { formatSolidity } from "../utils/format.js";
|
|
3
|
+
import { getSrcDirectory } from "../utils/foundry.js";
|
|
4
|
+
import { tablegen } from "../render-solidity/tablegen.js";
|
|
8
5
|
|
|
9
6
|
type Options = {
|
|
10
7
|
configPath?: string;
|
|
@@ -22,22 +19,11 @@ const commandModule: CommandModule<Options, Options> = {
|
|
|
22
19
|
},
|
|
23
20
|
|
|
24
21
|
async handler({ configPath }) {
|
|
25
|
-
const
|
|
22
|
+
const srcDirectory = await getSrcDirectory();
|
|
26
23
|
|
|
27
24
|
const config = await loadStoreConfig(configPath);
|
|
28
|
-
const renderedTables = renderTablesFromConfig(config);
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
const formattedOutput = await formatSolidity(output);
|
|
32
|
-
|
|
33
|
-
const tablePath = config.tables[tableName].route;
|
|
34
|
-
const outputDirectory = path.join(srcDir, tablePath);
|
|
35
|
-
mkdirSync(outputDirectory, { recursive: true });
|
|
36
|
-
|
|
37
|
-
const outputPath = path.join(outputDirectory, `${tableName}.sol`);
|
|
38
|
-
writeFileSync(outputPath, formattedOutput);
|
|
39
|
-
console.log(`Generated schema: ${outputPath}`);
|
|
40
|
-
}
|
|
26
|
+
await tablegen(config, srcDirectory);
|
|
41
27
|
|
|
42
28
|
process.exit(0);
|
|
43
29
|
},
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { getStaticByteLength, SchemaType } from "@latticexyz/schema-type";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import {
|
|
3
4
|
validateBaseRoute,
|
|
4
5
|
validateCapitalizedName,
|
|
6
|
+
validateEthereumAddress,
|
|
7
|
+
validateEnum,
|
|
8
|
+
validateName,
|
|
5
9
|
validateRoute,
|
|
6
10
|
validateSingleLevelRoute,
|
|
7
11
|
validateUncapitalizedName,
|
|
@@ -11,6 +15,10 @@ import {
|
|
|
11
15
|
export const ObjectName = z.string().superRefine(validateCapitalizedName);
|
|
12
16
|
/** Uncapitalized names of values, like keys and columns */
|
|
13
17
|
export const ValueName = z.string().superRefine(validateUncapitalizedName);
|
|
18
|
+
/** Name that can start with any case */
|
|
19
|
+
export const AnyCaseName = z.string().superRefine(validateName);
|
|
20
|
+
/** List of unique enum member names and 0 < length < 256 */
|
|
21
|
+
export const UserEnum = z.array(ObjectName).superRefine(validateEnum);
|
|
14
22
|
|
|
15
23
|
/** Ordinary routes */
|
|
16
24
|
export const OrdinaryRoute = z.string().superRefine(validateRoute);
|
|
@@ -18,3 +26,11 @@ export const OrdinaryRoute = z.string().superRefine(validateRoute);
|
|
|
18
26
|
export const SingleLevelRoute = z.string().superRefine(validateSingleLevelRoute);
|
|
19
27
|
/** Base routes (can be an empty string) */
|
|
20
28
|
export const BaseRoute = z.string().superRefine(validateBaseRoute);
|
|
29
|
+
|
|
30
|
+
/** A valid Ethereum address */
|
|
31
|
+
export const EthereumAddress = z.string().superRefine(validateEthereumAddress);
|
|
32
|
+
|
|
33
|
+
/** Static subset of SchemaType enum */
|
|
34
|
+
export const StaticSchemaType = z
|
|
35
|
+
.nativeEnum(SchemaType)
|
|
36
|
+
.refine((arg) => getStaticByteLength(arg) > 0, "SchemaType must be static");
|
package/src/config/index.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { StoreUserConfig, StoreConfig } from "./parseStoreConfig.js";
|
|
2
|
+
import { WorldUserConfig, ResolvedWorldConfig } from "./loadWorldConfig.js";
|
|
3
|
+
|
|
4
|
+
export type MUDUserConfig = StoreUserConfig & WorldUserConfig;
|
|
5
|
+
export type MUDConfig = StoreConfig & ResolvedWorldConfig;
|
|
6
|
+
|
|
1
7
|
export * from "./commonSchemas.js";
|
|
2
8
|
export * from "./loadConfig.js";
|
|
3
9
|
export * from "./loadStoreConfig.js";
|
|
10
|
+
export * from "./parseStoreConfig.js";
|
|
11
|
+
export * from "./loadWorldConfig.js";
|
|
4
12
|
export * from "./validation.js";
|
|
@@ -1,89 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { z, ZodError } from "zod";
|
|
1
|
+
import { ZodError } from "zod";
|
|
3
2
|
import { fromZodErrorCustom } from "../utils/errors.js";
|
|
4
|
-
import { BaseRoute, ObjectName, OrdinaryRoute, ValueName } from "./commonSchemas.js";
|
|
5
3
|
import { loadConfig } from "./loadConfig.js";
|
|
6
|
-
|
|
7
|
-
const TableName = ObjectName;
|
|
8
|
-
const KeyName = ValueName;
|
|
9
|
-
const ColumnName = ValueName;
|
|
10
|
-
|
|
11
|
-
const PrimaryKey = z
|
|
12
|
-
.nativeEnum(SchemaType)
|
|
13
|
-
.refine((arg) => getStaticByteLength(arg) > 0, "Primary key must not use dynamic SchemaType");
|
|
14
|
-
const PrimaryKeys = z.record(KeyName, PrimaryKey).default({ key: SchemaType.BYTES32 });
|
|
15
|
-
|
|
16
|
-
const Schema = z
|
|
17
|
-
.record(ColumnName, z.nativeEnum(SchemaType))
|
|
18
|
-
.refine((arg) => Object.keys(arg).length > 0, "Table schema may not be empty");
|
|
19
|
-
|
|
20
|
-
const FullTable = z
|
|
21
|
-
.object({
|
|
22
|
-
route: OrdinaryRoute.default("/tables"),
|
|
23
|
-
tableIdArgument: z.boolean().default(false),
|
|
24
|
-
storeArgument: z.boolean().default(false),
|
|
25
|
-
primaryKeys: PrimaryKeys,
|
|
26
|
-
schema: Schema,
|
|
27
|
-
dataStruct: z.boolean().optional(),
|
|
28
|
-
})
|
|
29
|
-
.transform((arg) => {
|
|
30
|
-
// default dataStruct value depends on schema's length
|
|
31
|
-
if (Object.keys(arg.schema).length === 1) {
|
|
32
|
-
arg.dataStruct ??= false;
|
|
33
|
-
} else {
|
|
34
|
-
arg.dataStruct ??= true;
|
|
35
|
-
}
|
|
36
|
-
return arg as Omit<typeof arg, "dataStruct"> & Required<Pick<typeof arg, "dataStruct">>;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const DefaultSingleValueTable = z.nativeEnum(SchemaType).transform((schemaType) => {
|
|
40
|
-
return FullTable.parse({
|
|
41
|
-
schema: {
|
|
42
|
-
value: schemaType,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export const StoreConfig = z.object({
|
|
48
|
-
baseRoute: BaseRoute.default(""),
|
|
49
|
-
storeImportPath: z.string().default("@latticexyz/store/src/"),
|
|
50
|
-
tables: z.record(TableName, z.union([DefaultSingleValueTable, FullTable])),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// zod doesn't preserve doc comments
|
|
54
|
-
export interface StoreUserConfig {
|
|
55
|
-
/** The base route prefix for table ids. Default is "" (empty string) */
|
|
56
|
-
baseRoute?: string;
|
|
57
|
-
/** Path for store package imports. Default is "@latticexyz/store/src/" */
|
|
58
|
-
storeImportPath?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Configuration for each table.
|
|
61
|
-
*
|
|
62
|
-
* The key is the table name (capitalized).
|
|
63
|
-
*
|
|
64
|
-
* The value:
|
|
65
|
-
* - SchemaType for a single-value table (aka ECS component).
|
|
66
|
-
* - FullTableConfig object for multi-value tables (or for customizable options).
|
|
67
|
-
*/
|
|
68
|
-
tables: Record<string, SchemaType | FullTableConfig>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface FullTableConfig {
|
|
72
|
-
/** Output path for the file, and relevant for the table id. The table id will be keccak256(concat(baseRoute,route,tableName)). Default is "tables/" */
|
|
73
|
-
route?: string;
|
|
74
|
-
/** Make methods accept `tableId` argument instead of it being a hardcoded constant. Default is false */
|
|
75
|
-
tableIdArgument?: boolean;
|
|
76
|
-
/** Include methods that accept a manual `IStore` argument. Default is false. */
|
|
77
|
-
storeArgument?: boolean;
|
|
78
|
-
/** Include a data struct and methods for it. Default is false for 1-column tables; true for multi-column tables. */
|
|
79
|
-
dataStruct?: boolean;
|
|
80
|
-
/** Table's primary key names mapped to their types. Default is `{ key: SchemaType.BYTES32 }` */
|
|
81
|
-
primaryKeys?: Record<string, SchemaType>;
|
|
82
|
-
/** Table's column names mapped to their types. Table name's 1st letter should be lowercase. */
|
|
83
|
-
schema: Record<string, SchemaType>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export type StoreConfig = z.output<typeof StoreConfig>;
|
|
4
|
+
import { parseStoreConfig } from "./parseStoreConfig.js";
|
|
87
5
|
|
|
88
6
|
export async function loadStoreConfig(configPath?: string) {
|
|
89
7
|
const config = await loadConfig(configPath);
|
|
@@ -98,7 +16,3 @@ export async function loadStoreConfig(configPath?: string) {
|
|
|
98
16
|
}
|
|
99
17
|
}
|
|
100
18
|
}
|
|
101
|
-
|
|
102
|
-
export async function parseStoreConfig(config: unknown) {
|
|
103
|
-
return StoreConfig.parse(config);
|
|
104
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expectTypeOf } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { WorldConfig, WorldUserConfig } from "./loadWorldConfig.js";
|
|
4
|
+
|
|
5
|
+
describe("loadWorldConfig", () => {
|
|
6
|
+
// Typecheck manual interfaces against zod
|
|
7
|
+
expectTypeOf<WorldUserConfig>().toEqualTypeOf<z.input<typeof WorldConfig>>();
|
|
8
|
+
// type equality isn't deep for optionals
|
|
9
|
+
expectTypeOf<WorldUserConfig["overrideSystems"]>().toEqualTypeOf<z.input<typeof WorldConfig>["overrideSystems"]>();
|
|
10
|
+
// TODO If more nested schemas are added, provide separate tests for them
|
|
11
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z, ZodError } from "zod";
|
|
2
|
+
import { fromZodErrorCustom, UnrecognizedSystemErrorFactory } from "../utils/errors.js";
|
|
3
|
+
import { BaseRoute, EthereumAddress, ObjectName } from "./commonSchemas.js";
|
|
4
|
+
import { loadConfig } from "./loadConfig.js";
|
|
5
|
+
|
|
6
|
+
const SystemName = ObjectName;
|
|
7
|
+
const SystemRoute = BaseRoute.optional();
|
|
8
|
+
const SystemAccessList = z.array(SystemName.or(EthereumAddress)).default([]);
|
|
9
|
+
|
|
10
|
+
// The system config is a combination of a route config and access config
|
|
11
|
+
const SystemConfig = z.intersection(
|
|
12
|
+
z.object({
|
|
13
|
+
route: SystemRoute,
|
|
14
|
+
}),
|
|
15
|
+
z.discriminatedUnion("openAccess", [
|
|
16
|
+
z.object({
|
|
17
|
+
openAccess: z.literal(true),
|
|
18
|
+
}),
|
|
19
|
+
z.object({
|
|
20
|
+
openAccess: z.literal(false),
|
|
21
|
+
accessList: SystemAccessList,
|
|
22
|
+
}),
|
|
23
|
+
])
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// The parsed world config is the result of parsing the user config
|
|
27
|
+
export const WorldConfig = z.object({
|
|
28
|
+
baseRoute: BaseRoute.default(""),
|
|
29
|
+
worldContractName: z.string().optional(),
|
|
30
|
+
overrideSystems: z.record(SystemName, SystemConfig).default({}),
|
|
31
|
+
excludeSystems: z.array(SystemName).default([]),
|
|
32
|
+
postDeployScript: z.string().default("PostDeploy"),
|
|
33
|
+
deploymentInfoDirectory: z.string().default("."),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves the system config by combining the default and overridden system configs,
|
|
38
|
+
* @param systemName name of the system
|
|
39
|
+
* @param config optional SystemConfig object, if none is provided the default config is used
|
|
40
|
+
* @param existingContracts optional list of existing contract names, used to validate system names in the access list. If not provided, no validation is performed.
|
|
41
|
+
* @returns ResolvedSystemConfig object
|
|
42
|
+
* Default value for route is `/${systemName}`
|
|
43
|
+
* Default value for openAccess is true
|
|
44
|
+
* Default value for accessListAddresses is []
|
|
45
|
+
* Default value for accessListSystems is []
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSystemConfig(systemName: string, config?: SystemUserConfig, existingContracts?: string[]) {
|
|
48
|
+
const route = config?.route ?? `/${systemName}`;
|
|
49
|
+
const openAccess = config?.openAccess ?? true;
|
|
50
|
+
const accessListAddresses: string[] = [];
|
|
51
|
+
const accessListSystems: string[] = [];
|
|
52
|
+
const accessList = config && !config.openAccess ? config.accessList : [];
|
|
53
|
+
|
|
54
|
+
// Split the access list into addresses and system names
|
|
55
|
+
for (const accessListItem of accessList) {
|
|
56
|
+
if (accessListItem.startsWith("0x")) {
|
|
57
|
+
accessListAddresses.push(accessListItem);
|
|
58
|
+
} else {
|
|
59
|
+
// Validate every system refers to an existing system contract
|
|
60
|
+
if (existingContracts && !existingContracts.includes(accessListItem)) {
|
|
61
|
+
throw UnrecognizedSystemErrorFactory(["overrideSystems", systemName, "accessList"], accessListItem);
|
|
62
|
+
}
|
|
63
|
+
accessListSystems.push(accessListItem);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { route, openAccess, accessListAddresses, accessListSystems };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the world config by combining the default and overridden system configs,
|
|
72
|
+
* filtering out excluded systems, validate system names refer to existing contracts, and
|
|
73
|
+
* splitting the access list into addresses and system names.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveWorldConfig(config: ParsedWorldConfig, existingContracts?: string[]) {
|
|
76
|
+
// Include contract names ending in "System", but not the base "System" contract
|
|
77
|
+
const defaultSystemNames = existingContracts?.filter((name) => name.endsWith("System") && name !== "System") ?? [];
|
|
78
|
+
const overriddenSystemNames = Object.keys(config.overrideSystems);
|
|
79
|
+
|
|
80
|
+
// Validate every key in overrideSystems refers to an existing system contract (and is not called "World")
|
|
81
|
+
if (existingContracts) {
|
|
82
|
+
for (const systemName of overriddenSystemNames) {
|
|
83
|
+
if (!existingContracts.includes(systemName) || systemName === "World") {
|
|
84
|
+
throw UnrecognizedSystemErrorFactory(["overrideSystems", systemName], systemName);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Combine the default and overridden system names and filter out excluded systems
|
|
90
|
+
const systemNames = [...new Set([...defaultSystemNames, ...overriddenSystemNames])].filter(
|
|
91
|
+
(name) => !config.excludeSystems.includes(name)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Resolve the config
|
|
95
|
+
const resolvedSystems: Record<string, ResolvedSystemConfig> = systemNames.reduce((acc, systemName) => {
|
|
96
|
+
return {
|
|
97
|
+
...acc,
|
|
98
|
+
[systemName]: resolveSystemConfig(systemName, config.overrideSystems[systemName], existingContracts),
|
|
99
|
+
};
|
|
100
|
+
}, {});
|
|
101
|
+
|
|
102
|
+
const { overrideSystems, excludeSystems, ...otherConfig } = config;
|
|
103
|
+
return { ...otherConfig, systems: resolvedSystems };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Loads and resolves the world config.
|
|
108
|
+
* @param configPath Path to load the config from. Defaults to "mud.config.mts" or "mud.config.ts"
|
|
109
|
+
* @param existingContracts Optional list of existing contract names to validate system names against. If not provided, no validation is performed. Contract names ending in `System` will be added to the config with default values.
|
|
110
|
+
* @returns Promise of ResolvedWorldConfig object
|
|
111
|
+
*/
|
|
112
|
+
export async function loadWorldConfig(configPath?: string, existingContracts?: string[]) {
|
|
113
|
+
const config = await loadConfig(configPath);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const parsedConfig = WorldConfig.parse(config);
|
|
117
|
+
return resolveWorldConfig(parsedConfig, existingContracts);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (error instanceof ZodError) {
|
|
120
|
+
throw fromZodErrorCustom(error, "WorldConfig Validation Error");
|
|
121
|
+
} else {
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function parseWorldConfig(config: unknown) {
|
|
128
|
+
return WorldConfig.parse(config);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// zod doesn't preserve doc comments
|
|
132
|
+
export type SystemUserConfig =
|
|
133
|
+
| {
|
|
134
|
+
/** The system will be deployed at `baseRoute + route` */
|
|
135
|
+
route?: string;
|
|
136
|
+
} & (
|
|
137
|
+
| {
|
|
138
|
+
/** If openAccess is true, any address can call the system */
|
|
139
|
+
openAccess: true;
|
|
140
|
+
}
|
|
141
|
+
| {
|
|
142
|
+
/** If openAccess is false, only the addresses or systems in `access` can call the system */
|
|
143
|
+
openAccess: false;
|
|
144
|
+
/** An array of addresses or system names that can access the system */
|
|
145
|
+
accessList: string[];
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// zod doesn't preserve doc comments
|
|
150
|
+
export interface WorldUserConfig {
|
|
151
|
+
/** The base route to register tables and systems at. Defaults to the root route (empty string) */
|
|
152
|
+
baseRoute?: string;
|
|
153
|
+
/** The name of the World contract to deploy. If no name is provided, a vanilla World is deployed */
|
|
154
|
+
worldContractName?: string;
|
|
155
|
+
/**
|
|
156
|
+
* Contracts named *System will be deployed by default
|
|
157
|
+
* as public systems at `baseRoute/ContractName`, unless overridden
|
|
158
|
+
*
|
|
159
|
+
* The key is the system name (capitalized).
|
|
160
|
+
* The value is a SystemConfig object.
|
|
161
|
+
*/
|
|
162
|
+
overrideSystems?: Record<string, SystemUserConfig>;
|
|
163
|
+
/** Systems to exclude from automatic deployment */
|
|
164
|
+
excludeSystems?: string[];
|
|
165
|
+
/**
|
|
166
|
+
* Script to execute after the deployment is complete (Default "PostDeploy").
|
|
167
|
+
* Script must be placed in the forge scripts directory (see foundry.toml) and have a ".s.sol" extension.
|
|
168
|
+
*/
|
|
169
|
+
postDeployScript?: string;
|
|
170
|
+
/** Directory to write the deployment info to (Default ".") */
|
|
171
|
+
deploymentInfoDirectory?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export type ParsedWorldConfig = z.output<typeof WorldConfig>;
|
|
175
|
+
|
|
176
|
+
export type ResolvedSystemConfig = ReturnType<typeof resolveSystemConfig>;
|
|
177
|
+
|
|
178
|
+
export type ResolvedWorldConfig = ReturnType<typeof resolveWorldConfig>;
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { describe, expectTypeOf } from "vitest";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { StoreConfig, StoreUserConfig } from "./
|
|
3
|
+
import { StoreConfig, StoreUserConfig } from "./parseStoreConfig.js";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("StoreUserConfig", () => {
|
|
6
6
|
// Typecheck manual interfaces against zod
|
|
7
7
|
expectTypeOf<StoreUserConfig>().toEqualTypeOf<z.input<typeof StoreConfig>>();
|
|
8
8
|
// type equality isn't deep for optionals
|
|
9
9
|
expectTypeOf<StoreUserConfig["tables"][string]>().toEqualTypeOf<z.input<typeof StoreConfig>["tables"][string]>();
|
|
10
|
+
expectTypeOf<NonNullable<NonNullable<StoreUserConfig["userTypes"]>["enums"]>[string]>().toEqualTypeOf<
|
|
11
|
+
NonNullable<NonNullable<z.input<typeof StoreConfig>["userTypes"]>["enums"]>[string]
|
|
12
|
+
>();
|
|
10
13
|
// TODO If more nested schemas are added, provide separate tests for them
|
|
11
14
|
});
|