Skip to content

Commit

Permalink
grant/revoke access
Browse files Browse the repository at this point in the history
  • Loading branch information
holic committed Oct 6, 2023
1 parent 862eb42 commit fc9f8c8
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 48 deletions.
4 changes: 4 additions & 0 deletions examples/minimal/packages/contracts/mud.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default mudConfig({
name: "increment",
openAccess: true,
},
// DoubleSystem: {
// openAccess: false,
// accessList: ["IncrementSystem"],
// },
},
excludeSystems: [
// Until namespace overrides, this system must be manually deployed in PostDeploy
Expand Down
101 changes: 60 additions & 41 deletions packages/cli/src/commands/deploy2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { CommandModule, Options } from "yargs";
import { logError } from "../utils/errors";
import { DeployOptions } from "../utils/deployHandler";
import { deploy } from "../deploy/deploy";
import { createWalletClient, http, Hex, Abi, getCreate2Address, getFunctionSelector } from "viem";
import { createWalletClient, http, Hex, Abi, getCreate2Address, getFunctionSelector, getAddress } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { loadConfig } from "@latticexyz/config/node";
import { StoreConfig } from "@latticexyz/store";
Expand All @@ -11,13 +11,14 @@ import { getOutDirectory, getSrcDirectory } from "@latticexyz/common/foundry";
import { getExistingContracts } from "../utils/getExistingContracts";
import { configToTables } from "../deploy/configToTables";
import { System, WorldFunction, salt } from "../deploy/common";
import { resourceToHex } from "@latticexyz/common";
import { hexToResource, resourceToHex } from "@latticexyz/common";
import glob from "glob";
import { basename } from "path";
import { getContractData } from "../utils/utils/getContractData";
import { deployer } from "../deploy/ensureDeployer";
import { getRegisterFunctionSelectorsCallData } from "../utils/systems/getRegisterFunctionSelectorsCallData";
import { loadFunctionSignatures } from "../utils/systems/utils";
import { resourceLabel } from "../deploy/resourceLabel";

// TODO: redo options
export const yDeployOptions = {
Expand Down Expand Up @@ -69,52 +70,70 @@ const commandModule: CommandModule<DeployOptions, DeployOptions> = {
const resolvedConfig = resolveWorldConfig(config, contractNames);
const baseSystemFunctions = loadFunctionSignatures("System", outDir);

const systems = Object.fromEntries<System>(
Object.entries(resolvedConfig.systems).map(([systemName, system]) => {
const namespace = config.namespace;
const name = system.name ?? systemName;
const systemId = resourceToHex({ type: "system", namespace, name });
const contractData = getContractData(systemName, outDir);
const systems = Object.entries(resolvedConfig.systems).map(([systemName, system]) => {
const namespace = config.namespace;
const name = system.name;
const systemId = resourceToHex({ type: "system", namespace, name });
const contractData = getContractData(systemName, outDir);

const systemFunctions = loadFunctionSignatures(systemName, outDir)
.filter((sig) => !baseSystemFunctions.includes(sig))
.map((sig): WorldFunction => {
// TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
const worldSignature = namespace === "" ? sig : `${namespace}_${name}_${sig}`;
return {
signature: worldSignature,
selector: getFunctionSelector(worldSignature),
systemId,
systemFunctionSignature: sig,
systemFunctionSelector: getFunctionSelector(sig),
};
});

return [
`${namespace}_${name}`,
{
namespace,
name,
const systemFunctions = loadFunctionSignatures(systemName, outDir)
.filter((sig) => !baseSystemFunctions.includes(sig))
.map((sig): WorldFunction => {
// TODO: figure out how to not duplicate contract behavior (https://github.com/latticexyz/mud/issues/1708)
const worldSignature = namespace === "" ? sig : `${namespace}_${name}_${sig}`;
return {
signature: worldSignature,
selector: getFunctionSelector(worldSignature),
systemId,
allowAll: system.openAccess,
allowedAddresses: system.accessListAddresses as Hex[],
allowedSystemIds: system.accessListSystems.map((name) =>
resourceToHex({ type: "system", namespace, name })
),
bytecode: contractData.bytecode,
abi: contractData.abi,
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
functions: systemFunctions,
},
] as const;
})
);
systemFunctionSignature: sig,
systemFunctionSelector: getFunctionSelector(sig),
};
});

return {
namespace,
name,
systemId,
allowAll: system.openAccess,
allowedAddresses: system.accessListAddresses as Hex[],
allowedSystemIds: system.accessListSystems.map((name) =>
resourceToHex({ type: "system", namespace, name: resolvedConfig.systems[name].name })
),
bytecode: contractData.bytecode,
abi: contractData.abi,
address: getCreate2Address({ from: deployer, bytecode: contractData.bytecode, salt }),
functions: systemFunctions,
};
});

// resolve allowedSystemIds
// 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
const systemsWithAccess = systems.map(({ allowedAddresses, allowedSystemIds, ...system }) => {
const allowedSystemAddresses = allowedSystemIds.map((systemId) => {
const targetSystem = systems.find((s) => s.systemId === systemId);
if (!targetSystem) {
throw new Error(
`System ${resourceLabel(system)} wanted access to ${resourceLabel(
hexToResource(systemId)
)}, but it wasn't found in the config.`
);
}
return targetSystem.address;
});
return {
...system,
allowedAddresses: Array.from(
new Set([...allowedAddresses, ...allowedSystemAddresses].map((addr) => getAddress(addr)))
),
};
});

await deploy({
worldAddress: args.worldAddress as Hex | undefined,
client,
config: {
tables: configToTables(config),
systems,
systems: systemsWithAccess,
},
});
} catch (error: any) {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/deploy/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ export type System = {
systemId: Hex;
allowAll: boolean;
allowedAddresses: Hex[];
allowedSystemIds: string[];
bytecode: Hex;
abi: Abi;
functions: WorldFunction[];
Expand All @@ -53,5 +52,5 @@ export type System = {
export type ConfigInput = StoreConfig & WorldConfig;
export type Config<config extends ConfigInput> = {
tables: Tables<config>;
systems: Record<string, System>;
systems: System[];
};
7 changes: 7 additions & 0 deletions packages/cli/src/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { waitForTransactionReceipt } from "viem/actions";
import { getResourceIds } from "./getResourceIds";
import { getWorldDeploy } from "./getWorldDeploy";
import { ensureFunctions } from "./ensureFunctions";
import { getResourceAccess } from "./getResourceAccess";

type DeployOptions<configInput extends ConfigInput> = {
client: Client<Transport, Chain | undefined, Account>;
Expand All @@ -29,6 +30,9 @@ export async function deploy<configInput extends ConfigInput>({

// TODO: update RPC get calls to use `worldDeploy.toBlock` to align the block number everywhere

// TODO: check if there are any namespaces we don't have access to before attempting to register things on them
// TODO: check for and register namespaces? these are registered by default when registering tables/systems through the world

const tableTxs = await ensureTables({
client,
worldDeploy,
Expand All @@ -45,6 +49,9 @@ export async function deploy<configInput extends ConfigInput>({
functions: Object.values(config.systems).flatMap((system) => system.functions),
});

// TODO: grant access to systems
// TODO: install modules

const receipts = await Promise.all(
[...tableTxs, ...systemTxs, ...functionTxs].map((tx) => waitForTransactionReceipt(client, { hash: tx }))
);
Expand Down
61 changes: 58 additions & 3 deletions packages/cli/src/deploy/ensureSystems.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Client, Transport, Chain, Account, Hex, getAddress } from "viem";
import { writeContract } from "@latticexyz/common";
import { hexToResource, writeContract } from "@latticexyz/common";
import { System, WorldDeploy, worldAbi } from "./common";
import { ensureContract } from "./ensureContract";
import { debug } from "./debug";
import { resourceLabel } from "./resourceLabel";
import { getSystems } from "./getSystems";
import { getResourceAccess } from "./getResourceAccess";

export async function ensureSystems({
client,
Expand All @@ -15,7 +16,61 @@ export async function ensureSystems({
worldDeploy: WorldDeploy;
systems: System[];
}): Promise<Hex[]> {
const worldSystems = await getSystems({ client, worldDeploy });
const [worldSystems, worldAccess] = await Promise.all([
getSystems({ client, worldDeploy }),
getResourceAccess({ client, worldDeploy }),
]);
const systemIds = systems.map((system) => system.systemId);
const currentAccess = worldAccess.filter(({ resourceId }) => systemIds.includes(resourceId));
const desiredAccess = systems.flatMap((system) =>
system.allowedAddresses.map((address) => ({ resourceId: system.systemId, address }))
);

const accessToAdd = desiredAccess.filter(
(access) =>
!currentAccess.some(
({ resourceId, address }) =>
resourceId === access.resourceId && getAddress(address) === getAddress(access.address)
)
);

const accessToRemove = currentAccess.filter(
(access) =>
!desiredAccess.some(
({ resourceId, address }) =>
resourceId === access.resourceId && getAddress(address) === getAddress(access.address)
)
);

// TODO: move each system access+registration to batch call to be atomic

if (accessToRemove.length) {
debug("revoking", accessToRemove.length, "access grants");
}
if (accessToAdd.length) {
debug("adding", accessToAdd.length, "access grants");
}

const accessTxs = await Promise.all([
...accessToRemove.map((access) =>
writeContract(client, {
chain: client.chain ?? null,
address: worldDeploy.address,
abi: worldAbi,
functionName: "revokeAccess",
args: [access.resourceId, access.address],
})
),
...accessToAdd.map((access) =>
writeContract(client, {
chain: client.chain ?? null,
address: worldDeploy.address,
abi: worldAbi,
functionName: "grantAccess",
args: [access.resourceId, access.address],
})
),
]);

const existing = systems.filter((system) =>
worldSystems.find(
Expand Down Expand Up @@ -69,5 +124,5 @@ export async function ensureSystems({
)
);

return (await Promise.all([...contractTxs, ...registerTxs])).flat();
return (await Promise.all([...accessTxs, ...contractTxs, ...registerTxs])).flat();
}
50 changes: 50 additions & 0 deletions packages/cli/src/deploy/getResourceAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Client, parseAbiItem, Hex, Address, getAddress } from "viem";
import { WorldDeploy, worldTables } from "./common";
import { debug } from "./debug";
import { storeSpliceStaticDataEvent } from "@latticexyz/store";
import { getLogs } from "viem/actions";
import { decodeKey } from "@latticexyz/protocol-parser";
import { getTableValue } from "./getTableValue";

export async function getResourceAccess({
client,
worldDeploy,
}: {
client: Client;
worldDeploy: WorldDeploy;
}): Promise<{ resourceId: Hex; address: Address }[]> {
// This assumes we only use `ResourceAccess._set(...)`, which is true as of this writing.
// 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

debug("looking up resource access for", worldDeploy.address);

const logs = await getLogs(client, {
strict: true,
fromBlock: worldDeploy.fromBlock,
address: worldDeploy.address,
// our usage of `ResourceAccess._set(...)` emits a splice instead of set record
// TODO: https://github.com/latticexyz/mud/issues/479
event: parseAbiItem(storeSpliceStaticDataEvent),
args: { tableId: worldTables.world_ResourceAccess.tableId },
});

const keys = logs.map((log) => decodeKey(worldTables.world_ResourceAccess.keySchema, log.args.keyTuple));

const access = (
await Promise.all(
keys.map(
async (key) =>
[key, await getTableValue({ client, worldDeploy, table: worldTables.world_ResourceAccess, key })] as const
)
)
)
.filter(([, value]) => value.access)
.map(([key]) => ({
resourceId: key.resourceId,
address: getAddress(key.caller),
}));

debug("found", access.length, "resource<>address access pairs");

return access;
}
3 changes: 1 addition & 2 deletions packages/world/ts/library/config/resolveWorldConfig.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { getDuplicates, STORE_NAME_MAX_LENGTH, UnrecognizedSystemErrorFactory } from "@latticexyz/config";
import { MUDError } from "@latticexyz/common/errors";
import { STORE_NAME_MAX_LENGTH, UnrecognizedSystemErrorFactory } from "@latticexyz/config";
import { StoreConfig } from "@latticexyz/store";
import { SystemConfig, WorldConfig } from "./types";

Expand Down

0 comments on commit fc9f8c8

Please sign in to comment.