@latticexyz/cli 2.0.0-next.11 → 2.0.0-next.12
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/index.js +0 -1
- package/dist/mud.js +19 -13
- package/dist/mud.js.map +1 -1
- package/package.json +16 -12
- package/src/commands/deploy.ts +7 -30
- package/src/commands/dev-contracts.ts +74 -138
- package/src/commands/test.ts +30 -36
- package/src/commands/trace.ts +7 -5
- package/src/debug.ts +3 -0
- package/src/deploy/assertNamespaceOwner.ts +42 -0
- package/src/deploy/common.ts +72 -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 +3 -0
- package/src/deploy/deploy.ts +108 -0
- package/src/deploy/deployWorld.ts +33 -0
- package/src/deploy/ensureContract.ts +49 -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 +72 -0
- package/src/deploy/ensureSystems.ts +161 -0
- package/src/deploy/ensureTables.ts +65 -0
- package/src/deploy/ensureWorldFactory.ts +34 -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 +154 -0
- package/src/deploy/resourceLabel.ts +3 -0
- package/src/index.ts +1 -1
- package/src/runDeploy.ts +128 -0
- package/src/utils/modules/constants.ts +1 -2
- package/src/utils/utils/getContractData.ts +2 -5
- package/dist/chunk-TW3YGZ4D.js +0 -11
- package/dist/chunk-TW3YGZ4D.js.map +0 -1
- package/src/utils/deploy.ts +0 -254
- package/src/utils/deployHandler.ts +0 -93
- package/src/utils/modules/getInstallModuleCallData.ts +0 -27
- package/src/utils/modules/getUserModules.ts +0 -5
- package/src/utils/modules/types.ts +0 -14
- package/src/utils/systems/getGrantAccessCallData.ts +0 -29
- package/src/utils/systems/getRegisterFunctionSelectorsCallData.ts +0 -57
- package/src/utils/systems/getRegisterSystemCallData.ts +0 -17
- package/src/utils/systems/types.ts +0 -9
- package/src/utils/systems/utils.ts +0 -42
- package/src/utils/tables/getRegisterTableCallData.ts +0 -49
- package/src/utils/tables/getTableIds.ts +0 -18
- package/src/utils/tables/types.ts +0 -12
- package/src/utils/utils/confirmNonce.ts +0 -24
- package/src/utils/utils/deployContract.ts +0 -33
- package/src/utils/utils/fastTxExecute.ts +0 -56
- package/src/utils/utils/getChainId.ts +0 -10
- package/src/utils/utils/setInternalFeePerGas.ts +0 -49
- package/src/utils/utils/types.ts +0 -21
- package/src/utils/world.ts +0 -28
@@ -0,0 +1,68 @@
|
|
1
|
+
import { resourceToHex } from "@latticexyz/common";
|
2
|
+
import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser";
|
3
|
+
import { SchemaAbiType, StaticAbiType } from "@latticexyz/schema-type";
|
4
|
+
import { StoreConfig, resolveUserTypes } from "@latticexyz/store";
|
5
|
+
import { Hex } from "viem";
|
6
|
+
|
7
|
+
// TODO: we shouldn't need this file once our config parsing returns nicely formed tables
|
8
|
+
|
9
|
+
type UserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"];
|
10
|
+
// TODO: fix strong enum types and avoid every schema getting `{ [k: string]: "uint8" }`
|
11
|
+
// type UserTypes<config extends StoreConfig = StoreConfig> = config["userTypes"] & {
|
12
|
+
// [k in keyof config["enums"]]: { internalType: "uint8" };
|
13
|
+
// };
|
14
|
+
|
15
|
+
export type TableKey<
|
16
|
+
config extends StoreConfig = StoreConfig,
|
17
|
+
table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
|
18
|
+
> = `${config["namespace"]}_${table["name"]}`;
|
19
|
+
|
20
|
+
export type Table<
|
21
|
+
config extends StoreConfig = StoreConfig,
|
22
|
+
table extends config["tables"][keyof config["tables"]] = config["tables"][keyof config["tables"]]
|
23
|
+
> = {
|
24
|
+
readonly namespace: config["namespace"];
|
25
|
+
readonly name: table["name"];
|
26
|
+
readonly tableId: Hex;
|
27
|
+
readonly keySchema: table["keySchema"] extends KeySchema<UserTypes<config>>
|
28
|
+
? KeySchema & {
|
29
|
+
readonly [k in keyof table["keySchema"]]: UserTypes<config>[table["keySchema"][k]]["internalType"] extends StaticAbiType
|
30
|
+
? UserTypes<config>[table["keySchema"][k]]["internalType"]
|
31
|
+
: table["keySchema"][k];
|
32
|
+
}
|
33
|
+
: KeySchema;
|
34
|
+
readonly valueSchema: table["valueSchema"] extends ValueSchema<UserTypes<config>>
|
35
|
+
? {
|
36
|
+
readonly [k in keyof table["valueSchema"]]: UserTypes<config>[table["valueSchema"][k]]["internalType"] extends SchemaAbiType
|
37
|
+
? UserTypes<config>[table["valueSchema"][k]]["internalType"]
|
38
|
+
: table["valueSchema"][k];
|
39
|
+
}
|
40
|
+
: ValueSchema;
|
41
|
+
};
|
42
|
+
|
43
|
+
export type Tables<config extends StoreConfig = StoreConfig> = {
|
44
|
+
readonly [k in keyof config["tables"] as TableKey<config, config["tables"][k]>]: Table<config, config["tables"][k]>;
|
45
|
+
};
|
46
|
+
|
47
|
+
export function configToTables<config extends StoreConfig>(config: config): Tables<config> {
|
48
|
+
const userTypes = {
|
49
|
+
...config.userTypes,
|
50
|
+
...Object.fromEntries(Object.entries(config.enums).map(([key]) => [key, { internalType: "uint8" }] as const)),
|
51
|
+
};
|
52
|
+
return Object.fromEntries(
|
53
|
+
Object.entries(config.tables).map(([tableName, table]) => [
|
54
|
+
`${config.namespace}_${tableName}` satisfies TableKey<config, config["tables"][keyof config["tables"]]>,
|
55
|
+
{
|
56
|
+
namespace: config.namespace,
|
57
|
+
name: table.name,
|
58
|
+
tableId: resourceToHex({
|
59
|
+
type: table.offchainOnly ? "offchainTable" : "table",
|
60
|
+
namespace: config.namespace,
|
61
|
+
name: table.name,
|
62
|
+
}),
|
63
|
+
keySchema: resolveUserTypes(table.keySchema, userTypes) as any,
|
64
|
+
valueSchema: resolveUserTypes(table.valueSchema, userTypes) as any,
|
65
|
+
} satisfies Table<config, config["tables"][keyof config["tables"]]>,
|
66
|
+
])
|
67
|
+
) as Tables<config>;
|
68
|
+
}
|
@@ -0,0 +1,7 @@
|
|
1
|
+
{
|
2
|
+
"gasPrice": 100000000000,
|
3
|
+
"gasLimit": 100000,
|
4
|
+
"signerAddress": "3fab184622dc19b6109349b94811493bf2a45362",
|
5
|
+
"transaction": "f8a58085174876e800830186a08080b853604580600e600039806000f350fe7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf31ba02222222222222222222222222222222222222222222222222222222222222222a02222222222222222222222222222222222222222222222222222222222222222",
|
6
|
+
"address": "4e59b44847b379578588920ca78fbf26c0b4956c"
|
7
|
+
}
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import { Account, Address, Chain, Client, Transport, getAddress } from "viem";
|
2
|
+
import { ensureDeployer } from "./ensureDeployer";
|
3
|
+
import { deployWorld } from "./deployWorld";
|
4
|
+
import { ensureTables } from "./ensureTables";
|
5
|
+
import { Config, ConfigInput, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common";
|
6
|
+
import { ensureSystems } from "./ensureSystems";
|
7
|
+
import { waitForTransactionReceipt } from "viem/actions";
|
8
|
+
import { getWorldDeploy } from "./getWorldDeploy";
|
9
|
+
import { ensureFunctions } from "./ensureFunctions";
|
10
|
+
import { ensureModules } from "./ensureModules";
|
11
|
+
import { Table } from "./configToTables";
|
12
|
+
import { assertNamespaceOwner } from "./assertNamespaceOwner";
|
13
|
+
import { debug } from "./debug";
|
14
|
+
import { resourceLabel } from "./resourceLabel";
|
15
|
+
import { ensureContract } from "./ensureContract";
|
16
|
+
import { uniqueBy } from "@latticexyz/common/utils";
|
17
|
+
import { ensureContractsDeployed } from "./ensureContractsDeployed";
|
18
|
+
import { coreModuleBytecode, worldFactoryBytecode } from "./ensureWorldFactory";
|
19
|
+
|
20
|
+
type DeployOptions<configInput extends ConfigInput> = {
|
21
|
+
client: Client<Transport, Chain | undefined, Account>;
|
22
|
+
config: Config<configInput>;
|
23
|
+
worldAddress?: Address;
|
24
|
+
};
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Given a viem client and MUD config, we attempt to introspect the world
|
28
|
+
* (or deploy a new one if no world address is provided) and do the minimal
|
29
|
+
* amount of work to make the world match the config (e.g. deploy new tables,
|
30
|
+
* replace systems, etc.)
|
31
|
+
*/
|
32
|
+
export async function deploy<configInput extends ConfigInput>({
|
33
|
+
client,
|
34
|
+
config,
|
35
|
+
worldAddress: existingWorldAddress,
|
36
|
+
}: DeployOptions<configInput>): Promise<WorldDeploy> {
|
37
|
+
const tables = Object.values(config.tables) as Table[];
|
38
|
+
const systems = Object.values(config.systems);
|
39
|
+
|
40
|
+
await ensureDeployer(client);
|
41
|
+
|
42
|
+
// deploy all dependent contracts, because system registration, module install, etc. all expect these contracts to be callable.
|
43
|
+
await ensureContractsDeployed({
|
44
|
+
client,
|
45
|
+
contracts: [
|
46
|
+
{ bytecode: coreModuleBytecode, label: "core module" },
|
47
|
+
{ bytecode: worldFactoryBytecode, label: "world factory" },
|
48
|
+
...uniqueBy(systems, (system) => getAddress(system.address)).map((system) => ({
|
49
|
+
bytecode: system.bytecode,
|
50
|
+
label: `${resourceLabel(system)} system`,
|
51
|
+
})),
|
52
|
+
...uniqueBy(config.modules, (mod) => getAddress(mod.address)).map((mod) => ({
|
53
|
+
bytecode: mod.bytecode,
|
54
|
+
label: `${mod.name} module`,
|
55
|
+
})),
|
56
|
+
],
|
57
|
+
});
|
58
|
+
|
59
|
+
const worldDeploy = existingWorldAddress
|
60
|
+
? await getWorldDeploy(client, existingWorldAddress)
|
61
|
+
: await deployWorld(client);
|
62
|
+
|
63
|
+
if (!supportedStoreVersions.includes(worldDeploy.storeVersion)) {
|
64
|
+
throw new Error(`Unsupported Store version: ${worldDeploy.storeVersion}`);
|
65
|
+
}
|
66
|
+
if (!supportedWorldVersions.includes(worldDeploy.worldVersion)) {
|
67
|
+
throw new Error(`Unsupported World version: ${worldDeploy.worldVersion}`);
|
68
|
+
}
|
69
|
+
|
70
|
+
await assertNamespaceOwner({
|
71
|
+
client,
|
72
|
+
worldDeploy,
|
73
|
+
resourceIds: [...tables.map((table) => table.tableId), ...systems.map((system) => system.systemId)],
|
74
|
+
});
|
75
|
+
|
76
|
+
const tableTxs = await ensureTables({
|
77
|
+
client,
|
78
|
+
worldDeploy,
|
79
|
+
tables,
|
80
|
+
});
|
81
|
+
const systemTxs = await ensureSystems({
|
82
|
+
client,
|
83
|
+
worldDeploy,
|
84
|
+
systems,
|
85
|
+
});
|
86
|
+
const functionTxs = await ensureFunctions({
|
87
|
+
client,
|
88
|
+
worldDeploy,
|
89
|
+
functions: systems.flatMap((system) => system.functions),
|
90
|
+
});
|
91
|
+
const moduleTxs = await ensureModules({
|
92
|
+
client,
|
93
|
+
worldDeploy,
|
94
|
+
modules: config.modules,
|
95
|
+
});
|
96
|
+
|
97
|
+
const txs = [...tableTxs, ...systemTxs, ...functionTxs, ...moduleTxs];
|
98
|
+
|
99
|
+
// wait for each tx separately/serially, because parallelizing results in RPC errors
|
100
|
+
debug("waiting for all transactions to confirm");
|
101
|
+
for (const tx of txs) {
|
102
|
+
await waitForTransactionReceipt(client, { hash: tx });
|
103
|
+
// TODO: throw if there was a revert?
|
104
|
+
}
|
105
|
+
|
106
|
+
debug("deploy complete");
|
107
|
+
return worldDeploy;
|
108
|
+
}
|
@@ -0,0 +1,33 @@
|
|
1
|
+
import { Account, Chain, Client, Log, Transport } from "viem";
|
2
|
+
import { waitForTransactionReceipt } from "viem/actions";
|
3
|
+
import { ensureWorldFactory, worldFactory } from "./ensureWorldFactory";
|
4
|
+
import WorldFactoryAbi from "@latticexyz/world/out/WorldFactory.sol/WorldFactory.abi.json" assert { type: "json" };
|
5
|
+
import { writeContract } from "@latticexyz/common";
|
6
|
+
import { debug } from "./debug";
|
7
|
+
import { logsToWorldDeploy } from "./logsToWorldDeploy";
|
8
|
+
import { WorldDeploy } from "./common";
|
9
|
+
|
10
|
+
export async function deployWorld(client: Client<Transport, Chain | undefined, Account>): Promise<WorldDeploy> {
|
11
|
+
await ensureWorldFactory(client);
|
12
|
+
|
13
|
+
debug("deploying world");
|
14
|
+
const tx = await writeContract(client, {
|
15
|
+
chain: client.chain ?? null,
|
16
|
+
address: worldFactory,
|
17
|
+
abi: WorldFactoryAbi,
|
18
|
+
functionName: "deployWorld",
|
19
|
+
});
|
20
|
+
|
21
|
+
debug("waiting for world deploy");
|
22
|
+
const receipt = await waitForTransactionReceipt(client, { hash: tx });
|
23
|
+
if (receipt.status !== "success") {
|
24
|
+
console.error("world deploy failed", receipt);
|
25
|
+
throw new Error("world deploy failed");
|
26
|
+
}
|
27
|
+
|
28
|
+
// TODO: remove type casting once https://github.com/wagmi-dev/viem/pull/1330 is merged
|
29
|
+
const deploy = logsToWorldDeploy(receipt.logs.map((log) => log as Log<bigint, number, false>));
|
30
|
+
debug("deployed world to", deploy.address, "at block", deploy.deployBlock);
|
31
|
+
|
32
|
+
return { ...deploy, stateBlock: deploy.deployBlock };
|
33
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Client, Transport, Chain, Account, concatHex, getCreate2Address, Hex } from "viem";
|
2
|
+
import { getBytecode } from "viem/actions";
|
3
|
+
import { deployer } from "./ensureDeployer";
|
4
|
+
import { salt } from "./common";
|
5
|
+
import { sendTransaction } from "@latticexyz/common";
|
6
|
+
import { debug } from "./debug";
|
7
|
+
import pRetry from "p-retry";
|
8
|
+
import { wait } from "@latticexyz/common/utils";
|
9
|
+
|
10
|
+
export type Contract = {
|
11
|
+
bytecode: Hex;
|
12
|
+
label?: string;
|
13
|
+
};
|
14
|
+
|
15
|
+
export async function ensureContract({
|
16
|
+
client,
|
17
|
+
bytecode,
|
18
|
+
label = "contract",
|
19
|
+
}: {
|
20
|
+
readonly client: Client<Transport, Chain | undefined, Account>;
|
21
|
+
} & Contract): Promise<readonly Hex[]> {
|
22
|
+
const address = getCreate2Address({ from: deployer, salt, bytecode });
|
23
|
+
|
24
|
+
const contractCode = await getBytecode(client, { address, blockTag: "pending" });
|
25
|
+
if (contractCode) {
|
26
|
+
debug("found", label, "at", address);
|
27
|
+
return [];
|
28
|
+
}
|
29
|
+
|
30
|
+
debug("deploying", label, "at", address);
|
31
|
+
return [
|
32
|
+
await pRetry(
|
33
|
+
() =>
|
34
|
+
sendTransaction(client, {
|
35
|
+
chain: client.chain ?? null,
|
36
|
+
to: deployer,
|
37
|
+
data: concatHex([salt, bytecode]),
|
38
|
+
}),
|
39
|
+
{
|
40
|
+
retries: 3,
|
41
|
+
onFailedAttempt: async (error) => {
|
42
|
+
const delay = error.attemptNumber * 500;
|
43
|
+
debug(`failed to deploy ${label}, retrying in ${delay}ms...`);
|
44
|
+
await wait(delay);
|
45
|
+
},
|
46
|
+
}
|
47
|
+
),
|
48
|
+
];
|
49
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Client, Transport, Chain, Account, Hex } from "viem";
|
2
|
+
import { waitForTransactionReceipt } from "viem/actions";
|
3
|
+
import { debug } from "./debug";
|
4
|
+
import { Contract, ensureContract } from "./ensureContract";
|
5
|
+
|
6
|
+
export async function ensureContractsDeployed({
|
7
|
+
client,
|
8
|
+
contracts,
|
9
|
+
}: {
|
10
|
+
readonly client: Client<Transport, Chain | undefined, Account>;
|
11
|
+
readonly contracts: readonly Contract[];
|
12
|
+
}): Promise<readonly Hex[]> {
|
13
|
+
const txs = (await Promise.all(contracts.map((contract) => ensureContract({ client, ...contract })))).flat();
|
14
|
+
|
15
|
+
if (txs.length) {
|
16
|
+
debug("waiting for contracts");
|
17
|
+
// wait for each tx separately/serially, because parallelizing results in RPC errors
|
18
|
+
for (const tx of txs) {
|
19
|
+
await waitForTransactionReceipt(client, { hash: tx });
|
20
|
+
// TODO: throw if there was a revert?
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
return txs;
|
25
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { Account, Chain, Client, Transport } from "viem";
|
2
|
+
import { getBytecode, sendRawTransaction, sendTransaction, waitForTransactionReceipt } from "viem/actions";
|
3
|
+
import deployment from "./create2/deployment.json";
|
4
|
+
import { debug } from "./debug";
|
5
|
+
|
6
|
+
export const deployer = `0x${deployment.address}` as const;
|
7
|
+
|
8
|
+
export async function ensureDeployer(client: Client<Transport, Chain | undefined, Account>): Promise<void> {
|
9
|
+
const bytecode = await getBytecode(client, { address: deployer });
|
10
|
+
if (bytecode) {
|
11
|
+
debug("found create2 deployer at", deployer);
|
12
|
+
return;
|
13
|
+
}
|
14
|
+
|
15
|
+
// send gas to signer
|
16
|
+
debug("sending gas for create2 deployer to signer at", deployment.signerAddress);
|
17
|
+
const gasTx = await sendTransaction(client, {
|
18
|
+
chain: client.chain ?? null,
|
19
|
+
to: `0x${deployment.signerAddress}`,
|
20
|
+
value: BigInt(deployment.gasLimit) * BigInt(deployment.gasPrice),
|
21
|
+
});
|
22
|
+
const gasReceipt = await waitForTransactionReceipt(client, { hash: gasTx });
|
23
|
+
if (gasReceipt.status !== "success") {
|
24
|
+
console.error("failed to send gas to deployer signer", gasReceipt);
|
25
|
+
throw new Error("failed to send gas to deployer signer");
|
26
|
+
}
|
27
|
+
|
28
|
+
// deploy the deployer
|
29
|
+
debug("deploying create2 deployer at", deployer);
|
30
|
+
const deployTx = await sendRawTransaction(client, { serializedTransaction: `0x${deployment.transaction}` });
|
31
|
+
const deployReceipt = await waitForTransactionReceipt(client, { hash: deployTx });
|
32
|
+
if (deployReceipt.contractAddress !== deployer) {
|
33
|
+
console.error("unexpected contract address for deployer", deployReceipt);
|
34
|
+
throw new Error("unexpected contract address for deployer");
|
35
|
+
}
|
36
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import { Client, Transport, Chain, Account, Hex } from "viem";
|
2
|
+
import { hexToResource, writeContract } from "@latticexyz/common";
|
3
|
+
import { WorldDeploy, WorldFunction, worldAbi } from "./common";
|
4
|
+
import { debug } from "./debug";
|
5
|
+
import { getFunctions } from "./getFunctions";
|
6
|
+
import pRetry from "p-retry";
|
7
|
+
import { wait } from "@latticexyz/common/utils";
|
8
|
+
|
9
|
+
export async function ensureFunctions({
|
10
|
+
client,
|
11
|
+
worldDeploy,
|
12
|
+
functions,
|
13
|
+
}: {
|
14
|
+
readonly client: Client<Transport, Chain | undefined, Account>;
|
15
|
+
readonly worldDeploy: WorldDeploy;
|
16
|
+
readonly functions: readonly WorldFunction[];
|
17
|
+
}): Promise<readonly Hex[]> {
|
18
|
+
const worldFunctions = await getFunctions({ client, worldDeploy });
|
19
|
+
const worldSelectorToFunction = Object.fromEntries(worldFunctions.map((func) => [func.selector, func]));
|
20
|
+
|
21
|
+
const toSkip = functions.filter((func) => worldSelectorToFunction[func.selector]);
|
22
|
+
const toAdd = functions.filter((func) => !toSkip.includes(func));
|
23
|
+
|
24
|
+
if (toSkip.length) {
|
25
|
+
debug("functions already registered:", toSkip.map((func) => func.signature).join(", "));
|
26
|
+
const wrongSystem = toSkip.filter((func) => func.systemId !== worldSelectorToFunction[func.selector]?.systemId);
|
27
|
+
if (wrongSystem.length) {
|
28
|
+
console.warn(
|
29
|
+
"found",
|
30
|
+
wrongSystem.length,
|
31
|
+
"functions already registered but pointing at a different system ID:",
|
32
|
+
wrongSystem.map((func) => func.signature).join(", ")
|
33
|
+
);
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
if (!toAdd.length) return [];
|
38
|
+
|
39
|
+
debug("registering functions:", toAdd.map((func) => func.signature).join(", "));
|
40
|
+
|
41
|
+
return Promise.all(
|
42
|
+
toAdd.map((func) => {
|
43
|
+
const { namespace } = hexToResource(func.systemId);
|
44
|
+
if (namespace === "") {
|
45
|
+
return pRetry(
|
46
|
+
() =>
|
47
|
+
writeContract(client, {
|
48
|
+
chain: client.chain ?? null,
|
49
|
+
address: worldDeploy.address,
|
50
|
+
abi: worldAbi,
|
51
|
+
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
|
52
|
+
functionName: "registerRootFunctionSelector",
|
53
|
+
args: [func.systemId, func.systemFunctionSignature, func.systemFunctionSelector],
|
54
|
+
}),
|
55
|
+
{
|
56
|
+
retries: 3,
|
57
|
+
onFailedAttempt: async (error) => {
|
58
|
+
const delay = error.attemptNumber * 500;
|
59
|
+
debug(`failed to register function ${func.signature}, retrying in ${delay}ms...`);
|
60
|
+
await wait(delay);
|
61
|
+
},
|
62
|
+
}
|
63
|
+
);
|
64
|
+
}
|
65
|
+
return pRetry(
|
66
|
+
() =>
|
67
|
+
writeContract(client, {
|
68
|
+
chain: client.chain ?? null,
|
69
|
+
address: worldDeploy.address,
|
70
|
+
abi: worldAbi,
|
71
|
+
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
|
72
|
+
functionName: "registerFunctionSelector",
|
73
|
+
args: [func.systemId, func.systemFunctionSignature],
|
74
|
+
}),
|
75
|
+
{
|
76
|
+
retries: 3,
|
77
|
+
onFailedAttempt: async (error) => {
|
78
|
+
const delay = error.attemptNumber * 500;
|
79
|
+
debug(`failed to register function ${func.signature}, retrying in ${delay}ms...`);
|
80
|
+
await wait(delay);
|
81
|
+
},
|
82
|
+
}
|
83
|
+
);
|
84
|
+
})
|
85
|
+
);
|
86
|
+
}
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import { Client, Transport, Chain, Account, Hex, BaseError, getAddress } from "viem";
|
2
|
+
import { writeContract } from "@latticexyz/common";
|
3
|
+
import { Module, WorldDeploy, worldAbi } from "./common";
|
4
|
+
import { debug } from "./debug";
|
5
|
+
import { isDefined, uniqueBy, wait } from "@latticexyz/common/utils";
|
6
|
+
import pRetry from "p-retry";
|
7
|
+
import { ensureContractsDeployed } from "./ensureContractsDeployed";
|
8
|
+
|
9
|
+
export async function ensureModules({
|
10
|
+
client,
|
11
|
+
worldDeploy,
|
12
|
+
modules,
|
13
|
+
}: {
|
14
|
+
readonly client: Client<Transport, Chain | undefined, Account>;
|
15
|
+
readonly worldDeploy: WorldDeploy;
|
16
|
+
readonly modules: readonly Module[];
|
17
|
+
}): Promise<readonly Hex[]> {
|
18
|
+
if (!modules.length) return [];
|
19
|
+
|
20
|
+
await ensureContractsDeployed({
|
21
|
+
client,
|
22
|
+
contracts: uniqueBy(modules, (mod) => getAddress(mod.address)).map((mod) => ({
|
23
|
+
bytecode: mod.bytecode,
|
24
|
+
label: `${mod.name} module`,
|
25
|
+
})),
|
26
|
+
});
|
27
|
+
|
28
|
+
debug("installing modules:", modules.map((mod) => mod.name).join(", "));
|
29
|
+
return (
|
30
|
+
await Promise.all(
|
31
|
+
modules.map((mod) =>
|
32
|
+
pRetry(
|
33
|
+
async () => {
|
34
|
+
try {
|
35
|
+
return mod.installAsRoot
|
36
|
+
? await writeContract(client, {
|
37
|
+
chain: client.chain ?? null,
|
38
|
+
address: worldDeploy.address,
|
39
|
+
abi: worldAbi,
|
40
|
+
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
|
41
|
+
functionName: "installRootModule",
|
42
|
+
args: [mod.address, mod.installData],
|
43
|
+
})
|
44
|
+
: await writeContract(client, {
|
45
|
+
chain: client.chain ?? null,
|
46
|
+
address: worldDeploy.address,
|
47
|
+
abi: worldAbi,
|
48
|
+
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
|
49
|
+
functionName: "installModule",
|
50
|
+
args: [mod.address, mod.installData],
|
51
|
+
});
|
52
|
+
} catch (error) {
|
53
|
+
if (error instanceof BaseError && error.message.includes("Module_AlreadyInstalled")) {
|
54
|
+
debug(`module ${mod.name} already installed`);
|
55
|
+
return;
|
56
|
+
}
|
57
|
+
throw error;
|
58
|
+
}
|
59
|
+
},
|
60
|
+
{
|
61
|
+
retries: 3,
|
62
|
+
onFailedAttempt: async (error) => {
|
63
|
+
const delay = error.attemptNumber * 500;
|
64
|
+
debug(`failed to install module ${mod.name}, retrying in ${delay}ms...`);
|
65
|
+
await wait(delay);
|
66
|
+
},
|
67
|
+
}
|
68
|
+
)
|
69
|
+
)
|
70
|
+
)
|
71
|
+
).filter(isDefined);
|
72
|
+
}
|
@@ -0,0 +1,161 @@
|
|
1
|
+
import { Client, Transport, Chain, Account, Hex, getAddress } from "viem";
|
2
|
+
import { writeContract } from "@latticexyz/common";
|
3
|
+
import { System, WorldDeploy, worldAbi } from "./common";
|
4
|
+
import { debug } from "./debug";
|
5
|
+
import { resourceLabel } from "./resourceLabel";
|
6
|
+
import { getSystems } from "./getSystems";
|
7
|
+
import { getResourceAccess } from "./getResourceAccess";
|
8
|
+
import { uniqueBy, wait } from "@latticexyz/common/utils";
|
9
|
+
import pRetry from "p-retry";
|
10
|
+
import { ensureContractsDeployed } from "./ensureContractsDeployed";
|
11
|
+
|
12
|
+
export async function ensureSystems({
|
13
|
+
client,
|
14
|
+
worldDeploy,
|
15
|
+
systems,
|
16
|
+
}: {
|
17
|
+
readonly client: Client<Transport, Chain | undefined, Account>;
|
18
|
+
readonly worldDeploy: WorldDeploy;
|
19
|
+
readonly systems: readonly System[];
|
20
|
+
}): Promise<readonly Hex[]> {
|
21
|
+
const [worldSystems, worldAccess] = await Promise.all([
|
22
|
+
getSystems({ client, worldDeploy }),
|
23
|
+
getResourceAccess({ client, worldDeploy }),
|
24
|
+
]);
|
25
|
+
const systemIds = systems.map((system) => system.systemId);
|
26
|
+
const currentAccess = worldAccess.filter(({ resourceId }) => systemIds.includes(resourceId));
|
27
|
+
const desiredAccess = systems.flatMap((system) =>
|
28
|
+
system.allowedAddresses.map((address) => ({ resourceId: system.systemId, address }))
|
29
|
+
);
|
30
|
+
|
31
|
+
const accessToAdd = desiredAccess.filter(
|
32
|
+
(access) =>
|
33
|
+
!currentAccess.some(
|
34
|
+
({ resourceId, address }) =>
|
35
|
+
resourceId === access.resourceId && getAddress(address) === getAddress(access.address)
|
36
|
+
)
|
37
|
+
);
|
38
|
+
|
39
|
+
const accessToRemove = currentAccess.filter(
|
40
|
+
(access) =>
|
41
|
+
!desiredAccess.some(
|
42
|
+
({ resourceId, address }) =>
|
43
|
+
resourceId === access.resourceId && getAddress(address) === getAddress(access.address)
|
44
|
+
)
|
45
|
+
);
|
46
|
+
|
47
|
+
// TODO: move each system access+registration to batch call to be atomic
|
48
|
+
|
49
|
+
if (accessToRemove.length) {
|
50
|
+
debug("revoking", accessToRemove.length, "access grants");
|
51
|
+
}
|
52
|
+
if (accessToAdd.length) {
|
53
|
+
debug("adding", accessToAdd.length, "access grants");
|
54
|
+
}
|
55
|
+
|
56
|
+
const accessTxs = [
|
57
|
+
...accessToRemove.map((access) =>
|
58
|
+
pRetry(
|
59
|
+
() =>
|
60
|
+
writeContract(client, {
|
61
|
+
chain: client.chain ?? null,
|
62
|
+
address: worldDeploy.address,
|
63
|
+
abi: worldAbi,
|
64
|
+
functionName: "revokeAccess",
|
65
|
+
args: [access.resourceId, access.address],
|
66
|
+
}),
|
67
|
+
{
|
68
|
+
retries: 3,
|
69
|
+
onFailedAttempt: async (error) => {
|
70
|
+
const delay = error.attemptNumber * 500;
|
71
|
+
debug(`failed to revoke access, retrying in ${delay}ms...`);
|
72
|
+
await wait(delay);
|
73
|
+
},
|
74
|
+
}
|
75
|
+
)
|
76
|
+
),
|
77
|
+
...accessToAdd.map((access) =>
|
78
|
+
pRetry(
|
79
|
+
() =>
|
80
|
+
writeContract(client, {
|
81
|
+
chain: client.chain ?? null,
|
82
|
+
address: worldDeploy.address,
|
83
|
+
abi: worldAbi,
|
84
|
+
functionName: "grantAccess",
|
85
|
+
args: [access.resourceId, access.address],
|
86
|
+
}),
|
87
|
+
{
|
88
|
+
retries: 3,
|
89
|
+
onFailedAttempt: async (error) => {
|
90
|
+
const delay = error.attemptNumber * 500;
|
91
|
+
debug(`failed to grant access, retrying in ${delay}ms...`);
|
92
|
+
await wait(delay);
|
93
|
+
},
|
94
|
+
}
|
95
|
+
)
|
96
|
+
),
|
97
|
+
];
|
98
|
+
|
99
|
+
const existingSystems = systems.filter((system) =>
|
100
|
+
worldSystems.some(
|
101
|
+
(worldSystem) =>
|
102
|
+
worldSystem.systemId === system.systemId && getAddress(worldSystem.address) === getAddress(system.address)
|
103
|
+
)
|
104
|
+
);
|
105
|
+
if (existingSystems.length) {
|
106
|
+
debug("existing systems", existingSystems.map(resourceLabel).join(", "));
|
107
|
+
}
|
108
|
+
const existingSystemIds = existingSystems.map((system) => system.systemId);
|
109
|
+
|
110
|
+
const missingSystems = systems.filter((system) => !existingSystemIds.includes(system.systemId));
|
111
|
+
if (!missingSystems.length) return [];
|
112
|
+
|
113
|
+
const systemsToUpgrade = missingSystems.filter((system) =>
|
114
|
+
worldSystems.some(
|
115
|
+
(worldSystem) =>
|
116
|
+
worldSystem.systemId === system.systemId && getAddress(worldSystem.address) !== getAddress(system.address)
|
117
|
+
)
|
118
|
+
);
|
119
|
+
if (systemsToUpgrade.length) {
|
120
|
+
debug("upgrading systems", systemsToUpgrade.map(resourceLabel).join(", "));
|
121
|
+
}
|
122
|
+
|
123
|
+
const systemsToAdd = missingSystems.filter(
|
124
|
+
(system) => !worldSystems.some((worldSystem) => worldSystem.systemId === system.systemId)
|
125
|
+
);
|
126
|
+
if (systemsToAdd.length) {
|
127
|
+
debug("registering new systems", systemsToAdd.map(resourceLabel).join(", "));
|
128
|
+
}
|
129
|
+
|
130
|
+
await ensureContractsDeployed({
|
131
|
+
client,
|
132
|
+
contracts: uniqueBy(missingSystems, (system) => getAddress(system.address)).map((system) => ({
|
133
|
+
bytecode: system.bytecode,
|
134
|
+
label: `${resourceLabel(system)} system`,
|
135
|
+
})),
|
136
|
+
});
|
137
|
+
|
138
|
+
const registerTxs = missingSystems.map((system) =>
|
139
|
+
pRetry(
|
140
|
+
() =>
|
141
|
+
writeContract(client, {
|
142
|
+
chain: client.chain ?? null,
|
143
|
+
address: worldDeploy.address,
|
144
|
+
abi: worldAbi,
|
145
|
+
// TODO: replace with batchCall (https://github.com/latticexyz/mud/issues/1645)
|
146
|
+
functionName: "registerSystem",
|
147
|
+
args: [system.systemId, system.address, system.allowAll],
|
148
|
+
}),
|
149
|
+
{
|
150
|
+
retries: 3,
|
151
|
+
onFailedAttempt: async (error) => {
|
152
|
+
const delay = error.attemptNumber * 500;
|
153
|
+
debug(`failed to register system ${resourceLabel(system)}, retrying in ${delay}ms...`);
|
154
|
+
await wait(delay);
|
155
|
+
},
|
156
|
+
}
|
157
|
+
)
|
158
|
+
);
|
159
|
+
|
160
|
+
return await Promise.all([...accessTxs, ...registerTxs]);
|
161
|
+
}
|