From de47d698f031a28ef8d9e329e3cffc85e904c6a1 Mon Sep 17 00:00:00 2001
From: Kevin Ingersoll <kingersoll@gmail.com>
Date: Tue, 31 Oct 2023 11:14:41 +0000
Subject: [PATCH] feat(store-sync): extra table definitions (#1840)

---
 .changeset/fair-buckets-dress.md              | 27 ++++++
 .../client-react/src/mud/setupNetwork.ts      | 17 +++-
 .../minimal/packages/contracts/mud.config.ts  | 15 ++-
 packages/store-sync/src/recs/common.test-d.ts | 31 -------
 packages/store-sync/src/recs/common.ts        | 27 +-----
 .../src/recs/configToRecsComponents.ts        | 45 ---------
 .../store-sync/src/recs/recsStorage.test.ts   |  8 +-
 packages/store-sync/src/recs/recsStorage.ts   | 33 +++----
 packages/store-sync/src/recs/syncToRecs.ts    | 23 +++--
 .../store-sync/src/recs/tableToComponent.ts   | 49 ++++++++++
 .../store-sync/src/recs/tablesToComponents.ts | 15 +++
 packages/store/package.json                   |  1 +
 packages/store/ts/common.ts                   | 13 ++-
 .../experimental/resolveConfig.test-d.ts      | 91 +++++++++++--------
 .../ts/config/experimental/resolveConfig.ts   | 24 +++--
 packages/store/ts/config/index.ts             |  2 +
 packages/store/ts/config/storeConfig.ts       |  2 +-
 pnpm-lock.yaml                                |  9 +-
 18 files changed, 239 insertions(+), 193 deletions(-)
 create mode 100644 .changeset/fair-buckets-dress.md
 delete mode 100644 packages/store-sync/src/recs/common.test-d.ts
 delete mode 100644 packages/store-sync/src/recs/configToRecsComponents.ts
 create mode 100644 packages/store-sync/src/recs/tableToComponent.ts
 create mode 100644 packages/store-sync/src/recs/tablesToComponents.ts

diff --git a/.changeset/fair-buckets-dress.md b/.changeset/fair-buckets-dress.md
new file mode 100644
index 0000000000..6f1d41576c
--- /dev/null
+++ b/.changeset/fair-buckets-dress.md
@@ -0,0 +1,27 @@
+---
+"@latticexyz/store-sync": minor
+---
+
+Added an optional `tables` option to `syncToRecs` to allow you to sync from tables that may not be expressed by your MUD config. This will be useful for namespaced tables used by [ERC20](https://github.com/latticexyz/mud/pull/1789) and [ERC721](https://github.com/latticexyz/mud/pull/1844) token modules until the MUD config gains [namespace support](https://github.com/latticexyz/mud/issues/994).
+
+Here's how we use this in our example project with the `KeysWithValue` module:
+
+```ts
+syncToRecs({
+  ...
+  tables: {
+    KeysWithValue: {
+      namespace: "keywval",
+      name: "Inventory",
+      tableId: resourceToHex({ type: "table", namespace: "keywval", name: "Inventory" }),
+      keySchema: {
+        valueHash: { type: "bytes32" },
+      },
+      valueSchema: {
+        keysWithValue: { type: "bytes32[]" },
+      },
+    },
+  },
+  ...
+});
+```
diff --git a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts
index 1353c0803c..96f6a3f17c 100644
--- a/examples/minimal/packages/client-react/src/mud/setupNetwork.ts
+++ b/examples/minimal/packages/client-react/src/mud/setupNetwork.ts
@@ -4,7 +4,7 @@ import { encodeEntity, syncToRecs } from "@latticexyz/store-sync/recs";
 import { getNetworkConfig } from "./getNetworkConfig";
 import { world } from "./world";
 import IWorldAbi from "contracts/out/IWorld.sol/IWorld.abi.json";
-import { ContractWrite, createBurnerAccount, getContract, transportObserver } from "@latticexyz/common";
+import { ContractWrite, createBurnerAccount, getContract, resourceToHex, transportObserver } from "@latticexyz/common";
 import { Subject, share } from "rxjs";
 import mudConfig from "contracts/mud.config";
 import { createClient as createFaucetClient } from "@latticexyz/faucet";
@@ -40,10 +40,23 @@ export async function setupNetwork() {
   const { components, latestBlock$, storedBlockLogs$, waitForTransaction } = await syncToRecs({
     world,
     config: mudConfig,
+    tables: {
+      KeysWithValue: {
+        namespace: "keywval",
+        name: "Inventory",
+        tableId: resourceToHex({ type: "table", namespace: "keywval", name: "Inventory" }),
+        keySchema: {
+          valueHash: { type: "bytes32" },
+        },
+        valueSchema: {
+          keysWithValue: { type: "bytes32[]" },
+        },
+      },
+    },
     address: networkConfig.worldAddress as Hex,
     publicClient,
     startBlock: BigInt(networkConfig.initialBlockNumber),
-  });
+  } as const);
 
   try {
     console.log("creating faucet client");
diff --git a/examples/minimal/packages/contracts/mud.config.ts b/examples/minimal/packages/contracts/mud.config.ts
index dfeeaffb52..c2432def66 100644
--- a/examples/minimal/packages/contracts/mud.config.ts
+++ b/examples/minimal/packages/contracts/mud.config.ts
@@ -37,12 +37,11 @@ export default mudConfig({
       valueSchema: { amount: "uint32" },
     },
   },
-  // KeysWithValue doesn't seem to like singleton keys
-  // modules: [
-  //   {
-  //     name: "KeysWithValueModule",
-  //     root: true,
-  //     args: [resolveTableId("CounterTable")],
-  //   },
-  // ],
+  modules: [
+    {
+      name: "KeysWithValueModule",
+      root: true,
+      args: [resolveTableId("Inventory")],
+    },
+  ],
 });
