From 51cb207551d3dd2f74d14e3602476978bfae2135 Mon Sep 17 00:00:00 2001 From: Kevin Ingersoll Date: Mon, 3 Apr 2023 16:15:10 -0700 Subject: [PATCH] feat(network,recs,std-client): support StoreSetField before StoreSetRecord closes #479 closes #523 --- packages/network/src/types.ts | 1 + .../network/src/v2/decodeStoreSetField.ts | 33 +++++++++++++++++-- .../network/src/v2/decodeStoreSetRecord.ts | 3 ++ packages/network/src/v2/ecsEventFromLog.ts | 14 ++------ .../src/v2/schemas/decodeDynamicField.ts | 6 ++-- .../src/v2/schemas/decodeStaticField.spec.ts | 20 +++++++++++ .../src/v2/schemas/decodeStaticField.ts | 17 +++++----- packages/network/src/workers/CacheStore.ts | 20 +++++------ packages/recs/src/Component.ts | 14 ++++++-- packages/std-client/src/setup/utils.ts | 10 +----- 10 files changed, 89 insertions(+), 49 deletions(-) diff --git a/packages/network/src/types.ts b/packages/network/src/types.ts index 8e1ef0750b..ea5b9d6abf 100644 --- a/packages/network/src/types.ts +++ b/packages/network/src/types.ts @@ -87,6 +87,7 @@ export type NetworkComponentUpdate = { component: key & string; value: ComponentValue> | undefined; partialValue?: Partial>>; + initialValue?: ComponentValue>; }; }[keyof C] & { entity: EntityID; diff --git a/packages/network/src/v2/decodeStoreSetField.ts b/packages/network/src/v2/decodeStoreSetField.ts index 93cc0bd1c2..4c191b1319 100644 --- a/packages/network/src/v2/decodeStoreSetField.ts +++ b/packages/network/src/v2/decodeStoreSetField.ts @@ -1,10 +1,13 @@ -import { ComponentValue } from "@latticexyz/recs"; +import { ComponentValue, Schema } from "@latticexyz/recs"; import { TableId } from "@latticexyz/utils"; import { Contract } from "ethers"; import { registerSchema } from "./schemas/tableSchemas"; import { registerMetadata } from "./schemas/tableMetadata"; import { decodeField } from "./schemas/decodeField"; import { TableSchema } from "./common"; +import { decodeStaticField } from "./schemas/decodeStaticField"; +import { DynamicSchemaType, StaticSchemaType } from "@latticexyz/schema-type"; +import { decodeDynamicField } from "./schemas/decodeDynamicField"; export async function decodeStoreSetField( contract: Contract, @@ -12,21 +15,45 @@ export async function decodeStoreSetField( keyTuple: string[], schemaIndex: number, data: string -): Promise<{ schema: TableSchema; value: ComponentValue }> { +): Promise<{ schema: TableSchema; value: ComponentValue; initialValue: ComponentValue }> { const schema = await registerSchema(contract, table); const value = decodeField(schema, schemaIndex, data); + // Create an object that represents an "uninitialized" record as it would exist in Solidity + // to help populate RECS state when using StoreSetField before StoreSetRecord. + const defaultValues = [ + ...schema.staticFields.map((fieldType) => decodeStaticField(fieldType as StaticSchemaType, new Uint8Array(0), 0)), + ...schema.dynamicFields.map((fieldType) => decodeDynamicField(fieldType as DynamicSchemaType, new Uint8Array(0))), + ]; + const initialValue = Object.fromEntries(defaultValues.map((value, index) => [index, value])) as ComponentValue; + const metadata = await registerMetadata(contract, table); if (metadata) { const { tableName, fieldNames } = metadata; + const initialValueWithNames = Object.fromEntries( + defaultValues.map((fieldValue, schemaIndex) => { + return [fieldNames[schemaIndex], fieldValue]; + }) + ) as ComponentValue; return { schema, value: { ...value, [fieldNames[schemaIndex]]: value[schemaIndex], }, + initialValue: { + ...initialValue, + ...initialValueWithNames, + }, }; } - return { schema, value }; + console.warn( + `Received data for ${table.toString()}, but could not find table metadata for field names. Did your contracts get autogenerated and deployed properly?` + ); + return { + schema, + value, + initialValue, + }; } diff --git a/packages/network/src/v2/decodeStoreSetRecord.ts b/packages/network/src/v2/decodeStoreSetRecord.ts index d186cfe37e..845aa50985 100644 --- a/packages/network/src/v2/decodeStoreSetRecord.ts +++ b/packages/network/src/v2/decodeStoreSetRecord.ts @@ -53,5 +53,8 @@ export async function decodeStoreSetRecord( }; } + console.warn( + `Received data for ${table.toString()}, but could not find table metadata for field names. Did your contracts get autogenerated and deployed properly?` + ); return decoded; } diff --git a/packages/network/src/v2/ecsEventFromLog.ts b/packages/network/src/v2/ecsEventFromLog.ts index f2954eff47..4307f8090c 100644 --- a/packages/network/src/v2/ecsEventFromLog.ts +++ b/packages/network/src/v2/ecsEventFromLog.ts @@ -41,23 +41,13 @@ export const ecsEventFromLog = async ( } if (name === "StoreSetField") { - const { schema, value } = await decodeStoreSetField(contract, tableId, args.key, args.schemaIndex, args.data); + const { value, initialValue } = await decodeStoreSetField(contract, tableId, args.key, args.schemaIndex, args.data); console.log("StoreSetField:", { table: tableId.toString(), component, entity, value }); - // workaround for https://github.com/latticexyz/mud/issues/479 - // TODO: figure out if this is the approach we want to take - const keysToUpdate = Object.keys(value); - const expectedKeys = [...schema.staticFields, ...schema.dynamicFields].map((type, index) => `${index}`); - if (expectedKeys.every((key) => keysToUpdate.includes(key))) { - return { - ...ecsEvent, - value, - }; - } - return { ...ecsEvent, partialValue: value, + initialValue, }; } diff --git a/packages/network/src/v2/schemas/decodeDynamicField.ts b/packages/network/src/v2/schemas/decodeDynamicField.ts index 93fa417090..d3356579ad 100644 --- a/packages/network/src/v2/schemas/decodeDynamicField.ts +++ b/packages/network/src/v2/schemas/decodeDynamicField.ts @@ -1,5 +1,5 @@ import { SchemaType, DynamicSchemaType, SchemaTypeArrayToElement, getStaticByteLength } from "@latticexyz/schema-type"; -import { arrayToHex } from "@latticexyz/utils"; +import { toHex, bytesToString } from "viem"; import { decodeStaticField } from "./decodeStaticField"; // TODO: figure out how to switch back to `fieldType: never` for exhaustiveness check @@ -10,10 +10,10 @@ const unsupportedDynamicField = (fieldType: SchemaType): never => { // TODO: figure out how to use with SchemaTypeToPrimitive return type to ensure correctness here export const decodeDynamicField = (fieldType: T, bytes: Uint8Array) => { if (fieldType === SchemaType.BYTES) { - return arrayToHex(bytes); + return toHex(bytes); } if (fieldType === SchemaType.STRING) { - return new TextDecoder().decode(bytes); + return bytesToString(bytes); } const staticType = SchemaTypeArrayToElement[fieldType]; diff --git a/packages/network/src/v2/schemas/decodeStaticField.spec.ts b/packages/network/src/v2/schemas/decodeStaticField.spec.ts index 76e41ca311..7e033232c2 100644 --- a/packages/network/src/v2/schemas/decodeStaticField.spec.ts +++ b/packages/network/src/v2/schemas/decodeStaticField.spec.ts @@ -17,6 +17,9 @@ describe("decodeStaticField", () => { expect(decodeStaticField(SchemaType.BOOL, new Uint8Array(buffer, 0, 1), 0)).toEqual(false); expect(decodeStaticField(SchemaType.BOOL, new Uint8Array(buffer, 1, 1), 0)).toEqual(true); }); + it("should decode empty array", () => { + expect(decodeStaticField(SchemaType.BOOL, new Uint8Array(0), 0)).toEqual(false); + }); }); describe("SchemaType.UINT256", () => { @@ -24,6 +27,17 @@ describe("decodeStaticField", () => { it("should decode with no offset", () => { expect(decodeStaticField(SchemaType.UINT256, bytes, 0)).toEqual(9314668n); }); + it("should decode empty array", () => { + expect(decodeStaticField(SchemaType.UINT256, new Uint8Array(0), 0)).toEqual(0n); + }); + }); + + describe("SchemaType.ADDRESS", () => { + it("should decode empty array", () => { + expect(decodeStaticField(SchemaType.ADDRESS, new Uint8Array(0), 0)).toEqual( + "0x0000000000000000000000000000000000000000" + ); + }); }); describe("SchemaType.INT8", () => { @@ -33,6 +47,9 @@ describe("decodeStaticField", () => { it("should decode type(int8).min", () => { expect(decodeStaticField(SchemaType.INT8, hexToArray("0x80"), 0)).toEqual(-128); }); + it("should decode empty array", () => { + expect(decodeStaticField(SchemaType.INT8, new Uint8Array(0), 0)).toEqual(0); + }); }); describe("SchemaType.INT48", () => { @@ -59,5 +76,8 @@ describe("decodeStaticField", () => { it("should decode with offset", () => { expect(decodeStaticField(SchemaType.BYTES2, bytes, 2)).toEqual("0x4567"); }); + it("should decode empty array", () => { + expect(decodeStaticField(SchemaType.BYTES2, new Uint8Array(0), 2)).toEqual("0x0000"); + }); }); }); diff --git a/packages/network/src/v2/schemas/decodeStaticField.ts b/packages/network/src/v2/schemas/decodeStaticField.ts index c74def0aac..30f81fd607 100644 --- a/packages/network/src/v2/schemas/decodeStaticField.ts +++ b/packages/network/src/v2/schemas/decodeStaticField.ts @@ -1,5 +1,5 @@ import { getStaticByteLength, SchemaType, StaticSchemaType } from "@latticexyz/schema-type"; -import { arrayToHex } from "@latticexyz/utils"; +import { toHex, pad } from "viem"; const unsupportedStaticField = (fieldType: never): never => { throw new Error(`Unsupported static field type: ${SchemaType[fieldType] ?? fieldType}`); @@ -9,18 +9,19 @@ const unsupportedStaticField = (fieldType: never): never => { export const decodeStaticField = (fieldType: T, bytes: Uint8Array, offset: number) => { const staticLength = getStaticByteLength(fieldType); const slice = bytes.slice(offset, offset + staticLength); - const hex = arrayToHex(slice); + const hex = toHex(slice); + const numberHex = hex.replace(/^0x$/, "0x0"); switch (fieldType) { case SchemaType.BOOL: - return Number(hex) !== 0; + return Number(numberHex) !== 0; case SchemaType.UINT8: case SchemaType.UINT16: case SchemaType.UINT24: case SchemaType.UINT32: case SchemaType.UINT40: case SchemaType.UINT48: - return Number(hex); + return Number(numberHex); case SchemaType.UINT56: case SchemaType.UINT64: case SchemaType.UINT72: @@ -47,7 +48,7 @@ export const decodeStaticField = (fieldType: T, byte case SchemaType.UINT240: case SchemaType.UINT248: case SchemaType.UINT256: - return BigInt(hex); + return BigInt(numberHex); case SchemaType.INT8: case SchemaType.INT16: case SchemaType.INT24: @@ -55,7 +56,7 @@ export const decodeStaticField = (fieldType: T, byte case SchemaType.INT40: case SchemaType.INT48: { const max = 2 ** (staticLength * 8); - const num = Number(hex); + const num = Number(numberHex); return num < max / 2 ? num : num - max; } case SchemaType.INT56: @@ -85,7 +86,7 @@ export const decodeStaticField = (fieldType: T, byte case SchemaType.INT248: case SchemaType.INT256: { const max = 2n ** (BigInt(staticLength) * 8n); - const num = BigInt(hex); + const num = BigInt(numberHex); return num < max / 2n ? num : num - max; } case SchemaType.BYTES1: @@ -121,7 +122,7 @@ export const decodeStaticField = (fieldType: T, byte case SchemaType.BYTES31: case SchemaType.BYTES32: case SchemaType.ADDRESS: - return hex; + return pad(hex, { dir: "right", size: staticLength }); default: return unsupportedStaticField(fieldType); } diff --git a/packages/network/src/workers/CacheStore.ts b/packages/network/src/workers/CacheStore.ts index ebded3a258..ba9542dcac 100644 --- a/packages/network/src/workers/CacheStore.ts +++ b/packages/network/src/workers/CacheStore.ts @@ -29,7 +29,14 @@ export function createCacheStore() { export function storeEvent( cacheStore: CacheStore, - { component, entity, value, partialValue, blockNumber }: Omit, "lastEventInTx" | "txHash"> + { + component, + entity, + value, + partialValue, + initialValue, + blockNumber, + }: Omit, "lastEventInTx" | "txHash"> ) { const entityId = normalizeEntityID(entity); @@ -55,16 +62,7 @@ export function storeEvent( // keep this logic aligned with applyNetworkUpdates if (partialValue !== undefined) { const currentValue = state.get(key); - if (currentValue === undefined) { - console.warn("Can't make partial update on unset component value. Ignoring update.", { - component, - entity, - entityIndex, - partialValue, - }); - } else { - state.set(key, { ...currentValue, ...partialValue }); - } + state.set(key, { ...initialValue, ...currentValue, ...partialValue }); } else if (value === undefined) { console.log("deleting key", key); state.delete(key); diff --git a/packages/recs/src/Component.ts b/packages/recs/src/Component.ts index 469016ddb4..4e8615ee47 100644 --- a/packages/recs/src/Component.ts +++ b/packages/recs/src/Component.ts @@ -121,10 +121,18 @@ export function setComponent( export function updateComponent( component: Component, entity: EntityIndex, - value: Partial> + value: Partial>, + initialValue?: ComponentValue ) { - const currentValue = getComponentValueStrict(component, entity); - setComponent(component, entity, { ...currentValue, ...value }); + const currentValue = getComponentValue(component, entity); + if (currentValue === undefined) { + if (initialValue === undefined) { + throw new Error("Can't update component without a current value or initial value"); + } + setComponent(component, entity, { ...initialValue, ...value }); + } else { + setComponent(component, entity, { ...currentValue, ...value }); + } } /** diff --git a/packages/std-client/src/setup/utils.ts b/packages/std-client/src/setup/utils.ts index 069a2d289f..88ba8b649b 100644 --- a/packages/std-client/src/setup/utils.ts +++ b/packages/std-client/src/setup/utils.ts @@ -168,15 +168,7 @@ export function applyNetworkUpdates( // keep this logic aligned with CacheStore's storeEvent if (update.partialValue !== undefined) { - if (!getComponentValue(component, entityIndex)) { - console.warn("Can't make partial update on unset component value. Ignoring update.", { - componentMetadata: component.metadata, - entityIndex, - update, - }); - } else { - updateComponent(component, entityIndex, update.partialValue); - } + updateComponent(component, entityIndex, update.partialValue, update.initialValue); } else if (update.value === undefined) { // undefined value means component removed removeComponent(component, entityIndex);