From b819749268918559589c12451d88ec1f933182d8 Mon Sep 17 00:00:00 2001 From: alvarius Date: Wed, 4 Dec 2024 16:56:25 +0100 Subject: [PATCH] feat: add `getRecords` util to fetch records from indexer, update deployer and explorer to use indexer (#3385) --- .changeset/big-cooks-fetch.md | 5 + .changeset/shy-eels-hide.md | 8 ++ .changeset/slimy-cheetahs-vanish.md | 5 + packages/cli/package.json | 1 + packages/cli/src/commands/dev-contracts.ts | 1 + packages/cli/src/commands/pull.ts | 11 ++ packages/cli/src/defaultChains.ts | 3 + packages/cli/src/deploy/common.ts | 9 +- packages/cli/src/deploy/deploy.ts | 44 +++++--- packages/cli/src/deploy/ensureFunctions.ts | 14 +-- .../cli/src/deploy/ensureNamespaceOwner.ts | 12 +-- packages/cli/src/deploy/ensureResourceTags.ts | 38 +++---- packages/cli/src/deploy/ensureSystems.ts | 14 +-- packages/cli/src/deploy/ensureTables.ts | 11 +- packages/cli/src/deploy/getResourceAccess.ts | 65 ++++------- packages/cli/src/deploy/getResourceIds.ts | 33 +++--- packages/cli/src/deploy/getSystems.ts | 19 ++-- packages/cli/src/deploy/getTables.ts | 67 ++++-------- packages/cli/src/pull/pull.ts | 12 ++- packages/cli/src/runDeploy.ts | 12 ++- .../src/app/(explorer)/api/world-abi/route.ts | 29 ++++- packages/store-sync/package.json | 5 +- packages/store-sync/src/getRecords.ts | 88 +++++++++++++++ packages/store-sync/src/index.ts | 1 + packages/store-sync/src/world/common.ts | 9 ++ .../src/world}/functionSignatureToAbiItem.ts | 0 packages/store-sync/src/world/getFunctions.ts | 65 +++++++++++ .../src/world}/getWorldAbi.test.ts | 0 .../src/world}/getWorldAbi.ts | 14 ++- packages/store-sync/src/world/index.ts | 2 + packages/store-sync/test/mockGame.ts | 4 +- packages/store-sync/tsup.config.ts | 1 + packages/world/ts/exports/internal.ts | 3 - packages/world/ts/getFunctions.ts | 101 ------------------ pnpm-lock.yaml | 6 +- 35 files changed, 404 insertions(+), 308 deletions(-) create mode 100644 .changeset/big-cooks-fetch.md create mode 100644 .changeset/shy-eels-hide.md create mode 100644 .changeset/slimy-cheetahs-vanish.md create mode 100644 packages/cli/src/defaultChains.ts create mode 100644 packages/store-sync/src/getRecords.ts create mode 100644 packages/store-sync/src/world/common.ts rename packages/{world/ts => store-sync/src/world}/functionSignatureToAbiItem.ts (100%) create mode 100644 packages/store-sync/src/world/getFunctions.ts rename packages/{world/ts => store-sync/src/world}/getWorldAbi.test.ts (100%) rename packages/{world/ts => store-sync/src/world}/getWorldAbi.ts (79%) create mode 100644 packages/store-sync/src/world/index.ts delete mode 100644 packages/world/ts/getFunctions.ts diff --git a/.changeset/big-cooks-fetch.md b/.changeset/big-cooks-fetch.md new file mode 100644 index 0000000000..bf917b0921 --- /dev/null +++ b/.changeset/big-cooks-fetch.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/explorer": patch +--- + +Improved the performance of the explorer's `Interact` tab by fetching the ABI from an indexer instead of from an Ethereum RPC if available. diff --git a/.changeset/shy-eels-hide.md b/.changeset/shy-eels-hide.md new file mode 100644 index 0000000000..2b4779b786 --- /dev/null +++ b/.changeset/shy-eels-hide.md @@ -0,0 +1,8 @@ +--- +"@latticexyz/store-sync": patch +"@latticexyz/world": patch +--- + +Added a `getRecords` util to fetch table records from an indexer or RPC. + +Migrated the `getFunctions` and `getWorldAbi` utils from `@latticexyz/world` to `@latticexyz/store-sync/world` to allow `getFunctions` and `getWorldAbi` to use `getRecords` internally without circular dependencies. diff --git a/.changeset/slimy-cheetahs-vanish.md b/.changeset/slimy-cheetahs-vanish.md new file mode 100644 index 0000000000..d6e792be25 --- /dev/null +++ b/.changeset/slimy-cheetahs-vanish.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/cli": patch +--- + +Added an `indexerUrl` option to the `mud deploy` and `mud pull` CLI commands to read table records from an indexer instead of fetching logs from an Ethereum RPC. diff --git a/packages/cli/package.json b/packages/cli/package.json index 6c6cd58ee9..30e4a107e5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -49,6 +49,7 @@ "@latticexyz/protocol-parser": "workspace:*", "@latticexyz/schema-type": "workspace:*", "@latticexyz/store": "workspace:*", + "@latticexyz/store-sync": "workspace:*", "@latticexyz/utils": "workspace:*", "@latticexyz/world": "workspace:*", "@latticexyz/world-module-metadata": "workspace:*", diff --git a/packages/cli/src/commands/dev-contracts.ts b/packages/cli/src/commands/dev-contracts.ts index dff19975dc..22266460ae 100644 --- a/packages/cli/src/commands/dev-contracts.ts +++ b/packages/cli/src/commands/dev-contracts.ts @@ -92,6 +92,7 @@ const commandModule: CommandModule; @@ -44,6 +51,8 @@ const commandModule: CommandModule = { : undefined, }), }); + const chainId = await getChainId(client); + const indexerUrl = opts.indexerUrl ?? defaultChains.find((chain) => chain.id === chainId)?.indexerUrl; console.log(chalk.bgBlue(chalk.whiteBright(`\n Pulling MUD config from world at ${opts.worldAddress} \n`))); const rootDir = process.cwd(); @@ -53,6 +62,8 @@ const commandModule: CommandModule = { rootDir, client, worldAddress: opts.worldAddress as Address, + indexerUrl, + chainId, replace: opts.replace, }); await build({ rootDir, config, foundryProfile: profile }); diff --git a/packages/cli/src/defaultChains.ts b/packages/cli/src/defaultChains.ts new file mode 100644 index 0000000000..db8f2c93dd --- /dev/null +++ b/packages/cli/src/defaultChains.ts @@ -0,0 +1,3 @@ +import { redstone, garnet, rhodolite } from "@latticexyz/common/chains"; + +export const defaultChains = [redstone, garnet, rhodolite]; diff --git a/packages/cli/src/deploy/common.ts b/packages/cli/src/deploy/common.ts index f88db9f65f..3cef84101b 100644 --- a/packages/cli/src/deploy/common.ts +++ b/packages/cli/src/deploy/common.ts @@ -1,4 +1,4 @@ -import { Abi, Address, Hex, padHex } from "viem"; +import { Abi, Account, Address, Chain, Client, Hex, Transport, padHex } from "viem"; import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" }; import { helloStoreEvent } from "@latticexyz/store"; import { helloWorldEvent } from "@latticexyz/world"; @@ -119,3 +119,10 @@ export type Module = DeterministicContract & { */ readonly optional?: boolean; }; + +export type CommonDeployOptions = { + readonly client: Client; + readonly worldDeploy: WorldDeploy; + readonly indexerUrl?: string; + readonly chainId?: number; +}; diff --git a/packages/cli/src/deploy/deploy.ts b/packages/cli/src/deploy/deploy.ts index 4bdc29fc07..6d6e7c427a 100644 --- a/packages/cli/src/deploy/deploy.ts +++ b/packages/cli/src/deploy/deploy.ts @@ -1,8 +1,16 @@ -import { Account, Address, Chain, Client, Hex, Transport, stringToHex } from "viem"; +import { Address, Hex, stringToHex } from "viem"; import { ensureDeployer } from "./ensureDeployer"; import { deployWorld } from "./deployWorld"; import { ensureTables } from "./ensureTables"; -import { Library, Module, System, WorldDeploy, supportedStoreVersions, supportedWorldVersions } from "./common"; +import { + CommonDeployOptions, + Library, + Module, + System, + WorldDeploy, + supportedStoreVersions, + supportedWorldVersions, +} from "./common"; import { ensureSystems } from "./ensureSystems"; import { getWorldDeploy } from "./getWorldDeploy"; import { ensureFunctions } from "./ensureFunctions"; @@ -23,7 +31,6 @@ import { getLibraryMap } from "./getLibraryMap"; type DeployOptions = { config: World; - client: Client; tables: readonly Table[]; systems: readonly System[]; libraries: readonly Library[]; @@ -39,7 +46,7 @@ type DeployOptions = { */ deployerAddress?: Hex; withWorldProxy?: boolean; -}; +} & Omit; /** * Given a viem client and MUD config, we attempt to introspect the world @@ -58,6 +65,8 @@ export async function deploy({ salt, worldAddress: existingWorldAddress, deployerAddress: initialDeployerAddress, + indexerUrl, + chainId, }: DeployOptions): Promise { const deployerAddress = initialDeployerAddress ?? (await ensureDeployer(client)); @@ -77,6 +86,13 @@ export async function deploy({ config.deploy.upgradeableWorldImplementation, ); + const commonDeployOptions = { + client, + indexerUrl, + chainId, + worldDeploy, + } satisfies CommonDeployOptions; + if (!supportedStoreVersions.includes(worldDeploy.storeVersion)) { throw new Error(`Unsupported Store version: ${worldDeploy.storeVersion}`); } @@ -86,7 +102,7 @@ export async function deploy({ const libraryMap = getLibraryMap(libraries); await ensureContractsDeployed({ - client, + ...commonDeployOptions, deployerAddress, contracts: [ ...libraries.map((library) => ({ @@ -108,8 +124,7 @@ export async function deploy({ }); const namespaceTxs = await ensureNamespaceOwner({ - client, - worldDeploy, + ...commonDeployOptions, resourceIds: [...tables.map(({ tableId }) => tableId), ...systems.map(({ systemId }) => systemId)], }); // Wait for namespaces to be available, otherwise referencing them below may fail. @@ -117,15 +132,13 @@ export async function deploy({ await waitForTransactions({ client, hashes: namespaceTxs, debugLabel: "namespace registrations" }); const tableTxs = await ensureTables({ - client, - worldDeploy, + ...commonDeployOptions, tables, }); const systemTxs = await ensureSystems({ - client, + ...commonDeployOptions, deployerAddress, libraryMap, - worldDeploy, systems, }); // Wait for tables and systems to be available, otherwise referencing their resource IDs below may fail. @@ -137,15 +150,13 @@ export async function deploy({ }); const functionTxs = await ensureFunctions({ - client, - worldDeploy, + ...commonDeployOptions, functions: systems.flatMap((system) => system.worldFunctions), }); const moduleTxs = await ensureModules({ - client, + ...commonDeployOptions, deployerAddress, libraryMap, - worldDeploy, modules, }); @@ -174,10 +185,9 @@ export async function deploy({ ]); const tagTxs = await ensureResourceTags({ - client, + ...commonDeployOptions, deployerAddress, libraryMap, - worldDeploy, tags: [...namespaceTags, ...tableTags, ...systemTags], valueToHex: stringToHex, }); diff --git a/packages/cli/src/deploy/ensureFunctions.ts b/packages/cli/src/deploy/ensureFunctions.ts index 49c9edf102..74bc4db0c7 100644 --- a/packages/cli/src/deploy/ensureFunctions.ts +++ b/packages/cli/src/deploy/ensureFunctions.ts @@ -1,7 +1,7 @@ -import { Client, Transport, Chain, Account, Hex } from "viem"; +import { Hex } from "viem"; import { hexToResource, writeContract } from "@latticexyz/common"; -import { getFunctions } from "@latticexyz/world/internal"; -import { WorldDeploy, WorldFunction, worldAbi } from "./common"; +import { getFunctions } from "@latticexyz/store-sync/world"; +import { CommonDeployOptions, WorldFunction, worldAbi } from "./common"; import { debug } from "./debug"; import pRetry from "p-retry"; @@ -9,9 +9,9 @@ export async function ensureFunctions({ client, worldDeploy, functions, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; + indexerUrl, + chainId, +}: CommonDeployOptions & { readonly functions: readonly WorldFunction[]; }): Promise { const worldFunctions = await getFunctions({ @@ -19,6 +19,8 @@ export async function ensureFunctions({ worldAddress: worldDeploy.address, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + indexerUrl, + chainId, }); const worldSelectorToFunction = Object.fromEntries(worldFunctions.map((func) => [func.selector, func])); diff --git a/packages/cli/src/deploy/ensureNamespaceOwner.ts b/packages/cli/src/deploy/ensureNamespaceOwner.ts index b94a1e7111..4250b113e0 100644 --- a/packages/cli/src/deploy/ensureNamespaceOwner.ts +++ b/packages/cli/src/deploy/ensureNamespaceOwner.ts @@ -1,5 +1,5 @@ -import { Account, Chain, Client, Hex, Transport, getAddress } from "viem"; -import { WorldDeploy, worldAbi } from "./common"; +import { Hex, getAddress } from "viem"; +import { CommonDeployOptions, worldAbi } from "./common"; import { hexToResource, resourceToHex, writeContract } from "@latticexyz/common"; import { getResourceIds } from "./getResourceIds"; import { getTableValue } from "./getTableValue"; @@ -10,13 +10,13 @@ export async function ensureNamespaceOwner({ client, worldDeploy, resourceIds, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; + indexerUrl, + chainId, +}: CommonDeployOptions & { readonly resourceIds: readonly Hex[]; }): Promise { const desiredNamespaces = Array.from(new Set(resourceIds.map((resourceId) => hexToResource(resourceId).namespace))); - const existingResourceIds = await getResourceIds({ client, worldDeploy }); + const existingResourceIds = await getResourceIds({ client, worldDeploy, indexerUrl, chainId }); const existingNamespaces = new Set(existingResourceIds.map((resourceId) => hexToResource(resourceId).namespace)); if (existingNamespaces.size) { debug( diff --git a/packages/cli/src/deploy/ensureResourceTags.ts b/packages/cli/src/deploy/ensureResourceTags.ts index 497a337a56..0b286e954a 100644 --- a/packages/cli/src/deploy/ensureResourceTags.ts +++ b/packages/cli/src/deploy/ensureResourceTags.ts @@ -1,5 +1,4 @@ -import { Hex, Client, Transport, Chain, Account, stringToHex, BaseError, concatHex } from "viem"; -import { WorldDeploy } from "./common"; +import { Hex, stringToHex, BaseError, concatHex } from "viem"; import { debug } from "./debug"; import { hexToResource, writeContract } from "@latticexyz/common"; import { identity, isDefined } from "@latticexyz/common/utils"; @@ -11,9 +10,9 @@ import { getContractArtifact } from "../utils/getContractArtifact"; import { createPrepareDeploy } from "./createPrepareDeploy"; import { waitForTransactions } from "./waitForTransactions"; import { LibraryMap } from "./getLibraryMap"; -import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; -import { getStoreLogs, flattenStoreLogs, logToRecord } from "@latticexyz/store/internal"; import { getKeyTuple, getSchemaPrimitives } from "@latticexyz/protocol-parser/internal"; +import { getRecords } from "@latticexyz/store-sync"; +import { CommonDeployOptions } from "./common"; const metadataModuleArtifact = getContractArtifact(metadataModule); @@ -30,33 +29,28 @@ export async function ensureResourceTags({ worldDeploy, tags, valueToHex = identity, -}: { - readonly client: Client; + indexerUrl, + chainId, +}: CommonDeployOptions & { readonly deployerAddress: Hex; readonly libraryMap: LibraryMap; - readonly worldDeploy: WorldDeploy; readonly tags: readonly ResourceTag[]; } & (value extends Hex - ? { readonly valueToHex?: (value: value) => Hex } - : { readonly valueToHex: (value: value) => Hex })): Promise { + ? { readonly valueToHex?: (value: value) => Hex } + : { readonly valueToHex: (value: value) => Hex })): Promise { debug("ensuring", tags.length, "resource tags"); - debug("looking up existing resource tags"); - const blockLogs = await fetchBlockLogs({ + + const { records } = await getRecords({ + table: metadataConfig.tables.metadata__ResourceTag, + worldAddress: worldDeploy.address, + chainId, + indexerUrl, + client, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldDeploy.address, - fromBlock, - toBlock, - tableId: metadataConfig.tables.metadata__ResourceTag.tableId, - }); - }, }); - const logs = flattenStoreLogs(blockLogs.flatMap((block) => block.logs)); - const records = logs.map((log) => logToRecord({ log, table: metadataConfig.tables.metadata__ResourceTag })); + debug("found", records.length, "resource tags"); const existingTags = new Map( diff --git a/packages/cli/src/deploy/ensureSystems.ts b/packages/cli/src/deploy/ensureSystems.ts index 48da5611c3..0db527850d 100644 --- a/packages/cli/src/deploy/ensureSystems.ts +++ b/packages/cli/src/deploy/ensureSystems.ts @@ -1,6 +1,6 @@ -import { Client, Transport, Chain, Account, Hex, getAddress, Address } from "viem"; +import { Hex, getAddress, Address } from "viem"; import { writeContract, resourceToLabel } from "@latticexyz/common"; -import { System, WorldDeploy, worldAbi } from "./common"; +import { CommonDeployOptions, System, worldAbi } from "./common"; import { debug } from "./debug"; import { getSystems } from "./getSystems"; import { getResourceAccess } from "./getResourceAccess"; @@ -16,16 +16,16 @@ export async function ensureSystems({ libraryMap, worldDeploy, systems, -}: { - readonly client: Client; + indexerUrl, + chainId, +}: CommonDeployOptions & { readonly deployerAddress: Hex; readonly libraryMap: LibraryMap; - readonly worldDeploy: WorldDeploy; readonly systems: readonly System[]; }): Promise { const [worldSystems, worldAccess] = await Promise.all([ - getSystems({ client, worldDeploy }), - getResourceAccess({ client, worldDeploy }), + getSystems({ client, worldDeploy, indexerUrl, chainId }), + getResourceAccess({ client, worldDeploy, indexerUrl, chainId }), ]); // Register or replace systems diff --git a/packages/cli/src/deploy/ensureTables.ts b/packages/cli/src/deploy/ensureTables.ts index c8ef52e2e2..14178cb5d3 100644 --- a/packages/cli/src/deploy/ensureTables.ts +++ b/packages/cli/src/deploy/ensureTables.ts @@ -1,6 +1,6 @@ -import { Client, Transport, Chain, Account, Hex } from "viem"; +import { Hex } from "viem"; import { resourceToLabel, writeContract } from "@latticexyz/common"; -import { WorldDeploy, worldAbi } from "./common"; +import { CommonDeployOptions, WorldDeploy, worldAbi } from "./common"; import { valueSchemaToFieldLayoutHex, keySchemaToHex, @@ -19,8 +19,9 @@ export async function ensureTables({ client, worldDeploy, tables, -}: { - readonly client: Client; + indexerUrl, + chainId, +}: CommonDeployOptions & { readonly worldDeploy: WorldDeploy; readonly tables: readonly Table[]; }): Promise { @@ -43,7 +44,7 @@ export async function ensureTables({ }), ); - const worldTables = await getTables({ client, worldDeploy }); + const worldTables = await getTables({ client, worldDeploy, indexerUrl, chainId }); const existingTables = worldTables.filter(({ tableId }) => configTables.has(tableId)); if (existingTables.length) { debug("existing tables:", existingTables.map(resourceToLabel).join(", ")); diff --git a/packages/cli/src/deploy/getResourceAccess.ts b/packages/cli/src/deploy/getResourceAccess.ts index 61ecd4f688..bb826dd3d4 100644 --- a/packages/cli/src/deploy/getResourceAccess.ts +++ b/packages/cli/src/deploy/getResourceAccess.ts @@ -1,60 +1,33 @@ -import { Client, Hex, Address, getAddress } from "viem"; -import { WorldDeploy } from "./common"; +import { Hex, Address, getAddress, Client } from "viem"; +import { CommonDeployOptions } from "./common"; import { debug } from "./debug"; -import { decodeKey, getKeySchema, getSchemaTypes } from "@latticexyz/protocol-parser/internal"; -import { getTableValue } from "./getTableValue"; import worldConfig from "@latticexyz/world/mud.config"; -import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; -import { flattenStoreLogs, getStoreLogs } from "@latticexyz/store/internal"; +import { getRecords } from "@latticexyz/store-sync"; export async function getResourceAccess({ client, worldDeploy, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; -}): Promise { + indexerUrl, + chainId, +}: Omit & { client: Client }): Promise< + readonly { readonly resourceId: Hex; readonly address: Address }[] +> { debug("looking up resource access for", worldDeploy.address); - const blockLogs = await fetchBlockLogs({ + const { records } = await getRecords({ + table: worldConfig.namespaces.world.tables.ResourceAccess, + worldAddress: worldDeploy.address, + indexerUrl, + chainId, + client, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldDeploy.address, - fromBlock, - toBlock, - tableId: worldConfig.namespaces.world.tables.ResourceAccess.tableId, - }); - }, }); - const logs = flattenStoreLogs(blockLogs.flatMap((block) => block.logs)); - - const keys = logs.map((log) => - decodeKey(getSchemaTypes(getKeySchema(worldConfig.namespaces.world.tables.ResourceAccess)), log.args.keyTuple), - ); - - const access = ( - await Promise.all( - keys.map( - async (key) => - [ - key, - await getTableValue({ - client, - worldDeploy, - table: worldConfig.namespaces.world.tables.ResourceAccess, - key, - }), - ] as const, - ), - ) - ) - .filter(([, value]) => value.access) - .map(([key]) => ({ - resourceId: key.resourceId, - address: getAddress(key.caller), + const access = records + .filter((record) => record.access) + .map((record) => ({ + resourceId: record.resourceId, + address: getAddress(record.caller), })); debug("found", access.length, "resource<>address access pairs"); diff --git a/packages/cli/src/deploy/getResourceIds.ts b/packages/cli/src/deploy/getResourceIds.ts index d0714c4a2d..4feb0a88e2 100644 --- a/packages/cli/src/deploy/getResourceIds.ts +++ b/packages/cli/src/deploy/getResourceIds.ts @@ -1,36 +1,27 @@ import { Client, Hex } from "viem"; -import { flattenStoreLogs, getStoreLogs } from "@latticexyz/store/internal"; -import { WorldDeploy } from "./common"; +import { CommonDeployOptions } from "./common"; import { debug } from "./debug"; import storeConfig from "@latticexyz/store/mud.config"; -import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; +import { getRecords } from "@latticexyz/store-sync"; export async function getResourceIds({ client, worldDeploy, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; -}): Promise { + indexerUrl, + chainId, +}: Omit & { client: Client }): Promise { debug("looking up resource IDs for", worldDeploy.address); - - const blockLogs = await fetchBlockLogs({ + const { records } = await getRecords({ + table: storeConfig.namespaces.store.tables.ResourceIds, + worldAddress: worldDeploy.address, + indexerUrl, + chainId, + client, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldDeploy.address, - fromBlock, - toBlock, - tableId: storeConfig.namespaces.store.tables.ResourceIds.tableId, - }); - }, }); - const logs = flattenStoreLogs(blockLogs.flatMap((block) => block.logs)); + const resourceIds = records.map((record) => record.resourceId); - const resourceIds = logs.map((log) => log.args.keyTuple[0]); debug("found", resourceIds.length, "resource IDs for", worldDeploy.address); - return resourceIds; } diff --git a/packages/cli/src/deploy/getSystems.ts b/packages/cli/src/deploy/getSystems.ts index f6a892a1d2..505cdb29ec 100644 --- a/packages/cli/src/deploy/getSystems.ts +++ b/packages/cli/src/deploy/getSystems.ts @@ -1,29 +1,30 @@ -import { DeployedSystem, WorldDeploy } from "./common"; -import { Client } from "viem"; +import { CommonDeployOptions, DeployedSystem } from "./common"; import { hexToResource, resourceToLabel } from "@latticexyz/common"; -import { getFunctions } from "@latticexyz/world/internal"; +import { getFunctions } from "@latticexyz/store-sync/world"; import { getResourceIds } from "./getResourceIds"; import { getTableValue } from "./getTableValue"; import { debug } from "./debug"; import { getResourceAccess } from "./getResourceAccess"; import worldConfig from "@latticexyz/world/mud.config"; +import { Client } from "viem"; export async function getSystems({ client, worldDeploy, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; -}): Promise { + indexerUrl, + chainId, +}: Omit & { client: Client }): Promise { const [resourceIds, functions, resourceAccess] = await Promise.all([ - getResourceIds({ client, worldDeploy }), + getResourceIds({ client, worldDeploy, indexerUrl, chainId }), getFunctions({ client, worldAddress: worldDeploy.address, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + indexerUrl, + chainId, }), - getResourceAccess({ client, worldDeploy }), + getResourceAccess({ client, worldDeploy, indexerUrl, chainId }), ]); const systems = resourceIds.map(hexToResource).filter((resource) => resource.type === "system"); diff --git a/packages/cli/src/deploy/getTables.ts b/packages/cli/src/deploy/getTables.ts index 54b0b717cf..cc3a6613b4 100644 --- a/packages/cli/src/deploy/getTables.ts +++ b/packages/cli/src/deploy/getTables.ts @@ -1,19 +1,11 @@ import { Client, Hex, decodeAbiParameters, parseAbiParameters } from "viem"; import { hexToResource } from "@latticexyz/common"; -import { WorldDeploy } from "./common"; +import { CommonDeployOptions } from "./common"; import { debug } from "./debug"; -import { - decodeKey, - decodeValueArgs, - getKeySchema, - getSchemaTypes, - getValueSchema, - hexToSchema, -} from "@latticexyz/protocol-parser/internal"; +import { hexToSchema } from "@latticexyz/protocol-parser/internal"; import { Schema, Table } from "@latticexyz/config"; import storeConfig from "@latticexyz/store/mud.config"; -import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; -import { flattenStoreLogs, getStoreLogs } from "@latticexyz/store/internal"; +import { getRecords } from "@latticexyz/store-sync"; // TODO: add label and namespaceLabel once we register it onchain type DeployedTable = Omit & { @@ -26,45 +18,28 @@ type DeployedTable = Omit & { export async function getTables({ client, worldDeploy, -}: { - readonly client: Client; - readonly worldDeploy: WorldDeploy; -}): Promise[]> { + indexerUrl, + chainId, +}: Omit & { client: Client }): Promise[]> { debug("looking up tables for", worldDeploy.address); - const blockLogs = await fetchBlockLogs({ + const { records } = await getRecords({ + table: storeConfig.namespaces.store.tables.Tables, + worldAddress: worldDeploy.address, + indexerUrl, + chainId, + client, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldDeploy.address, - fromBlock, - toBlock, - tableId: storeConfig.namespaces.store.tables.Tables.tableId, - }); - }, }); - const logs = flattenStoreLogs(blockLogs.flatMap((block) => block.logs)); - // TODO: combine with store-sync logToTable and export from somewhere - const tables = logs.map((log): DeployedTable => { - const { tableId } = decodeKey( - getSchemaTypes(getKeySchema(storeConfig.namespaces.store.tables.Tables)), - log.args.keyTuple, - ); - const { type, namespace, name } = hexToResource(tableId); - const recordValue = decodeValueArgs( - getSchemaTypes(getValueSchema(storeConfig.namespaces.store.tables.Tables)), - log.args, - ); + const tables = records.map((record) => { + const { type, namespace, name } = hexToResource(record.tableId); - const keySchemaHex = recordValue.keySchema; - const valueSchemaHex = recordValue.valueSchema; - const solidityKeySchema = hexToSchema(keySchemaHex); - const solidityValueSchema = hexToSchema(valueSchemaHex); - const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), recordValue.abiEncodedKeyNames)[0]; - const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), recordValue.abiEncodedFieldNames)[0]; + const solidityKeySchema = hexToSchema(record.keySchema); + const solidityValueSchema = hexToSchema(record.valueSchema); + const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), record.abiEncodedKeyNames)[0]; + const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), record.abiEncodedFieldNames)[0]; const valueAbiTypes = [...solidityValueSchema.staticFields, ...solidityValueSchema.dynamicFields]; @@ -80,13 +55,13 @@ export async function getTables({ type: type as never, namespace, name, - tableId, + tableId: record.tableId, schema: { ...keySchema, ...valueSchema }, key: Object.keys(keySchema), keySchema, - keySchemaHex, + keySchemaHex: record.keySchema, valueSchema, - valueSchemaHex, + valueSchemaHex: record.valueSchema, }; }); diff --git a/packages/cli/src/pull/pull.ts b/packages/cli/src/pull/pull.ts index 8616c6f5d4..a7259a4dfb 100644 --- a/packages/cli/src/pull/pull.ts +++ b/packages/cli/src/pull/pull.ts @@ -8,7 +8,7 @@ import { getRecord } from "../deploy/getRecord"; import path from "node:path"; import fs from "node:fs/promises"; import { getResourceIds } from "../deploy/getResourceIds"; -import { getFunctions } from "@latticexyz/world/internal"; +import { getFunctions } from "@latticexyz/store-sync/world"; import { abiToInterface, formatSolidity, formatTypescript } from "@latticexyz/common/codegen"; import { debug } from "./debug"; import { defineWorld } from "@latticexyz/world"; @@ -37,15 +37,17 @@ export type PullOptions = { * Defaults to `true` if `rootDir` is within a git repo, otherwise `false`. * */ replace?: boolean; + indexerUrl?: string; + chainId?: number; }; -export async function pull({ rootDir, client, worldAddress, replace }: PullOptions) { +export async function pull({ rootDir, client, worldAddress, replace, indexerUrl, chainId }: PullOptions) { const replaceFiles = replace ?? (await findUp(".git", { cwd: rootDir })) != null; const worldDeploy = await getWorldDeploy(client, worldAddress); - const resourceIds = await getResourceIds({ client, worldDeploy }); + const resourceIds = await getResourceIds({ client, worldDeploy, indexerUrl, chainId }); const resources = resourceIds.map(hexToResource).filter((resource) => !ignoredNamespaces.has(resource.namespace)); - const tables = await getTables({ client, worldDeploy }); + const tables = await getTables({ client, worldDeploy, indexerUrl, chainId }); const labels = Object.fromEntries( ( @@ -71,6 +73,8 @@ export async function pull({ rootDir, client, worldAddress, replace }: PullOptio worldAddress: worldDeploy.address, fromBlock: worldDeploy.deployBlock, toBlock: worldDeploy.stateBlock, + indexerUrl, + chainId, }); const namespaces = resources.filter((resource) => resource.type === "namespace"); diff --git a/packages/cli/src/runDeploy.ts b/packages/cli/src/runDeploy.ts index 49e4f5c50c..4f66c180d0 100644 --- a/packages/cli/src/runDeploy.ts +++ b/packages/cli/src/runDeploy.ts @@ -18,6 +18,7 @@ import { kmsKeyToAccount } from "@latticexyz/common/kms"; import { configToModules } from "./deploy/configToModules"; import { findContractArtifacts } from "@latticexyz/world/node"; import { enableAutomine } from "./utils/enableAutomine"; +import { defaultChains } from "./defaultChains"; export const deployOptions = { configPath: { type: "string", desc: "Path to the MUD config file" }, @@ -48,6 +49,11 @@ export const deployOptions = { type: "boolean", desc: "Deploy the World with an AWS KMS key instead of local private key.", }, + indexerUrl: { + type: "string", + desc: "The indexer URL to use.", + required: false, + }, } as const satisfies Record; export type DeployOptions = InferredOptionTypes; @@ -136,6 +142,9 @@ export async function runDeploy(opts: DeployOptions): Promise { account, }); + const chainId = await getChainId(client); + const indexerUrl = opts.indexerUrl ?? defaultChains.find((chain) => chain.id === chainId)?.indexerUrl; + console.log("Deploying from", client.account.address); // Attempt to enable automine for the duration of the deploy. Noop if automine is not available. @@ -153,6 +162,8 @@ export async function runDeploy(opts: DeployOptions): Promise { libraries, modules, artifacts, + indexerUrl, + chainId, }); if (opts.worldAddress == null || opts.alwaysRunPostDeploy) { await postDeploy( @@ -176,7 +187,6 @@ export async function runDeploy(opts: DeployOptions): Promise { }; if (opts.saveDeployment) { - const chainId = await getChainId(client); const deploysDir = path.join(config.deploy.deploysDirectory, chainId.toString()); mkdirSync(deploysDir, { recursive: true }); writeFileSync(path.join(deploysDir, "latest.json"), JSON.stringify(deploymentInfo, null, 2)); diff --git a/packages/explorer/src/app/(explorer)/api/world-abi/route.ts b/packages/explorer/src/app/(explorer)/api/world-abi/route.ts index 9c38daf4a0..81458f6d74 100644 --- a/packages/explorer/src/app/(explorer)/api/world-abi/route.ts +++ b/packages/explorer/src/app/(explorer)/api/world-abi/route.ts @@ -1,10 +1,10 @@ -import { Address, Hex, createClient, http, parseAbi } from "viem"; -import { getBlockNumber } from "viem/actions"; +import { Address, Hex, createClient, http, parseAbi, size } from "viem"; +import { getBlockNumber, getCode } from "viem/actions"; import { getAction } from "viem/utils"; import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; import { helloStoreEvent } from "@latticexyz/store"; +import { getWorldAbi } from "@latticexyz/store-sync/world"; import { helloWorldEvent } from "@latticexyz/world"; -import { getWorldAbi } from "@latticexyz/world/internal"; import { chainIdToName, supportedChainId, supportedChains, validateChainId } from "../../../../common"; export const dynamic = "force-dynamic"; @@ -19,6 +19,11 @@ async function getClient(chainId: supportedChainId) { return client; } +function getIndexerUrl(chainId: supportedChainId) { + const chain = supportedChains[chainIdToName[chainId]]; + return "indexerUrl" in chain ? chain.indexerUrl : undefined; +} + async function getParameters(chainId: supportedChainId, worldAddress: Address) { const client = await getClient(chainId); const toBlock = await getAction(client, getBlockNumber, "getBlockNumber")({}); @@ -50,12 +55,30 @@ export async function GET(req: Request) { try { const client = await getClient(chainId); + const indexerUrl = getIndexerUrl(chainId); + + if (indexerUrl) { + const [code, abi] = await Promise.all([ + getCode(client, { address: worldAddress }), + getWorldAbi({ + client, + worldAddress, + indexerUrl, + chainId, + }), + ]); + + return Response.json({ abi, isWorldDeployed: code && size(code) > 0 }); + } + const { fromBlock, toBlock, isWorldDeployed } = await getParameters(chainId, worldAddress); const abi = await getWorldAbi({ client, worldAddress, fromBlock, toBlock, + indexerUrl: getIndexerUrl(chainId), + chainId, }); return Response.json({ abi, isWorldDeployed }); diff --git a/packages/store-sync/package.json b/packages/store-sync/package.json index b484410fd2..bcc243ae2f 100644 --- a/packages/store-sync/package.json +++ b/packages/store-sync/package.json @@ -18,6 +18,7 @@ "./recs": "./dist/recs/index.js", "./sqlite": "./dist/sqlite/index.js", "./trpc-indexer": "./dist/trpc-indexer/index.js", + "./world": "./dist/world/index.js", "./zustand": "./dist/zustand/index.js" }, "typesVersions": { @@ -46,6 +47,9 @@ "trpc-indexer": [ "./dist/trpc-indexer/index.d.ts" ], + "world": [ + "./dist/world/index.d.ts" + ], "zustand": [ "./dist/zustand/index.d.ts" ] @@ -96,7 +100,6 @@ "@types/node": "20.12.12", "@types/sql.js": "^1.4.4", "@viem/anvil": "^0.0.7", - "mock-game-contracts": "workspace:*", "tsup": "^6.7.0", "vitest": "0.34.6" }, diff --git a/packages/store-sync/src/getRecords.ts b/packages/store-sync/src/getRecords.ts new file mode 100644 index 0000000000..640a49aa77 --- /dev/null +++ b/packages/store-sync/src/getRecords.ts @@ -0,0 +1,88 @@ +import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; +import { Table } from "@latticexyz/config"; +import { getSchemaPrimitives } from "@latticexyz/protocol-parser/internal"; +import { LogToRecordArgs, flattenStoreLogs, getStoreLogs, logToRecord } from "@latticexyz/store/internal"; +import { Address, Client, createPublicClient, http } from "viem"; +import { getAction } from "viem/utils"; +import { getBlockNumber } from "viem/actions"; +import { debug } from "./debug"; +import { getSnapshot } from "./getSnapshot"; +import { StorageAdapterLog } from "./common"; + +type GetRecordsOptions = { + table: table; + worldAddress: Address; + fromBlock?: bigint; + toBlock?: bigint; +} & ( + | { + indexerUrl: string; + chainId: number; + rpcUrl?: string; + client?: Client; + } + | { + indexerUrl?: string; + chainId?: number; + rpcUrl: string; + client?: Client; + } + | { + indexerUrl?: string; + chainId?: number; + rpcUrl?: string; + client: Client; + } +); + +type GetRecordsResult
= { + records: getSchemaPrimitives[]; + blockNumber: bigint; +}; + +export async function getRecords
( + options: GetRecordsOptions
, +): Promise> { + async function getLogs(): Promise { + if (options.indexerUrl && options.chainId) { + debug("fetching records for", options.table.label, "via indexer from", options.indexerUrl); + const logs = await getSnapshot({ + chainId: options.chainId, + address: options.worldAddress, + indexerUrl: options.indexerUrl, + filters: [{ tableId: options.table.tableId }], + }); + // By default, the indexer includes the `store.Tables` table as part of the snapshot. + // Once we change this default, we can remove the filter here. + // See https://github.com/latticexyz/mud/issues/3386. + return logs?.logs.filter((log) => log.args.tableId === options.table.tableId) ?? []; + } else { + const client = + options.client ?? + createPublicClient({ + transport: http(options.rpcUrl), + }); + debug("fetching records for", options.table.label, "via RPC from", client.transport.url); + const blockLogs = await fetchBlockLogs({ + fromBlock: options.fromBlock ?? 0n, + toBlock: options.toBlock ?? (await getAction(client, getBlockNumber, "getBlockNumber")({})), + maxBlockRange: 100_000n, + async getLogs({ fromBlock, toBlock }) { + return getStoreLogs(client, { + address: options.worldAddress, + fromBlock, + toBlock, + tableId: options.table.tableId, + }); + }, + }); + return flattenStoreLogs(blockLogs.flatMap((block) => block.logs)); + } + } + + const logs = await getLogs(); + const records = logs.map((log) => logToRecord({ log: log as LogToRecordArgs
["log"], table: options.table })); + const blockNumber = logs.length > 0 ? logs[logs.length - 1].blockNumber ?? 0n : 0n; + debug("found", records.length, "records for table", options.table.label, "at block", blockNumber); + return { records, blockNumber }; +} diff --git a/packages/store-sync/src/index.ts b/packages/store-sync/src/index.ts index e452f19a0d..7c68aeb447 100644 --- a/packages/store-sync/src/index.ts +++ b/packages/store-sync/src/index.ts @@ -7,5 +7,6 @@ export * from "./logToTable"; export * from "./tablesWithRecordsToLogs"; export * from "./tableToLog"; export * from "./recordToLog"; +export * from "./getRecords"; export { logToRecord, type LogToRecordArgs } from "@latticexyz/store/internal"; diff --git a/packages/store-sync/src/world/common.ts b/packages/store-sync/src/world/common.ts new file mode 100644 index 0000000000..7769b1bc36 --- /dev/null +++ b/packages/store-sync/src/world/common.ts @@ -0,0 +1,9 @@ +import { Hex } from "viem"; + +export type WorldFunction = { + readonly signature: string; + readonly selector: Hex; + readonly systemId: Hex; + readonly systemFunctionSignature: string; + readonly systemFunctionSelector: Hex; +}; diff --git a/packages/world/ts/functionSignatureToAbiItem.ts b/packages/store-sync/src/world/functionSignatureToAbiItem.ts similarity index 100% rename from packages/world/ts/functionSignatureToAbiItem.ts rename to packages/store-sync/src/world/functionSignatureToAbiItem.ts diff --git a/packages/store-sync/src/world/getFunctions.ts b/packages/store-sync/src/world/getFunctions.ts new file mode 100644 index 0000000000..4ae3fd8858 --- /dev/null +++ b/packages/store-sync/src/world/getFunctions.ts @@ -0,0 +1,65 @@ +import { Client, Address } from "viem"; +import worldConfig from "@latticexyz/world/mud.config"; +import { debug } from "../debug"; +import { WorldFunction } from "./common"; +import { getRecords } from "../getRecords"; + +export async function getFunctions({ + client, + worldAddress, + fromBlock, + toBlock, + indexerUrl, + chainId, +}: { + readonly client: Client; + readonly worldAddress: Address; + readonly fromBlock?: bigint; + readonly toBlock?: bigint; + readonly indexerUrl?: string; + readonly chainId?: number; +}): Promise { + // This assumes we only use `FunctionSelectors._set(...)`, which is true as of this writing. + debug("looking up function selectors for", worldAddress); + + const { records: selectors } = await getRecords({ + indexerUrl, + chainId, + client, + table: worldConfig.namespaces.world.tables.FunctionSelectors, + worldAddress, + fromBlock, + toBlock, + }); + + debug("found", selectors.length, "function selectors for", worldAddress); + + // This assumes we only use `FunctionSignatures._set(...)`, which is true as of this writing. + debug("looking up function signatures for", worldAddress); + + const { records: signatures } = await getRecords({ + indexerUrl, + chainId, + client, + table: worldConfig.namespaces.world.tables.FunctionSignatures, + worldAddress, + fromBlock, + toBlock, + }); + + const selectorToSignature = Object.fromEntries( + signatures.map((record) => [record.functionSelector, record.functionSignature]), + ); + + debug("found", signatures.length, "function signatures for", worldAddress); + + const functions = selectors.map(({ worldFunctionSelector, systemFunctionSelector, systemId }) => ({ + selector: worldFunctionSelector, + signature: selectorToSignature[worldFunctionSelector], + systemFunctionSelector, + systemFunctionSignature: selectorToSignature[systemFunctionSelector], + systemId, + })); + + return functions; +} diff --git a/packages/world/ts/getWorldAbi.test.ts b/packages/store-sync/src/world/getWorldAbi.test.ts similarity index 100% rename from packages/world/ts/getWorldAbi.test.ts rename to packages/store-sync/src/world/getWorldAbi.test.ts diff --git a/packages/world/ts/getWorldAbi.ts b/packages/store-sync/src/world/getWorldAbi.ts similarity index 79% rename from packages/world/ts/getWorldAbi.ts rename to packages/store-sync/src/world/getWorldAbi.ts index 919471b34a..a47386bd50 100644 --- a/packages/world/ts/getWorldAbi.ts +++ b/packages/store-sync/src/world/getWorldAbi.ts @@ -1,5 +1,5 @@ import { Client, Abi, AbiItem, AbiFunction, Address, getAddress, toFunctionSelector } from "viem"; -import IBaseWorldAbi from "../out/IBaseWorld.sol/IBaseWorld.abi.json"; +import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" }; import { functionSignatureToAbiItem } from "./functionSignatureToAbiItem"; import { getFunctions } from "./getFunctions"; import { isDefined } from "@latticexyz/common/utils"; @@ -13,17 +13,23 @@ export async function getWorldAbi({ worldAddress, fromBlock, toBlock, + indexerUrl, + chainId, }: { readonly client: Client; readonly worldAddress: Address; - readonly fromBlock: bigint; - readonly toBlock: bigint; + readonly fromBlock?: bigint; + readonly toBlock?: bigint; + readonly indexerUrl?: string; + readonly chainId?: number; }): Promise { const worldFunctions = await getFunctions({ client, worldAddress: getAddress(worldAddress), fromBlock, toBlock, + indexerUrl, + chainId, }); const baseFunctionSelectors = (IBaseWorldAbi as Abi).filter(isAbiFunction).map(toFunctionSelector); const worldFunctionsAbi = worldFunctions @@ -38,5 +44,5 @@ export async function getWorldAbi({ .filter((abiItem) => !baseFunctionSelectors.includes(toFunctionSelector(abiItem))); const abi = [...IBaseWorldAbi, ...worldFunctionsAbi]; - return abi; + return abi as never; } diff --git a/packages/store-sync/src/world/index.ts b/packages/store-sync/src/world/index.ts new file mode 100644 index 0000000000..33f768f9bd --- /dev/null +++ b/packages/store-sync/src/world/index.ts @@ -0,0 +1,2 @@ +export * from "./getFunctions"; +export * from "./getWorldAbi"; diff --git a/packages/store-sync/test/mockGame.ts b/packages/store-sync/test/mockGame.ts index 3cc49bbb44..4329b6178b 100644 --- a/packages/store-sync/test/mockGame.ts +++ b/packages/store-sync/test/mockGame.ts @@ -1,8 +1,8 @@ import { execa } from "execa"; import { anvilRpcUrl } from "./common"; import { Hex, isHex } from "viem"; -import config from "mock-game-contracts/mud.config"; -import worldAbi from "mock-game-contracts/out/IWorld.sol/IWorld.abi.json"; +import config from "../../../test/mock-game-contracts/mud.config"; +import worldAbi from "../../../test/mock-game-contracts/out/IWorld.sol/IWorld.abi.json"; export { config, worldAbi }; diff --git a/packages/store-sync/tsup.config.ts b/packages/store-sync/tsup.config.ts index 1fb070d548..18b1edd6ea 100644 --- a/packages/store-sync/tsup.config.ts +++ b/packages/store-sync/tsup.config.ts @@ -9,6 +9,7 @@ export default defineConfig((opts) => ({ "src/recs/index.ts", "src/trpc-indexer/index.ts", "src/indexer-client/index.ts", + "src/world/index.ts", "src/zustand/index.ts", "src/exports/internal.ts", ], diff --git a/packages/world/ts/exports/internal.ts b/packages/world/ts/exports/internal.ts index 786c9e2e58..8ea437a898 100644 --- a/packages/world/ts/exports/internal.ts +++ b/packages/world/ts/exports/internal.ts @@ -7,9 +7,6 @@ export * from "../actions/callFrom"; export * from "../callWithSignatureTypes"; -export * from "../getFunctions"; -export * from "../getWorldAbi"; - export * from "../config/v2/codegen"; export * from "../config/v2/defaults"; export * from "../config/v2/deploy"; diff --git a/packages/world/ts/getFunctions.ts b/packages/world/ts/getFunctions.ts deleted file mode 100644 index cbdb2c2eb8..0000000000 --- a/packages/world/ts/getFunctions.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Client, Address } from "viem"; -import { WorldFunction } from "./common"; -import { debug } from "./debug"; -import { - decodeKey, - decodeValueArgs, - getKeySchema, - getSchemaTypes, - getValueSchema, -} from "@latticexyz/protocol-parser/internal"; -import worldConfig from "../mud.config"; -import { fetchBlockLogs } from "@latticexyz/block-logs-stream"; -import { flattenStoreLogs, getStoreLogs } from "@latticexyz/store/internal"; - -export async function getFunctions({ - client, - worldAddress, - fromBlock, - toBlock, -}: { - readonly client: Client; - readonly worldAddress: Address; - readonly fromBlock: bigint; - readonly toBlock: bigint; -}): Promise { - // This assumes we only use `FunctionSelectors._set(...)`, which is true as of this writing. - debug("looking up function selectors for", worldAddress); - - const selectorBlocks = await fetchBlockLogs({ - fromBlock, - toBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldAddress, - fromBlock, - toBlock, - tableId: worldConfig.namespaces.world.tables.FunctionSelectors.tableId, - }); - }, - }); - const selectorLogs = flattenStoreLogs(selectorBlocks.flatMap((block) => block.logs)); - - const selectors = selectorLogs.map((log) => { - return { - ...decodeValueArgs( - getSchemaTypes(getValueSchema(worldConfig.namespaces.world.tables.FunctionSelectors)), - log.args, - ), - ...decodeKey( - getSchemaTypes(getKeySchema(worldConfig.namespaces.world.tables.FunctionSelectors)), - log.args.keyTuple, - ), - }; - }); - debug("found", selectors.length, "function selectors for", worldAddress); - - // This assumes we only use `FunctionSignatures._set(...)`, which is true as of this writing. - debug("looking up function signatures for", worldAddress); - - const signatureBlocks = await fetchBlockLogs({ - fromBlock, - toBlock, - maxBlockRange: 100_000n, - async getLogs({ fromBlock, toBlock }) { - return getStoreLogs(client, { - address: worldAddress, - fromBlock, - toBlock, - tableId: worldConfig.namespaces.world.tables.FunctionSignatures.tableId, - }); - }, - }); - const signatureLogs = flattenStoreLogs(signatureBlocks.flatMap((block) => block.logs)); - - const selectorToSignature = Object.fromEntries( - signatureLogs.map((log) => { - return [ - decodeKey( - getSchemaTypes(getKeySchema(worldConfig.namespaces.world.tables.FunctionSignatures)), - log.args.keyTuple, - ).functionSelector, - decodeValueArgs( - getSchemaTypes(getValueSchema(worldConfig.namespaces.world.tables.FunctionSignatures)), - log.args, - ).functionSignature, - ]; - }), - ); - debug("found", signatureLogs.length, "function signatures for", worldAddress); - - const functions = selectors.map(({ worldFunctionSelector, systemFunctionSelector, systemId }) => ({ - selector: worldFunctionSelector, - signature: selectorToSignature[worldFunctionSelector], - systemFunctionSelector, - systemFunctionSignature: selectorToSignature[systemFunctionSelector], - systemId, - })); - - return functions; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3db6ff8f2..f5203926f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: '@latticexyz/store': specifier: workspace:* version: link:../store + '@latticexyz/store-sync': + specifier: workspace:* + version: link:../store-sync '@latticexyz/utils': specifier: workspace:* version: link:../utils @@ -1225,9 +1228,6 @@ importers: '@viem/anvil': specifier: ^0.0.7 version: 0.0.7(bufferutil@4.0.8)(debug@4.3.4)(utf-8-validate@5.0.10) - mock-game-contracts: - specifier: workspace:* - version: link:../../test/mock-game-contracts tsup: specifier: ^6.7.0 version: 6.7.0(postcss@8.4.47)(typescript@5.4.2)