diff --git a/packages/store-sync/src/recs/common.test-d.ts b/packages/store-sync/src/recs/common.test-d.ts
deleted file mode 100644
index a2f81ff7a4..0000000000
--- a/packages/store-sync/src/recs/common.test-d.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Component, Type as RecsType } from "@latticexyz/recs";
-import { describe, expectTypeOf } from "vitest";
-import storeConfig from "@latticexyz/store/mud.config";
-import { ConfigToRecsComponents } from "./common";
-
-describe("ConfigToRecsComponents", () => {
-  expectTypeOf<ConfigToRecsComponents<typeof storeConfig>["Tables"]>().toEqualTypeOf<
-    Component<
-      {
-        keySchema: RecsType.String;
-        valueSchema: RecsType.String;
-        abiEncodedKeyNames: RecsType.String;
-        abiEncodedFieldNames: RecsType.String;
-      },
-      {
-        componentName: "Tables";
-        // TODO: fix config namespace so it comes back as a const
-        tableName: `${string}:Tables`;
-        keySchema: {
-          tableId: "bytes32";
-        };
-        valueSchema: {
-          keySchema: "bytes32";
-          valueSchema: "bytes32";
-          abiEncodedKeyNames: "bytes";
-          abiEncodedFieldNames: "bytes";
-        };
-      }
-    >
-  >();
-});
diff --git a/packages/store-sync/src/recs/common.ts b/packages/store-sync/src/recs/common.ts
index 09622f2fe8..f999d864eb 100644
--- a/packages/store-sync/src/recs/common.ts
+++ b/packages/store-sync/src/recs/common.ts
@@ -1,31 +1,10 @@
-import { StoreConfig } from "@latticexyz/store";
-import { Component as RecsComponent, Metadata as RecsMetadata, Type as RecsType } from "@latticexyz/recs";
-import { SchemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType";
-import { SchemaAbiType } from "@latticexyz/schema-type";
+import { Metadata } from "@latticexyz/recs";
 import { KeySchema, ValueSchema } from "@latticexyz/protocol-parser";
 
-export type StoreComponentMetadata = RecsMetadata & {
+export type StoreComponentMetadata = Metadata & {
   componentName: string;
   tableName: string;
+  // TODO: migrate to store's KeySchema/ValueSchema
   keySchema: KeySchema;
   valueSchema: ValueSchema;
 };
-
-export type ConfigToRecsComponents<TConfig extends StoreConfig> = {
-  [tableName in keyof TConfig["tables"] & string]: RecsComponent<
-    {
-      __staticData: RecsType.OptionalString;
-      __encodedLengths: RecsType.OptionalString;
-      __dynamicData: RecsType.OptionalString;
-    } & {
-      [fieldName in keyof TConfig["tables"][tableName]["valueSchema"] & string]: RecsType &
-        SchemaAbiTypeToRecsType<SchemaAbiType & TConfig["tables"][tableName]["valueSchema"][fieldName]>;
-    },
-    StoreComponentMetadata & {
-      componentName: tableName;
-      tableName: `${TConfig["namespace"]}:${tableName}`;
-      keySchema: TConfig["tables"][tableName]["keySchema"];
-      valueSchema: TConfig["tables"][tableName]["valueSchema"];
-    }
-  >;
-};
diff --git a/packages/store-sync/src/recs/configToRecsComponents.ts b/packages/store-sync/src/recs/configToRecsComponents.ts
deleted file mode 100644
index 1b22f69a98..0000000000
--- a/packages/store-sync/src/recs/configToRecsComponents.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { StoreConfig } from "@latticexyz/store";
-import { SchemaAbiType } from "@latticexyz/schema-type";
-import { resourceToHex } from "@latticexyz/common";
-import { World, defineComponent, Type } from "@latticexyz/recs";
-import { ConfigToRecsComponents } from "./common";
-import { schemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType";
-
-export function configToRecsComponents<TConfig extends StoreConfig>(
-  world: World,
-  config: TConfig
-): ConfigToRecsComponents<TConfig> {
-  return Object.fromEntries(
-    Object.entries(config.tables).map(([tableName, table]) => [
-      tableName,
-      defineComponent(
-        world,
-        {
-          ...Object.fromEntries(
-            Object.entries(table.valueSchema).map(([fieldName, schemaAbiType]) => [
-              fieldName,
-              schemaAbiTypeToRecsType[schemaAbiType as SchemaAbiType],
-            ])
-          ),
-          __staticData: Type.OptionalString,
-          __encodedLengths: Type.OptionalString,
-          __dynamicData: Type.OptionalString,
-        },
-        {
-          // TODO: support table namespaces https://github.com/latticexyz/mud/issues/994
-          id: resourceToHex({
-            type: table.offchainOnly ? "offchainTable" : "table",
-            namespace: config.namespace,
-            name: tableName,
-          }),
-          metadata: {
-            componentName: tableName,
-            tableName: `${config.namespace}:${tableName}`,
-            keySchema: table.keySchema,
-            valueSchema: table.valueSchema,
-          },
-        }
-      ),
-    ])
-  ) as ConfigToRecsComponents<TConfig>;
-}
diff --git a/packages/store-sync/src/recs/recsStorage.test.ts b/packages/store-sync/src/recs/recsStorage.test.ts
index 28879f788b..7a19c9cbaf 100644
--- a/packages/store-sync/src/recs/recsStorage.test.ts
+++ b/packages/store-sync/src/recs/recsStorage.test.ts
@@ -7,7 +7,9 @@ import { groupLogsByBlockNumber } from "@latticexyz/block-logs-stream";
 import { StoreEventsLog } from "../common";
 import { singletonEntity } from "./singletonEntity";
 import { RpcLog, formatLog, decodeEventLog, Hex } from "viem";
-import { storeEventsAbi } from "@latticexyz/store";
+import { resolveConfig, storeEventsAbi } from "@latticexyz/store";
+
+const tables = resolveConfig(mudConfig).tables;
 
 // TODO: make test-data a proper package and export this
 const blocks = groupLogsByBlockNumber(
@@ -25,7 +27,7 @@ const blocks = groupLogsByBlockNumber(
 describe("recsStorage", () => {
   it("creates components", async () => {
     const world = createWorld();
-    const { components } = recsStorage({ world, config: mudConfig });
+    const { components } = recsStorage({ world, tables });
     expect(components.NumberList.id).toMatchInlineSnapshot(
       '"0x746200000000000000000000000000004e756d6265724c697374000000000000"'
     );
@@ -33,7 +35,7 @@ describe("recsStorage", () => {
 
   it("sets component values from logs", async () => {
     const world = createWorld();
-    const { storageAdapter, components } = recsStorage({ world, config: mudConfig });
+    const { storageAdapter, components } = recsStorage({ world, tables });
 
     for (const block of blocks) {
       await storageAdapter(block);
diff --git a/packages/store-sync/src/recs/recsStorage.ts b/packages/store-sync/src/recs/recsStorage.ts
index 10eca49779..e610787ab3 100644
--- a/packages/store-sync/src/recs/recsStorage.ts
+++ b/packages/store-sync/src/recs/recsStorage.ts
@@ -1,4 +1,4 @@
-import { StoreConfig } from "@latticexyz/store";
+import { Table, resolveConfig } from "@latticexyz/store";
 import { debug } from "./debug";
 import { World as RecsWorld, getComponentValue, hasComponent, removeComponent, setComponent } from "@latticexyz/recs";
 import { defineInternalComponents } from "./defineInternalComponents";
@@ -9,39 +9,40 @@ import { Hex, size } from "viem";
 import { isTableRegistrationLog } from "../isTableRegistrationLog";
 import { logToTable } from "../logToTable";
 import { hexKeyTupleToEntity } from "./hexKeyTupleToEntity";
-import { ConfigToRecsComponents } from "./common";
 import { StorageAdapter, StorageAdapterBlock } from "../common";
-import { configToRecsComponents } from "./configToRecsComponents";
 import { singletonEntity } from "./singletonEntity";
 import storeConfig from "@latticexyz/store/mud.config";
 import worldConfig from "@latticexyz/world/mud.config";
+import { TablesToComponents, tablesToComponents } from "./tablesToComponents";
 
-export type RecsStorageOptions<TConfig extends StoreConfig = StoreConfig> = {
+const storeTables = resolveConfig(storeConfig).tables;
+const worldTables = resolveConfig(worldConfig).tables;
+
+export type RecsStorageOptions<tables extends Record<string, Table>> = {
   world: RecsWorld;
-  // TODO: make config optional?
-  config: TConfig;
+  tables: tables;
   shouldSkipUpdateStream?: () => boolean;
 };
 
-export type RecsStorageAdapter<TConfig extends StoreConfig = StoreConfig> = {
+export type RecsStorageAdapter<tables extends Record<string, Table>> = {
   storageAdapter: StorageAdapter;
-  components: ConfigToRecsComponents<TConfig> &
-    ConfigToRecsComponents<typeof storeConfig> &
-    ConfigToRecsComponents<typeof worldConfig> &
+  components: TablesToComponents<tables> &
+    TablesToComponents<typeof storeTables> &
+    TablesToComponents<typeof worldTables> &
     ReturnType<typeof defineInternalComponents>;
 };
 
-export function recsStorage<TConfig extends StoreConfig = StoreConfig>({
+export function recsStorage<tables extends Record<string, Table>>({
   world,
-  config,
+  tables,
   shouldSkipUpdateStream,
-}: RecsStorageOptions<TConfig>): RecsStorageAdapter<TConfig> {
+}: RecsStorageOptions<tables>): RecsStorageAdapter<tables> {
   world.registerEntity({ id: singletonEntity });
 
   const components = {
-    ...configToRecsComponents(world, config),
-    ...configToRecsComponents(world, storeConfig),
-    ...configToRecsComponents(world, worldConfig),
+    ...tablesToComponents(world, tables),
+    ...tablesToComponents(world, storeTables),
+    ...tablesToComponents(world, worldTables),
     ...defineInternalComponents(world),
   };
 
diff --git a/packages/store-sync/src/recs/syncToRecs.ts b/packages/store-sync/src/recs/syncToRecs.ts
index 87dc02b737..22c4892ed8 100644
--- a/packages/store-sync/src/recs/syncToRecs.ts
+++ b/packages/store-sync/src/recs/syncToRecs.ts
@@ -1,4 +1,4 @@
-import { StoreConfig } from "@latticexyz/store";
+import { StoreConfig, Table, ResolvedStoreConfig, resolveConfig } from "@latticexyz/store";
 import { Component as RecsComponent, World as RecsWorld, getComponentValue, setComponent } from "@latticexyz/recs";
 import { SyncOptions, SyncResult } from "../common";
 import { RecsStorageAdapter, recsStorage } from "./recsStorage";
@@ -6,26 +6,33 @@ import { createStoreSync } from "../createStoreSync";
 import { singletonEntity } from "./singletonEntity";
 import { SyncStep } from "../SyncStep";
 
-type SyncToRecsOptions<TConfig extends StoreConfig = StoreConfig> = SyncOptions<TConfig> & {
+type SyncToRecsOptions<config extends StoreConfig, extraTables extends Record<string, Table>> = SyncOptions<config> & {
   world: RecsWorld;
-  config: TConfig;
+  config: config;
+  tables?: extraTables;
   startSync?: boolean;
 };
 
-type SyncToRecsResult<TConfig extends StoreConfig = StoreConfig> = SyncResult & {
-  components: RecsStorageAdapter<TConfig>["components"];
+type SyncToRecsResult<config extends StoreConfig, extraTables extends Record<string, Table>> = SyncResult & {
+  components: RecsStorageAdapter<ResolvedStoreConfig<config>["tables"] & extraTables>["components"];
   stopSync: () => void;
 };
 
-export async function syncToRecs<TConfig extends StoreConfig = StoreConfig>({
+export async function syncToRecs<config extends StoreConfig, extraTables extends Record<string, Table>>({
   world,
   config,
+  tables: extraTables,
   startSync = true,
   ...syncOptions
-}: SyncToRecsOptions<TConfig>): Promise<SyncToRecsResult<TConfig>> {
+}: SyncToRecsOptions<config, extraTables>): Promise<SyncToRecsResult<config, extraTables>> {
+  const tables = {
+    ...resolveConfig(config).tables,
+    ...extraTables,
+  } as ResolvedStoreConfig<config>["tables"] & extraTables;
+
   const { storageAdapter, components } = recsStorage({
     world,
-    config,
+    tables,
     shouldSkipUpdateStream: (): boolean =>
       getComponentValue(components.SyncProgress, singletonEntity)?.step !== SyncStep.LIVE,
   });
diff --git a/packages/store-sync/src/recs/tableToComponent.ts b/packages/store-sync/src/recs/tableToComponent.ts
new file mode 100644
index 0000000000..8a3e89cc7c
--- /dev/null
+++ b/packages/store-sync/src/recs/tableToComponent.ts
@@ -0,0 +1,49 @@
+import { Component, Type, World, defineComponent } from "@latticexyz/recs";
+import { StoreComponentMetadata } from "./common";
+import { SchemaAbiTypeToRecsType, schemaAbiTypeToRecsType } from "./schemaAbiTypeToRecsType";
+import { SchemaAbiType } from "@latticexyz/schema-type";
+import { Table } from "@latticexyz/store";
+import { mapObject } from "@latticexyz/common/utils";
+
+export type TableToComponent<table extends Table> = Component<
+  {
+    __staticData: Type.OptionalString;
+    __encodedLengths: Type.OptionalString;
+    __dynamicData: Type.OptionalString;
+  } & {
+    [fieldName in keyof table["valueSchema"] & string]: Type &
+      SchemaAbiTypeToRecsType<SchemaAbiType & table["valueSchema"][fieldName]["type"]>;
+  },
+  StoreComponentMetadata & {
+    componentName: table["name"];
+    tableName: `${table["namespace"]}:${table["name"]}`;
+    keySchema: { [name in keyof table["keySchema"] & string]: table["keySchema"][name]["type"] };
+    valueSchema: { [name in keyof table["valueSchema"] & string]: table["valueSchema"][name]["type"] };
+  }
+>;
+
+export function tableToComponent<table extends Table>(world: World, table: table): TableToComponent<table> {
+  return defineComponent(
+    world,
+    {
+      ...Object.fromEntries(
+        Object.entries(table.valueSchema).map(([fieldName, { type: schemaAbiType }]) => [
+          fieldName,
+          schemaAbiTypeToRecsType[schemaAbiType as SchemaAbiType],
+        ])
+      ),
+      __staticData: Type.OptionalString,
+      __encodedLengths: Type.OptionalString,
+      __dynamicData: Type.OptionalString,
+    },
+    {
+      id: table.tableId,
+      metadata: {
+        componentName: table.name,
+        tableName: `${table.namespace}:${table.name}`,
+        keySchema: mapObject(table.keySchema, ({ type }) => type),
+        valueSchema: mapObject(table.valueSchema, ({ type }) => type),
+      },
+    }
+  ) as TableToComponent<table>;
+}
diff --git a/packages/store-sync/src/recs/tablesToComponents.ts b/packages/store-sync/src/recs/tablesToComponents.ts
new file mode 100644
index 0000000000..be1a2cde4e
--- /dev/null
+++ b/packages/store-sync/src/recs/tablesToComponents.ts
@@ -0,0 +1,15 @@
+import { Table } from "@latticexyz/store";
+import { TableToComponent, tableToComponent } from "./tableToComponent";
+import { mapObject } from "@latticexyz/common/utils";
+import { World } from "@latticexyz/recs";
+
+export type TablesToComponents<tables extends Record<string, Table>> = {
+  [tableName in keyof tables]: TableToComponent<tables[tableName]>;
+};
+
+export function tablesToComponents<tables extends Record<string, Table>>(
+  world: World,
+  tables: tables
+): TablesToComponents<tables> {
+  return mapObject(tables, (table) => tableToComponent(world, table));
+}
diff --git a/packages/store/package.json b/packages/store/package.json
index f231b25388..58ddb59f98 100644
--- a/packages/store/package.json
+++ b/packages/store/package.json
@@ -55,6 +55,7 @@
     "@latticexyz/config": "workspace:*",
     "@latticexyz/schema-type": "workspace:*",
     "abitype": "0.9.8",
+    "viem": "1.14.0",
     "zod": "^3.21.4"
   },
   "devDependencies": {
diff --git a/packages/store/ts/common.ts b/packages/store/ts/common.ts
index 5e6ce3e000..f1b17075e3 100644
--- a/packages/store/ts/common.ts
+++ b/packages/store/ts/common.ts
@@ -1,5 +1,16 @@
-import { SchemaAbiType, SchemaAbiTypeToPrimitiveType } from "@latticexyz/schema-type";
+import { SchemaAbiType, SchemaAbiTypeToPrimitiveType, StaticAbiType } from "@latticexyz/schema-type";
 import { FieldData, FullSchemaConfig, StoreConfig } from "./config";
+import { Hex } from "viem";
+
+export type KeySchema = Record<string, { type: StaticAbiType }>;
+export type ValueSchema = Record<string, { type: SchemaAbiType }>;
+export type Table = {
+  tableId: Hex;
+  namespace: string;
+  name: string;
+  keySchema: KeySchema;
+  valueSchema: ValueSchema;
+};
 
 export type ConfigFieldTypeToSchemaAbiType<T extends FieldData<string>> = T extends SchemaAbiType
   ? T
diff --git a/packages/store/ts/config/experimental/resolveConfig.test-d.ts b/packages/store/ts/config/experimental/resolveConfig.test-d.ts
index 1390bbd9fb..0c5f5e52a3 100644
--- a/packages/store/ts/config/experimental/resolveConfig.test-d.ts
+++ b/packages/store/ts/config/experimental/resolveConfig.test-d.ts
@@ -1,51 +1,62 @@
 import { describe, expectTypeOf } from "vitest";
 import { mudConfig } from "../../register/mudConfig";
 import { resolveConfig } from "./resolveConfig";
+import { Table } from "../../common";
+import storeConfig from "../../../mud.config";
 
-const config = resolveConfig(
-  mudConfig({
-    // Seems like we need `as const` here to keep the strong type.
-    // Note it resolves to the strong `""` type if no namespace is provided.
-    // TODO: require the entire input config to be `const`
-    namespace: "the-namespace" as const,
-    userTypes: {
-      ResourceId: {
-        internalType: "bytes32",
-        filePath: "",
-      },
-    },
-    enums: {
-      ResourceType: ["namespace", "system", "table"],
-    },
-    tables: {
-      Shorthand: {
-        keySchema: {
-          key: "ResourceId",
+describe("resolveConfig", () => {
+  describe("inline config", () => {
+    const config = resolveConfig(
+      mudConfig({
+        // Seems like we need `as const` here to keep the strong type.
+        // Note it resolves to the strong `""` type if no namespace is provided.
+        // TODO: require the entire input config to be `const`
+        namespace: "the-namespace" as const,
+        userTypes: {
+          ResourceId: {
+            internalType: "bytes32",
+            filePath: "",
+          },
+        },
+        enums: {
+          ResourceType: ["namespace", "system", "table"],
+        },
+        tables: {
+          Shorthand: {
+            keySchema: {
+              key: "ResourceId",
+            },
+            valueSchema: "ResourceType",
+          },
         },
-        valueSchema: "ResourceType",
-      },
-    },
-  })
-);
+      })
+    );
 
-describe("resolveConfig", () => {
-  expectTypeOf<typeof config.tables.Shorthand.namespace>().toEqualTypeOf<"the-namespace">();
+    expectTypeOf<typeof config.tables.Shorthand.namespace>().toEqualTypeOf<"the-namespace">();
+
+    expectTypeOf<typeof config.tables.Shorthand.name>().toEqualTypeOf<"Shorthand">();
+
+    expectTypeOf<typeof config.tables.Shorthand.tableId>().toEqualTypeOf<`0x${string}`>();
 
-  expectTypeOf<typeof config.tables.Shorthand.name>().toEqualTypeOf<"Shorthand">();
+    expectTypeOf<typeof config.tables.Shorthand.keySchema>().toEqualTypeOf<{
+      key: {
+        internalType: "ResourceId";
+        type: "bytes32";
+      };
+    }>();
 
-  expectTypeOf<typeof config.tables.Shorthand.tableId>().toEqualTypeOf<`0x${string}`>();
+    expectTypeOf<typeof config.tables.Shorthand.valueSchema>().toEqualTypeOf<{
+      value: {
+        internalType: "ResourceType";
+        type: "uint8";
+      };
+    }>();
 
-  expectTypeOf<typeof config.tables.Shorthand.keySchema>().toEqualTypeOf<{
-    key: {
-      internalType: "ResourceId";
-      type: "bytes32";
-    };
-  }>();
+    expectTypeOf<typeof config.tables.Shorthand>().toMatchTypeOf<Table>();
+  });
 
-  expectTypeOf<typeof config.tables.Shorthand.valueSchema>().toEqualTypeOf<{
-    value: {
-      internalType: "ResourceType";
-      type: "uint8";
-    };
-  }>();
+  describe("store config", () => {
+    const config = resolveConfig(storeConfig);
+    expectTypeOf<typeof config.tables.Tables.valueSchema.abiEncodedFieldNames>().toMatchTypeOf<{ type: "bytes" }>();
+  });
 });
diff --git a/packages/store/ts/config/experimental/resolveConfig.ts b/packages/store/ts/config/experimental/resolveConfig.ts
index b02cb2e713..ea869ddc56 100644
--- a/packages/store/ts/config/experimental/resolveConfig.ts
+++ b/packages/store/ts/config/experimental/resolveConfig.ts
@@ -3,7 +3,12 @@ import { StoreConfig, TableConfig, UserTypesConfig } from "../storeConfig";
 import { UserType } from "@latticexyz/common/codegen";
 import { mapObject } from "@latticexyz/common/utils";
 import { resourceToHex } from "@latticexyz/common";
+import { SchemaAbiType } from "@latticexyz/schema-type";
 
+/**
+ * @internal Internal only
+ * @deprecated Internal only
+ */
 export type ResolvedStoreConfig<TStoreConfig extends StoreConfig> = {
   tables: {
     [TableKey in keyof TStoreConfig["tables"] & string]: ResolvedTableConfig<
@@ -16,13 +21,13 @@ export type ResolvedStoreConfig<TStoreConfig extends StoreConfig> = {
   };
 };
 
-export type ResolvedTableConfig<
+type ResolvedTableConfig<
   TTableConfig extends TableConfig,
   TUserTypes extends UserTypesConfig["userTypes"],
   TEnumNames extends StringForUnion,
   TNamespace extends string = string,
   TName extends string = string
-> = Omit<TTableConfig, "keySchema" | "valueSchema"> & {
+> = {
   keySchema: ResolvedKeySchema<TTableConfig["keySchema"], TUserTypes, TEnumNames>;
   valueSchema: ResolvedValueSchema<TTableConfig["valueSchema"], TUserTypes, TEnumNames>;
   namespace: TNamespace;
@@ -30,25 +35,27 @@ export type ResolvedTableConfig<
   tableId: `0x${string}`;
 };
 
-export type ResolvedKeySchema<
+type ResolvedKeySchema<
   TKeySchema extends TableConfig["keySchema"],
   TUserTypes extends UserTypesConfig["userTypes"],
   TEnumNames extends StringForUnion
 > = ResolvedSchema<TKeySchema, TUserTypes, TEnumNames>;
 
-export type ResolvedValueSchema<
+type ResolvedValueSchema<
   TValueSchema extends TableConfig["valueSchema"],
   TUserTypes extends UserTypesConfig["userTypes"],
   TEnumNames extends StringForUnion
 > = ResolvedSchema<Exclude<TValueSchema, string>, TUserTypes, TEnumNames>;
 
-export type ResolvedSchema<
+type ResolvedSchema<
   TSchema extends Exclude<TableConfig["keySchema"] | TableConfig["valueSchema"], string>,
   TUserTypes extends UserTypesConfig["userTypes"],
   TEnumNames extends StringForUnion
 > = {
   [key in keyof TSchema]: {
-    type: TSchema[key] extends keyof TUserTypes
+    type: TSchema[key] extends SchemaAbiType
+      ? TSchema[key]
+      : TSchema[key] extends keyof TUserTypes
       ? TUserTypes[TSchema[key]] extends UserType
         ? // Note: we mistakenly named the plain ABI type "internalType",
           // while in Solidity ABIs the plain ABI type is called "type" and
@@ -58,7 +65,7 @@ export type ResolvedSchema<
         : never
       : TSchema[key] extends TEnumNames
       ? "uint8"
-      : TSchema[key];
+      : never;
     internalType: TSchema[key];
   };
 };
@@ -100,10 +107,9 @@ function resolveTable<
   namespace: TNamespace,
   name: TName
 ): ResolvedTableConfig<typeof tableConfig, TUserTypes, TEnums[number]> {
-  const { keySchema, valueSchema, ...rest } = tableConfig;
+  const { keySchema, valueSchema } = tableConfig;
 
   return {
-    ...rest,
     keySchema: resolveKeySchema(keySchema, userTypes, enums),
     valueSchema: resolveValueSchema(valueSchema, userTypes, enums) as ResolvedSchema<
       Exclude<TTableConfig["valueSchema"], string>,
diff --git a/packages/store/ts/config/index.ts b/packages/store/ts/config/index.ts
index 5de71694e7..a00a9a9cd7 100644
--- a/packages/store/ts/config/index.ts
+++ b/packages/store/ts/config/index.ts
@@ -1,2 +1,4 @@
 export * from "./defaults";
 export * from "./storeConfig";
+
+export * from "./experimental/resolveConfig";
diff --git a/packages/store/ts/config/storeConfig.ts b/packages/store/ts/config/storeConfig.ts
index a9f9071438..ca7dc23201 100644
--- a/packages/store/ts/config/storeConfig.ts
+++ b/packages/store/ts/config/storeConfig.ts
@@ -71,7 +71,7 @@ const zShorthandSchemaConfig = zFieldData.transform((fieldData) => {
 
 export const zSchemaConfig = zFullSchemaConfig.or(zShorthandSchemaConfig);
 
-export type ResolvedSchema<
+type ResolvedSchema<
   TSchema extends Record<string, string>,
   TUserTypes extends Record<string, Pick<UserType, "internalType">>
 > = {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index edd3c189c8..06119a5fc2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -767,6 +767,9 @@ importers:
       abitype:
         specifier: 0.9.8
         version: 0.9.8(typescript@5.1.6)(zod@3.21.4)
+      viem:
+        specifier: 1.14.0
+        version: 1.14.0(typescript@5.1.6)(zod@3.21.4)
       zod:
         specifier: ^3.21.4
         version: 3.21.4
@@ -2949,10 +2952,6 @@ packages:
       - zenObservable
     dev: true
 
-  /@scure/base@1.1.1:
-    resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==}
-    dev: false
-
   /@scure/base@1.1.3:
     resolution: {integrity: sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==}
 
@@ -2983,7 +2982,7 @@ packages:
     resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==}
     dependencies:
       '@noble/hashes': 1.3.2
-      '@scure/base': 1.1.1
+      '@scure/base': 1.1.3
     dev: false
 
   /@sentry/core@5.30.0: