From 904fd7d4ee06a86e481e3e02fd5744224376d0c9 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Thu, 6 Jul 2023 12:00:52 +0100 Subject: [PATCH] feat(store-sync): add store sync package (#1075) * feat(block-events-stream): add block events stream package * feat(store-sync): add store sync package * wip anvil test * Revert "wip anvil test" This reverts commit 1952a98f80a84ef3aa4ad1146762322fb264c193. * accidentally left in a store refernence * Update packages/block-events-stream/src/createBlockEventsStream.ts Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> * make streams closeable I don't love this design * clean up * add log back in * move comments * refactor with just streams * add README with example * renamed * rename again and take in a tuple as input * fix scope * add TODO * add tests for grouping logs * wip rxjs tests * move fetchLogs to async generator, add tests * add block range tests * get rid of old approach * add note about timers * use concatMap instead of exhaustMap * update readme * feat(schema-type): add isDynamicAbiType (#1096) * update with new block events stream * missed a spot * refine types, move logging to debug * add test * wip config types * refactor as a list of storage operations * getting closer * config is already expanded * fix types * clean up * remove log * pass through log to storage operation * update test snapshot * Create eighty-tigers-argue.md --------- Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com> Co-authored-by: alvrs --- .changeset/eighty-tigers-argue.md | 8 + docs/pages/client-side.mdx | 8 +- .../src/groupLogsByBlockNumber.ts | 10 +- packages/block-logs-stream/src/index.ts | 1 + packages/protocol-parser/src/decodeField.ts | 13 + packages/protocol-parser/src/index.ts | 5 + .../src/schemaIndexToAbiType.ts | 9 + packages/store-sync/.eslintrc | 6 + packages/store-sync/.gitignore | 1 + packages/store-sync/.npmignore | 6 + packages/store-sync/package.json | 45 ++++ .../src/blockEventsToStorage.test.ts | 153 ++++++++++++ .../store-sync/src/blockEventsToStorage.ts | 234 ++++++++++++++++++ packages/store-sync/src/debug.ts | 3 + packages/store-sync/src/index.ts | 1 + packages/store-sync/tsconfig.json | 14 ++ packages/store-sync/tsup.config.ts | 11 + packages/store/ts/storeEventsAbi.ts | 3 + pnpm-lock.yaml | 42 +++- 19 files changed, 567 insertions(+), 6 deletions(-) create mode 100644 .changeset/eighty-tigers-argue.md create mode 100644 packages/protocol-parser/src/decodeField.ts create mode 100644 packages/protocol-parser/src/schemaIndexToAbiType.ts create mode 100644 packages/store-sync/.eslintrc create mode 100644 packages/store-sync/.gitignore create mode 100644 packages/store-sync/.npmignore create mode 100644 packages/store-sync/package.json create mode 100644 packages/store-sync/src/blockEventsToStorage.test.ts create mode 100644 packages/store-sync/src/blockEventsToStorage.ts create mode 100644 packages/store-sync/src/debug.ts create mode 100644 packages/store-sync/src/index.ts create mode 100644 packages/store-sync/tsconfig.json create mode 100644 packages/store-sync/tsup.config.ts diff --git a/.changeset/eighty-tigers-argue.md b/.changeset/eighty-tigers-argue.md new file mode 100644 index 0000000000..83d56a7457 --- /dev/null +++ b/.changeset/eighty-tigers-argue.md @@ -0,0 +1,8 @@ +--- +"@latticexyz/block-logs-stream": patch +"@latticexyz/protocol-parser": patch +"@latticexyz/store-sync": minor +"@latticexyz/store": patch +--- + +Add store sync package diff --git a/docs/pages/client-side.mdx b/docs/pages/client-side.mdx index 8206b52140..ec65bd5d9e 100644 --- a/docs/pages/client-side.mdx +++ b/docs/pages/client-side.mdx @@ -197,7 +197,9 @@ const multiKey = client.tables.MultiKey.get({ first: "0xDEAD", second: 42 }); ```tsx import { useRow } from "@latticexyz/react"; function ExampleComponent() { - const { network: { storeCache } } = useMUD(); + const { + network: { storeCache }, + } = useMUD(); const position = useRow(storeCache, { table: "Position", key: { key: "0x01" } }); // -> { namespace: "", table: "Position", key: { key: "0x01" }, value: { x: 1, y: 2 } } const { value } = position; @@ -232,7 +234,9 @@ const positionsFiltered = client.tables.Position.scan({ ```tsx import { useRows } from "@latticexyz/react"; function ExampleComponent() { - const { network: { storeCache } } = useMUD(); + const { + network: { storeCache }, + } = useMUD(); const positions = useRows(storeCache, { table: "Position" }); // -> [{key: "0x00", value: {x: 10, y: 32}, namespace: config["namespace"], table: Position}, ...] const positionsFiltered = useRows(storeCache, { table: "Position", key: { gt: { key: "0x02" } } }); diff --git a/packages/block-logs-stream/src/groupLogsByBlockNumber.ts b/packages/block-logs-stream/src/groupLogsByBlockNumber.ts index 0c24ce1312..93a711a1c4 100644 --- a/packages/block-logs-stream/src/groupLogsByBlockNumber.ts +++ b/packages/block-logs-stream/src/groupLogsByBlockNumber.ts @@ -4,6 +4,12 @@ import { bigIntSort } from "./utils"; import { isDefined } from "@latticexyz/common/utils"; import { debug } from "./debug"; +export type GroupLogsByBlockNumberResult = { + blockNumber: BlockNumber; + blockHash: Hex; + logs: readonly NonPendingLog[]; +}[]; + /** * Groups logs by their block number. * @@ -17,9 +23,7 @@ import { debug } from "./debug"; * @returns An array of objects where each object represents a distinct block and includes the block number, * the block hash, and an array of logs for that block. */ -export function groupLogsByBlockNumber( - logs: readonly TLog[] -): { blockNumber: BlockNumber; blockHash: Hex; logs: readonly NonPendingLog[] }[] { +export function groupLogsByBlockNumber(logs: readonly TLog[]): GroupLogsByBlockNumberResult { // Pending logs don't have block numbers, so filter them out. const nonPendingLogs = logs.filter(isNonPendingLog); if (logs.length !== nonPendingLogs.length) { diff --git a/packages/block-logs-stream/src/index.ts b/packages/block-logs-stream/src/index.ts index f7ee62ef5b..da2dfb6bc0 100644 --- a/packages/block-logs-stream/src/index.ts +++ b/packages/block-logs-stream/src/index.ts @@ -1,6 +1,7 @@ export * from "./blockRangeToLogs"; export * from "./createBlockStream"; export * from "./fetchLogs"; +export * from "./getLogs"; export * from "./groupLogsByBlockNumber"; export * from "./isNonPendingBlock"; export * from "./isNonPendingLog"; diff --git a/packages/protocol-parser/src/decodeField.ts b/packages/protocol-parser/src/decodeField.ts new file mode 100644 index 0000000000..b3a63059d5 --- /dev/null +++ b/packages/protocol-parser/src/decodeField.ts @@ -0,0 +1,13 @@ +import { Hex } from "viem"; +import { SchemaAbiType, SchemaAbiTypeToPrimitiveType, isDynamicAbiType } from "@latticexyz/schema-type"; +import { decodeDynamicField } from "./decodeDynamicField"; +import { decodeStaticField } from "./decodeStaticField"; + +export function decodeField< + TAbiType extends SchemaAbiType, + TPrimitiveType extends SchemaAbiTypeToPrimitiveType +>(abiType: TAbiType, data: Hex): TPrimitiveType { + return ( + isDynamicAbiType(abiType) ? decodeDynamicField(abiType, data) : decodeStaticField(abiType, data) + ) as TPrimitiveType; +} diff --git a/packages/protocol-parser/src/index.ts b/packages/protocol-parser/src/index.ts index 6217f74de0..bffae37cd2 100644 --- a/packages/protocol-parser/src/index.ts +++ b/packages/protocol-parser/src/index.ts @@ -1,6 +1,7 @@ export * from "./abiTypesToSchema"; export * from "./common"; export * from "./decodeDynamicField"; +export * from "./decodeField"; export * from "./decodeKeyTuple"; export * from "./decodeRecord"; export * from "./decodeStaticField"; @@ -9,4 +10,8 @@ export * from "./encodeKeyTuple"; export * from "./encodeRecord"; export * from "./errors"; export * from "./hexToPackedCounter"; +export * from "./hexToSchema"; export * from "./hexToTableSchema"; +export * from "./schemaIndexToAbiType"; +export * from "./schemaToHex"; +export * from "./staticDataLength"; diff --git a/packages/protocol-parser/src/schemaIndexToAbiType.ts b/packages/protocol-parser/src/schemaIndexToAbiType.ts new file mode 100644 index 0000000000..b06a08634b --- /dev/null +++ b/packages/protocol-parser/src/schemaIndexToAbiType.ts @@ -0,0 +1,9 @@ +import { SchemaAbiType } from "@latticexyz/schema-type"; +import { Schema } from "./common"; + +export function schemaIndexToAbiType(schema: Schema, schemaIndex: number): SchemaAbiType { + if (schemaIndex < schema.staticFields.length) { + return schema.staticFields[schemaIndex]; + } + return schema.dynamicFields[schemaIndex - schema.staticFields.length]; +} diff --git a/packages/store-sync/.eslintrc b/packages/store-sync/.eslintrc new file mode 100644 index 0000000000..6db0063ad7 --- /dev/null +++ b/packages/store-sync/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": ["../../.eslintrc"], + "rules": { + "@typescript-eslint/explicit-function-return-type": "error" + } +} diff --git a/packages/store-sync/.gitignore b/packages/store-sync/.gitignore new file mode 100644 index 0000000000..1521c8b765 --- /dev/null +++ b/packages/store-sync/.gitignore @@ -0,0 +1 @@ +dist diff --git a/packages/store-sync/.npmignore b/packages/store-sync/.npmignore new file mode 100644 index 0000000000..84815f1eba --- /dev/null +++ b/packages/store-sync/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!src/** +!package.json +!README.md diff --git a/packages/store-sync/package.json b/packages/store-sync/package.json new file mode 100644 index 0000000000..55bb09e4aa --- /dev/null +++ b/packages/store-sync/package.json @@ -0,0 +1,45 @@ +{ + "name": "@latticexyz/store-sync", + "version": "1.42.0", + "description": "Utilities to sync MUD Store events with a client or cache", + "repository": { + "type": "git", + "url": "https://github.com/latticexyz/mud.git", + "directory": "packages/store-sync" + }, + "license": "MIT", + "type": "module", + "exports": { + ".": "./dist/index.js" + }, + "types": "src/index.ts", + "scripts": { + "build": "pnpm run build:js", + "build:js": "tsup", + "clean": "pnpm run clean:js", + "clean:js": "rimraf dist", + "dev": "tsup --watch", + "lint": "eslint .", + "test": "vitest --run" + }, + "dependencies": { + "@latticexyz/block-logs-stream": "workspace:*", + "@latticexyz/common": "workspace:*", + "@latticexyz/protocol-parser": "workspace:*", + "@latticexyz/schema-type": "workspace:*", + "@latticexyz/store": "workspace:*", + "@latticexyz/store-cache": "workspace:*", + "@latticexyz/utils": "workspace:*", + "debug": "^4.3.4", + "viem": "1.1.7" + }, + "devDependencies": { + "@types/debug": "^4.1.7", + "tsup": "^6.7.0", + "vitest": "0.31.4" + }, + "publishConfig": { + "access": "public" + }, + "gitHead": "914a1e0ae4a573d685841ca2ea921435057deb8f" +} diff --git a/packages/store-sync/src/blockEventsToStorage.test.ts b/packages/store-sync/src/blockEventsToStorage.test.ts new file mode 100644 index 0000000000..486834f3d1 --- /dev/null +++ b/packages/store-sync/src/blockEventsToStorage.test.ts @@ -0,0 +1,153 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { BlockEventsToStorageOptions, blockEventsToStorage } from "./blockEventsToStorage"; +import storeConfig from "@latticexyz/store/mud.config"; + +const mockedCallbacks = { + registerTableSchema: vi.fn< + Parameters, + ReturnType + >(), + registerTableMetadata: vi.fn< + Parameters, + ReturnType + >(), + getTableSchema: vi.fn< + Parameters, + ReturnType + >(), + getTableMetadata: vi.fn< + Parameters, + ReturnType + >(), +}; + +const mockedDecode = blockEventsToStorage(mockedCallbacks as any as BlockEventsToStorageOptions); + +describe("blockEventsToStorage", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("call setField with data properly decoded", async () => { + mockedCallbacks.getTableSchema.mockImplementation(async ({ namespace, name }) => { + if (namespace === "mudstore" && name === "StoreMetadata") { + return { + namespace: "mudstore", + name: "StoreMetadata", + schema: { + keySchema: { + staticFields: ["bytes32"], + dynamicFields: [], + }, + valueSchema: { + staticFields: [], + dynamicFields: ["string", "bytes"], + }, + }, + }; + } + + if (namespace === "" && name === "Inventory") { + return { + namespace: "", + name: "Inventory", + schema: { + keySchema: { + staticFields: ["address", "uint32", "uint32"], + dynamicFields: [], + }, + valueSchema: { + staticFields: ["uint32"], + dynamicFields: [], + }, + }, + }; + } + }); + + mockedCallbacks.getTableMetadata.mockImplementation(async ({ namespace, name }) => { + if (namespace === "" && name === "Inventory") { + return { + namespace: "", + name: "Inventory", + keyNames: ["owner", "item", "itemVariant"], + valueNames: ["amount"], + }; + } + }); + + const operations = await mockedDecode({ + blockNumber: 5448n, + blockHash: "0x03e962e7402b2ab295b92feac342a132111dd14b0d1fd4d4a0456fdc77981577", + logs: [ + { + address: "0x5fbdb2315678afecb367f032d93f642f64180aa3", + topics: ["0xd01f9f1368f831528fc9fe6442366b2b7d957fbfff3bcf7c24d9ab5fe51f8c46"], + data: "0x00000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000796eb990a3f9c431c69149c7a168b91596d87f600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000040000000800000000000000000000000000000000000000000000000000000000", + blockHash: "0x03e962e7402b2ab295b92feac342a132111dd14b0d1fd4d4a0456fdc77981577", + blockNumber: 5448n, + transactionHash: "0xa6986924609542dc4c2d81c53799d8eab47109ef34ee1e422de595e19ee9bfa4", + transactionIndex: 0, + logIndex: 0, + removed: false, + args: { + table: "0x00000000000000000000000000000000496e76656e746f727900000000000000", + key: [ + "0x000000000000000000000000796eb990a3f9c431c69149c7a168b91596d87f60", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ], + schemaIndex: 0, + data: "0x00000008", + }, + eventName: "StoreSetField", + }, + ], + }); + + expect(operations).toMatchInlineSnapshot(` + { + "blockHash": "0x03e962e7402b2ab295b92feac342a132111dd14b0d1fd4d4a0456fdc77981577", + "blockNumber": 5448n, + "operations": [ + { + "keyTuple": { + "item": 1, + "itemVariant": 1, + "owner": "0x796eb990A3F9C431C69149c7a168b91596D87F60", + }, + "log": { + "address": "0x5fbdb2315678afecb367f032d93f642f64180aa3", + "args": { + "data": "0x00000008", + "key": [ + "0x000000000000000000000000796eb990a3f9c431c69149c7a168b91596d87f60", + "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000001", + ], + "schemaIndex": 0, + "table": "0x00000000000000000000000000000000496e76656e746f727900000000000000", + }, + "blockHash": "0x03e962e7402b2ab295b92feac342a132111dd14b0d1fd4d4a0456fdc77981577", + "blockNumber": 5448n, + "data": "0x00000000000000000000000000000000496e76656e746f7279000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000796eb990a3f9c431c69149c7a168b91596d87f600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000040000000800000000000000000000000000000000000000000000000000000000", + "eventName": "StoreSetField", + "logIndex": 0, + "removed": false, + "topics": [ + "0xd01f9f1368f831528fc9fe6442366b2b7d957fbfff3bcf7c24d9ab5fe51f8c46", + ], + "transactionHash": "0xa6986924609542dc4c2d81c53799d8eab47109ef34ee1e422de595e19ee9bfa4", + "transactionIndex": 0, + }, + "name": "Inventory", + "namespace": "", + "type": "SetField", + "value": 8, + "valueName": "amount", + }, + ], + } + `); + }); +}); diff --git a/packages/store-sync/src/blockEventsToStorage.ts b/packages/store-sync/src/blockEventsToStorage.ts new file mode 100644 index 0000000000..23059b2cbd --- /dev/null +++ b/packages/store-sync/src/blockEventsToStorage.ts @@ -0,0 +1,234 @@ +import { + TableSchema, + decodeField, + decodeKeyTuple, + decodeRecord, + hexToTableSchema, + schemaIndexToAbiType, +} from "@latticexyz/protocol-parser"; +import { GroupLogsByBlockNumberResult, GetLogsResult } from "@latticexyz/block-logs-stream"; +import { StoreEventsAbi, StoreConfig } from "@latticexyz/store"; +import { TableId } from "@latticexyz/common"; +import { Hex, decodeAbiParameters, parseAbiParameters } from "viem"; +import { debug } from "./debug"; +// TODO: move these type helpers into store? +import { Key, Value } from "@latticexyz/store-cache"; +import { isDefined } from "@latticexyz/common/utils"; + +// TODO: change table schema/metadata APIs once we get both schema and field names in the same event + +// TODO: export these from store or world +export const schemaTableId = new TableId("mudstore", "schema"); +export const metadataTableId = new TableId("mudstore", "StoreMetadata"); + +// I don't love carrying all these types through. Ideally this should be the shape of the thing we want, rather than the specific return type from a function. +export type StoreEventsLog = GetLogsResult[number]; +export type BlockEvents = GroupLogsByBlockNumberResult[number]; + +export type StoredTableSchema = { + namespace: string; + name: string; + schema: TableSchema; +}; + +export type StoredTableMetadata = { + namespace: string; + name: string; + keyNames: readonly string[]; + valueNames: readonly string[]; +}; + +export type BaseStorageOperation = { + log: StoreEventsLog; + namespace: string; +}; + +export type SetRecordOperation = BaseStorageOperation & { + type: "SetRecord"; +} & { + [TTable in keyof TConfig["tables"]]: { + name: TTable; + keyTuple: Key; + record: Value; + }; + }[keyof TConfig["tables"]]; + +export type SetFieldOperation = BaseStorageOperation & { + type: "SetField"; +} & { + [TTable in keyof TConfig["tables"]]: { + name: TTable; + keyTuple: Key; + } & { + [TValue in keyof Value]: { + // TODO: standardize on calling these "fields" or "values" or maybe "columns" + valueName: TValue; + value: Value[TValue]; + }; + }[keyof Value]; + }[keyof TConfig["tables"]]; + +export type DeleteRecordOperation = BaseStorageOperation & { + type: "DeleteRecord"; +} & { + [TTable in keyof TConfig["tables"]]: { + name: TTable; + keyTuple: Key; + }; + }[keyof TConfig["tables"]]; + +export type StorageOperation = + | SetFieldOperation + | SetRecordOperation + | DeleteRecordOperation; + +export type BlockEventsToStorageOptions = { + registerTableSchema: (data: StoredTableSchema) => Promise; + registerTableMetadata: (data: StoredTableMetadata) => Promise; + getTableSchema: (opts: Pick) => Promise; + getTableMetadata: (opts: Pick) => Promise; +}; + +export function blockEventsToStorage({ + registerTableMetadata, + registerTableSchema, + getTableMetadata, + getTableSchema, +}: BlockEventsToStorageOptions): (block: BlockEvents) => Promise<{ + blockNumber: BlockEvents["blockNumber"]; + blockHash: BlockEvents["blockHash"]; + operations: StorageOperation[]; +}> { + return async (block) => { + // Find and register all new table schemas + // Store schemas are immutable, so we can parallelize this + await Promise.all( + block.logs.map(async (log) => { + if (log.eventName !== "StoreSetRecord") return; + if (log.args.table !== schemaTableId.toHex()) return; + + const [tableForSchema, ...otherKeys] = log.args.key; + if (otherKeys.length) { + debug("registerSchema event is expected to have only one key in key tuple, but got multiple", log); + } + + const tableId = TableId.fromHex(tableForSchema); + const schema = hexToTableSchema(log.args.data); + + await registerTableSchema({ ...tableId, schema }); + }) + ); + + const metadataTableSchema = await getTableSchema(metadataTableId); + if (!metadataTableSchema) { + // TODO: better error + throw new Error("metadata table schema was not registered"); + } + + // Find and register all new table metadata + // Table metadata is technically mutable, but all of our code assumes its immutable, so we'll continue that trend + // TODO: rework contracts so schemas+tables are combined and immutable + await Promise.all( + block.logs.map(async (log) => { + if (log.eventName !== "StoreSetRecord") return; + if (log.args.table !== metadataTableId.toHex()) return; + + const [tableForSchema, ...otherKeys] = log.args.key; + if (otherKeys.length) { + debug("setMetadata event is expected to have only one key in key tuple, but got multiple", log); + } + + const tableId = TableId.fromHex(tableForSchema); + const [tableName, abiEncodedFieldNames] = decodeRecord(metadataTableSchema.schema.valueSchema, log.args.data); + const valueNames = decodeAbiParameters(parseAbiParameters("string[]"), abiEncodedFieldNames as Hex)[0]; + + // TODO: add key names to table registration when we refactor it + await registerTableMetadata({ ...tableId, keyNames: [], valueNames }); + }) + ); + + const tables = Array.from(new Set(block.logs.map((log) => log.args.table))).map((tableHex) => + TableId.fromHex(tableHex) + ); + // TODO: combine these once we refactor table registration + const tableSchemas = Object.fromEntries( + await Promise.all(tables.map(async (table) => [table.toHex(), await getTableSchema(table)])) + ) as Record; + const tableMetadatas = Object.fromEntries( + await Promise.all(tables.map(async (table) => [table.toHex(), await getTableMetadata(table)])) + ) as Record; + + const operations = block.logs + .map((log): StorageOperation | undefined => { + const tableId = TableId.fromHex(log.args.table); + const tableSchema = tableSchemas[log.args.table]; + const tableMetadata = tableMetadatas[log.args.table]; + if (!tableSchema) { + debug("no table schema found for event, skipping", tableId.toString(), log); + return; + } + if (!tableMetadata) { + debug("no table metadata found for event, skipping", tableId.toString(), log); + return; + } + + const keyTupleValues = decodeKeyTuple(tableSchema.schema.keySchema, log.args.key); + const keyTuple = Object.fromEntries( + keyTupleValues.map((value, i) => [tableMetadata.keyNames[i] ?? i, value]) + ) as Key; + + if (log.eventName === "StoreSetRecord" || log.eventName === "StoreEphemeralRecord") { + const values = decodeRecord(tableSchema.schema.valueSchema, log.args.data); + const record = Object.fromEntries(tableMetadata.valueNames.map((name, i) => [name, values[i]])) as Value< + TConfig, + keyof TConfig["tables"] + >; + // TODO: decide if we should handle ephemeral records separately? + // they'll eventually be turned into "events", but unclear if that should translate to client storage operations + return { + log, + type: "SetRecord", + ...tableId, + keyTuple, + record, + }; + } + + if (log.eventName === "StoreSetField") { + const valueName = tableMetadata.valueNames[log.args.schemaIndex] as string & + keyof Value; + const value = decodeField( + schemaIndexToAbiType(tableSchema.schema.valueSchema, log.args.schemaIndex), + log.args.data + ) as Value[typeof valueName]; + return { + log, + type: "SetField", + ...tableId, + keyTuple, + valueName, + value, + }; + } + + if (log.eventName === "StoreDeleteRecord") { + return { + log, + type: "DeleteRecord", + ...tableId, + keyTuple, + }; + } + + debug("unknown store event or log, skipping", log); + return; + }) + .filter(isDefined); + + return { + blockNumber: block.blockNumber, + blockHash: block.blockHash, + operations, + }; + }; +} diff --git a/packages/store-sync/src/debug.ts b/packages/store-sync/src/debug.ts new file mode 100644 index 0000000000..b8d8179922 --- /dev/null +++ b/packages/store-sync/src/debug.ts @@ -0,0 +1,3 @@ +import createDebug from "debug"; + +export const debug = createDebug("mud:store-sync"); diff --git a/packages/store-sync/src/index.ts b/packages/store-sync/src/index.ts new file mode 100644 index 0000000000..209e8e1fc5 --- /dev/null +++ b/packages/store-sync/src/index.ts @@ -0,0 +1 @@ +export * from "./blockEventsToStorage"; diff --git a/packages/store-sync/tsconfig.json b/packages/store-sync/tsconfig.json new file mode 100644 index 0000000000..e590f0c026 --- /dev/null +++ b/packages/store-sync/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2021", + "module": "esnext", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true + } +} diff --git a/packages/store-sync/tsup.config.ts b/packages/store-sync/tsup.config.ts new file mode 100644 index 0000000000..b755469f90 --- /dev/null +++ b/packages/store-sync/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + target: "esnext", + format: ["esm"], + dts: false, + sourcemap: true, + clean: true, + minify: true, +}); diff --git a/packages/store/ts/storeEventsAbi.ts b/packages/store/ts/storeEventsAbi.ts index 3d383d6e1c..d15b0b3c67 100644 --- a/packages/store/ts/storeEventsAbi.ts +++ b/packages/store/ts/storeEventsAbi.ts @@ -2,3 +2,6 @@ import { parseAbi, AbiEvent } from "abitype"; import { storeEvents } from "./storeEvents"; export const storeEventsAbi = parseAbi(storeEvents) satisfies readonly AbiEvent[]; + +export type StoreEventsAbi = typeof storeEventsAbi; +export type StoreEventsAbiItem = (typeof storeEventsAbi)[number]; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e59ef7512..e129aa01e7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -1052,6 +1052,46 @@ importers: specifier: 0.31.4 version: 0.31.4 + packages/store-sync: + dependencies: + '@latticexyz/block-logs-stream': + specifier: workspace:* + version: link:../block-logs-stream + '@latticexyz/common': + specifier: workspace:* + version: link:../common + '@latticexyz/protocol-parser': + specifier: workspace:* + version: link:../protocol-parser + '@latticexyz/schema-type': + specifier: workspace:* + version: link:../schema-type + '@latticexyz/store': + specifier: workspace:* + version: link:../store + '@latticexyz/store-cache': + specifier: workspace:* + version: link:../store-cache + '@latticexyz/utils': + specifier: workspace:* + version: link:../utils + debug: + specifier: ^4.3.4 + version: 4.3.4(supports-color@8.1.1) + viem: + specifier: 1.1.7 + version: 1.1.7(typescript@5.0.4) + devDependencies: + '@types/debug': + specifier: ^4.1.7 + version: 4.1.7 + tsup: + specifier: ^6.7.0 + version: 6.7.0(postcss@8.4.23)(typescript@5.0.4) + vitest: + specifier: 0.31.4 + version: 0.31.4 + packages/utils: dependencies: '@latticexyz/common':