Skip to content

Commit

Permalink
feat(store): add onDelete hook, rename IOnUpdateHooks to IStoreHooks,…
Browse files Browse the repository at this point in the history
… make naming consistent
  • Loading branch information
alvrs committed Feb 1, 2023
1 parent 3cba328 commit f49bf75
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 39 deletions.
15 changes: 10 additions & 5 deletions packages/store/src/IStore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ interface IStore {
bytes memory data
) external;

// Register a callback to be called when a record is updated
function registerOnUpdateHook(bytes32 table, IOnUpdateHook onUpdateHook) external;
// Register hooks to be called when a record or field is set or deleted
function registerHooks(bytes32 table, IStoreHooks hooks) external;

// Set full record (including full dynamic data)
function deleteRecord(bytes32 table, bytes32[] memory key) external;

// Get full record (including full array, load table schema from storage)
function getRecord(bytes32 table, bytes32[] memory key) external view returns (bytes memory data);
Expand All @@ -53,17 +56,19 @@ interface IStore {
function isStore() external view;
}

interface IOnUpdateHook {
function onUpdateRecord(
interface IStoreHooks {
function onSetRecord(
bytes32 table,
bytes32[] memory key,
bytes memory data
) external;

function onUpdateField(
function onSetField(
bytes32 table,
bytes32[] memory key,
uint8 schemaIndex,
bytes memory data
) external;

function onDeleteRecord(bytes32 table, bytes32[] memory key) external;
}
39 changes: 23 additions & 16 deletions packages/store/src/StoreCore.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { console } from "forge-std/console.sol";
import { Schema } from "./Schema.sol";
import { PackedCounter } from "./PackedCounter.sol";
import { Buffer, Buffer_ } from "./Buffer.sol";
import { OnUpdateHookTable, tableId as OnUpdateHookTableId } from "./tables/OnUpdateHookTable.sol";
import { IOnUpdateHook } from "./IStore.sol";
import { HooksTable, tableId as HooksTableId } from "./tables/HooksTable.sol";
import { IStoreHooks } from "./IStore.sol";

// TODO
// - Turn all storage pointer to uint256 for consistency (uint256 is better than bytes32 because it's easier to do arithmetic on)
Expand Down Expand Up @@ -39,7 +39,7 @@ library StoreCore {
* TODO: should we turn the schema table into a "proper table" and register it here?
*/
function initialize() internal {
registerSchema(OnUpdateHookTableId, OnUpdateHookTable.getSchema());
registerSchema(HooksTableId, HooksTable.getSchema());
}

/************************************************************************
Expand Down Expand Up @@ -98,10 +98,10 @@ library StoreCore {
************************************************************************/

/*
* Add a hook to be called when a record is set
* Register hooks to be called when a record or field is set or deleted
*/
function registerOnUpdateHook(bytes32 table, IOnUpdateHook onUpdateHook) internal {
OnUpdateHookTable.push(table, address(onUpdateHook));
function registerHooks(bytes32 table, IStoreHooks hooks) external {
HooksTable.push(table, address(hooks));
}

/************************************************************************
Expand Down Expand Up @@ -136,11 +136,11 @@ library StoreCore {
revert StoreCore_InvalidDataLength(expectedLength, data.length);
}

// Call update hooks (before actually modifying the state, so observers have access to the previous state if needed)
address[] memory onUpdateHooks = OnUpdateHookTable.get(table);
for (uint256 i = 0; i < onUpdateHooks.length; i++) {
IOnUpdateHook hook = IOnUpdateHook(onUpdateHooks[i]);
hook.onUpdateRecord(table, key, data);
// Call onSetRecord hooks (before actually modifying the state, so observers have access to the previous state if needed)
address[] memory hooks = HooksTable.get(table);
for (uint256 i = 0; i < hooks.length; i++) {
IStoreHooks hook = IStoreHooks(hooks[i]);

This comment has been minimized.

Copy link
@holic

holic Feb 1, 2023

Member

oh the plural vs singular here is gonna get confusing

maybe we call it IStoreHook and registerHook and then use hooks for values from the table?

hook.onSetRecord(table, key, data);
}

// Store the static data at the static data location
Expand Down Expand Up @@ -181,11 +181,11 @@ library StoreCore {
) internal {
Schema schema = getSchema(table);

// Call update hooks (before actually modifying the state, so observers have access to the previous state if needed)
address[] memory onUpdateHooks = OnUpdateHookTable.get(table);
for (uint256 i = 0; i < onUpdateHooks.length; i++) {
IOnUpdateHook hook = IOnUpdateHook(onUpdateHooks[i]);
hook.onUpdateField(table, key, schemaIndex, data);
// Call onSetField hooks (before actually modifying the state, so observers have access to the previous state if needed)
address[] memory hooks = HooksTable.get(table);
for (uint256 i = 0; i < hooks.length; i++) {
IStoreHooks hook = IStoreHooks(hooks[i]);
hook.onSetField(table, key, schemaIndex, data);
}

if (schemaIndex < schema.numStaticFields()) {
Expand Down Expand Up @@ -237,6 +237,13 @@ library StoreCore {
// Get schema for this table
Schema schema = getSchema(table);

// Call onDeleteRecord hooks (before actually modifying the state, so observers have access to the previous state if needed)
address[] memory hooks = HooksTable.get(table);
for (uint256 i = 0; i < hooks.length; i++) {
IStoreHooks hook = IStoreHooks(hooks[i]);
hook.onDeleteRecord(table, key);
}

// Delete static data
bytes32 staticDataLocation = _getStaticDataLocation(table, key);
Storage.write(staticDataLocation, 0, new bytes(schema.staticDataLength()));
Expand Down
8 changes: 8 additions & 0 deletions packages/store/src/StoreSwitch.sol
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ library StoreSwitch {
}
}

function deleteRecord(bytes32 table, bytes32[] memory key) internal {
if (isDelegateCall()) {
StoreCore.deleteRecord(table, key);
} else {
IStore(msg.sender).deleteRecord(table, key);
}
}

function getRecord(bytes32 table, bytes32[] memory key) internal view returns (bytes memory) {
if (isDelegateCall()) {
return StoreCore.getRecord(table, key);
Expand Down
8 changes: 6 additions & 2 deletions packages/store/src/StoreView.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity >=0.8.0;

import { SchemaType } from "./Types.sol";
import { IStore, IOnUpdateHook } from "./IStore.sol";
import { IStore, IStoreHooks } from "./IStore.sol";
import { StoreCore } from "./StoreCore.sol";
import { Schema } from "./Schema.sol";

Expand Down Expand Up @@ -40,7 +40,11 @@ contract StoreView is IStore {
revert Store_BaseContractNotImplemented();
}

function registerOnUpdateHook(bytes32, IOnUpdateHook) public virtual {
function registerHooks(bytes32, IStoreHooks) public virtual {
revert Store_BaseContractNotImplemented();
}

function deleteRecord(bytes32, bytes32[] memory) public virtual {
revert Store_BaseContractNotImplemented();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import { PackedCounter, PackedCounter_ } from "../PackedCounter.sol";
import { AddressArray, AddressArray_ } from "../schemas/AddressArray.sol";

// -- User defined schema and tableId --
bytes32 constant tableId = keccak256("mud.store.table.onUpdateHookTable");
bytes32 constant tableId = keccak256("mud.store.table.hooks");

// -- Autogenerated library to interact with tables with this schema --
// TODO: autogenerate

library OnUpdateHookTable {
library HooksTable {
/** Get the table's schema */
function getSchema() internal pure returns (Schema schema) {
return AddressArray_.getSchema();
Expand Down
52 changes: 38 additions & 14 deletions packages/store/src/test/StoreCore.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { Buffer, Buffer_ } from "../Buffer.sol";
import { Schema, Schema_ } from "../Schema.sol";
import { PackedCounter, PackedCounter_ } from "../PackedCounter.sol";
import { StoreView } from "../StoreView.sol";
import { IStore, IOnUpdateHook } from "../IStore.sol";
import { IStore, IStoreHooks } from "../IStore.sol";
import { StoreSwitch } from "../StoreSwitch.sol";

struct TestStruct {
Expand All @@ -27,7 +27,7 @@ contract StoreCoreTest is Test, StoreView {

mapping(uint256 => bytes) private testMapping;

// Expose an external setRecord function for testing purposes of indexers (see testOnUpdateHook)
// Expose an external setRecord function for testing purposes of indexers (see testHooks)
function setRecord(
bytes32 table,
bytes32[] memory key,
Expand All @@ -36,7 +36,7 @@ contract StoreCoreTest is Test, StoreView {
StoreCore.setRecord(table, key, data);
}

// Expose an external setField function for testing purposes of indexers (see testOnUpdateHook)
// Expose an external setField function for testing purposes of indexers (see testHooks)
function setField(
bytes32 table,
bytes32[] memory key,
Expand All @@ -46,7 +46,12 @@ contract StoreCoreTest is Test, StoreView {
StoreCore.setField(table, key, schemaIndex, data);
}

// Expose an external registerSchema function for testing purposes of indexers (see testOnUpdateHook)
// Expose an external deleteRecord function for testing purposes of indexers (see testHooks)
function deleteRecord(bytes32 table, bytes32[] memory key) public override {
StoreCore.deleteRecord(table, key);
}

// Expose an external registerSchema function for testing purposes of indexers (see testHooks)
function registerSchema(bytes32 table, Schema schema) public override {
StoreCore.registerSchema(table, schema);
}
Expand Down Expand Up @@ -469,7 +474,7 @@ contract StoreCoreTest is Test, StoreView {
assertEq(data3.length, 0);
}

function testOnUpdateHook() public {
function testHooks() public {
bytes32 table = keccak256("some.table");
bytes32[] memory key = new bytes32[](1);
key[0] = keccak256("some key");
Expand All @@ -482,7 +487,7 @@ contract StoreCoreTest is Test, StoreView {
MirrorSubscriber subscriber = new MirrorSubscriber(table, schema);

// !gasreport register subscriber
StoreCore.registerOnUpdateHook(table, subscriber);
StoreCore.registerHooks(table, subscriber);

bytes memory data = bytes.concat(bytes16(0x0102030405060708090a0b0c0d0e0f10));

Expand All @@ -495,15 +500,22 @@ contract StoreCoreTest is Test, StoreView {

data = bytes.concat(bytes16(0x1112131415161718191a1b1c1d1e1f20));

// !gasreport set field on table with subscriber
// !gasreport set static field on table with subscriber
StoreCore.setField(table, key, 0, data);

// Get data from indexed table - the indexer should have mirrored the data there
indexedData = StoreCore.getRecord(indexerTableId, key);
assertEq(keccak256(data), keccak256(indexedData));

// !gasreport delete record on table with subscriber
StoreCore.deleteRecord(table, key);

// Get data from indexed table - the indexer should have mirrored the data there
indexedData = StoreCore.getRecord(indexerTableId, key);
assertEq(keccak256(indexedData), keccak256(bytes.concat(bytes16(0))));
}

function testOnUpdateHookDynamicData() public {
function testHooksDynamicData() public {
bytes32 table = keccak256("some.table");
bytes32[] memory key = new bytes32[](1);
key[0] = keccak256("some key");
Expand All @@ -516,7 +528,7 @@ contract StoreCoreTest is Test, StoreView {
MirrorSubscriber subscriber = new MirrorSubscriber(table, schema);

// !gasreport register subscriber
StoreCore.registerOnUpdateHook(table, subscriber);
StoreCore.registerHooks(table, subscriber);

uint32[] memory arrayData = new uint32[](1);
arrayData[0] = 0x01020304;
Expand All @@ -526,7 +538,7 @@ contract StoreCoreTest is Test, StoreView {
bytes memory staticData = bytes.concat(bytes16(0x0102030405060708090a0b0c0d0e0f10));
bytes memory data = bytes.concat(staticData, dynamicData);

// !gasreport set record on table with subscriber
// !gasreport set (dynamic) record on table with subscriber
StoreCore.setRecord(table, key, data);

// Get data from indexed table - the indexer should have mirrored the data there
Expand All @@ -539,26 +551,33 @@ contract StoreCoreTest is Test, StoreView {
dynamicData = bytes.concat(encodedArrayDataLength.unwrap(), arrayDataBytes);
data = bytes.concat(staticData, dynamicData);

// !gasreport set field on table with subscriber
// !gasreport set (dynamic) field on table with subscriber
StoreCore.setField(table, key, 1, arrayDataBytes);

// Get data from indexed table - the indexer should have mirrored the data there
indexedData = StoreCore.getRecord(indexerTableId, key);
assertEq(keccak256(data), keccak256(indexedData));

// !gasreport delete (dynamic) record on table with subscriber
StoreCore.deleteRecord(table, key);

// Get data from indexed table - the indexer should have mirrored the data there
indexedData = StoreCore.getRecord(indexerTableId, key);
assertEq(keccak256(indexedData), keccak256(bytes.concat(bytes16(0))));
}
}

bytes32 constant indexerTableId = keccak256("indexer.table");

contract MirrorSubscriber is IOnUpdateHook {
contract MirrorSubscriber is IStoreHooks {
bytes32 _table;

constructor(bytes32 table, Schema schema) {
IStore(msg.sender).registerSchema(indexerTableId, schema);
_table = table;
}

function onUpdateRecord(
function onSetRecord(
bytes32 table,
bytes32[] memory key,
bytes memory data
Expand All @@ -567,7 +586,7 @@ contract MirrorSubscriber is IOnUpdateHook {
StoreSwitch.setRecord(indexerTableId, key, data);
}

function onUpdateField(
function onSetField(
bytes32 table,
bytes32[] memory key,
uint8 schemaIndex,
Expand All @@ -576,4 +595,9 @@ contract MirrorSubscriber is IOnUpdateHook {
if (table != table) revert("invalid table");
StoreSwitch.setField(indexerTableId, key, schemaIndex, data);
}

function onDeleteRecord(bytes32 table, bytes32[] memory key) public {
if (table != table) revert("invalid table");
StoreSwitch.deleteRecord(indexerTableId, key);
}
}

0 comments on commit f49bf75

Please sign in to comment.