diff --git a/.changeset/wicked-cheetahs-cough.md b/.changeset/wicked-cheetahs-cough.md new file mode 100644 index 0000000000..acaced9bfb --- /dev/null +++ b/.changeset/wicked-cheetahs-cough.md @@ -0,0 +1,5 @@ +--- +"@latticexyz/protocol-parser": patch +--- + +Allow arbitrary key order when encoding values diff --git a/packages/protocol-parser/src/encodeLengths.test.ts b/packages/protocol-parser/src/encodeLengths.test.ts index c55204ddc6..70d6b53187 100644 --- a/packages/protocol-parser/src/encodeLengths.test.ts +++ b/packages/protocol-parser/src/encodeLengths.test.ts @@ -2,6 +2,12 @@ import { describe, expect, it } from "vitest"; import { encodeLengths } from "./encodeLengths"; describe("encodeLengths", () => { + it("can encode empty tuple", () => { + expect(encodeLengths([])).toMatchInlineSnapshot( + '"0x0000000000000000000000000000000000000000000000000000000000000000"' + ); + }); + it("can encode bool key tuple", () => { expect(encodeLengths(["0x1234", "0x12345678"])).toMatchInlineSnapshot( '"0x0000000000000000000000000000000000000004000000000200000000000006"' diff --git a/packages/protocol-parser/src/encodeValueArgs.test.ts b/packages/protocol-parser/src/encodeValueArgs.test.ts new file mode 100644 index 0000000000..67cf373922 --- /dev/null +++ b/packages/protocol-parser/src/encodeValueArgs.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { encodeValueArgs } from "./encodeValueArgs"; +import { stringToHex } from "viem"; + +describe("encodeValueArgs", () => { + it("can encode record value to hex", () => { + const valueSchema = { + entityId: "bytes32", + exists: "bool", + playerName: "string", + badges: "uint256[]", + } as const; + + const result = encodeValueArgs(valueSchema, { + entityId: stringToHex("hello", { size: 32 }), + exists: true, + playerName: "henry", + badges: [42n], + }); + + expect(result).toMatchInlineSnapshot(` + { + "dynamicData": "0x68656e7279000000000000000000000000000000000000000000000000000000000000002a", + "encodedLengths": "0x0000000000000000000000000000000000000020000000000500000000000025", + "staticData": "0x68656c6c6f00000000000000000000000000000000000000000000000000000001", + } + `); + }); + + it("encodes record when key order of value and valueSchema do not match", () => { + const valueSchema = { + entityId: "bytes32", + playerName: "string", + exists: "bool", + badges: "uint256[]", + } as const; + + const result = encodeValueArgs(valueSchema, { + exists: true, + playerName: "henry", + entityId: stringToHex("hello", { size: 32 }), + badges: [42n], + }); + + expect(result).toMatchInlineSnapshot(` + { + "dynamicData": "0x68656e7279000000000000000000000000000000000000000000000000000000000000002a", + "encodedLengths": "0x0000000000000000000000000000000000000020000000000500000000000025", + "staticData": "0x68656c6c6f00000000000000000000000000000000000000000000000000000001", + } + `); + }); +}); diff --git a/packages/protocol-parser/src/encodeValueArgs.ts b/packages/protocol-parser/src/encodeValueArgs.ts index 0f58a01b8a..5100f4f753 100644 --- a/packages/protocol-parser/src/encodeValueArgs.ts +++ b/packages/protocol-parser/src/encodeValueArgs.ts @@ -1,4 +1,11 @@ -import { StaticPrimitiveType, DynamicPrimitiveType, isStaticAbiType, isDynamicAbiType } from "@latticexyz/schema-type"; +import { + StaticPrimitiveType, + DynamicPrimitiveType, + isStaticAbiType, + isDynamicAbiType, + StaticAbiType, + DynamicAbiType, +} from "@latticexyz/schema-type"; import { concatHex } from "viem"; import { encodeField } from "./encodeField"; import { SchemaToPrimitives, ValueArgs, ValueSchema } from "./common"; @@ -8,15 +15,17 @@ export function encodeValueArgs( valueSchema: TSchema, value: SchemaToPrimitives ): ValueArgs { - const staticFields = Object.values(valueSchema).filter(isStaticAbiType); - const dynamicFields = Object.values(valueSchema).filter(isDynamicAbiType); + const valueSchemaEntries = Object.entries(valueSchema); + const staticFields = valueSchemaEntries.filter(([, type]) => isStaticAbiType(type)) as [string, StaticAbiType][]; + const dynamicFields = valueSchemaEntries.filter(([, type]) => isDynamicAbiType(type)) as [string, DynamicAbiType][]; + // TODO: validate <=5 dynamic fields + // TODO: validate <=28 total fields - const values = Object.values(value); - const staticValues = values.slice(0, staticFields.length) as readonly StaticPrimitiveType[]; - const dynamicValues = values.slice(staticFields.length) as readonly DynamicPrimitiveType[]; + const encodedStaticValues = staticFields.map(([name, type]) => encodeField(type, value[name] as StaticPrimitiveType)); + const encodedDynamicValues = dynamicFields.map(([name, type]) => + encodeField(type, value[name] as DynamicPrimitiveType) + ); - const encodedStaticValues = staticValues.map((value, i) => encodeField(staticFields[i], value)); - const encodedDynamicValues = dynamicValues.map((value, i) => encodeField(dynamicFields[i], value)); const encodedLengths = encodeLengths(encodedDynamicValues); return {