Skip to content

Commit

Permalink
fix(store-indexer): catch errors when parsing logs to tables and stor…
Browse files Browse the repository at this point in the history
…age operations (#1488)
  • Loading branch information
holic authored Sep 14, 2023
1 parent aea67c5 commit 7e6e515
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-lizards-sleep.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@latticexyz/store-sync": patch
---

Catch errors when parsing logs to tables and storage operations, log and skip
189 changes: 99 additions & 90 deletions packages/store-sync/src/blockLogsToStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,39 +34,43 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({
// Find table schema registration events
const newTables = block.logs
.map((log) => {
if (log.eventName !== "StoreSetRecord") return;
if (log.args.table !== schemasTableId) return;
try {
if (log.eventName !== "StoreSetRecord") return;
if (log.args.table !== schemasTableId) return;

// TODO: refactor encode/decode to use Record<string, SchemaAbiType> schemas
// TODO: refactor to decode key with protocol-parser utils
// TODO: refactor encode/decode to use Record<string, SchemaAbiType> schemas
// TODO: refactor to decode key with protocol-parser utils

const [tableId, ...otherKeys] = log.args.key;
if (otherKeys.length) {
console.warn("registerSchema event is expected to have only one key in key tuple, but got multiple", log);
}
const [tableId, ...otherKeys] = log.args.key;
if (otherKeys.length) {
console.warn("registerSchema event is expected to have only one key in key tuple, but got multiple", log);
}

const table = hexToTableId(tableId);
const table = hexToTableId(tableId);

const valueTuple = decodeRecord(abiTypesToSchema(Object.values(schemasTable.schema)), log.args.data);
const value = Object.fromEntries(
Object.keys(schemasTable.schema).map((name, i) => [name, valueTuple[i]])
) as ConfigToValuePrimitives<typeof storeConfig, typeof schemasTable.name>;
const valueTuple = decodeRecord(abiTypesToSchema(Object.values(schemasTable.schema)), log.args.data);
const value = Object.fromEntries(
Object.keys(schemasTable.schema).map((name, i) => [name, valueTuple[i]])
) as ConfigToValuePrimitives<typeof storeConfig, typeof schemasTable.name>;

const keySchema = hexToSchema(value.keySchema);
const valueSchema = hexToSchema(value.valueSchema);
const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0];
const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0];
const keySchema = hexToSchema(value.keySchema);
const valueSchema = hexToSchema(value.valueSchema);
const keyNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedKeyNames)[0];
const fieldNames = decodeAbiParameters(parseAbiParameters("string[]"), value.abiEncodedFieldNames)[0];

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

return {
address: log.address,
tableId,
namespace: table.namespace,
name: table.name,
keySchema: Object.fromEntries(keySchema.staticFields.map((abiType, i) => [keyNames[i], abiType])),
valueSchema: Object.fromEntries(valueAbiTypes.map((abiType, i) => [fieldNames[i], abiType])),
};
return {
address: log.address,
tableId,
namespace: table.namespace,
name: table.name,
keySchema: Object.fromEntries(keySchema.staticFields.map((abiType, i) => [keyNames[i], abiType])),
valueSchema: Object.fromEntries(valueAbiTypes.map((abiType, i) => [fieldNames[i], abiType])),
};
} catch (error: unknown) {
console.error("Failed to get table from log", log, error);
}
})
.filter(isDefined);

Expand Down Expand Up @@ -101,75 +105,80 @@ export function blockLogsToStorage<TConfig extends StoreConfig = StoreConfig>({

const operations = block.logs
.map((log): StorageOperation<TConfig> | undefined => {
const table = tables[`${getAddress(log.address)}:${log.args.table}`];
if (!table) {
debug("no table found for event, skipping", hexToTableId(log.args.table), log);
return;
}

const keyNames = Object.keys(table.keySchema);
const keyValues = decodeKeyTuple(
{ staticFields: Object.values(table.keySchema), dynamicFields: [] },
log.args.key
);
const key = Object.fromEntries(keyValues.map((value, i) => [keyNames[i], value])) as Key<
TConfig,
keyof TConfig["tables"]
>;

const valueAbiTypes = Object.values(table.valueSchema);
const valueSchema = abiTypesToSchema(valueAbiTypes);
const fieldNames = Object.keys(table.valueSchema);

// TODO: decide if we should split these up into distinct operations so the storage adapter can decide whether to combine or not
if (log.eventName === "StoreSetRecord" || log.eventName === "StoreEphemeralRecord") {
const valueTuple = decodeRecord(valueSchema, log.args.data);
const value = Object.fromEntries(fieldNames.map((name, i) => [name, valueTuple[i]])) as Value<
try {
const table = tables[`${getAddress(log.address)}:${log.args.table}`];
if (!table) {
debug("no table found for event, skipping", hexToTableId(log.args.table), log);
return;
}

const keyNames = Object.keys(table.keySchema);
const keyValues = decodeKeyTuple(
{ staticFields: Object.values(table.keySchema), dynamicFields: [] },
log.args.key
);
const key = Object.fromEntries(keyValues.map((value, i) => [keyNames[i], value])) as Key<
TConfig,
keyof TConfig["tables"]
>;
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "SetRecord",
key,
value,
};
}

if (log.eventName === "StoreSetField") {
const fieldName = fieldNames[log.args.schemaIndex] as string & keyof Value<TConfig, keyof TConfig["tables"]>;
const fieldValue = decodeField(valueAbiTypes[log.args.schemaIndex], log.args.data) as Value<
TConfig,
keyof TConfig["tables"]
>[typeof fieldName];
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "SetField",
key,
fieldName,
fieldValue,
};
}

if (log.eventName === "StoreDeleteRecord") {
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "DeleteRecord",
key,
};
const valueAbiTypes = Object.values(table.valueSchema);
const valueSchema = abiTypesToSchema(valueAbiTypes);
const fieldNames = Object.keys(table.valueSchema);

// TODO: decide if we should split these up into distinct operations so the storage adapter can decide whether to combine or not
if (log.eventName === "StoreSetRecord" || log.eventName === "StoreEphemeralRecord") {
const valueTuple = decodeRecord(valueSchema, log.args.data);
const value = Object.fromEntries(fieldNames.map((name, i) => [name, valueTuple[i]])) as Value<
TConfig,
keyof TConfig["tables"]
>;
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "SetRecord",
key,
value,
};
}

if (log.eventName === "StoreSetField") {
const fieldName = fieldNames[log.args.schemaIndex] as string &
keyof Value<TConfig, keyof TConfig["tables"]>;
const fieldValue = decodeField(valueAbiTypes[log.args.schemaIndex], log.args.data) as Value<
TConfig,
keyof TConfig["tables"]
>[typeof fieldName];
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "SetField",
key,
fieldName,
fieldValue,
};
}

if (log.eventName === "StoreDeleteRecord") {
return {
log,
address: getAddress(log.address),
namespace: table.namespace,
name: table.name,
type: "DeleteRecord",
key,
};
}

debug("unknown store event or log, skipping", log);
return;
} catch (error: unknown) {
console.error("Failed to translate log to storage operation", log, error);
}

debug("unknown store event or log, skipping", log);
return;
})
.filter(isDefined);

Expand Down

0 comments on commit 7e6e515

Please sign in to comment.