Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP namespaces #2929

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions packages/cli/src/commands/trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,6 @@ import { getChainId } from "viem/actions";
import { World as WorldConfig } from "@latticexyz/world";
import { worldToV1 } from "@latticexyz/world/config/v2";

// TODO account for multiple namespaces (https://github.com/latticexyz/mud/issues/994)
const systemsTableId = resourceToHex({
type: "system",
namespace: worldConfig.namespace,
name: worldConfig.tables.world__Systems.name,
});

type Options = {
tx: string;
worldAddress?: string;
Expand Down Expand Up @@ -80,12 +73,17 @@ const commandModule: CommandModule<Options, Options> = {
const names = Object.values(resolvedConfig.systems).map(({ name }) => name);

// Fetch system table field layout from chain
const systemTableFieldLayout = await WorldContract.getFieldLayout(systemsTableId);
const systemTableFieldLayout = await WorldContract.getFieldLayout(worldConfig.tables.Systems.tableId);
const labels: { name: string; address: string }[] = [];
for (const name of names) {
const systemSelector = resourceToHex({ type: "system", namespace, name });
// Get the first field of `Systems` table (the table maps system name to its address and other data)
const address = await WorldContract.getField(systemsTableId, [systemSelector], 0, systemTableFieldLayout);
const address = await WorldContract.getField(
worldConfig.tables.Systems.tableId,
[systemSelector],
0,
systemTableFieldLayout,
);
labels.push({ name, address });
}

Expand Down
3 changes: 2 additions & 1 deletion packages/config/src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ export type Schema = {
};

export type Table = {
readonly label: string;
readonly type: satisfy<ResourceType, "table" | "offchainTable">;
readonly name: string;
readonly namespace: string;
readonly name: string;
readonly tableId: Hex;
readonly schema: Schema;
readonly key: readonly string[];
Expand Down
154 changes: 78 additions & 76 deletions packages/store/ts/codegen/tableOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,92 +29,94 @@ export function getTableOptions(
config: StoreConfig,
solidityUserTypes: Record<string, SolidityUserDefinedType>,
): TableOptions[] {
const options = Object.values(config.tables).map((table): TableOptions => {
const keySchema = getKeySchema(table);
const valueSchema = getValueSchema(table);
const options = Object.values(config.tables)
.filter((table) => !table.codegen.disabled)
.map((table): TableOptions => {
const keySchema = getKeySchema(table);
const valueSchema = getValueSchema(table);

// struct adds methods to get/set all values at once
const withStruct = table.codegen.dataStruct;
// operate on all fields at once; always render for offchain tables; for only 1 field keep them if struct is also kept
const withRecordMethods = withStruct || table.type === "offchainTable" || Object.keys(valueSchema).length > 1;
// field methods can include simply get/set if there's only 1 field and no record methods
const withSuffixlessFieldMethods = !withRecordMethods && Object.keys(valueSchema).length === 1;
// list of any symbols that need to be imported
const imports: ImportDatum[] = [];
// struct adds methods to get/set all values at once
const withStruct = table.codegen.dataStruct;
// operate on all fields at once; always render for offchain tables; for only 1 field keep them if struct is also kept
const withRecordMethods = withStruct || table.type === "offchainTable" || Object.keys(valueSchema).length > 1;
// field methods can include simply get/set if there's only 1 field and no record methods
const withSuffixlessFieldMethods = !withRecordMethods && Object.keys(valueSchema).length === 1;
// list of any symbols that need to be imported
const imports: ImportDatum[] = [];

const keyTuple = Object.entries(keySchema).map(([name, field]): RenderKeyTuple => {
const abiOrUserType = field.internalType;
const { renderType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
const keyTuple = Object.entries(keySchema).map(([name, field]): RenderKeyTuple => {
const abiOrUserType = field.internalType;
const { renderType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);

const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
config,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);
const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
config,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);

return {
...renderType,
name,
isDynamic: false,
};
});
return {
...renderType,
name,
isDynamic: false,
};
});

const fields = Object.entries(valueSchema).map(([name, field]): RenderField => {
const abiOrUserType = field.internalType;
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);
const fields = Object.entries(valueSchema).map(([name, field]): RenderField => {
const abiOrUserType = field.internalType;
const { renderType, schemaType } = resolveAbiOrUserType(abiOrUserType, config, solidityUserTypes);

const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
config,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);
const importDatum = importForAbiOrUserType(
abiOrUserType,
table.codegen.outputDirectory,
config,
solidityUserTypes,
);
if (importDatum) imports.push(importDatum);

const elementType = SchemaTypeArrayToElement[schemaType];
return {
...renderType,
arrayElement: elementType !== undefined ? getSchemaTypeInfo(elementType) : undefined,
name,
};
});
const elementType = SchemaTypeArrayToElement[schemaType];
return {
...renderType,
arrayElement: elementType !== undefined ? getSchemaTypeInfo(elementType) : undefined,
name,
};
});

const staticFields = fields.filter(({ isDynamic }) => !isDynamic) as RenderStaticField[];
const dynamicFields = fields.filter(({ isDynamic }) => isDynamic) as RenderDynamicField[];
const staticFields = fields.filter(({ isDynamic }) => !isDynamic) as RenderStaticField[];
const dynamicFields = fields.filter(({ isDynamic }) => isDynamic) as RenderDynamicField[];

// With tableIdArgument: tableId is a dynamic argument for each method
// Without tableIdArgument: tableId is a file-level constant generated from `staticResourceData`
const staticResourceData = table.codegen.tableIdArgument
? undefined
: {
namespace: table.namespace,
name: table.name,
offchainOnly: table.type === "offchainTable",
};
// With tableIdArgument: tableId is a dynamic argument for each method
// Without tableIdArgument: tableId is a file-level constant generated from `staticResourceData`
const staticResourceData = table.codegen.tableIdArgument
? undefined
: {
namespace: table.namespace,
name: table.name,
offchainOnly: table.type === "offchainTable",
};

return {
outputPath: path.join(table.codegen.outputDirectory, `${table.name}.sol`),
tableName: table.name,
renderOptions: {
imports,
libraryName: table.name,
structName: withStruct ? table.name + "Data" : undefined,
staticResourceData,
storeImportPath: config.codegen.storeImportPath,
keyTuple,
fields,
staticFields,
dynamicFields,
withGetters: table.type === "table",
withRecordMethods,
withDynamicFieldMethods: table.type === "table",
withSuffixlessFieldMethods,
storeArgument: table.codegen.storeArgument,
},
};
});
return {
outputPath: path.join(table.codegen.outputDirectory, `${table.label}.sol`),
tableName: table.label,
renderOptions: {
imports,
libraryName: table.label,
structName: withStruct ? table.label + "Data" : undefined,
staticResourceData,
storeImportPath: config.codegen.storeImportPath,
keyTuple,
fields,
staticFields,
dynamicFields,
withGetters: table.type === "table",
withRecordMethods,
withDynamicFieldMethods: table.type === "table",
withSuffixlessFieldMethods,
storeArgument: table.codegen.storeArgument,
},
};
});

return options;
}
1 change: 1 addition & 0 deletions packages/store/ts/config/v2/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const CODEGEN_DEFAULTS = {
export type CODEGEN_DEFAULTS = typeof CODEGEN_DEFAULTS;

export const TABLE_CODEGEN_DEFAULTS = {
disabled: false,
outputDirectory: "tables" as string,
tableIdArgument: false,
storeArgument: false,
Expand Down
9 changes: 9 additions & 0 deletions packages/store/ts/config/v2/generics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,12 @@ export function mergeIfUndefined<base extends object, merged extends object>(
}

export type parseNumber<T> = T extends `${infer N extends number}` ? N : never;

// TODO: is this helper worth it for the strongly typed substring?
// https://stackoverflow.com/a/66218917
export type truncate<
T extends string,
N extends number,
L extends unknown[] = [],
A extends string = "",
> = N extends L["length"] ? A : T extends `${infer F}${infer R}` ? truncate<R, N, [0, ...L], `${A}${F}`> : A;
3 changes: 3 additions & 0 deletions packages/store/ts/config/v2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export * from "./compat";
export * from "./codegen";
export * from "./enums";
export * from "./userTypes";
export * from "./namespace";
export * from "./namespaces";
export * from "./namespacedTables";
67 changes: 50 additions & 17 deletions packages/store/ts/config/v2/input.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Hex } from "viem";
import { Codegen, TableCodegen, TableDeploy, UserTypes } from "./output";
import { Scope } from "./scope";
import { evaluate } from "@arktype/util";
Expand All @@ -19,43 +18,77 @@ export type TableCodegenInput = Partial<TableCodegen>;
export type TableDeployInput = Partial<TableDeploy>;

export type TableInput = {
/**
* Human-readable table label. Used as config keys, table library names, and filenames.
* Labels are not length constrained like table names, but special characters should be avoided to be compatible with the filesystem, Solidity compiler, etc.
*/
readonly label: string;
/**
* Defaults to `table` if not set.
*/
readonly type?: "table" | "offchainTable";
/**
* Defaults to the root namespace if not set.
*/
readonly namespace?: string;
/**
* Defaults to the first 16 characters of `label` if not set.
*/
readonly name?: string;
readonly schema: SchemaInput;
readonly key: readonly string[];
readonly tableId?: Hex;
readonly name: string;
readonly namespace?: string;
readonly type?: "table" | "offchainTable";
readonly codegen?: TableCodegenInput;
readonly deploy?: TableDeployInput;
};

export type TablesInput = {
readonly [label: string]: Omit<TableInput, "namespace" | "name">;
// remove label and namespace as these are set contextually
readonly [label: string]: Omit<TableInput, "label" | "namespace">;
};

export type CodegenInput = Partial<Codegen>;

export type StoreInput = {
export type NamespaceInput = {
/**
* Human-readable namespace label. Used as config keys and directory names.
* Labels are not length constrained like namespaces, but special characters should be avoided to be compatible with the filesystem, Solidity compiler, etc.
*/
readonly label: string;
/**
* Directory of Solidity source relative to the MUD config.
* This is used to resolve other paths in the config, like codegen and user types.
*
* Defaults to `src` to match `foundry.toml`'s default. If you change this from the default, you may also need to configure foundry with the same source directory.
* Defaults to the root namespace if not set.
*/
readonly sourceDirectory?: string;
readonly namespace?: string;
readonly tables?: TablesInput;
readonly userTypes?: UserTypes;
readonly enums?: EnumsInput;
readonly codegen?: CodegenInput;
};

export type NamespacesInput = {
// remove label as this is set contextually
readonly [label: string]: Omit<NamespaceInput, "label">;
};

export type StoreInput = evaluate<
Omit<NamespaceInput, "label"> & {
/**
* Directory of Solidity source relative to the MUD config.
* This is used to resolve other paths in the config, like codegen and user types.
*
* Defaults to `src` to match `foundry.toml`'s default. If you change this from the default, you may also need to configure foundry with the same source directory.
*/
readonly sourceDirectory?: string;
readonly userTypes?: UserTypes;
readonly enums?: EnumsInput;
readonly codegen?: CodegenInput;
}
>;

/******** Variations with shorthands ********/

export type TableShorthandInput = SchemaInput | string;

export type TablesWithShorthandsInput = {
readonly [label: string]: TableInput | TableShorthandInput;
readonly [label: string]: TablesInput[string] | TableShorthandInput;
};

export type StoreWithShorthandsInput = evaluate<Omit<StoreInput, "tables"> & { tables: TablesWithShorthandsInput }>;
export type StoreWithShorthandsInput = evaluate<
Omit<StoreInput, "tables"> & { readonly tables?: TablesWithShorthandsInput }
>;
Loading
Loading