-
Notifications
You must be signed in to change notification settings - Fork 196
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(protocol-parser): add keySchema/valueSchema helpers #1443
Changes from all commits
aaec669
66d92fd
6d3a4b3
075a8ec
d7b6ef0
f2be76b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@latticexyz/store": minor | ||
--- | ||
|
||
Moved `KeySchema`, `ValueSchema`, `SchemaToPrimitives` and `TableRecord` types into `@latticexyz/protocol-parser` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@latticexyz/protocol-parser": minor | ||
--- | ||
|
||
Adds `decodeKey`, `decodeValue`, `encodeKey`, and `encodeValue` helpers to decode/encode from key/value schemas. Deprecates previous methods that use a schema object with static/dynamic field arrays, originally attempting to model our on-chain behavior but ended up not very ergonomic when working with table configs. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,26 @@ | ||
import { DynamicAbiType, StaticAbiType } from "@latticexyz/schema-type"; | ||
import { DynamicAbiType, SchemaAbiType, SchemaAbiTypeToPrimitiveType, StaticAbiType } from "@latticexyz/schema-type"; | ||
|
||
/** @deprecated use `KeySchema` or `ValueSchema` instead */ | ||
export type Schema = { | ||
readonly staticFields: readonly StaticAbiType[]; | ||
readonly dynamicFields: readonly DynamicAbiType[]; | ||
}; | ||
|
||
/** @deprecated use `KeySchema` and `ValueSchema` instead */ | ||
export type TableSchema = { | ||
readonly keySchema: Schema; // TODO: refine to set dynamicFields to [] | ||
readonly valueSchema: Schema; | ||
}; | ||
|
||
export type KeySchema = Record<string, StaticAbiType>; | ||
export type ValueSchema = Record<string, SchemaAbiType>; | ||
|
||
/** Map a table schema like `{ value: "uint256" }` to its primitive types like `{ value: bigint }` */ | ||
export type SchemaToPrimitives<TSchema extends ValueSchema> = { | ||
[key in keyof TSchema]: SchemaAbiTypeToPrimitiveType<TSchema[key]>; | ||
}; | ||
|
||
export type TableRecord<TKeySchema extends KeySchema = KeySchema, TValueSchema extends ValueSchema = ValueSchema> = { | ||
key: SchemaToPrimitives<TKeySchema>; | ||
value: SchemaToPrimitives<TValueSchema>; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Hex } from "viem"; | ||
import { SchemaToPrimitives, KeySchema } from "./common"; | ||
import { decodeKeyTuple } from "./decodeKeyTuple"; | ||
|
||
export function decodeKey<TSchema extends KeySchema>( | ||
keySchema: TSchema, | ||
data: readonly Hex[] | ||
): SchemaToPrimitives<TSchema> { | ||
// TODO: refactor and move all decodeKeyTuple logic into this method so we can delete decodeKeyTuple | ||
const keyValues = decodeKeyTuple({ staticFields: Object.values(keySchema), dynamicFields: [] }, data); | ||
|
||
return Object.fromEntries( | ||
Object.keys(keySchema).map((name, i) => [name, keyValues[i]]) | ||
) as SchemaToPrimitives<TSchema>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,4 +21,16 @@ describe("decodeRecord", () => { | |
] | ||
`); | ||
}); | ||
|
||
it("can decode an out of bounds array", () => { | ||
const schema = { staticFields: [], dynamicFields: ["uint32[]"] } as const; | ||
const values = decodeRecord(schema, "0x0000000000000000000000000000000000000000000000000400000000000004"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The hex represents an encoded dynamic length field with total length 4 and length of the first array of 4, correct? Can you add more context for why the decoded array has one element with value 0? Somehow I would have expected either an empty because there is no data after the encoded data length, or an array with 4 elements that are all There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I had added this test for proof that To explain what's going on: The total length and first array length is the byte length (4 bytes in the case of The record hex is "trimmed" to the right-most byte (end of the counter). On chain, any bytes after the right-most byte would be treated as zeros, so we want to have the same behavior in the client. So if we were to read the same value on chain, we should get a value of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh makes sense, had forgotten that the length corresponds to bytes and assumed elements. Thanks for the explanation! |
||
expect(values).toMatchInlineSnapshot(` | ||
[ | ||
[ | ||
0, | ||
], | ||
] | ||
`); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { isStaticAbiType, isDynamicAbiType } from "@latticexyz/schema-type"; | ||
import { Hex } from "viem"; | ||
import { SchemaToPrimitives, ValueSchema } from "./common"; | ||
import { decodeRecord } from "./decodeRecord"; | ||
|
||
export function decodeValue<TSchema extends ValueSchema>(valueSchema: TSchema, data: Hex): SchemaToPrimitives<TSchema> { | ||
const staticFields = Object.values(valueSchema).filter(isStaticAbiType); | ||
const dynamicFields = Object.values(valueSchema).filter(isDynamicAbiType); | ||
|
||
// TODO: refactor and move all decodeRecord logic into this method so we can delete decodeRecord | ||
const valueTuple = decodeRecord({ staticFields, dynamicFields }, data); | ||
|
||
return Object.fromEntries( | ||
Object.keys(valueSchema).map((name, i) => [name, valueTuple[i]]) | ||
) as SchemaToPrimitives<TSchema>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,10 +8,13 @@ export function encodeField<TSchemaAbiType extends SchemaAbiType>( | |
): Hex { | ||
if (isArrayAbiType(fieldType) && Array.isArray(value)) { | ||
const staticFieldType = arrayAbiTypeToStaticAbiType(fieldType); | ||
return encodePacked( | ||
value.map(() => staticFieldType), | ||
value | ||
); | ||
// TODO: we can remove conditional once this is fixed: https://github.com/wagmi-dev/viem/pull/1147 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems like it got merged already! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep! we'll need to bump viem to get this change so gonna leave this here for now |
||
return value.length === 0 | ||
? "0x" | ||
: encodePacked( | ||
value.map(() => staticFieldType), | ||
value | ||
); | ||
} | ||
return encodePacked([fieldType], [value]); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { isStaticAbiType } from "@latticexyz/schema-type"; | ||
import { Hex } from "viem"; | ||
import { SchemaToPrimitives, KeySchema } from "./common"; | ||
import { encodeKeyTuple } from "./encodeKeyTuple"; | ||
|
||
export function encodeKey<TSchema extends KeySchema>(keySchema: TSchema, key: SchemaToPrimitives<TSchema>): Hex[] { | ||
const staticFields = Object.values(keySchema).filter(isStaticAbiType); | ||
// TODO: refactor and move all encodeKeyTuple logic into this method so we can delete encodeKeyTuple | ||
return encodeKeyTuple({ staticFields, dynamicFields: [] }, Object.values(key)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { isStaticAbiType, isDynamicAbiType } from "@latticexyz/schema-type"; | ||
import { Hex } from "viem"; | ||
import { SchemaToPrimitives, ValueSchema } from "./common"; | ||
import { encodeRecord } from "./encodeRecord"; | ||
|
||
export function encodeValue<TSchema extends ValueSchema>( | ||
valueSchema: TSchema, | ||
value: SchemaToPrimitives<TSchema> | ||
): Hex { | ||
const staticFields = Object.values(valueSchema).filter(isStaticAbiType); | ||
const dynamicFields = Object.values(valueSchema).filter(isDynamicAbiType); | ||
|
||
// TODO: refactor and move all encodeRecord logic into this method so we can delete encodeRecord | ||
|
||
// This currently assumes fields/values are ordered by static, dynamic | ||
// TODO: make sure we preserve ordering based on schema definition | ||
return encodeRecord({ staticFields, dynamicFields }, Object.values(value)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { isStaticAbiType } from "@latticexyz/schema-type"; | ||
import { Hex } from "viem"; | ||
import { KeySchema } from "./common"; | ||
import { schemaToHex } from "./schemaToHex"; | ||
|
||
export function keySchemaToHex(schema: KeySchema): Hex { | ||
return schemaToHex({ staticFields: Object.values(schema).filter(isStaticAbiType), dynamicFields: [] }); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { describe, expect, it } from "vitest"; | ||
import { readHex } from "./readHex"; | ||
|
||
describe("readHex", () => { | ||
it("can slice empty hex", () => { | ||
expect(readHex("0x", 6)).toBe("0x"); | ||
expect(readHex("0x", 6, 10)).toBe("0x00000000"); | ||
}); | ||
it("can slice hex out of bounds", () => { | ||
expect(readHex("0x000100", 1)).toBe("0x0100"); | ||
expect(readHex("0x000100", 1, 4)).toBe("0x010000"); | ||
expect(readHex("0x000100", 3)).toBe("0x"); | ||
expect(readHex("0x000100", 3, 4)).toBe("0x00"); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Hex } from "viem"; | ||
|
||
/** | ||
* Get the hex value at start/end positions. This will always return a valid hex string. | ||
* | ||
* If `start` is out of range, this returns `"0x"`. | ||
* | ||
* If `end` is specified and out of range, the result is right zero-padded to the desired length (`end - start`). | ||
*/ | ||
export function readHex(data: Hex, start: number, end?: number): Hex { | ||
return `0x${data | ||
.replace(/^0x/, "") | ||
.slice(start * 2, end != null ? end * 2 : undefined) | ||
.padEnd(((end ?? start) - start) * 2, "0")}`; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { isDynamicAbiType, isStaticAbiType } from "@latticexyz/schema-type"; | ||
import { Hex } from "viem"; | ||
import { ValueSchema } from "./common"; | ||
import { schemaToHex } from "./schemaToHex"; | ||
|
||
export function valueSchemaToHex(schema: ValueSchema): Hex { | ||
return schemaToHex({ | ||
staticFields: Object.values(schema).filter(isStaticAbiType), | ||
dynamicFields: Object.values(schema).filter(isDynamicAbiType), | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is
decodeKeyTuple
still used somewhere else? If not, should we just move the logic in there in this PR?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's used by
blockLogsToStorage
which is getting refactored in the other PRdidn't wanna refactor everything quite yet to avoid a big rebase of the other PR, just wanted to add new methods and deprecate old ones for an easy first pass
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated the original issue to remind me to refactor this: #1296