Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(network,recs,std-client): support StoreSetField before StoreSetRecord #581

Merged
merged 1 commit into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/network/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export type NetworkComponentUpdate<C extends Components = Components> = {
component: key & string;
value: ComponentValue<SchemaOf<C[key]>> | undefined;
partialValue?: Partial<ComponentValue<SchemaOf<C[key]>>>;
initialValue?: ComponentValue<SchemaOf<C[key]>>;
};
}[keyof C] & {
entity: EntityID;
Expand Down
33 changes: 30 additions & 3 deletions packages/network/src/v2/decodeStoreSetField.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,59 @@
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,
table: TableId,
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,
};
}
3 changes: 3 additions & 0 deletions packages/network/src/v2/decodeStoreSetRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
14 changes: 2 additions & 12 deletions packages/network/src/v2/ecsEventFromLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}

Expand Down
6 changes: 3 additions & 3 deletions packages/network/src/v2/schemas/decodeDynamicField.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,10 +10,10 @@ const unsupportedDynamicField = (fieldType: SchemaType): never => {
// TODO: figure out how to use with SchemaTypeToPrimitive<T> return type to ensure correctness here
export const decodeDynamicField = <T extends DynamicSchemaType>(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];
Expand Down
20 changes: 20 additions & 0 deletions packages/network/src/v2/schemas/decodeStaticField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,27 @@ 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", () => {
const bytes = hexToArray("0x00000000000000000000000000000000000000000000000000000000008e216c");
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", () => {
Expand All @@ -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", () => {
Expand All @@ -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");
});
});
});
17 changes: 9 additions & 8 deletions packages/network/src/v2/schemas/decodeStaticField.ts
Original file line number Diff line number Diff line change
@@ -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}`);
Expand All @@ -9,18 +9,19 @@ const unsupportedStaticField = (fieldType: never): never => {
export const decodeStaticField = <T extends StaticSchemaType>(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:
Expand All @@ -47,15 +48,15 @@ export const decodeStaticField = <T extends StaticSchemaType>(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:
case SchemaType.INT32:
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:
Expand Down Expand Up @@ -85,7 +86,7 @@ export const decodeStaticField = <T extends StaticSchemaType>(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:
Expand Down Expand Up @@ -121,7 +122,7 @@ export const decodeStaticField = <T extends StaticSchemaType>(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);
}
Expand Down
20 changes: 9 additions & 11 deletions packages/network/src/workers/CacheStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,14 @@ export function createCacheStore() {

export function storeEvent<Cm extends Components>(
cacheStore: CacheStore,
{ component, entity, value, partialValue, blockNumber }: Omit<NetworkComponentUpdate<Cm>, "lastEventInTx" | "txHash">
{
component,
entity,
value,
partialValue,
initialValue,
blockNumber,
}: Omit<NetworkComponentUpdate<Cm>, "lastEventInTx" | "txHash">
) {
const entityId = normalizeEntityID(entity);

Expand All @@ -55,16 +62,7 @@ export function storeEvent<Cm extends Components>(
// 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);
Expand Down
14 changes: 11 additions & 3 deletions packages/recs/src/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,18 @@ export function setComponent<S extends Schema, T = undefined>(
export function updateComponent<S extends Schema, T = undefined>(
component: Component<S, Metadata, T>,
entity: EntityIndex,
value: Partial<ComponentValue<S, T>>
value: Partial<ComponentValue<S, T>>,
initialValue?: ComponentValue<S, T>
) {
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 });
}
}

/**
Expand Down
10 changes: 1 addition & 9 deletions packages/std-client/src/setup/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,7 @@ export function applyNetworkUpdates<C extends Components>(

// 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);
Expand Down