From aee56dce280c78ebab8f3345269ceb4fd6a97db7 Mon Sep 17 00:00:00 2001 From: Joydeep Date: Mon, 16 Dec 2024 16:03:55 +0530 Subject: [PATCH 1/5] feat(core): update build(factory.ts), config(address.ts) to support breaking down struct to find param --- examples/feature-factory/src/LlamaCore.ts | 13 ---- packages/core/src/build/factory.test.ts | 73 +++++++++++++++++++++++ packages/core/src/build/factory.ts | 45 +++++++++++++- packages/core/src/config/address.ts | 19 +++++- packages/core/src/utils/offset.ts | 2 +- 5 files changed, 134 insertions(+), 18 deletions(-) delete mode 100644 examples/feature-factory/src/LlamaCore.ts diff --git a/examples/feature-factory/src/LlamaCore.ts b/examples/feature-factory/src/LlamaCore.ts deleted file mode 100644 index 08ad1f418..000000000 --- a/examples/feature-factory/src/LlamaCore.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ponder } from "ponder:registry"; - -ponder.on("LlamaCore:ActionCreated", async ({ event }) => { - console.log( - `Handling ActionCreated event from LlamaCore @ ${event.log.address}`, - ); -}); - -ponder.on("LlamaPolicy:Initialized", async ({ event }) => { - console.log( - `Handling Initialized event from LlamaPolicy @ ${event.log.address}`, - ); -}); diff --git a/packages/core/src/build/factory.test.ts b/packages/core/src/build/factory.test.ts index fc373ec44..f2089187a 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 000e38c17..453e70401 100644 --- a/packages/core/src/build/factory.ts +++ b/packages/core/src/build/factory.ts @@ -1,7 +1,7 @@ import type { LogFactory } from "@/sync/source.js"; import { toLowerCase } from "@/utils/lowercase.js"; -import { getBytesConsumedByParam } from "@/utils/offset.js"; import { dedupe } from "@ponder/common"; +import { getBytesConsumedByParam, hasDynamicChild } from "@/utils/offset.js"; import type { AbiEvent } from "abitype"; import { type Address, toEventSelector } from "viem"; @@ -16,6 +16,12 @@ export function buildLogFactory({ parameter: string; chainId: number; }): LogFactory { + const parameterParts = parameter.split("."); + + let offset = 0; + + parameter = parameterParts[0]!; + const address = Array.isArray(_address) ? dedupe(_address).map(toLowerCase) : toLowerCase(_address); @@ -52,11 +58,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; From f2f8a54efdab5f43ee345acce22ec4d7fa9634a8 Mon Sep 17 00:00:00 2001 From: Joydeep Date: Mon, 16 Dec 2024 16:05:52 +0530 Subject: [PATCH 2/5] feat(example): add example for showcasing new factory --- .../feature-factory/abis/ChildContractAbi.ts | 62 +++++++++++++++++++ examples/feature-factory/ponder.config.ts | 17 ++++- examples/feature-factory/ponder.schema.ts | 4 ++ examples/feature-factory/src/index.ts | 24 +++++++ 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 examples/feature-factory/abis/ChildContractAbi.ts create mode 100644 examples/feature-factory/src/index.ts 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/index.ts b/examples/feature-factory/src/index.ts new file mode 100644 index 000000000..3f55a220c --- /dev/null +++ b/examples/feature-factory/src/index.ts @@ -0,0 +1,24 @@ +import { ponder } from "ponder:registry"; +import { childContract } from "../ponder.schema"; + +ponder.on("LlamaCore:ActionCreated", async ({ event }) => { + console.log( + `Handling ActionCreated event from LlamaCore @ ${event.log.address}`, + ); +}); + +ponder.on("LlamaPolicy:Initialized", async ({ event }) => { + console.log( + `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}`, + ); +}); From 8a5dbaf0a14f92369a9d134fb095f3623436b4cf Mon Sep 17 00:00:00 2001 From: Joydeep Date: Mon, 16 Dec 2024 23:50:41 +0530 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20add=20check=20and=20throw=20necessar?= =?UTF-8?q?y=20errors=20and=20refactors=20params=20to=20=F0=9F=90=AA=20cas?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/build/factory.test.ts | 18 +++++++++--------- packages/core/src/build/factory.ts | 4 ++++ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/core/src/build/factory.test.ts b/packages/core/src/build/factory.test.ts index f2089187a..5731bea4b 100644 --- a/packages/core/src/build/factory.test.ts +++ b/packages/core/src/build/factory.test.ts @@ -6,17 +6,17 @@ const llamaFactoryEventAbiItem = parseAbiItem( "event LlamaInstanceCreated(address indexed deployer, string indexed name, address llamaCore, address llamaExecutor, address llamaPolicy, uint256 chainId)", ); -const FactoryEventSimpleParamsAbiItem = parseAbiItem([ +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([ +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([ +const factoryEventWithDynamicChildParamsAbiItem2 = parseAbiItem([ "event ChildCreated(address creator, ChildInfo child, uint256 timestamp)", "struct ChildInfo { address childAddress; string name; uint256 initialValue; uint256 creationTime; address creator; }", ]); @@ -80,14 +80,14 @@ test("buildLogFactory throws if provided parameter not found in inputs", () => { test("buildLogFactory handles CreateMarket", () => { const criteria = buildLogFactory({ address: "0xa", - event: FactoryEventSimpleParamsAbiItem, + event: factoryEventSimpleParamsAbiItem, parameter: "marketParams.oracle", chainId: 1, }); expect(criteria).toMatchObject({ address: "0xa", - eventSelector: getEventSelector(FactoryEventSimpleParamsAbiItem), + eventSelector: getEventSelector(factoryEventSimpleParamsAbiItem), childAddressLocation: "offset64", }); }); @@ -95,14 +95,14 @@ test("buildLogFactory handles CreateMarket", () => { test("buildLogFactory handles ChildCreated", () => { const criteria = buildLogFactory({ address: "0xa", - event: FactoryEventWithDynamicChildParamsAbiItem, + event: factoryEventWithDynamicChildParamsAbiItem, parameter: "child.childAddress", chainId: 1, }); expect(criteria).toMatchObject({ address: "0xa", - eventSelector: getEventSelector(FactoryEventWithDynamicChildParamsAbiItem), + eventSelector: getEventSelector(factoryEventWithDynamicChildParamsAbiItem), childAddressLocation: "offset32", }); }); @@ -110,14 +110,14 @@ test("buildLogFactory handles ChildCreated", () => { test("buildLogFactory handles ChildCreated", () => { const criteria = buildLogFactory({ address: "0xa", - event: FactoryEventWithDynamicChildParamsAbiItem2, + event: factoryEventWithDynamicChildParamsAbiItem2, parameter: "child.childAddress", chainId: 1, }); expect(criteria).toMatchObject({ address: "0xa", - eventSelector: getEventSelector(FactoryEventWithDynamicChildParamsAbiItem2), + eventSelector: getEventSelector(factoryEventWithDynamicChildParamsAbiItem2), childAddressLocation: "offset96", }); }); diff --git a/packages/core/src/build/factory.ts b/packages/core/src/build/factory.ts index 453e70401..7b3605f9c 100644 --- a/packages/core/src/build/factory.ts +++ b/packages/core/src/build/factory.ts @@ -22,6 +22,10 @@ export function buildLogFactory({ parameter = parameterParts[0]!; + if (parameter === undefined) { + throw new Error("No parameter provided."); + } + const address = Array.isArray(_address) ? dedupe(_address).map(toLowerCase) : toLowerCase(_address); From 211b0f61d3eb98f8395d79077803aa65169cb7ca Mon Sep 17 00:00:00 2001 From: Joydeep Date: Mon, 13 Jan 2025 00:05:58 +0400 Subject: [PATCH 4/5] refac: follow camalcase & avoid reinitiating function params --- packages/core/src/build/factory.test.ts | 2 +- packages/core/src/build/factory.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/core/src/build/factory.test.ts b/packages/core/src/build/factory.test.ts index 5731bea4b..eec0c8477 100644 --- a/packages/core/src/build/factory.test.ts +++ b/packages/core/src/build/factory.test.ts @@ -68,7 +68,7 @@ test("buildLogFactory throws if provided parameter not found in inputs", () => { expect(() => buildLogFactory({ address: "0xa", - event: FactoryEventSimpleParamsAbiItem, + event: factoryEventSimpleParamsAbiItem, parameter: "marketParams.fake", chainId: 1, }), diff --git a/packages/core/src/build/factory.ts b/packages/core/src/build/factory.ts index 7b3605f9c..14ef2c939 100644 --- a/packages/core/src/build/factory.ts +++ b/packages/core/src/build/factory.ts @@ -20,9 +20,9 @@ export function buildLogFactory({ let offset = 0; - parameter = parameterParts[0]!; + const firstParameterSegment = parameterParts[0]; - if (parameter === undefined) { + if (firstParameterSegment === undefined) { throw new Error("No parameter provided."); } @@ -34,7 +34,7 @@ export function buildLogFactory({ // 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 === parameter); + .findIndex((input) => input.name === firstParameterSegment); if (indexedInputPosition > -1) { return { @@ -51,12 +51,12 @@ export function buildLogFactory({ (x) => !("indexed" in x && x.indexed), ); const nonIndexedInputPosition = nonIndexedInputs.findIndex( - (input) => input.name === parameter, + (input) => input.name === firstParameterSegment, ); if (nonIndexedInputPosition === -1) { throw new Error( - `Factory event parameter not found in factory event signature. Got '${parameter}', expected one of [${event.inputs + `Factory event parameter not found in factory event signature. Got '${firstParameterSegment}', expected one of [${event.inputs .map((i) => `'${i.name}'`) .join(", ")}].`, ); @@ -70,7 +70,9 @@ export function buildLogFactory({ for (let i = 1; i < parameterParts.length; i++) { if (!("components" in prvInput)) { - throw new Error(`Parameter ${parameter} is not a tuple or struct type`); + throw new Error( + `Parameter ${firstParameterSegment} is not a tuple or struct type`, + ); } const dynamicChildFlag = hasDynamicChild(prvInput); @@ -84,14 +86,14 @@ export function buildLogFactory({ const components = prvInput.components; - parameter = parameterParts[i]!; + const nextParameterSegment = parameterParts[i]!; const inputIndex = components.findIndex( - (input) => input.name === parameter, + (input) => input.name === nextParameterSegment, ); if (inputIndex === -1) { throw new Error( - `Factory event parameter not found in factory event signature. Got '${parameter}', expected one of [${components + `Factory event parameter not found in factory event signature. Got '${nextParameterSegment}', expected one of [${components .map((i) => `'${i.name}'`) .join(", ")}].`, ); From dc4a1bd400a6652459ecbbfa188a42b3f557be04 Mon Sep 17 00:00:00 2001 From: Joydeep Date: Tue, 14 Jan 2025 02:25:42 +0400 Subject: [PATCH 5/5] fix: handle where a struct/array is one of the indexed arguments and the user attempts to use one of its properties as the child address parameter. --- packages/core/src/build/factory.test.ts | 16 ++++++++++++++++ packages/core/src/build/factory.ts | 7 ++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/core/src/build/factory.test.ts b/packages/core/src/build/factory.test.ts index eec0c8477..7ac93eb92 100644 --- a/packages/core/src/build/factory.test.ts +++ b/packages/core/src/build/factory.test.ts @@ -21,6 +21,11 @@ const factoryEventWithDynamicChildParamsAbiItem2 = parseAbiItem([ "struct ChildInfo { address childAddress; string name; uint256 initialValue; uint256 creationTime; address creator; }", ]); +const factoryEventWithIndexedDynamicChildParamsAbiItem = parseAbiItem([ + "event ChildCreated(address indexed creator, ChildInfo indexed 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({ @@ -121,3 +126,14 @@ test("buildLogFactory handles ChildCreated", () => { childAddressLocation: "offset96", }); }); + +test("buildLogFactory handles indexed ChildCreated", () => { + expect(() => + buildLogFactory({ + address: "0xa", + event: factoryEventWithIndexedDynamicChildParamsAbiItem, + parameter: "child.childAddress", + chainId: 1, + }), + ).toThrowError("Child parameters of indexed parameters are not accessible"); +}); diff --git a/packages/core/src/build/factory.ts b/packages/core/src/build/factory.ts index 14ef2c939..72ea4c029 100644 --- a/packages/core/src/build/factory.ts +++ b/packages/core/src/build/factory.ts @@ -1,7 +1,7 @@ import type { LogFactory } from "@/sync/source.js"; import { toLowerCase } from "@/utils/lowercase.js"; -import { dedupe } from "@ponder/common"; import { getBytesConsumedByParam, hasDynamicChild } from "@/utils/offset.js"; +import { dedupe } from "@ponder/common"; import type { AbiEvent } from "abitype"; import { type Address, toEventSelector } from "viem"; @@ -37,6 +37,11 @@ export function buildLogFactory({ .findIndex((input) => input.name === firstParameterSegment); if (indexedInputPosition > -1) { + if (parameterParts.length !== 1) { + throw new Error( + "Child parameters of indexed parameters are not accessible", + ); + } return { type: "log", chainId,