Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/factory supports parameter within struct #1343

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
62 changes: 62 additions & 0 deletions examples/feature-factory/abis/ChildContractAbi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export const ChildContractAbi = [
{
inputs: [
{ internalType: "string", name: "_name", type: "string" },
{ internalType: "uint256", name: "_initialValue", type: "uint256" },
],
stateMutability: "nonpayable",
type: "constructor",
},
{
anonymous: false,
inputs: [
{
indexed: true,
internalType: "address",
name: "child",
type: "address",
},
{
indexed: true,
internalType: "address",
name: "updater",
type: "address",
},
{
indexed: false,
internalType: "uint256",
name: "oldValue",
type: "uint256",
},
{
indexed: false,
internalType: "uint256",
name: "newValue",
type: "uint256",
},
],
name: "ValueUpdated",
type: "event",
},
{
inputs: [],
name: "factory",
outputs: [{ internalType: "address", name: "", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ internalType: "uint256", name: "_newValue", type: "uint256" }],
name: "setValue",
outputs: [],
stateMutability: "nonpayable",
type: "function",
},
{
inputs: [],
name: "value",
outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
] as const;
17 changes: 16 additions & 1 deletion examples/feature-factory/ponder.config.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { parseAbiItem } from "abitype";
import { createConfig, factory } from "ponder";

import { http } from "viem";

import { ChildContractAbi } from "./abis/ChildContractAbi";
import { LlamaCoreAbi } from "./abis/LlamaCoreAbi";
import { LlamaPolicyAbi } from "./abis/LlamaPolicyAbi";

const llamaFactoryEvent = parseAbiItem(
"event LlamaInstanceCreated(address indexed deployer, string indexed name, address llamaCore, address llamaExecutor, address llamaPolicy, uint256 chainId)",
);

const FactoryEvent = parseAbiItem([
"event ChildCreated(address indexed creator, ChildInfo child, uint256 indexed timestamp)",
"struct ChildInfo { address childAddress; string name; uint256 initialValue; uint256 creationTime; address creator; }",
]);

export default createConfig({
networks: {
sepolia: {
Expand Down Expand Up @@ -38,5 +43,15 @@ export default createConfig({
}),
startBlock: 4121269,
},
ChildContract: {
network: "sepolia",
abi: ChildContractAbi,
address: factory({
address: "0x021626321f74956da6d64605780D738A569F24DD",
event: FactoryEvent,
parameter: "child.childAddress",
}),
startBlock: 7262700,
},
},
});
4 changes: 4 additions & 0 deletions examples/feature-factory/ponder.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import { onchainTable } from "ponder";
export const llama = onchainTable("llama", (t) => ({
id: t.text().primaryKey(),
}));

export const childContract = onchainTable("childContract", (t) => ({
id: t.text().primaryKey(),
}));
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ponder } from "ponder:registry";
import { childContract } from "../ponder.schema";

ponder.on("LlamaCore:ActionCreated", async ({ event }) => {
console.log(
Expand All @@ -11,3 +12,13 @@ ponder.on("LlamaPolicy:Initialized", async ({ event }) => {
`Handling Initialized event from LlamaPolicy @ ${event.log.address}`,
);
});

ponder.on("ChildContract:ValueUpdated", async ({ event, context }) => {
const { child, updater, oldValue, newValue } = event.args;
context.db.insert(childContract).values({
id: child,
});
console.log(
`Handling ValueUpdated event from ChildContract @ ${event.log.address}`,
);
});
73 changes: 73 additions & 0 deletions packages/core/src/build/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ const llamaFactoryEventAbiItem = parseAbiItem(
"event LlamaInstanceCreated(address indexed deployer, string indexed name, address llamaCore, address llamaExecutor, address llamaPolicy, uint256 chainId)",
);

const factoryEventSimpleParamsAbiItem = parseAbiItem([
"event CreateMarket(bytes32 indexed id ,MarketParams marketParams)",
"struct MarketParams {address loanToken; address collateralToken; address oracle; address irm; uint256 lltv;}",
]);

const factoryEventWithDynamicChildParamsAbiItem = parseAbiItem([
"event ChildCreated(address indexed creator, ChildInfo child, uint256 indexed timestamp)",
"struct ChildInfo { address childAddress; string name; uint256 initialValue; uint256 creationTime; address creator; }",
]);

const factoryEventWithDynamicChildParamsAbiItem2 = parseAbiItem([
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which of the fields here is dynamic? This one still looks static to me.

"event ChildCreated(address creator, ChildInfo child, uint256 timestamp)",
"struct ChildInfo { address childAddress; string name; uint256 initialValue; uint256 creationTime; address creator; }",
]);

test("buildLogFactory throws if provided parameter not found in inputs", () => {
expect(() =>
buildLogFactory({
Expand Down Expand Up @@ -48,3 +63,61 @@ test("buildLogFactory handles LlamaInstanceCreated llamaPolicy", () => {
childAddressLocation: "offset64",
});
});

test("buildLogFactory throws if provided parameter not found in inputs", () => {
expect(() =>
buildLogFactory({
address: "0xa",
event: FactoryEventSimpleParamsAbiItem,
parameter: "marketParams.fake",
chainId: 1,
}),
).toThrowError(
"Factory event parameter not found in factory event signature. Got 'fake', expected one of ['loanToken', 'collateralToken', 'oracle', 'irm', 'lltv'].",
);
});

test("buildLogFactory handles CreateMarket", () => {
const criteria = buildLogFactory({
address: "0xa",
event: factoryEventSimpleParamsAbiItem,
parameter: "marketParams.oracle",
chainId: 1,
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(factoryEventSimpleParamsAbiItem),
childAddressLocation: "offset64",
});
});

test("buildLogFactory handles ChildCreated", () => {
const criteria = buildLogFactory({
address: "0xa",
event: factoryEventWithDynamicChildParamsAbiItem,
parameter: "child.childAddress",
chainId: 1,
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(factoryEventWithDynamicChildParamsAbiItem),
childAddressLocation: "offset32",
});
});

test("buildLogFactory handles ChildCreated", () => {
const criteria = buildLogFactory({
address: "0xa",
event: factoryEventWithDynamicChildParamsAbiItem2,
parameter: "child.childAddress",
chainId: 1,
});

expect(criteria).toMatchObject({
address: "0xa",
eventSelector: getEventSelector(factoryEventWithDynamicChildParamsAbiItem2),
childAddressLocation: "offset96",
});
});
49 changes: 47 additions & 2 deletions packages/core/src/build/factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LogFactory } from "@/sync/source.js";
import { toLowerCase } from "@/utils/lowercase.js";
import { getBytesConsumedByParam } from "@/utils/offset.js";
import { getBytesConsumedByParam, hasDynamicChild } from "@/utils/offset.js";
import type { AbiEvent } from "abitype";
import { type Address, getEventSelector } from "viem";

Expand All @@ -15,6 +15,16 @@ export function buildLogFactory({
parameter: string;
chainId: number;
}): LogFactory {
const parameterParts = parameter.split(".");

let offset = 0;

parameter = parameterParts[0]!;

Comment on lines +18 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be an explicit validation check that this exists, like if (parameter === undefined) throw new Error(...) like on line 53.

Also, I think these lines should be below where we build the address and eventSelector.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the check, but I think these lines should be before building the address and eventSelector. As there's the possibility of parameter being undefined, the building of the address and eventSelector will be unnecessary. 🤔

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, we just generally try to group concerns for readability, eg first parse address, then parse selector, then event, then parameter.

Another nit - we avoid re-assigning function parameters like you're doing with parameter here, would prefer something like

const firstParameterSegment = parameterSegments[0];

if (parameter === undefined) {
throw new Error("No parameter provided.");
}

const address = Array.isArray(_address)
? _address.map(toLowerCase)
: toLowerCase(_address);
Expand Down Expand Up @@ -51,11 +61,46 @@ export function buildLogFactory({
);
}

let offset = 0;
for (let i = 0; i < nonIndexedInputPosition; i++) {
offset += getBytesConsumedByParam(nonIndexedInputs[i]!);
}

let prvInput = nonIndexedInputs[nonIndexedInputPosition]!;

for (let i = 1; i < parameterParts.length; i++) {
if (!("components" in prvInput)) {
throw new Error(`Parameter ${parameter} is not a tuple or struct type`);
}

const dynamicChildFlag = hasDynamicChild(prvInput);

if (dynamicChildFlag) {
for (let j = nonIndexedInputPosition; j < nonIndexedInputs.length; j++) {
// bytes consumed by successor siblings after the current one
offset += getBytesConsumedByParam(nonIndexedInputs[j]!);
}
}

const components = prvInput.components;

parameter = parameterParts[i]!;

const inputIndex = components.findIndex(
(input) => input.name === parameter,
);
if (inputIndex === -1) {
throw new Error(
`Factory event parameter not found in factory event signature. Got '${parameter}', expected one of [${components
.map((i) => `'${i.name}'`)
.join(", ")}].`,
);
}
for (let j = 0; j < inputIndex; j++) {
offset += getBytesConsumedByParam(components[j]!);
}
prvInput = components[inputIndex]!;
}

return {
type: "log",
chainId,
Expand Down
19 changes: 17 additions & 2 deletions packages/core/src/config/address.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
import type { AbiEvent } from "viem";
import type { AbiEvent, AbiParameter } from "viem";

// Add a type helper to handle nested parameters
type NestedParameter<T extends AbiParameter> = T extends {
components: readonly AbiParameter[];
}
? `${Exclude<T["name"], undefined>}.${NestedParameterNames<T["components"]>}`
: Exclude<T["name"], undefined>;

type NestedParameterNames<T extends readonly AbiParameter[]> =
T extends readonly [
infer First extends AbiParameter,
...infer Rest extends AbiParameter[],
]
? NestedParameter<First> | NestedParameterNames<Rest>
: never;

export type Factory<event extends AbiEvent = AbiEvent> = {
/** Address of the factory contract that creates this contract. */
address: `0x${string}` | readonly `0x${string}`[];
/** ABI event that announces the creation of a new instance of this contract. */
event: event;
/** Name of the factory event parameter that contains the new child contract address. */
parameter: Exclude<event["inputs"][number]["name"], undefined>;
parameter: NestedParameterNames<event["inputs"]>;
};

export const factory = <event extends AbiEvent>(factory: Factory<event>) =>
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/offset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function getBytesConsumedByParam(param: AbiParameter): number {
});
}

function hasDynamicChild(param: AbiParameter) {
export function hasDynamicChild(param: AbiParameter) {
const { type } = param;
if (type === "string") return true;
if (type === "bytes") return true;
Expand Down
Loading