diff --git a/examples/feature-factory/abis/ChildContractAbi.ts b/examples/feature-factory/abis/ChildContractAbi.ts new file mode 100644 index 000000000..a73fd9c1f --- /dev/null +++ b/examples/feature-factory/abis/ChildContractAbi.ts @@ -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; diff --git a/examples/feature-factory/ponder.config.ts b/examples/feature-factory/ponder.config.ts index 0a7a1ce36..823017ccc 100644 --- a/examples/feature-factory/ponder.config.ts +++ b/examples/feature-factory/ponder.config.ts @@ -1,8 +1,8 @@ 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"; @@ -10,6 +10,11 @@ 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: { @@ -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, + }, }, }); diff --git a/examples/feature-factory/ponder.schema.ts b/examples/feature-factory/ponder.schema.ts index 8f349ba3a..2de5c627b 100644 --- a/examples/feature-factory/ponder.schema.ts +++ b/examples/feature-factory/ponder.schema.ts @@ -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(), +})); diff --git a/examples/feature-factory/src/LlamaCore.ts b/examples/feature-factory/src/index.ts similarity index 50% rename from examples/feature-factory/src/LlamaCore.ts rename to examples/feature-factory/src/index.ts index 08ad1f418..3f55a220c 100644 --- a/examples/feature-factory/src/LlamaCore.ts +++ b/examples/feature-factory/src/index.ts @@ -1,4 +1,5 @@ import { ponder } from "ponder:registry"; +import { childContract } from "../ponder.schema"; ponder.on("LlamaCore:ActionCreated", async ({ event }) => { console.log( @@ -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}`, + ); +}); diff --git a/packages/core/src/build/factory.test.ts b/packages/core/src/build/factory.test.ts index fc373ec44..5731bea4b 100644 --- a/packages/core/src/build/factory.test.ts +++ b/packages/core/src/build/factory.test.ts @@ -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([ + "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({ @@ -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", + }); +}); diff --git a/packages/core/src/build/factory.ts b/packages/core/src/build/factory.ts index c82534001..eecd5864c 100644 --- a/packages/core/src/build/factory.ts +++ b/packages/core/src/build/factory.ts @@ -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"; @@ -15,6 +15,16 @@ export function buildLogFactory({ parameter: string; chainId: number; }): LogFactory { + const parameterParts = parameter.split("."); + + let offset = 0; + + parameter = parameterParts[0]!; + + if (parameter === undefined) { + throw new Error("No parameter provided."); + } + const address = Array.isArray(_address) ? _address.map(toLowerCase) : toLowerCase(_address); @@ -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, diff --git a/packages/core/src/config/address.ts b/packages/core/src/config/address.ts index 42e202bba..761440c0e 100644 --- a/packages/core/src/config/address.ts +++ b/packages/core/src/config/address.ts @@ -1,4 +1,19 @@ -import type { AbiEvent } from "viem"; +import type { AbiEvent, AbiParameter } from "viem"; + +// Add a type helper to handle nested parameters +type NestedParameter = T extends { + components: readonly AbiParameter[]; +} + ? `${Exclude}.${NestedParameterNames}` + : Exclude; + +type NestedParameterNames = + T extends readonly [ + infer First extends AbiParameter, + ...infer Rest extends AbiParameter[], + ] + ? NestedParameter | NestedParameterNames + : never; export type Factory = { /** Address of the factory contract that creates this contract. */ @@ -6,7 +21,7 @@ export type Factory = { /** 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; + parameter: NestedParameterNames; }; export const factory = (factory: Factory) => diff --git a/packages/core/src/utils/offset.ts b/packages/core/src/utils/offset.ts index 6d5874454..55536f812 100644 --- a/packages/core/src/utils/offset.ts +++ b/packages/core/src/utils/offset.ts @@ -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;