Skip to content

Commit

Permalink
filters build: args required, filters array, singular event
Browse files Browse the repository at this point in the history
  • Loading branch information
khaidarkairbek committed Dec 28, 2024
1 parent 6ae709a commit a150566
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 152 deletions.
96 changes: 51 additions & 45 deletions packages/core/src/build/configAndIndexingFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,72 +342,78 @@ export async function buildConfigAndIndexingFunctions({
: [source.filter];

for (const filter of source.filter) {
if (Array.isArray(filter.event) && filter.args !== undefined) {
const abiEvent = abiEvents.bySafeName[filter.event];
if (!abiEvent) {
throw new Error(
`Validation failed: Event filter for contract '${source.name}' cannot contain indexed argument values if multiple events are provided.`,
`Validation failed: Invalid filter for contract '${
source.name
}'. Got event name '${filter.event}', expected one of [${Object.keys(
abiEvents.bySafeName,
)
.map((n) => `'${n}'`)
.join(", ")}].`,
);
}

const filterSafeEventNames = Array.isArray(filter.event)
? filter.event
: [filter.event];

for (const filterSafeEventName of filterSafeEventNames) {
const abiEvent = abiEvents.bySafeName[filterSafeEventName];
if (!abiEvent) {
throw new Error(
`Validation failed: Invalid filter for contract '${
source.name
}'. Got event name '${filterSafeEventName}', expected one of [${Object.keys(
abiEvents.bySafeName,
)
.map((n) => `'${n}'`)
.join(", ")}].`,
);
}
}
}

// TODO: Explicit validation of indexed argument value format (array or object).
// The first element of the array return from `buildTopics` being defined
// is an invariant of the current filter design.
// Note: This can throw.

topicsArray = buildTopics(source.abi, source.filter);
const filteredTopicsArray = buildTopics(
source.abi,
source.filter,
).reduce(
(acc, cur) => {
if (acc.includes(cur) === false) {
acc.push(cur);
}

const filteredEventSelectors = topicsArray.reduce((acc, cur) => {
const topic0 = Array.isArray(cur.topic0) ? cur.topic0 : [cur.topic0];
return acc;
},
[] as {
topic0: Hex;
topic1: Hex | Hex[] | null;
topic2: Hex | Hex[] | null;
topic3: Hex | Hex[] | null;
}[],
);

const filteredEventSelectors = filteredTopicsArray.reduce(
(acc, cur) => {
const eventSelector = cur.topic0;

for (const eventSelector of topic0) {
if (
eventSelector !== null &&
acc.includes(eventSelector) === false
) {
acc.push(eventSelector);
}
}

return acc;
}, [] as Hex[]);
return acc;
},
[] as Hex[],
);

// Validate that the topic0 value defined by the `eventFilter` is a superset of the
// registered indexing functions. Simply put, confirm that no indexing function is
// defined for a log event that is excluded by the filter.
for (const registeredEventSelector of registeredEventSelectors) {
if (!filteredEventSelectors.includes(registeredEventSelector)) {
const logEventName =
abiEvents.bySelector[registeredEventSelector]!.safeName;
// Merge filtered topics and registered event selectors
const excludedRegisteredEventSelectors =
registeredEventSelectors.filter(
(s) => filteredEventSelectors.includes(s) === false,
);

throw new Error(
`Validation failed: Event '${logEventName}' is excluded by the event filter defined on the contract '${
source.name
}'. Got '${logEventName}', expected one of [${filteredEventSelectors
.map((s) => abiEvents.bySelector[s]!.safeName)
.map((eventName) => `'${eventName}'`)
.join(", ")}].`,
);
}
}
topicsArray =
excludedRegisteredEventSelectors.length > 0
? [
{
topic0: excludedRegisteredEventSelectors,
topic1: null,
topic2: null,
topic3: null,
},
...filteredTopicsArray,
]
: filteredTopicsArray;
}

const startBlockMaybeNan = source.startBlock;
Expand Down
65 changes: 31 additions & 34 deletions packages/core/src/config/eventFilter.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,50 @@
import type { Abi, GetEventArgs } from "viem";
import type { ParseAbiEvent, SafeEventNames } from "./utilityTypes.js";

// TODO: Fix this type ( changes: args are required, event is singular for each filter )
// Filters are only for topic1/2/3 filtering
export type GetEventFilter<
abi extends Abi,
contract,
///
safeEventNames extends string = SafeEventNames<abi>,
> = contract extends {
filter: {
event: infer event extends readonly string[] | string;
event: infer event extends string;
};
}
? // 1. Contract has a filter with event
event extends readonly string[]
? // 1.a Filter event is an array
? event extends safeEventNames
? // 1.b.i Filter event is a valid string
{
filter?: {
event: readonly safeEventNames[];
event: safeEventNames | event;
args: GetEventArgs<
abi,
string,
{
EnableUnion: true;
IndexedOnly: true;
Required: false;
},
ParseAbiEvent<abi, event>
>;
};
}
: // 1.b Filter event is a string
event extends safeEventNames
? // 1.b.i Filter event is a valid string
{
filter?: {
event: safeEventNames | event;
args?: GetEventArgs<
abi,
string,
{
EnableUnion: true;
IndexedOnly: true;
Required: false;
},
ParseAbiEvent<abi, event>
>;
};
}
: // 1.b.ii Filter event is an invalid string
{
filter?: {
: // 1.b.ii Filter event is an invalid string
{
filter?: {
event: safeEventNames;
args: GetEventArgs<Abi | readonly unknown[], string>;
};
}
: {
filter?:
| {
event: safeEventNames;
args: GetEventArgs<Abi | readonly unknown[], string>;
}[]
| {
event: safeEventNames;
args?: GetEventArgs<Abi | readonly unknown[], string>;
args: GetEventArgs<Abi | readonly unknown[], string>;
};
}
: // 2. Contract doesn't have a filter with event
{
filter?: {
event: safeEventNames | readonly safeEventNames[];
args?: GetEventArgs<Abi | readonly unknown[], string>;
};
};
88 changes: 15 additions & 73 deletions packages/core/src/sync/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,85 +92,27 @@ export function buildTopics(
abi: Abi,
filter: NonNullable<Config["contracts"][string]["filter"]>,
): {
topic0: Hex | Hex[];
topic0: Hex;
topic1: Hex | Hex[] | null;
topic2: Hex | Hex[] | null;
topic3: Hex | Hex[] | null;
}[] {
const filters = Array.isArray(filter) ? filter : [filter];

const topics = filters
.map((filter) => {
if (Array.isArray(filter.event)) {
// List of event signatures
return {
topic0: filter.event.map((event) =>
toEventSelector(findAbiEvent(abi, event)),
),
topic1: null,
topic2: null,
topic3: null,
};
} else {
// Single event with args
const topics = encodeEventTopics({
abi: [findAbiEvent(abi, filter.event)],
args: filter.args as GetEventArgs<Abi, string>,
});

return {
topic0: topics[0],
topic1: topics[1] ?? null,
topic2: topics[2] ?? null,
topic3: topics[3] ?? null,
};
}
})
.reduce(
// Merge similar topics
(acc, cur) => {
// Ignore if topics in array
if (acc.some((t) => t === cur) === true) {
return acc;
}

// Push as is if args defined
if (cur.topic1 !== null || cur.topic2 !== null || cur.topic3 !== null) {
acc.push(cur);
return acc;
}

// Merge topic0 with topics with no args defined
const isMerged = acc.some((t, idx) => {
if (t.topic1 === null && t.topic2 === null && t.topic3 === null) {
const topic0 = Array.isArray(t.topic0) ? t.topic0 : [t.topic0];
const curTopic0 = Array.isArray(cur.topic0)
? cur.topic0
: [cur.topic0];
acc[idx] = {
topic0: Array.from(new Set([...topic0, ...curTopic0])),
topic1: null,
topic2: null,
topic3: null,
};
return true;
}
return false;
});

if (isMerged === false) {
acc.push(cur);
}

return acc;
},
[] as {
topic0: Hex | Hex[];
topic1: Hex | Hex[] | null;
topic2: Hex | Hex[] | null;
topic3: Hex | Hex[] | null;
}[],
);
const topics = filters.map((filter) => {
// Single event with args
const topics = encodeEventTopics({
abi: [findAbiEvent(abi, filter.event)],
args: filter.args as GetEventArgs<Abi, string>,
});

return {
topic0: topics[0],
topic1: topics[1] ?? null,
topic2: topics[2] ?? null,
topic3: topics[3] ?? null,
};
});

return topics;
}
Expand Down

0 comments on commit a150566

Please sign in to comment.