Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions packages/core/src/build/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,110 @@ test("buildLogFactory handles Morpho CreateMarket struct parameter", () => {
childAddressLocation: "offset288",
});
});

const factoryEventWithBlockNumber = parseAbiItem(
"event ChildCreated(address indexed child, uint256 indexed startBlock)",
);

test("buildLogFactory with childStartBlock static value", () => {
const criteria = buildLogFactory({
address: "0xa",
event: llamaFactoryEventAbiItem,
parameter: "llamaCore",
chainId: 1,
sourceId: "Llama",
fromBlock: undefined,
toBlock: undefined,
childStartBlock: 1000000,
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(llamaFactoryEventAbiItem),
childAddressLocation: "offset0",
childStartBlock: 1000000,
childStartBlockLocation: undefined,
});
});

test("buildLogFactory with startBlockParameter from indexed topic", () => {
const criteria = buildLogFactory({
address: "0xa",
event: factoryEventWithBlockNumber,
parameter: "child",
chainId: 1,
sourceId: "Factory",
fromBlock: undefined,
toBlock: undefined,
startBlockParameter: "startBlock",
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(factoryEventWithBlockNumber),
childAddressLocation: "topic1",
childStartBlockLocation: "topic2",
});
});

const factoryEventWithNonIndexedBlock = parseAbiItem(
"event ChildCreated(address indexed child, uint256 startBlock, uint256 extra)",
);

test("buildLogFactory with startBlockParameter from data offset", () => {
const criteria = buildLogFactory({
address: "0xa",
event: factoryEventWithNonIndexedBlock,
parameter: "child",
chainId: 1,
sourceId: "Factory",
fromBlock: undefined,
toBlock: undefined,
startBlockParameter: "startBlock",
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(factoryEventWithNonIndexedBlock),
childAddressLocation: "topic1",
childStartBlockLocation: "offset0",
});
});

test("buildLogFactory with startBlockParameter extracts from chainId (uint256)", () => {
// chainId is uint256 type in the event, should work
const criteria = buildLogFactory({
address: "0xa",
event: llamaFactoryEventAbiItem,
parameter: "llamaCore",
chainId: 1,
sourceId: "Llama",
fromBlock: undefined,
toBlock: undefined,
startBlockParameter: "chainId",
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(llamaFactoryEventAbiItem),
childAddressLocation: "offset0",
childStartBlockLocation: "offset96", // chainId is after 3 non-indexed addresses (3 * 32 bytes)
});
});

