Skip to content

Commit

Permalink
feat(store,world): use user-types for ResourceId, FieldLayout and…
Browse files Browse the repository at this point in the history
… `Schema` in table libraries (#1586)
  • Loading branch information
alvrs committed Sep 24, 2023
1 parent 24a6cd5 commit 22ee447
Show file tree
Hide file tree
Showing 57 changed files with 1,709 additions and 1,568 deletions.
7 changes: 7 additions & 0 deletions .changeset/seven-mangos-roll.md
@@ -0,0 +1,7 @@
---
"@latticexyz/store-sync": patch
"@latticexyz/store": patch
"@latticexyz/world": patch
---

All `Store` and `World` tables now use the appropriate user-types for `ResourceId`, `FieldLayout` and `Schema` to avoid manual `wrap`/`unwrap`.
Expand Up @@ -19,7 +19,7 @@ contract ChatNamespacedTest is MudTest {
MessageTableTableId,
keyTuple,
new bytes(0),
MessageTable.encodeLengths(value).unwrap(),
MessageTable.encodeLengths(value),
MessageTable.encodeDynamic(value)
);
IChatNamespacedSystem(worldAddress).namespace_ChatNamespaced_sendMessage(value);
Expand Down
9 changes: 7 additions & 2 deletions packages/store-sync/src/common.ts
@@ -1,5 +1,5 @@
import { Address, Block, Hex, Log, PublicClient } from "viem";
import { StoreConfig, StoreEventsAbiItem, StoreEventsAbi } from "@latticexyz/store";
import { StoreConfig, StoreEventsAbiItem, StoreEventsAbi, resolveUserTypes } from "@latticexyz/store";
import storeConfig from "@latticexyz/store/mud.config";
import { Observable } from "rxjs";
import { resourceIdToHex } from "@latticexyz/common";
Expand Down Expand Up @@ -76,7 +76,12 @@ export type StorageAdapterBlock = { blockNumber: BlockLogs["blockNumber"]; logs:
export type StorageAdapter = (block: StorageAdapterBlock) => Promise<void>;

// TODO: adjust when we get namespace support (https://github.com/latticexyz/mud/issues/994) and when table has namespace key (https://github.com/latticexyz/mud/issues/1201)
export const schemasTable = storeConfig.tables.Tables;
// TODO: adjust when schemas are automatically resolved
export const schemasTable = {
...storeConfig.tables.Tables,
valueSchema: resolveUserTypes(storeConfig.tables.Tables.valueSchema, storeConfig.userTypes),
};

export const schemasTableId = resourceIdToHex({
type: schemasTable.offchainOnly ? "offchainTable" : "table",
namespace: storeConfig.namespace,
Expand Down
21 changes: 14 additions & 7 deletions packages/store-sync/src/logToTable.ts
@@ -1,5 +1,5 @@
import { hexToSchema, decodeValue } from "@latticexyz/protocol-parser";
import { concatHex, decodeAbiParameters, parseAbiParameters } from "viem";
import { hexToSchema, decodeValue, ValueSchema } from "@latticexyz/protocol-parser";
import { Hex, concatHex, decodeAbiParameters, parseAbiParameters } from "viem";
import { StorageAdapterLog, Table, schemasTable } from "./common";
import { hexToResourceId } from "@latticexyz/common";

Expand All @@ -14,15 +14,22 @@ export function logToTable(log: StorageAdapterLog & { eventName: "Store_SetRecor
const table = hexToResourceId(tableId);

const value = decodeValue(
schemasTable.valueSchema,
// TODO: remove cast when we have strong types for user types
schemasTable.valueSchema as ValueSchema,
concatHex([log.args.staticData, log.args.encodedLengths, log.args.dynamicData])
);

const keySchema = hexToSchema(value.keySchema);
const valueSchema = hexToSchema(value.valueSchema);
// TODO: remove cast when we have strong types for user types
const keySchema = hexToSchema(value.keySchema as Hex);

const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0];
const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0];
// TODO: remove cast when we have strong types for user types
const valueSchema = hexToSchema(value.valueSchema as Hex);

// TODO: remove cast when we have strong types for user types
const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames as Hex)[0];

// TODO: remove cast when we have strong types for user types
const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames as Hex)[0];

const valueAbiTypes = [...valueSchema.staticFields, ...valueSchema.dynamicFields];

Expand Down
24 changes: 12 additions & 12 deletions packages/store/gas-report.json
Expand Up @@ -603,73 +603,73 @@
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetDynamicFieldSlice",
"name": "get field slice (cold, 1 slot)",
"gasUsed": 10240
"gasUsed": 5609
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetDynamicFieldSlice",
"name": "get field slice (warm, 1 slot)",
"gasUsed": 2308
"gasUsed": 1677
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetDynamicFieldSlice",
"name": "get field slice (semi-cold, 1 slot)",
"gasUsed": 4313
"gasUsed": 3680
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetDynamicFieldSlice",
"name": "get field slice (warm, 2 slots)",
"gasUsed": 4539
"gasUsed": 3907
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetSecondFieldLength",
"name": "get field length (cold, 1 slot)",
"gasUsed": 7795
"gasUsed": 3163
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetSecondFieldLength",
"name": "get field length (warm, 1 slot)",
"gasUsed": 1790
"gasUsed": 1160
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetThirdFieldLength",
"name": "get field length (warm due to , 2 slots)",
"gasUsed": 7794
"gasUsed": 3163
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testGetThirdFieldLength",
"name": "get field length (warm, 2 slots)",
"gasUsed": 1790
"gasUsed": 1160
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testPopFromSecondField",
"name": "pop from field (cold, 1 slot, 1 uint32 item)",
"gasUsed": 18728
"gasUsed": 18097
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testPopFromSecondField",
"name": "pop from field (warm, 1 slot, 1 uint32 item)",
"gasUsed": 12736
"gasUsed": 12104
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testPopFromThirdField",
"name": "pop from field (cold, 2 slots, 10 uint32 items)",
"gasUsed": 16496
"gasUsed": 15865
},
{
"file": "test/StoreCoreDynamic.t.sol",
"test": "testPopFromThirdField",
"name": "pop from field (warm, 2 slots, 10 uint32 items)",
"gasUsed": 12504
"gasUsed": 11872
},
{
"file": "test/StoreCoreGas.t.sol",
Expand Down
33 changes: 25 additions & 8 deletions packages/store/mud.config.ts
Expand Up @@ -6,35 +6,52 @@ export default mudConfig({
enums: {
ExampleEnum: ["None", "First", "Second", "Third"],
},
userTypes: {
ResourceId: { filePath: "./src/ResourceId.sol", internalType: "bytes32" },
FieldLayout: { filePath: "./src/FieldLayout.sol", internalType: "bytes32" },
Schema: { filePath: "./src/Schema.sol", internalType: "bytes32" },
},
tables: {
StoreHooks: "bytes21[]",
Callbacks: "bytes24[]",
StoreHooks: {
keySchema: {
tableId: "ResourceId",
},
valueSchema: {
hooks: "bytes21[]",
},
},
Tables: {
keySchema: {
tableId: "bytes32",
tableId: "ResourceId",
},
valueSchema: {
fieldLayout: "bytes32",
keySchema: "bytes32",
valueSchema: "bytes32",
fieldLayout: "FieldLayout",
keySchema: "Schema",
valueSchema: "Schema",
abiEncodedKeyNames: "bytes",
abiEncodedFieldNames: "bytes",
},
},
ResourceIds: {
keySchema: {
resourceId: "bytes32",
resourceId: "ResourceId",
},
valueSchema: {
exists: "bool",
},
},
// The Hooks table is a generic table used by the `filterFromList` util in `Hook.sol`
Hooks: {
valueSchema: "bytes21[]",
keySchema: {
resourceId: "ResourceId",
},
valueSchema: {
hooks: "bytes21[]",
},
tableIdArgument: true,
},
// TODO: move these test tables to a separate mud config
Callbacks: "bytes24[]",
Mixed: {
valueSchema: {
u32: "uint32",
Expand Down
4 changes: 2 additions & 2 deletions packages/store/src/Hook.sol
Expand Up @@ -26,7 +26,7 @@ library HookLib {
ResourceId tableWithHooks,
address hookAddressToRemove
) internal {
bytes21[] memory currentHooks = Hooks._get(hookTableId, ResourceId.unwrap(tableWithHooks));
bytes21[] memory currentHooks = Hooks._get(hookTableId, tableWithHooks);

// Initialize the new hooks array with the same length because we don't know if the hook is registered yet
bytes21[] memory newHooks = new bytes21[](currentHooks.length);
Expand All @@ -49,7 +49,7 @@ library HookLib {
}

// Set the new hooks table
Hooks._set(hookTableId, ResourceId.unwrap(tableWithHooks), newHooks);
Hooks._set(hookTableId, tableWithHooks, newHooks);
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/store/src/IStore.sol
Expand Up @@ -123,7 +123,7 @@ interface IStoreWrite {
ResourceId indexed tableId,
bytes32[] keyTuple,
bytes staticData,
bytes32 encodedLengths,
PackedCounter encodedLengths,
bytes dynamicData
);
event Store_SpliceStaticData(ResourceId indexed tableId, bytes32[] keyTuple, uint48 start, bytes data);
Expand All @@ -133,7 +133,7 @@ interface IStoreWrite {
uint48 start,
uint40 deleteCount,
bytes data,
bytes32 encodedLengths
PackedCounter encodedLengths
);
event Store_DeleteRecord(ResourceId indexed tableId, bytes32[] keyTuple);

Expand Down
37 changes: 15 additions & 22 deletions packages/store/src/StoreCore.sol
Expand Up @@ -32,7 +32,7 @@ library StoreCore {
ResourceId indexed tableId,
bytes32[] keyTuple,
bytes staticData,
bytes32 encodedLengths,
PackedCounter encodedLengths,
bytes dynamicData
);
event Store_SpliceStaticData(ResourceId indexed tableId, bytes32[] keyTuple, uint48 start, bytes data);
Expand All @@ -42,7 +42,7 @@ library StoreCore {
uint48 start,
uint40 deleteCount,
bytes data,
bytes32 encodedLengths
PackedCounter encodedLengths
);
event Store_DeleteRecord(ResourceId indexed tableId, bytes32[] keyTuple);

Expand Down Expand Up @@ -99,9 +99,9 @@ library StoreCore {
* Get the key schema for the given tableId
*/
function getKeySchema(ResourceId tableId) internal view returns (Schema keySchema) {
keySchema = Schema.wrap(Tables._getKeySchema(ResourceId.unwrap(tableId)));
keySchema = Tables._getKeySchema(tableId);
// key schemas can be empty for singleton tables, so we can't depend on key schema for table check
if (!ResourceIds._getExists(ResourceId.unwrap(tableId))) {
if (!ResourceIds._getExists(tableId)) {
revert IStoreErrors.Store_TableNotFound(tableId, string(abi.encodePacked(tableId)));
}
}
Expand All @@ -110,7 +110,7 @@ library StoreCore {
* Get the schema for the given tableId
*/
function getValueSchema(ResourceId tableId) internal view returns (Schema valueSchema) {
valueSchema = Schema.wrap(Tables._getValueSchema(ResourceId.unwrap(tableId)));
valueSchema = Tables._getValueSchema(tableId);
if (valueSchema.isEmpty()) {
revert IStoreErrors.Store_TableNotFound(tableId, string(abi.encodePacked(tableId)));
}
Expand Down Expand Up @@ -155,22 +155,15 @@ library StoreCore {
}

// Verify there is no resource with this ID yet
if (ResourceIds._getExists(ResourceId.unwrap(tableId))) {
if (ResourceIds._getExists(tableId)) {
revert IStoreErrors.Store_TableAlreadyExists(tableId, string(abi.encodePacked(tableId)));
}

// Register the table metadata
Tables._set(
ResourceId.unwrap(tableId),
FieldLayout.unwrap(fieldLayout),
Schema.unwrap(keySchema),
Schema.unwrap(valueSchema),
abi.encode(keyNames),
abi.encode(fieldNames)
);
Tables._set(tableId, fieldLayout, keySchema, valueSchema, abi.encode(keyNames), abi.encode(fieldNames));

// Register the table ID
ResourceIds._setExists(ResourceId.unwrap(tableId), true);
ResourceIds._setExists(tableId, true);
}

/************************************************************************
Expand All @@ -188,7 +181,7 @@ library StoreCore {
revert IStoreErrors.Store_InvalidResourceType(RESOURCE_TABLE, tableId, string(abi.encodePacked(tableId)));
}

StoreHooks.push(ResourceId.unwrap(tableId), Hook.unwrap(HookLib.encode(address(hookAddress), enabledHooksBitmap)));
StoreHooks.push(tableId, Hook.unwrap(HookLib.encode(address(hookAddress), enabledHooksBitmap)));
}

/**
Expand Down Expand Up @@ -231,15 +224,15 @@ library StoreCore {
FieldLayout fieldLayout
) internal {
// Emit event to notify indexers
emit Store_SetRecord(tableId, keyTuple, staticData, encodedLengths.unwrap(), dynamicData);
emit Store_SetRecord(tableId, keyTuple, staticData, encodedLengths, dynamicData);

// Early return if the table is an offchain table
if (tableId.getType() != RESOURCE_TABLE) {
return;
}

// Call onBeforeSetRecord hooks (before actually modifying the state, so observers have access to the previous state if needed)
bytes21[] memory hooks = StoreHooks._get(ResourceId.unwrap(tableId));
bytes21[] memory hooks = StoreHooks._get(tableId);
for (uint256 i; i < hooks.length; i++) {
Hook hook = Hook.wrap(hooks[i]);
if (hook.isEnabled(BEFORE_SET_RECORD)) {
Expand Down Expand Up @@ -320,7 +313,7 @@ library StoreCore {
}

// Call onBeforeSpliceStaticData hooks (before actually modifying the state, so observers have access to the previous state if needed)
bytes21[] memory hooks = StoreHooks._get(ResourceId.unwrap(tableId));
bytes21[] memory hooks = StoreHooks._get(tableId);
for (uint256 i; i < hooks.length; i++) {
Hook hook = Hook.wrap(hooks[i]);
if (hook.isEnabled(BEFORE_SPLICE_STATIC_DATA)) {
Expand Down Expand Up @@ -451,7 +444,7 @@ library StoreCore {
}

// Call onBeforeDeleteRecord hooks (before actually modifying the state, so observers have access to the previous state if needed)
bytes21[] memory hooks = StoreHooks._get(ResourceId.unwrap(tableId));
bytes21[] memory hooks = StoreHooks._get(tableId);
for (uint256 i; i < hooks.length; i++) {
Hook hook = Hook.wrap(hooks[i]);
if (hook.isEnabled(BEFORE_DELETE_RECORD)) {
Expand Down Expand Up @@ -771,11 +764,11 @@ library StoreCoreInternal {
start: uint48(start),
deleteCount: deleteCount,
data: data,
encodedLengths: updatedEncodedLengths.unwrap()
encodedLengths: updatedEncodedLengths
});

// Call onBeforeSpliceDynamicData hooks (before actually modifying the state, so observers have access to the previous state if needed)
bytes21[] memory hooks = StoreHooks._get(ResourceId.unwrap(tableId));
bytes21[] memory hooks = StoreHooks._get(tableId);
for (uint256 i; i < hooks.length; i++) {
Hook hook = Hook.wrap(hooks[i]);
if (hook.isEnabled(BEFORE_SPLICE_DYNAMIC_DATA)) {
Expand Down
2 changes: 1 addition & 1 deletion packages/store/src/codegen/index.sol

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 22ee447

Please sign in to comment.