test("buildLogFactory throws if startBlockParameter not found", () => {
expect(() =>
buildLogFactory({
address: "0xa",
event: llamaFactoryEventAbiItem,
parameter: "llamaCore",
chainId: 1,
sourceId: "Llama",
fromBlock: undefined,
toBlock: undefined,
startBlockParameter: "fakeParameter",
}),
).toThrowError(
"Factory event parameter not found in factory event signature. Got 'fakeParameter', expected one of ['deployer', 'name', 'llamaCore', 'llamaExecutor', 'llamaPolicy', 'chainId'].",
);
});
137 changes: 81 additions & 56 deletions packages/core/src/build/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,60 +9,26 @@ import {
import type { AbiEvent } from "abitype";
import { type Address, toEventSelector } from "viem";

export function buildLogFactory({
address: _address,
event,
parameter,
chainId,
sourceId,
fromBlock,
toBlock,
}: {
address?: Address | readonly Address[];
event: AbiEvent;
parameter: string;
chainId: number;
sourceId: string;
fromBlock: number | undefined;
toBlock: number | undefined;
}): LogFactory {
let address: Address | Address[] | undefined;
if (_address === undefined) {
// noop
} else if (Array.isArray(_address)) {
address = dedupe(_address)
.map(toLowerCase)
.sort((a, b) => (a < b ? -1 : 1));
} else {
address = toLowerCase(_address);
}

const eventSelector = toEventSelector(event);
type ParameterLocation = "topic1" | "topic2" | "topic3" | `offset${number}`;

/**
* Computes the location of a parameter in the event log data.
* Returns the topic index (for indexed params) or byte offset (for non-indexed params).
*/
function getParameterLocation(
event: AbiEvent,
parameter: string,
expectedType?: string,
): ParameterLocation {
const params = parameter.split(".");

if (params.length === 1) {
// Check if the provided parameter is present in the list of indexed inputs.
const indexedInputPosition = event.inputs
.filter((x) => "indexed" in x && x.indexed)
.findIndex((input) => {
return input.name === params[0];
});

if (indexedInputPosition > -1) {
return {
id: `log_${Array.isArray(address) ? address.join("_") : address}_${chainId}_topic${(indexedInputPosition + 1) as 1 | 2 | 3}_${eventSelector}_${fromBlock ?? "undefined"}_${toBlock ?? "undefined"}`,
type: "log",
chainId,
sourceId,
address,
eventSelector,
// Add 1 because inputs will not contain an element for topic0 (the signature).
childAddressLocation: `topic${(indexedInputPosition + 1) as 1 | 2 | 3}`,
fromBlock,
toBlock,
};
}
// Check if the provided parameter is present in the list of indexed inputs.
const indexedInputPosition = event.inputs
.filter((x) => "indexed" in x && x.indexed)
.findIndex((input) => input.name === params[0]);

if (indexedInputPosition > -1 && params.length === 1) {
return `topic${(indexedInputPosition + 1) as 1 | 2 | 3}`;
}

const nonIndexedInputs = event.inputs.filter(
Expand All @@ -82,9 +48,13 @@ export function buildLogFactory({

const nonIndexedParameter = nonIndexedInputs[nonIndexedInputPosition]!;

if (nonIndexedParameter.type !== "address" && params.length === 1) {
if (
expectedType &&
nonIndexedParameter.type !== expectedType &&
params.length === 1
) {
throw new Error(
`Factory event parameter type is not valid. Got '${nonIndexedParameter.type}', expected 'address'.`,
`Factory event parameter type is not valid. Got '${nonIndexedParameter.type}', expected '${expectedType}'.`,
);
}

Expand All @@ -104,19 +74,74 @@ export function buildLogFactory({
nonIndexedInputs[nonIndexedInputPosition]! as TupleAbiParameter,
params.slice(1),
);

offset += nestedOffset;
}

return `offset${offset}`;
}

export function buildLogFactory({
address: _address,
event,
parameter,
chainId,
sourceId,
fromBlock,
toBlock,
childStartBlock,
startBlockParameter,
}: {
address?: Address | readonly Address[];
event: AbiEvent;
parameter: string;
chainId: number;
sourceId: string;
fromBlock: number | undefined;
toBlock: number | undefined;
childStartBlock?: number;
startBlockParameter?: string;
}): LogFactory {
let address: Address | Address[] | undefined;
if (_address === undefined) {
// noop
} else if (Array.isArray(_address)) {
address = dedupe(_address)
.map(toLowerCase)
.sort((a, b) => (a < b ? -1 : 1));
} else {
address = toLowerCase(_address);
}

const eventSelector = toEventSelector(event);

// Get the location of the child address parameter
const childAddressLocation = getParameterLocation(
event,
parameter,
"address",
);

// Get the location of the start block parameter if specified
let childStartBlockLocation: ParameterLocation | undefined;
if (startBlockParameter) {
// For start block, we accept uint256 or similar numeric types
// We don't enforce a specific type since it could be uint256, uint64, etc.
childStartBlockLocation = getParameterLocation(event, startBlockParameter);
}

const id = `log_${Array.isArray(address) ? address.join("_") : address}_${chainId}_${childAddressLocation}_${eventSelector}_${fromBlock ?? "undefined"}_${toBlock ?? "undefined"}`;

return {
id: `log_${Array.isArray(address) ? address.join("_") : address}_${chainId}_offset${offset}_${eventSelector}_${fromBlock ?? "undefined"}_${toBlock ?? "undefined"}`,
id,
type: "log",
chainId,
sourceId,
address,
eventSelector,
childAddressLocation: `offset${offset}`,
childAddressLocation,
fromBlock,
toBlock,
childStartBlock,
childStartBlockLocation,
};
}
13 changes: 13 additions & 0 deletions packages/core/src/config/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ export type Factory<event extends AbiEvent = AbiEvent> = {
startBlock?: number | "latest";
/** To block */
endBlock?: number | "latest";
/**
* Static block number to start indexing child contract events from.
* If specified, all child contracts will be indexed from this block.
*/
childStartBlock?: number;
/**
* Name of the factory event parameter that contains the block number
* to start indexing the child contract from.
*/
startBlockParameter?: Exclude<
ParameterNames<event["inputs"][number]>,
undefined
>;
};

export const factory = <event extends AbiEvent>(factory: Factory<event>) =>
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,21 @@ export type LogFactory = {
childAddressLocation: "topic1" | "topic2" | "topic3" | `offset${number}`;
fromBlock: number | undefined;
toBlock: number | undefined;
/**
* Static block number to use as the start block for all child contracts.
* If set, overrides the factory event's block number.
*/
childStartBlock?: number | undefined;
/**
* Location of the start block parameter in the factory event.
* Similar to childAddressLocation but for extracting the start block.
*/
childStartBlockLocation?:
| "topic1"
| "topic2"
| "topic3"
| `offset${number}`
| undefined;
};

// Fragments
Expand Down
Loading