diff --git a/apps/indexer/config.yaml b/apps/indexer/config.yaml index 0ac8873..0e772ec 100644 --- a/apps/indexer/config.yaml +++ b/apps/indexer/config.yaml @@ -1,6 +1,12 @@ name: Allo Indexer unordered_multichain_mode: true raw_events: true + +field_selection: + transaction_fields: + - "hash" + - "transactionIndex" + - "from" ########################## # CONTRACTS # ########################## diff --git a/packages/chain-providers/src/providers/evmProvider.ts b/packages/chain-providers/src/providers/evmProvider.ts index 22c048a..65df554 100644 --- a/packages/chain-providers/src/providers/evmProvider.ts +++ b/packages/chain-providers/src/providers/evmProvider.ts @@ -16,6 +16,7 @@ import { fallback, FallbackTransport, GetBlockReturnType, + GetTransactionReturnType, Hex, http, HttpTransport, @@ -65,6 +66,10 @@ export class EvmProvider { return this.chain?.contracts?.multicall3?.address; } + async getTransaction(hash: Hex): Promise { + return this.client.getTransaction({ hash }); + } + /** * Retrieves the balance of the specified address. * @param {Address} address The address for which to retrieve the balance. diff --git a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts index da4681a..ca0b22f 100644 --- a/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts +++ b/packages/chain-providers/test/unit/providers/evmProvider.service.spec.ts @@ -14,6 +14,7 @@ import { import { arrayAbiFixture, structAbiFixture } from "../../fixtures/batchRequest.fixture.js"; const mockClient = { + getTransaction: vi.fn(), getBalance: vi.fn(), getBlockNumber: vi.fn(), getGasPrice: vi.fn(), @@ -70,6 +71,20 @@ describe("EvmProvider", () => { }).toThrowError(RpcUrlsEmpty); }); + describe("getTransaction", () => { + it("returns the transaction for the given hash", async () => { + viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); + const hash = "0x123456789"; + const expectedTransaction = { from: "0x123456789", to: "0x987654321" }; + vi.spyOn(mockClient, "getTransaction").mockResolvedValue(expectedTransaction); + + const transaction = await viemProvider.getTransaction(hash); + + expect(transaction).toBe(expectedTransaction); + expect(mockClient.getTransaction).toHaveBeenCalledWith({ hash }); + }); + }); + describe("getBalance", () => { it("returns the balance of the specified address", async () => { viemProvider = new EvmProvider(defaultRpcUrls, defaultMockChain, mockLogger); diff --git a/packages/data-flow/test/unit/eventsFetcher.spec.ts b/packages/data-flow/test/unit/eventsFetcher.spec.ts index 371f616..c2f1dad 100644 --- a/packages/data-flow/test/unit/eventsFetcher.spec.ts +++ b/packages/data-flow/test/unit/eventsFetcher.spec.ts @@ -28,6 +28,7 @@ describe("EventsFetcher", () => { srcAddress: "0x1234567890123456789012345678901234567890", logIndex: 0, params: { contractAddress: "0x1234" }, + transactionFields: { hash: "0x1234", transactionIndex: 0 }, }, { chainId: 1, @@ -38,6 +39,7 @@ describe("EventsFetcher", () => { srcAddress: "0x1234567890123456789012345678901234567890", logIndex: 0, params: { contractAddress: "0x1234" }, + transactionFields: { hash: "0x1234", transactionIndex: 1 }, }, ]; const chainId = 1n; diff --git a/packages/processors/package.json b/packages/processors/package.json index 5fec498..97af619 100644 --- a/packages/processors/package.json +++ b/packages/processors/package.json @@ -28,10 +28,12 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { + "@grants-stack-indexer/chain-providers": "workspace:*", "@grants-stack-indexer/metadata": "workspace:*", "@grants-stack-indexer/pricing": "workspace:*", "@grants-stack-indexer/repository": "workspace:*", "@grants-stack-indexer/shared": "workspace:*", - "viem": "2.21.19" + "viem": "2.21.19", + "zod": "3.23.8" } } diff --git a/packages/processors/src/abis/allo-v2/v1/DirectGrantsLiteStrategy.ts b/packages/processors/src/abis/allo-v2/v1/DirectGrantsLiteStrategy.ts new file mode 100644 index 0000000..cacbf28 --- /dev/null +++ b/packages/processors/src/abis/allo-v2/v1/DirectGrantsLiteStrategy.ts @@ -0,0 +1,857 @@ +export default [ + { + inputs: [ + { + internalType: "address", + name: "_allo", + type: "address", + }, + { + internalType: "string", + name: "_name", + type: "string", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ALLOCATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ENDED", + type: "error", + }, + { + inputs: [], + name: "ALREADY_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "AMOUNT_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "ANCHOR_ERROR", + type: "error", + }, + { + inputs: [], + name: "ARRAY_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "INVALID", + type: "error", + }, + { + inputs: [], + name: "INVALID_ADDRESS", + type: "error", + }, + { + inputs: [], + name: "INVALID_FEE", + type: "error", + }, + { + inputs: [], + name: "INVALID_METADATA", + type: "error", + }, + { + inputs: [], + name: "INVALID_REGISTRATION", + type: "error", + }, + { + inputs: [], + name: "IS_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "MISMATCH", + type: "error", + }, + { + inputs: [], + name: "NONCE_NOT_AVAILABLE", + type: "error", + }, + { + inputs: [], + name: "NON_ZERO_VALUE", + type: "error", + }, + { + inputs: [], + name: "NOT_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "NOT_ENOUGH_FUNDS", + type: "error", + }, + { + inputs: [], + name: "NOT_IMPLEMENTED", + type: "error", + }, + { + inputs: [], + name: "NOT_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "NOT_PENDING_OWNER", + type: "error", + }, + { + inputs: [], + name: "POOL_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "POOL_INACTIVE", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_ALREADY_ACCEPTED", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + name: "RECIPIENT_ERROR", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_NOT_ACCEPTED", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "UNAUTHORIZED", + type: "error", + }, + { + inputs: [], + name: "ZERO_ADDRESS", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Allocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Distributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "poolId", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bool", + name: "active", + type: "bool", + }, + ], + name: "PoolActive", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "rowIndex", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "fullRow", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RecipientStatusUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Registered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "registrationStartTime", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "registrationEndTime", + type: "uint64", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "TimestampsUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint8", + name: "status", + type: "uint8", + }, + ], + name: "UpdatedRegistration", + type: "event", + }, + { + inputs: [], + name: "NATIVE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "allocate", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "distribute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getAllo", + outputs: [ + { + internalType: "contract IAllo", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes[]", + name: "_data", + type: "bytes[]", + }, + ], + name: "getPayouts", + outputs: [ + { + components: [ + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + internalType: "struct IStrategy.PayoutSummary[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolId", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipient", + outputs: [ + { + components: [ + { + internalType: "bool", + name: "useRegistryAnchor", + type: "bool", + }, + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + ], + internalType: "struct DirectGrantsLiteStrategy.Recipient", + name: "recipient", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipientStatus", + outputs: [ + { + internalType: "enum IStrategy.Status", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getStrategyId", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "increasePoolAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_poolId", + type: "uint256", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "isPoolActive", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_allocator", + type: "address", + }, + ], + name: "isValidAllocator", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "metadataRequired", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "recipientToStatusIndexes", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "recipientsCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "registerRecipient", + outputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "registrationEndTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "registrationStartTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "uint256", + name: "statusRow", + type: "uint256", + }, + ], + internalType: "struct DirectGrantsLiteStrategy.ApplicationStatus[]", + name: "statuses", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "refRecipientsCounter", + type: "uint256", + }, + ], + name: "reviewRecipients", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "statusesBitMap", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint64", + name: "_registrationStartTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "_registrationEndTime", + type: "uint64", + }, + ], + name: "updatePoolTimestamps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "useRegistryAnchor", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; diff --git a/packages/processors/src/abis/allo-v2/v1/DirectGrantsSimpleStrategy.ts b/packages/processors/src/abis/allo-v2/v1/DirectGrantsSimpleStrategy.ts new file mode 100644 index 0000000..c802b3e --- /dev/null +++ b/packages/processors/src/abis/allo-v2/v1/DirectGrantsSimpleStrategy.ts @@ -0,0 +1,1167 @@ +export default [ + { + inputs: [ + { + internalType: "address", + name: "_allo", + type: "address", + }, + { + internalType: "string", + name: "_name", + type: "string", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ALLOCATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_EXCEEDS_POOL_AMOUNT", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ENDED", + type: "error", + }, + { + inputs: [], + name: "ALREADY_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "AMOUNT_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "ANCHOR_ERROR", + type: "error", + }, + { + inputs: [], + name: "ARRAY_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "INVALID", + type: "error", + }, + { + inputs: [], + name: "INVALID_ADDRESS", + type: "error", + }, + { + inputs: [], + name: "INVALID_FEE", + type: "error", + }, + { + inputs: [], + name: "INVALID_METADATA", + type: "error", + }, + { + inputs: [], + name: "INVALID_MILESTONE", + type: "error", + }, + { + inputs: [], + name: "INVALID_REGISTRATION", + type: "error", + }, + { + inputs: [], + name: "IS_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "MILESTONES_ALREADY_SET", + type: "error", + }, + { + inputs: [], + name: "MILESTONE_ALREADY_ACCEPTED", + type: "error", + }, + { + inputs: [], + name: "MISMATCH", + type: "error", + }, + { + inputs: [], + name: "NONCE_NOT_AVAILABLE", + type: "error", + }, + { + inputs: [], + name: "NON_ZERO_VALUE", + type: "error", + }, + { + inputs: [], + name: "NOT_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "NOT_ENOUGH_FUNDS", + type: "error", + }, + { + inputs: [], + name: "NOT_IMPLEMENTED", + type: "error", + }, + { + inputs: [], + name: "NOT_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "NOT_PENDING_OWNER", + type: "error", + }, + { + inputs: [], + name: "POOL_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "POOL_INACTIVE", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_ALREADY_ACCEPTED", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + name: "RECIPIENT_ERROR", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_NOT_ACCEPTED", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "UNAUTHORIZED", + type: "error", + }, + { + inputs: [], + name: "ZERO_ADDRESS", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Allocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Distributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "poolId", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "milestoneId", + type: "uint256", + }, + { + indexed: false, + internalType: "enum IStrategy.Status", + name: "status", + type: "uint8", + }, + ], + name: "MilestoneStatusChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "milestoneId", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + indexed: false, + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + ], + name: "MilestoneSubmitted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "enum IStrategy.Status", + name: "status", + type: "uint8", + }, + ], + name: "MilestonesReviewed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "milestonesLength", + type: "uint256", + }, + ], + name: "MilestonesSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bool", + name: "active", + type: "bool", + }, + ], + name: "PoolActive", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "enum IStrategy.Status", + name: "status", + type: "uint8", + }, + ], + name: "RecipientStatusChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Registered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "registrationStartTime", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "registrationEndTime", + type: "uint64", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "TimestampsUpdated", + type: "event", + }, + { + inputs: [], + name: "NATIVE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "allocate", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "allocatedGrantAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "distribute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "getAllo", + outputs: [ + { + internalType: "contract IAllo", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + { + internalType: "uint256", + name: "_milestoneId", + type: "uint256", + }, + ], + name: "getMilestoneStatus", + outputs: [ + { + internalType: "enum IStrategy.Status", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getMilestones", + outputs: [ + { + components: [ + { + internalType: "uint256", + name: "amountPercentage", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + { + internalType: "enum IStrategy.Status", + name: "milestoneStatus", + type: "uint8", + }, + ], + internalType: "struct DirectGrantsSimpleStrategy.Milestone[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes[]", + name: "_data", + type: "bytes[]", + }, + ], + name: "getPayouts", + outputs: [ + { + components: [ + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + internalType: "struct IStrategy.PayoutSummary[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolId", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipient", + outputs: [ + { + components: [ + { + internalType: "bool", + name: "useRegistryAnchor", + type: "bool", + }, + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + internalType: "uint256", + name: "grantAmount", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + { + internalType: "enum IStrategy.Status", + name: "recipientStatus", + type: "uint8", + }, + { + internalType: "enum IStrategy.Status", + name: "milestonesReviewStatus", + type: "uint8", + }, + ], + internalType: "struct DirectGrantsSimpleStrategy.Recipient", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipientStatus", + outputs: [ + { + internalType: "enum IStrategy.Status", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getStrategyId", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "grantAmountRequired", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "increasePoolAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_poolId", + type: "uint256", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "isPoolActive", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_allocator", + type: "address", + }, + ], + name: "isValidAllocator", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "metadataRequired", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "milestones", + outputs: [ + { + internalType: "uint256", + name: "amountPercentage", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + { + internalType: "enum IStrategy.Status", + name: "milestoneStatus", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "registerRecipient", + outputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "registrationEndTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "registrationStartTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "registryGating", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + { + internalType: "uint256", + name: "_milestoneId", + type: "uint256", + }, + ], + name: "rejectMilestone", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + { + internalType: "enum IStrategy.Status", + name: "_status", + type: "uint8", + }, + { + internalType: "bytes32", + name: "milestonesHash", + type: "bytes32", + }, + ], + name: "reviewSetMilestones", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + { + components: [ + { + internalType: "uint256", + name: "amountPercentage", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + { + internalType: "enum IStrategy.Status", + name: "milestoneStatus", + type: "uint8", + }, + ], + internalType: "struct DirectGrantsSimpleStrategy.Milestone[]", + name: "_milestones", + type: "tuple[]", + }, + ], + name: "setMilestones", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bool", + name: "_flag", + type: "bool", + }, + ], + name: "setPoolActive", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + ], + name: "setRecipientStatusToInReview", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + { + internalType: "uint256", + name: "_milestoneId", + type: "uint256", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "_metadata", + type: "tuple", + }, + ], + name: "submitMilestone", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "upcomingMilestone", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint64", + name: "_registrationStartTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "_registrationEndTime", + type: "uint64", + }, + ], + name: "updatePoolTimestamps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; diff --git a/packages/processors/src/abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.ts b/packages/processors/src/abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.ts new file mode 100644 index 0000000..8f5f0fe --- /dev/null +++ b/packages/processors/src/abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.ts @@ -0,0 +1,1172 @@ +export default [ + { + inputs: [ + { + internalType: "address", + name: "_allo", + type: "address", + }, + { + internalType: "string", + name: "_name", + type: "string", + }, + { + internalType: "contract ISignatureTransfer", + name: "_permit2", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "ALLOCATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "ALLOCATION_NOT_ENDED", + type: "error", + }, + { + inputs: [], + name: "ALREADY_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "AMOUNT_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "ANCHOR_ERROR", + type: "error", + }, + { + inputs: [], + name: "ARRAY_MISMATCH", + type: "error", + }, + { + inputs: [], + name: "INVALID", + type: "error", + }, + { + inputs: [], + name: "INVALID_ADDRESS", + type: "error", + }, + { + inputs: [], + name: "INVALID_FEE", + type: "error", + }, + { + inputs: [], + name: "INVALID_METADATA", + type: "error", + }, + { + inputs: [], + name: "INVALID_REGISTRATION", + type: "error", + }, + { + inputs: [], + name: "IS_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "MISMATCH", + type: "error", + }, + { + inputs: [], + name: "NONCE_NOT_AVAILABLE", + type: "error", + }, + { + inputs: [], + name: "NON_ZERO_VALUE", + type: "error", + }, + { + inputs: [], + name: "NOT_APPROVED_STRATEGY", + type: "error", + }, + { + inputs: [], + name: "NOT_ENOUGH_FUNDS", + type: "error", + }, + { + inputs: [], + name: "NOT_IMPLEMENTED", + type: "error", + }, + { + inputs: [], + name: "NOT_INITIALIZED", + type: "error", + }, + { + inputs: [], + name: "NOT_PENDING_OWNER", + type: "error", + }, + { + inputs: [], + name: "POOL_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "POOL_INACTIVE", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_ALREADY_ACCEPTED", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + name: "RECIPIENT_ERROR", + type: "error", + }, + { + inputs: [], + name: "RECIPIENT_NOT_ACCEPTED", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "REGISTRATION_NOT_ACTIVE", + type: "error", + }, + { + inputs: [], + name: "UNAUTHORIZED", + type: "error", + }, + { + inputs: [], + name: "ZERO_ADDRESS", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "origin", + type: "address", + }, + ], + name: "Allocated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "BatchPayoutSuccessful", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Distributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bytes32", + name: "merkleRoot", + type: "bytes32", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + indexed: false, + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + ], + name: "DistributionUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "grantee", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + name: "FundsDistributed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "poolId", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "bool", + name: "active", + type: "bool", + }, + ], + name: "PoolActive", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "rowIndex", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "fullRow", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RecipientStatusUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "Registered", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "registrationStartTime", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "registrationEndTime", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "allocationStartTime", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "allocationEndTime", + type: "uint64", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "TimestampsUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "recipientId", + type: "address", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint8", + name: "status", + type: "uint8", + }, + ], + name: "UpdatedRegistration", + type: "event", + }, + { + inputs: [], + name: "NATIVE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PERMIT2", + outputs: [ + { + internalType: "contract ISignatureTransfer", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "allocate", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "allocationEndTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "allocationStartTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "allowedTokens", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "distribute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "distributionMetadata", + outputs: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "distributionStarted", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getAllo", + outputs: [ + { + internalType: "contract IAllo", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_recipientIds", + type: "address[]", + }, + { + internalType: "bytes[]", + name: "_data", + type: "bytes[]", + }, + ], + name: "getPayouts", + outputs: [ + { + components: [ + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + internalType: "struct IStrategy.PayoutSummary[]", + name: "", + type: "tuple[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getPoolId", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipient", + outputs: [ + { + components: [ + { + internalType: "bool", + name: "useRegistryAnchor", + type: "bool", + }, + { + internalType: "address", + name: "recipientAddress", + type: "address", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "metadata", + type: "tuple", + }, + ], + internalType: "struct DonationVotingMerkleDistributionBaseStrategy.Recipient", + name: "recipient", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_recipientId", + type: "address", + }, + ], + name: "getRecipientStatus", + outputs: [ + { + internalType: "enum IStrategy.Status", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getStrategyId", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_index", + type: "uint256", + }, + ], + name: "hasBeenDistributed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "increasePoolAmount", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_poolId", + type: "uint256", + }, + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "isDistributionSet", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "isPoolActive", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_allocator", + type: "address", + }, + ], + name: "isValidAllocator", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "merkleRoot", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "metadataRequired", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes[]", + name: "data", + type: "bytes[]", + }, + ], + name: "multicall", + outputs: [ + { + internalType: "bytes[]", + name: "results", + type: "bytes[]", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + name: "recipientToStatusIndexes", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "recipientsCounter", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_data", + type: "bytes", + }, + { + internalType: "address", + name: "_sender", + type: "address", + }, + ], + name: "registerRecipient", + outputs: [ + { + internalType: "address", + name: "recipientId", + type: "address", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "registrationEndTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "registrationStartTime", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "index", + type: "uint256", + }, + { + internalType: "uint256", + name: "statusRow", + type: "uint256", + }, + ], + internalType: + "struct DonationVotingMerkleDistributionBaseStrategy.ApplicationStatus[]", + name: "statuses", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "refRecipientsCounter", + type: "uint256", + }, + ], + name: "reviewRecipients", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "sig", + type: "bytes", + }, + ], + name: "splitSignature", + outputs: [ + { + internalType: "bytes32", + name: "r", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "s", + type: "bytes32", + }, + { + internalType: "uint8", + name: "v", + type: "uint8", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + name: "statusesBitMap", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalPayoutAmount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "_merkleRoot", + type: "bytes32", + }, + { + components: [ + { + internalType: "uint256", + name: "protocol", + type: "uint256", + }, + { + internalType: "string", + name: "pointer", + type: "string", + }, + ], + internalType: "struct Metadata", + name: "_distributionMetadata", + type: "tuple", + }, + ], + name: "updateDistribution", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint64", + name: "_registrationStartTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "_registrationEndTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "_allocationStartTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "_allocationEndTime", + type: "uint64", + }, + ], + name: "updatePoolTimestamps", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "useRegistryAnchor", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index b59e1ef..2a5ffed 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -1,11 +1,22 @@ import { Changeset } from "@grants-stack-indexer/repository"; -import { AlloEvent, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; -import type { IProcessor } from "../internal.js"; +import type { IProcessor, ProcessorDependencies } from "../internal.js"; +import { UnsupportedEventException } from "../internal.js"; +import { PoolCreatedHandler } from "./handlers/index.js"; export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { - //TODO: Implement - process(_event: ProtocolEvent<"Allo", AlloEvent>): Promise { - throw new Error("Method not implemented."); + constructor( + private readonly chainId: ChainId, + private readonly dependencies: ProcessorDependencies, + ) {} + + async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { + switch (event.eventName) { + case "PoolCreated": + return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle(); + default: + throw new UnsupportedEventException("Allo", event.eventName); + } } } diff --git a/packages/processors/src/allo/handlers/index.ts b/packages/processors/src/allo/handlers/index.ts new file mode 100644 index 0000000..7f0b311 --- /dev/null +++ b/packages/processors/src/allo/handlers/index.ts @@ -0,0 +1 @@ +export * from "./poolCreated.handler.js"; diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts new file mode 100644 index 0000000..392b587 --- /dev/null +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -0,0 +1,227 @@ +import { Address, getAddress, parseUnits, zeroAddress } from "viem"; + +import type { Changeset, NewRound, PendingRoundRole } from "@grants-stack-indexer/repository"; +import type { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { isAlloNativeToken } from "@grants-stack-indexer/shared"; + +import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; +import { getRoundRoles } from "../../helpers/roles.js"; +import { RoundMetadataSchema } from "../../helpers/schemas.js"; +import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js"; +import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; +import { TokenPriceNotFoundError } from "../../internal.js"; + +type Dependencies = Pick< + ProcessorDependencies, + "evmProvider" | "pricingProvider" | "metadataProvider" | "roundRepository" +>; + +// sometimes coingecko returns no prices for 1 hour range, 2 hours works better +const TIMESTAMP_DELTA_RANGE = 2 * 60 * 60 * 1000; + +/** + /** + * Handles the PoolCreated event for the Allo protocol. + * + * This handler performs the following core actions when a new pool is created: + * - Retrieves the metadata associated with the pool + * - Determines the correct token address, handling native tokens appropriately. + * - Extracts the correct strategy information from the provided strategy ID. + * - Calculates the funded amount in USD based on the token's pricing. + * - Creates a new round object + */ +export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> { + constructor( + readonly event: ProtocolEvent<"Allo", "PoolCreated">, + private readonly chainId: ChainId, + private readonly dependencies: Dependencies, + ) {} + + async handle(): Promise { + const { metadataProvider, evmProvider } = this.dependencies; + const [metadataPointer] = this.event.params.metadata; + const { + poolId, + strategyId, + token: tokenAddress, + contractAddress: strategyAddress, + amount: fundedAmount, + } = this.event.params; + const { hash: txHash, from: txFrom } = this.event.transactionFields; + + const metadata = await metadataProvider.getMetadata<{ + round?: unknown; + application?: unknown; + }>(metadataPointer); + const parsedRoundMetadata = RoundMetadataSchema.safeParse(metadata?.round); + + const checksummedTokenAddress = getAddress(tokenAddress); + const matchTokenAddress = isAlloNativeToken(checksummedTokenAddress) + ? zeroAddress + : checksummedTokenAddress; + + const strategy = extractStrategyFromId(strategyId); + + // TODO: get token for the chain + const token = { + address: matchTokenAddress, + decimals: 18, //TODO: get decimals from token + symbol: "USDC", //TODO: get symbol from token + name: "USDC", //TODO: get name from token + }; + + let strategyTimings: StrategyTimings = { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }; + + let matchAmount = 0n; + let matchAmountInUsd = "0"; + + if (strategy) { + strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress); + + //TODO: when creating strategy handlers, should this be moved there? + if ( + strategy.name === "allov2.DonationVotingMerkleDistributionDirectTransferStrategy" && + parsedRoundMetadata.success && + token !== null + ) { + matchAmount = parseUnits( + parsedRoundMetadata.data.quadraticFundingConfig.matchingFundsAvailable.toString(), + token.decimals, + ); + + matchAmountInUsd = await this.getTokenAmountInUsd( + token, + matchAmount, + this.event.blockTimestamp, + ); + } + } + + let fundedAmountInUsd = "0"; + + if (token !== null && fundedAmount > 0n) { + fundedAmountInUsd = await this.getTokenAmountInUsd( + token, + fundedAmount, + this.event.blockTimestamp, + ); + } + + // transaction sender + const createdBy = txFrom ?? (await evmProvider.getTransaction(txHash)).from; + + const roundRoles = getRoundRoles(poolId); + + const newRound: NewRound = { + chainId: this.chainId, + id: poolId.toString(), + tags: ["allo-v2", ...(parsedRoundMetadata.success ? ["grants-stack"] : [])], + totalDonationsCount: 0, + totalAmountDonatedInUsd: "0", + uniqueDonorsCount: 0, + matchTokenAddress, + matchAmount, + matchAmountInUsd, + fundedAmount, + fundedAmountInUsd, + applicationMetadataCid: metadataPointer, + applicationMetadata: metadata?.application ?? {}, + roundMetadataCid: metadataPointer, + roundMetadata: metadata?.round ?? null, + ...strategyTimings, + ...roundRoles, + strategyAddress, + strategyId, + strategyName: strategy?.name ?? "", + createdByAddress: getAddress(createdBy), + createdAtBlock: BigInt(this.event.blockNumber), + updatedAtBlock: BigInt(this.event.blockNumber), + projectId: this.event.params.profileId, + totalDistributed: 0n, + readyForPayoutTransaction: null, + matchingDistribution: null, + }; + + const changes: Changeset[] = [ + { + type: "InsertRound", + args: { round: newRound }, + }, + ]; + + changes.push(...(await this.handlePendingRoles(this.chainId, poolId.toString()))); + + return changes; + } + + /** + * Creates the admin and manager roles for the pool and deletes the pending roles. + * @param chainId - The ID of the chain. + * @param roundId - The ID of the round. + * @returns The changesets. + * @Note + * Admin/Manager roles for the pool are emitted before the pool is created + * so a pending round role is inserted in the db. + * Now that the PoolCreated event is emitted, we can convert + * pending roles to actual round roles. + */ + private async handlePendingRoles(chainId: ChainId, roundId: string): Promise { + const { roundRepository } = this.dependencies; + const changes: Changeset[] = []; + const allPendingRoles: PendingRoundRole[] = []; + + for (const roleName of ["admin", "manager"] as const) { + const pendingRoles = await roundRepository.getPendingRoundRoles(chainId, roleName); + for (const pr of pendingRoles) { + changes.push({ + type: "InsertRoundRole", + args: { + roundRole: { + chainId, + roundId, + address: pr.address, + role: roleName, + createdAtBlock: pr.createdAtBlock, + }, + }, + }); + } + allPendingRoles.push(...pendingRoles); + } + + const pendingRoleIds = [...new Set(allPendingRoles.map((r) => r.id!))]; + if (pendingRoleIds.length > 0) { + changes.push({ + type: "DeletePendingRoundRoles", + args: { ids: pendingRoleIds }, + }); + } + + return changes; + } + + private async getTokenAmountInUsd( + token: { address: Address; decimals: number }, + amount: bigint, + timestamp: number, + ): Promise { + const { pricingProvider } = this.dependencies; + const tokenPrice = await pricingProvider.getTokenPrice( + this.chainId, + token.address, + timestamp, + timestamp + TIMESTAMP_DELTA_RANGE, + ); + + if (!tokenPrice) { + throw new TokenPriceNotFoundError(token.address, timestamp); + } + + return calculateAmountInUsd(amount, tokenPrice.priceUsd, token.decimals); + } +} diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts new file mode 100644 index 0000000..746c664 --- /dev/null +++ b/packages/processors/src/exceptions/index.ts @@ -0,0 +1,3 @@ +export * from "./tokenPriceNotFound.exception.js"; +export * from "./unsupportedEvent.exception.js"; +export * from "./invalidArgument.exception.js"; diff --git a/packages/processors/src/exceptions/invalidArgument.exception.ts b/packages/processors/src/exceptions/invalidArgument.exception.ts new file mode 100644 index 0000000..80ff703 --- /dev/null +++ b/packages/processors/src/exceptions/invalidArgument.exception.ts @@ -0,0 +1,5 @@ +export class InvalidArgument extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts b/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts new file mode 100644 index 0000000..77b6906 --- /dev/null +++ b/packages/processors/src/exceptions/tokenPriceNotFound.exception.ts @@ -0,0 +1,7 @@ +import { Address } from "@grants-stack-indexer/shared"; + +export class TokenPriceNotFoundError extends Error { + constructor(tokenAddress: Address, timestamp: number) { + super(`Token price not found for token ${tokenAddress} at timestamp ${timestamp}`); + } +} diff --git a/packages/processors/src/exceptions/unsupportedEvent.exception.ts b/packages/processors/src/exceptions/unsupportedEvent.exception.ts new file mode 100644 index 0000000..4760f12 --- /dev/null +++ b/packages/processors/src/exceptions/unsupportedEvent.exception.ts @@ -0,0 +1,10 @@ +import { ContractName } from "@grants-stack-indexer/shared"; + +export class UnsupportedEventException extends Error { + constructor( + contract: ContractName, + public readonly eventName: string, + ) { + super(`Event ${eventName} unsupported for ${contract} processor`); + } +} diff --git a/packages/processors/src/helpers/roles.ts b/packages/processors/src/helpers/roles.ts new file mode 100644 index 0000000..5eb5a88 --- /dev/null +++ b/packages/processors/src/helpers/roles.ts @@ -0,0 +1,18 @@ +import { encodePacked, keccak256, pad } from "viem"; + +/** + * Get the manager and admin roles for the pool + * Note: POOL_MANAGER_ROLE = bytes32(poolId); + * Note: POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); + * @param poolId - The ID of the pool. + * @returns The manager and admin roles. + */ +export const getRoundRoles = (poolId: bigint): { managerRole: string; adminRole: string } => { + // POOL_MANAGER_ROLE = bytes32(poolId); + const managerRole = pad(`0x${poolId.toString(16)}`); + + // POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); + const adminRawRole = encodePacked(["uint256", "string"], [poolId, "admin"]); + const adminRole = keccak256(adminRawRole); + return { managerRole, adminRole }; +}; diff --git a/packages/processors/src/helpers/schemas.ts b/packages/processors/src/helpers/schemas.ts new file mode 100644 index 0000000..6b23423 --- /dev/null +++ b/packages/processors/src/helpers/schemas.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const RoundMetadataSchema = z + .object({ + name: z.string(), + roundType: z.union([z.literal("private"), z.literal("public")]), + quadraticFundingConfig: z.object({ + matchingFundsAvailable: z.number(), + }), + }) + .passthrough(); diff --git a/packages/processors/src/helpers/strategy.ts b/packages/processors/src/helpers/strategy.ts new file mode 100644 index 0000000..a71937c --- /dev/null +++ b/packages/processors/src/helpers/strategy.ts @@ -0,0 +1,273 @@ +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { Address, Branded } from "@grants-stack-indexer/shared"; + +import DirectGrantsLiteStrategy from "../abis/allo-v2/v1/DirectGrantsLiteStrategy.js"; +import DonationVotingMerkleDistributionDirectTransferStrategy from "../abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.js"; +import { StrategyTimings } from "../internal.js"; +import { getDateFromTimestamp } from "./utils.js"; + +type SanitizedStrategyId = Branded; +type Strategy = { + id: SanitizedStrategyId; + name: string | null; + // TODO: check if groups are required + groups: string[]; +}; + +//TODO: refactor this into a mapping in Shared package from ID to the corresponding handler class +/* + * Extracts the strategy from the ID. + * @param _id - The ID of the strategy. + * @returns The strategy. + */ +export function extractStrategyFromId(_id: Address): Strategy | undefined { + const id = _id.toLowerCase(); + /* eslint-disable no-fallthrough */ + switch (id) { + // SQFSuperfluidv1 + case "0xf8a14294e80ff012e54157ec9d1b2827421f1e7f6bde38c06730b1c031b3f935": + return { + id: id as SanitizedStrategyId, + name: "allov2.SQFSuperFluidStrategy", + groups: ["allov2.SQFSuperFluidStrategy"], + }; + + // MicroGrantsv1 + case "0x697f0592ebd05466d2d24454477e11d69c475d7a7c4134f15ddc1ea9811bb16f": + return { + id: id as SanitizedStrategyId, + name: "allov2.MicroGrantsStrategy", + groups: ["allov2.MicroGrantsStrategy", "allov2.MicroGrantsCommon"], + }; + + // MicroGrantsGovv1 + case "0x741ac1e2f387d83f219f6b5349d35ec34902cf94019d117335e0045d2e0ed912": + return { + id: id as SanitizedStrategyId, + name: "allov2.MicroGrantsGovStrategy", + groups: ["allov2.MicroGrantsGovStrategy", "allov2.MicroGrantsCommon"], + }; + + // MicroGrantsHatsv1 + case "0x5aa24dcfcd55a1e059a172e987b3456736b4856c71e57aaf52e9a965897318dd": + return { + id: id as SanitizedStrategyId, + name: "allov2.MicroGrantsHatsStrategy", + groups: ["allov2.MicroGrantsHatsStrategy", "allov2.MicroGrantsCommon"], + }; + + // RFPSimpleStrategyv1.0 + case "0x0d459e12d9e91d2b2a8fa12be8c7eb2b4f1c35e74573990c34b436613bc2350f": + return { + id: id as SanitizedStrategyId, + name: "allov2.RFPSimpleStrategy", + groups: ["allov2.RFPSimpleStrategy"], + }; + + // RFPCommitteeStrategyv1.0 + case "0x7d143166a83c6a8a303ae32a6ccd287e48d79818f5d15d89e185391199909803": + return { + id: id as SanitizedStrategyId, + name: "allov2.RFPCommitteeStrategy", + groups: ["allov2.RFPCommitteeStrategy"], + }; + + // QVSimpleStrategyv1.0 + case "0x22d006e191d6dc5ff1a25bb0733f47f64a9c34860b6703df88dea7cb3987b4c3": + return { + id: id as SanitizedStrategyId, + name: "allov2.QVSimpleStrategy", + groups: ["allov2.QVSimpleStrategy"], + }; + + // DonationVotingMerkleDistributionDirectTransferStrategyv1.0 + case "0x6f9291df02b2664139cec5703c124e4ebce32879c74b6297faa1468aa5ff9ebf": + // DonationVotingMerkleDistributionDirectTransferStrategyv1.1 + case "0x2f46bf157821dc41daa51479e94783bb0c8699eac63bf75ec450508ab03867ce": + // DonationVotingMerkleDistributionDirectTransferStrategyv2.0 + case "0x2f0250d534b2d59b8b5cfa5eb0d0848a59ccbf5de2eaf72d2ba4bfe73dce7c6b": + // DonationVotingMerkleDistributionDirectTransferStrategyv2.1 + case "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0": + return { + id: id as SanitizedStrategyId, + name: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", + groups: ["allov2.DonationVotingMerkleDistributionDirectTransferStrategy"], + }; + + // DonationVotingMerkleDistributionVaultStrategyv1.0 + case "0x7e75375f0a7cd9f7ea159c8b065976e4f764f9dcef1edf692f31dd1842f70c87": + // DonationVotingMerkleDistributionVaultStrategyv1.1 + case "0x093072375737c0e8872fef36808849aeba7f865e182d495f2b98308115c9ef13": + return { + id: id as SanitizedStrategyId, + name: "allov2.DonationVotingMerkleDistributionVaultStrategy", + groups: ["allov2.DonationVotingMerkleDistributionVaultStrategy"], + }; + + // DirectGrantsSimpleStrategyv1.1 + case "0x263cb916541b6fc1fb5543a244829ccdba75264b097726e6ecc3c3cfce824bf5": + // DirectGrantsSimpleStrategyv2.1 + case "0x53fb9d3bce0956ca2db5bb1441f5ca23050cb1973b33789e04a5978acfd9ca93": + return { + id: id as SanitizedStrategyId, + name: "allov2.DirectGrantsSimpleStrategy", + groups: ["allov2.DirectGrantsSimpleStrategy"], + }; + + // DirectGrantsLiteStrategyv1.0 + case "0x103732a8e473467a510d4128ee11065262bdd978f0d9dad89ba68f2c56127e27": + return { + id: id as SanitizedStrategyId, + name: "allov2.DirectGrantsLiteStrategy", + groups: ["allov2.DirectGrantsLiteStrategy"], + }; + + // EasyRPGFStrategy1.0 + case "0x662f5a0d3ea7e9b6ed1b351a9d96ac636a3c3ed727390aeff4ec931ae760d5ae": + return { + id: id as SanitizedStrategyId, + name: "allov2.EasyRPGFStrategy", + groups: ["allov2.EasyRPGFStrategy"], + }; + + // DirectAllocationStrategyv1.1 + case "0x4cd0051913234cdd7d165b208851240d334786d6e5afbb4d0eec203515a9c6f3": + return { + id: id as SanitizedStrategyId, + name: "allov2.DirectAllocationStrategy", + groups: ["allov2.DirectAllocationStrategy"], + }; + } + + return undefined; +} + +//TODO: refactor this into the StrategyHandler when implemented +// see if we can use a common interface or abstract class for all strategies +// so we don't have to do this switch statement +// most of the strategies don't need to fetch anything and just return null for all the times +export const getStrategyTimings = async ( + evmProvider: EvmProvider, + strategy: Strategy, + strategyAddress: Address, +): Promise => { + switch (strategy.name) { + case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy": + return getDonationVotingMerkleDistributionDirectTransferStrategyTimings( + evmProvider, + strategyAddress, + ); + case "allov2.DirectGrantsSimpleStrategy": + case "allov2.DirectGrantsLiteStrategy": + return getDirectGrantsStrategyTimings(evmProvider, strategyAddress); + default: + return { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }; + } +}; + +/** + * Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy + * @param evmProvider - The evm provider + * @param strategyId - The address of the strategy + * @returns The strategy data + */ +export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async ( + evmProvider: EvmProvider, + strategyId: Address, +): Promise => { + let results: [bigint, bigint, bigint, bigint] = [0n, 0n, 0n, 0n]; + + const contractCalls = [ + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationEndTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationEndTime", + address: strategyId, + }, + ] as const; + + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, + allowFailure: false, + }); + } else { + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint, bigint, bigint]; + } + + return { + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: getDateFromTimestamp(results[2]), + donationsEndTime: getDateFromTimestamp(results[3]), + }; +}; + +/** + * Gets the strategy data for the DirectGrantsStrategy + * @param evmProvider - The evm provider + * @param strategyAddress - The address of the strategy + * @returns The strategy data + */ +export const getDirectGrantsStrategyTimings = async ( + evmProvider: EvmProvider, + strategyAddress: Address, +): Promise => { + let results: [bigint, bigint] = [0n, 0n]; + + const contractCalls = [ + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyAddress, + }, + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyAddress, + }, + ] as const; + + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, + allowFailure: false, + }); + } else { + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint]; + } + + return { + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: null, + donationsEndTime: null, + }; +}; diff --git a/packages/processors/src/helpers/tokenMath.ts b/packages/processors/src/helpers/tokenMath.ts new file mode 100644 index 0000000..e48da9b --- /dev/null +++ b/packages/processors/src/helpers/tokenMath.ts @@ -0,0 +1,34 @@ +import { BigNumber } from "@grants-stack-indexer/shared"; + +import { InvalidArgument } from "../internal.js"; + +/** + * Calculates the amount in USD + * @param amount - The amount to convert to USD + * @param tokenPrice - The price of the token in USD + * @param tokenDecimals - The number of decimals the token has + * @param truncateDecimals (optional) - The number of decimals to truncate the final result to. Must be between 0 and 18. + * @throws InvalidArgumentException if truncateDecimals is not between 0 and 18 + * @returns The amount in USD + */ +export const calculateAmountInUsd = ( + amount: bigint, + tokenPriceInUsd: string | number, + tokenDecimals: number, + truncateDecimals?: number, +): string => { + const amountBN = new BigNumber(amount.toString()); + const tokenPriceBN = new BigNumber(tokenPriceInUsd.toString()); + const scaleFactor = new BigNumber(10).pow(tokenDecimals); + + let amountInUsd = amountBN.multipliedBy(tokenPriceBN).dividedBy(scaleFactor); + + if (truncateDecimals !== undefined) { + if (truncateDecimals < 0 || truncateDecimals > 18) { + throw new InvalidArgument("Truncate decimals must be between 0 and 18"); + } + amountInUsd = amountInUsd.decimalPlaces(truncateDecimals); + } + + return amountInUsd.toString(); +}; diff --git a/packages/processors/src/helpers/utils.ts b/packages/processors/src/helpers/utils.ts new file mode 100644 index 0000000..cd1e01c --- /dev/null +++ b/packages/processors/src/helpers/utils.ts @@ -0,0 +1,10 @@ +const UINT64_MAX = 18446744073709551615n; + +/** + * Converts a timestamp to a date + * @param timestamp - The timestamp to convert to a date + * @returns The date or null if the timestamp is greater than 18446744073709551615 + */ +export const getDateFromTimestamp = (timestamp: bigint): Date | null => { + return timestamp >= 0n && timestamp < UINT64_MAX ? new Date(Number(timestamp) * 1000) : null; +}; diff --git a/packages/processors/src/interfaces/eventHandler.interface.ts b/packages/processors/src/interfaces/eventHandler.interface.ts new file mode 100644 index 0000000..03ee946 --- /dev/null +++ b/packages/processors/src/interfaces/eventHandler.interface.ts @@ -0,0 +1,24 @@ +import type { Changeset } from "@grants-stack-indexer/repository"; +import type { + ContractName, + ContractToEventName, + ProtocolEvent, +} from "@grants-stack-indexer/shared"; + +/** + * Interface for an event handler. + * @template C - The contract name. + * @template E - The event name. + */ +export interface IEventHandler> { + /** + * The event to handle. + */ + readonly event: ProtocolEvent; + + /** + * Handles the event. + * @returns A promise that resolves to an array of changesets. + */ + handle(): Promise; +} diff --git a/packages/processors/src/interfaces/index.ts b/packages/processors/src/interfaces/index.ts index d9d3380..057ac8d 100644 --- a/packages/processors/src/interfaces/index.ts +++ b/packages/processors/src/interfaces/index.ts @@ -1 +1,2 @@ export * from "./processor.interface.js"; +export * from "./eventHandler.interface.js"; diff --git a/packages/processors/src/interfaces/processor.interface.ts b/packages/processors/src/interfaces/processor.interface.ts index f8aeb02..4f8c943 100644 --- a/packages/processors/src/interfaces/processor.interface.ts +++ b/packages/processors/src/interfaces/processor.interface.ts @@ -7,5 +7,5 @@ export interface IProcessor): Promise; + process(event: ProtocolEvent): Promise; } diff --git a/packages/processors/src/internal.ts b/packages/processors/src/internal.ts index 10f9b05..9b54404 100644 --- a/packages/processors/src/internal.ts +++ b/packages/processors/src/internal.ts @@ -1,4 +1,6 @@ // Add your internal exports here +export * from "./types/index.js"; export * from "./interfaces/index.js"; +export * from "./exceptions/index.js"; export * from "./allo/index.js"; export * from "./strategy/index.js"; diff --git a/packages/processors/src/strategy/strategy.processor.ts b/packages/processors/src/strategy/strategy.processor.ts index 6f4d499..9c2c4ae 100644 --- a/packages/processors/src/strategy/strategy.processor.ts +++ b/packages/processors/src/strategy/strategy.processor.ts @@ -4,7 +4,7 @@ import { ProtocolEvent, StrategyEvent } from "@grants-stack-indexer/shared"; import type { IProcessor } from "../internal.js"; export class StrategyProcessor implements IProcessor<"Strategy", StrategyEvent> { - process(_event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { + process(_event: ProtocolEvent<"Strategy", StrategyEvent>): Promise { //TODO: Implement throw new Error("Method not implemented."); } diff --git a/packages/processors/src/types/index.ts b/packages/processors/src/types/index.ts new file mode 100644 index 0000000..cc56870 --- /dev/null +++ b/packages/processors/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "./processor.types.js"; +export * from "./strategy.types.js"; diff --git a/packages/processors/src/types/processor.types.ts b/packages/processors/src/types/processor.types.ts new file mode 100644 index 0000000..101a7aa --- /dev/null +++ b/packages/processors/src/types/processor.types.ts @@ -0,0 +1,15 @@ +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; + +export type ProcessorDependencies = { + evmProvider: EvmProvider; + pricingProvider: IPricingProvider; + metadataProvider: IMetadataProvider; + roundRepository: IRoundReadRepository; + projectRepository: IProjectReadRepository; +}; diff --git a/packages/processors/src/types/strategy.types.ts b/packages/processors/src/types/strategy.types.ts new file mode 100644 index 0000000..fc206a7 --- /dev/null +++ b/packages/processors/src/types/strategy.types.ts @@ -0,0 +1,9 @@ +/** + * This type represents the time fields for a strategy. + */ +export type StrategyTimings = { + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; +}; diff --git a/packages/processors/test/allo/allo.processor.spec.ts b/packages/processors/test/allo/allo.processor.spec.ts new file mode 100644 index 0000000..173a2b3 --- /dev/null +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { + IProjectReadRepository, + IRoundReadRepository, +} from "@grants-stack-indexer/repository"; +import type { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; + +import { AlloProcessor } from "../../src/allo/allo.processor.js"; +import { PoolCreatedHandler } from "../../src/allo/handlers/poolCreated.handler.js"; +import { UnsupportedEventException } from "../../src/internal.js"; + +// Mock the handlers +vi.mock("../../src/allo/handlers/poolCreated.handler.js", () => { + const PoolCreatedHandler = vi.fn(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + PoolCreatedHandler.prototype.handle = vi.fn(); + return { + PoolCreatedHandler, + }; +}); + +describe("AlloProcessor", () => { + const mockChainId = 10 as ChainId; + let processor: AlloProcessor; + let mockEvmProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + + beforeEach(() => { + mockEvmProvider = {} as EvmProvider; + mockPricingProvider = {} as IPricingProvider; + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + + processor = new AlloProcessor(mockChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: {} as IProjectReadRepository, + }); + + // Reset mocks before each test + vi.clearAllMocks(); + }); + + it("call PoolCreatedHandler for PoolCreated event", async () => { + const mockEvent: ProtocolEvent<"Allo", "PoolCreated"> = { + eventName: "PoolCreated", + // Add other necessary event properties here + } as ProtocolEvent<"Allo", "PoolCreated">; + + vi.spyOn(PoolCreatedHandler.prototype, "handle").mockResolvedValue([]); + + await processor.process(mockEvent); + + expect(PoolCreatedHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + processor["dependencies"], + ); + expect(PoolCreatedHandler.prototype.handle).toHaveBeenCalled(); + }); + + it("throw an error for unknown event names", async () => { + const mockEvent = { + eventName: "UnknownEvent", + } as unknown as ProtocolEvent<"Allo", AlloEvent>; + + await expect(() => processor.process(mockEvent)).rejects.toThrow(UnsupportedEventException); + }); +}); diff --git a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts new file mode 100644 index 0000000..49b3d43 --- /dev/null +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -0,0 +1,424 @@ +import { GetTransactionReturnType, parseUnits } from "viem"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { EvmProvider } from "@grants-stack-indexer/chain-providers"; +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; +import type { ChainId, DeepPartial, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { mergeDeep } from "@grants-stack-indexer/shared"; + +import { PoolCreatedHandler } from "../../../src/allo/handlers/poolCreated.handler.js"; + +// Function to create a mock event with optional overrides +function createMockEvent( + overrides: DeepPartial> = {}, +): ProtocolEvent<"Allo", "PoolCreated"> { + const defaultEvent: ProtocolEvent<"Allo", "PoolCreated"> = { + blockNumber: 116385567, + blockTimestamp: 1708369911, + chainId: 10 as ChainId, + contractName: "Allo", + eventName: "PoolCreated", + logIndex: 221, + srcAddress: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", + params: { + contractAddress: "0xD545fbA3f43EcA447CC7FBF41D4A8F0f575F2491", + poolId: 10n, + profileId: "0xcc3509068dfb6604965939f100e57dde21e9d764d8ce4b34284bbe9364b1f5ed", + strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + amount: 0n, + token: "0x4200000000000000000000000000000000000042", + metadata: ["bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", 1n], + }, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 6, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + }; + + return mergeDeep(defaultEvent, overrides) as ProtocolEvent<"Allo", "PoolCreated">; +} + +describe("PoolCreatedHandler", () => { + let mockEvmProvider: EvmProvider; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + + beforeEach(() => { + mockEvmProvider = { + readContract: vi.fn(), + getTransaction: vi.fn(), + multicall: vi.fn(), + getMulticall3Address: vi.fn().mockRejectedValue("0xmulticall3"), + } as unknown as EvmProvider; + mockPricingProvider = { + getTokenPrice: vi.fn(), + }; + mockMetadataProvider = { + getMetadata: vi.fn(), + }; + mockRoundRepository = { + getPendingRoundRoles: vi.fn(), + } as unknown as IRoundReadRepository; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("process an event with initial funds", async () => { + const fundedAmount = parseUnits("10", 18); + const mockEvent = createMockEvent({ + params: { amount: fundedAmount, strategyId: "0xunknown" }, + }); + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + priceUsd: 100, + timestampMs: 1708369911, + }); + vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.type).toBe("InsertRound"); + expect(changeset.args.round).toMatchObject({ + fundedAmount: fundedAmount, + fundedAmountInUsd: "1000", + }); + expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); + expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); + }); + + it("process an unknown strategyId", async () => { + const mockEvent = createMockEvent({ + params: { strategyId: "0xunknown" }, + }); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.type).toBe("InsertRound"); + expect(changeset.args.round).toMatchObject({ + chainId: 10, + id: "10", + tags: ["allo-v2"], + strategyAddress: mockEvent.params.contractAddress, + strategyId: "0xunknown", + strategyName: "", + createdByAddress: mockEvent.transactionFields.from, + }); + expect(mockPricingProvider.getTokenPrice).not.toHaveBeenCalled(); + expect(mockEvmProvider.multicall).not.toHaveBeenCalled(); + }); + + it("process a DonationVotingMerkleDistributionDirectTransferStrategy", async () => { + const mockEvent = createMockEvent(); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue({ + round: { + name: "Test Round", + roundType: "private", + quadraticFundingConfig: { + matchingFundsAvailable: 1, + }, + }, + application: { + version: "1.0.0", + }, + }); + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + priceUsd: 100, + timestampMs: 1708369911, + }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); + + vi.spyOn(mockRoundRepository, "getPendingRoundRoles") + .mockResolvedValueOnce([ + { + chainId: 10 as ChainId, + role: "admin", + address: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", + createdAtBlock: 116385567n, + }, + ]) + .mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result).toHaveLength(3); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.type).toBe("InsertRound"); + expect(changeset.args.round).toMatchObject({ + chainId: 10, + id: "10", + tags: ["allo-v2", "grants-stack"], + totalDonationsCount: 0, + totalAmountDonatedInUsd: "0", + uniqueDonorsCount: 0, + matchTokenAddress: mockEvent.params.token, + matchAmount: parseUnits("1", 18), + matchAmountInUsd: "100", + fundedAmount: 0n, + fundedAmountInUsd: "0", + applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", + applicationMetadata: { + version: "1.0.0", + }, + roundMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", + roundMetadata: { + name: "Test Round", + roundType: "private", + quadraticFundingConfig: { + matchingFundsAvailable: 1, + }, + }, + applicationsStartTime: new Date("2021-01-01T00:00:00.000Z"), + applicationsEndTime: new Date("2021-01-01T00:00:00.000Z"), + donationsStartTime: new Date("2021-01-01T00:00:00.000Z"), + donationsEndTime: new Date("2021-01-01T00:00:00.000Z"), + strategyAddress: mockEvent.params.contractAddress, + strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", + strategyName: "allov2.DonationVotingMerkleDistributionDirectTransferStrategy", + createdByAddress: mockEvent.transactionFields.from, + createdAtBlock: BigInt(mockEvent.blockNumber), + updatedAtBlock: BigInt(mockEvent.blockNumber), + projectId: mockEvent.params.profileId, + totalDistributed: 0n, + readyForPayoutTransaction: null, + matchingDistribution: null, + }); + expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); + expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); + expect(mockEvmProvider.multicall).toHaveBeenCalled(); + }); + + it("fetches transaction sender if not present in event", async () => { + const mockEvent = createMockEvent({ + params: { strategyId: "0xunknown" }, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + from: undefined, + }, + }); + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + priceUsd: 100, + timestampMs: 1708369911, + }); + vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); + vi.spyOn(mockEvmProvider, "getTransaction").mockResolvedValue({ + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as unknown as GetTransactionReturnType); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.args.round.createdByAddress).toBe( + "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + ); + expect(mockEvmProvider.getTransaction).toHaveBeenCalledWith( + "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + ); + }); + + it("handles an undefined metadata", async () => { + const mockEvent = createMockEvent(); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); + vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.type).toBe("InsertRound"); + expect(changeset.args.round).toMatchObject({ + chainId: 10, + id: "10", + tags: ["allo-v2"], + matchAmount: 0n, + matchAmountInUsd: "0", + fundedAmount: 0n, + fundedAmountInUsd: "0", + applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", + applicationMetadata: {}, + roundMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", + roundMetadata: null, + readyForPayoutTransaction: null, + matchingDistribution: null, + }); + + expect(mockPricingProvider.getTokenPrice).not.toHaveBeenCalled(); + expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); + }); + + it("throws an error if token price fetch fails", async () => { + const mockEvent = createMockEvent({ params: { amount: 1n, strategyId: "0xunknown" } }); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + await expect(() => handler.handle()).rejects.toThrow("Token price not found"); + }); + + it("handles pending round roles", async () => { + const mockEvent = createMockEvent(); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue({ + priceUsd: 100, + timestampMs: 1708369911, + }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); + + vi.spyOn(mockRoundRepository, "getPendingRoundRoles") + .mockResolvedValueOnce([ + { + id: 1, + chainId: 10 as ChainId, + role: "admin", + address: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", + createdAtBlock: 116385565n, + }, + ]) + .mockResolvedValueOnce([ + { + id: 2, + chainId: 10 as ChainId, + role: "manager", + address: "0x1234567890123456789012345678901234567890", + createdAtBlock: 116385565n, + }, + { + id: 3, + chainId: 10 as ChainId, + role: "manager", + address: "0xAaBBccdDeEFf0000000000000000000000000000", + createdAtBlock: 116385565n, + }, + ]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + + expect(result).toHaveLength(5); + + const changeset = result[0] as { type: "InsertRound"; args: { round: Round } }; + expect(changeset.type).toBe("InsertRound"); + + expect(result.filter((c) => c.type === "InsertRoundRole")).toHaveLength(3); + expect(result.filter((c) => c.type === "InsertRoundRole")[0]?.args.roundRole).toMatchObject( + { + chainId: 10 as ChainId, + roundId: "10", + role: "admin", + address: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", + createdAtBlock: 116385565n, + }, + ); + expect(result.filter((c) => c.type === "InsertRoundRole")[1]?.args.roundRole).toMatchObject( + { + chainId: 10 as ChainId, + roundId: "10", + role: "manager", + address: "0x1234567890123456789012345678901234567890", + createdAtBlock: 116385565n, + }, + ); + expect(result.filter((c) => c.type === "InsertRoundRole")[2]?.args.roundRole).toMatchObject( + { + chainId: 10 as ChainId, + roundId: "10", + role: "manager", + address: "0xAaBBccdDeEFf0000000000000000000000000000", + createdAtBlock: 116385565n, + }, + ); + expect(result.filter((c) => c.type === "DeletePendingRoundRoles")).toHaveLength(1); + expect(result.filter((c) => c.type === "DeletePendingRoundRoles")[0]?.args.ids).toContain( + 1, + ); + expect(result.filter((c) => c.type === "DeletePendingRoundRoles")[0]?.args.ids).toContain( + 2, + ); + expect(result.filter((c) => c.type === "DeletePendingRoundRoles")[0]?.args.ids).toContain( + 3, + ); + }); + + it.skip("handles a native token"); + it.skip("handles an unknown token"); +}); diff --git a/packages/processors/test/helpers/tokenMath.spec.ts b/packages/processors/test/helpers/tokenMath.spec.ts new file mode 100644 index 0000000..3aee76c --- /dev/null +++ b/packages/processors/test/helpers/tokenMath.spec.ts @@ -0,0 +1,123 @@ +import { parseGwei } from "viem"; +import { describe, expect, it, test } from "vitest"; + +import { calculateAmountInUsd } from "../../src/helpers/tokenMath.js"; +import { InvalidArgument } from "../../src/internal.js"; + +describe("calculateAmountInUsd", () => { + it("calculate USD amount for 18 decimal token with integer price", () => { + const amount = 1000000000000000000n; // 1 token + const tokenPriceInUsd = 100; // $100 per token + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("100"); + }); + + it("calculate USD amount for 18 decimal token with float price", () => { + const amount = 1500000000000000000n; // 1.5 tokens + const tokenPriceInUsd = 27.35; // $27.35 per token + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("41.025"); + }); + + it("calculate USD amount for 8 decimal token with integer price", () => { + const amount = 100000000n; // 1 token + const tokenPriceInUsd = 50; // $50 per token + const tokenDecimals = 8; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("50"); + }); + + // Test case for 8 decimal token with float price + it("should correctly calculate USD amount for 8 decimal token with float price", () => { + const amount = 150000000n; // 1.5 tokens + const tokenPriceInUsd = 12.75; // $12.75 per token + const tokenDecimals = 8; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("19.125"); + }); + + it("correctly calculate USD amount for 1gwei token amount", () => { + const amount = parseGwei("1"); // 1 gwei in wei + const tokenPriceInUsd = 1000; // $1000 per token + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("0.000001"); + }); + + it("correctly truncate decimals when specified", () => { + const amount = 1234567890123456789n; // 1.234567890123456789 tokens + const tokenPriceInUsd = 1.23; // $1.23 per token + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 4); + expect(result).toBe("1.5185"); + }); + + it("handle token price with 19 decimal digits", () => { + const amount = 1000000000000000000n; // 1 token + const tokenPriceInUsd = 1e-19; // 19 decimal places + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + + expect(result).toBe("0.0000000000000000001"); + }); + + it("handle scientific notation token price with interspersed non-zero digits in result", () => { + const amount = 123456789012345678n; // 0.123456789012345678 tokens + const tokenPriceInUsd = 1.23e-15; // 0.00000000000000123 + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + + expect(result).toBe("0.00000000000000015185"); + }); + + it("return zero for zero token amount", () => { + const amount = 0n; + const tokenPriceInUsd = 100; + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("0"); + }); + + it("should return zero for zero token price", () => { + const amount = 1000000000000000000n; // 1 token + const tokenPriceInUsd = 0; + const tokenDecimals = 18; + + const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); + expect(result).toBe("0"); + }); + + it("throw an error for invalid truncate decimals", () => { + const amount = 1000000000000000000n; // 1 token + const tokenPriceInUsd = 100; + const tokenDecimals = 18; + + expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, -1)).toThrow( + InvalidArgument, + ); + expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 19)).toThrow( + InvalidArgument, + ); + }); + + test("migrated cases", () => { + expect(calculateAmountInUsd(3400000000000000000n, 1, 18, 8)).toBe("3.4"); + + expect(calculateAmountInUsd(50000000000n, 1, 18, 8)).toBe("0.00000005"); + + expect(calculateAmountInUsd(3400000000000000000n, 0.5, 18, 8)).toBe("1.7"); + + expect(calculateAmountInUsd(3400000000000000000n, 2, 18, 8)).toBe("6.8"); + }); +}); diff --git a/packages/processors/test/helpers/utils.spec.ts b/packages/processors/test/helpers/utils.spec.ts new file mode 100644 index 0000000..eccc86b --- /dev/null +++ b/packages/processors/test/helpers/utils.spec.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; + +import { getDateFromTimestamp } from "../../src/helpers/utils.js"; + +describe("utils", () => { + describe("getDateFromTimestamp", () => { + it("converts a valid timestamp to a Date object", () => { + const timestamp = 1609459200n; // 2021-01-01 00:00:00 UTC + const result = getDateFromTimestamp(timestamp); + expect(result).toBeInstanceOf(Date); + expect(result?.toISOString()).toBe("2021-01-01T00:00:00.000Z"); + }); + + it("handles the minimum valid timestamp (0)", () => { + const timestamp = 0n; + const result = getDateFromTimestamp(timestamp); + expect(result).toBeInstanceOf(Date); + expect(result?.toISOString()).toBe("1970-01-01T00:00:00.000Z"); + }); + + it("handles the maximum valid timestamp", () => { + const maxTimestamp = 18446744073709551615n - 1n; + const result = getDateFromTimestamp(maxTimestamp); + expect(result).toBeInstanceOf(Date); + }); + + it("returns null for timestamps equal to or greater than UINT64_MAX", () => { + const maxTimestamp = 18446744073709551615n; + expect(getDateFromTimestamp(maxTimestamp)).toBeNull(); + expect(getDateFromTimestamp(maxTimestamp + 1n)).toBeNull(); + }); + + it("returns null for negative timestamps", () => { + expect(getDateFromTimestamp(-1n)).toBeNull(); + expect(getDateFromTimestamp(-1000000n)).toBeNull(); + }); + }); +}); diff --git a/packages/processors/vitest.config.ts b/packages/processors/vitest.config.ts index 36aeafb..402555e 100644 --- a/packages/processors/vitest.config.ts +++ b/packages/processors/vitest.config.ts @@ -10,7 +10,15 @@ export default defineConfig({ coverage: { provider: "v8", reporter: ["text", "json", "html"], - exclude: ["node_modules", "dist", "src/index.ts", ...configDefaults.exclude], + exclude: [ + "node_modules", + "dist", + "src/index.ts", + "src/internal.ts", + "src/external.ts", + "**/abis/**", + ...configDefaults.exclude, + ], }, }, resolve: { diff --git a/packages/repository/src/interfaces/roundRepository.interface.ts b/packages/repository/src/interfaces/roundRepository.interface.ts index 5e7b1b3..8cc2792 100644 --- a/packages/repository/src/interfaces/roundRepository.interface.ts +++ b/packages/repository/src/interfaces/roundRepository.interface.ts @@ -106,7 +106,7 @@ export interface IRoundRepository extends IRoundReadRepository { roundId: string; }, amount: bigint, - amountInUsd: number, + amountInUsd: string, ): Promise; /** diff --git a/packages/repository/src/repositories/kysely/round.repository.ts b/packages/repository/src/repositories/kysely/round.repository.ts index decc0b0..66d409b 100644 --- a/packages/repository/src/repositories/kysely/round.repository.ts +++ b/packages/repository/src/repositories/kysely/round.repository.ts @@ -111,7 +111,7 @@ export class KyselyRoundRepository implements IRoundRepository { roundId: string; }, amount: bigint, - amountInUsd: number, + amountInUsd: string, ): Promise { await this.db .withSchema(this.schemaName) diff --git a/packages/repository/src/types/changeset.types.ts b/packages/repository/src/types/changeset.types.ts index ee012e7..526332c 100644 --- a/packages/repository/src/types/changeset.types.ts +++ b/packages/repository/src/types/changeset.types.ts @@ -88,7 +88,7 @@ export type Changeset = chainId: ChainId; roundId: string; fundedAmount: bigint; - fundedAmountInUsd: number; + fundedAmountInUsd: string; }; } | { @@ -96,7 +96,7 @@ export type Changeset = args: { chainId: ChainId; roundId: Address; - amountInUsd: number; + amountInUsd: string; }; } | { diff --git a/packages/repository/src/types/round.types.ts b/packages/repository/src/types/round.types.ts index 09d8bff..a458421 100644 --- a/packages/repository/src/types/round.types.ts +++ b/packages/repository/src/types/round.types.ts @@ -16,9 +16,9 @@ export type Round = { chainId: ChainId; matchAmount: bigint; matchTokenAddress: Address; - matchAmountInUsd: number; + matchAmountInUsd: string; fundedAmount: bigint; - fundedAmountInUsd: number; + fundedAmountInUsd: string; applicationMetadataCid: string; applicationMetadata: unknown | null; roundMetadataCid: string | null; @@ -30,7 +30,7 @@ export type Round = { createdByAddress: Address; createdAtBlock: bigint; updatedAtBlock: bigint; - totalAmountDonatedInUsd: number; + totalAmountDonatedInUsd: string; totalDonationsCount: number; totalDistributed: bigint; uniqueDonorsCount: number; diff --git a/packages/shared/package.json b/packages/shared/package.json index 2faaa5b..cb4b1f7 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -28,6 +28,7 @@ "test:cov": "vitest run --config vitest.config.ts --coverage" }, "dependencies": { + "bignumber.js": "9.1.2", "viem": "2.21.19", "winston": "3.15.0" } diff --git a/packages/shared/src/constants/address.ts b/packages/shared/src/constants/address.ts index e1884d2..01a413e 100644 --- a/packages/shared/src/constants/address.ts +++ b/packages/shared/src/constants/address.ts @@ -2,6 +2,12 @@ import { Address } from "viem"; export const NATIVE_TOKEN_ADDRESS: Address = "0x0000000000000000000000000000000000000001"; +export const ALLO_NATIVE_TOKEN = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; + export const isNativeToken = (address: Address): boolean => { return address === NATIVE_TOKEN_ADDRESS; }; + +export const isAlloNativeToken = (address: Address): boolean => { + return address === ALLO_NATIVE_TOKEN; +}; diff --git a/packages/shared/src/external.ts b/packages/shared/src/external.ts index 29e4880..9b1b0af 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -1,4 +1,15 @@ export type * from "./types/index.js"; export type { Address } from "./internal.js"; +export { + NATIVE_TOKEN_ADDRESS, + isNativeToken, + ALLO_NATIVE_TOKEN, + isAlloNativeToken, +} from "./constants/index.js"; + +export type { DeepPartial } from "./utils/testing.js"; +export { mergeDeep } from "./utils/testing.js"; export type { ILogger, Logger } from "./internal.js"; -export { NATIVE_TOKEN_ADDRESS, isNativeToken } from "./constants/index.js"; + +export { BigNumber } from "./internal.js"; +export type { BigNumberType } from "./internal.js"; diff --git a/packages/shared/src/internal.ts b/packages/shared/src/internal.ts index 872e172..a6a96fb 100644 --- a/packages/shared/src/internal.ts +++ b/packages/shared/src/internal.ts @@ -1,4 +1,6 @@ export type { Address } from "viem"; +export * from "./math/bignumber.js"; export * from "./types/index.js"; export * from "./constants/index.js"; +export * from "./utils/testing.js"; export * from "./logger/index.js"; diff --git a/packages/shared/src/math/bignumber.ts b/packages/shared/src/math/bignumber.ts new file mode 100644 index 0000000..f95f80f --- /dev/null +++ b/packages/shared/src/math/bignumber.ts @@ -0,0 +1,4 @@ +import * as b from "bignumber.js"; + +export const BigNumber = b.BigNumber.clone({ EXPONENTIAL_AT: 32 }); +export type BigNumberType = typeof BigNumber; diff --git a/packages/shared/src/types/events/allo.ts b/packages/shared/src/types/events/allo.ts index 8e5a39d..32f5c44 100644 --- a/packages/shared/src/types/events/allo.ts +++ b/packages/shared/src/types/events/allo.ts @@ -17,4 +17,10 @@ export type AlloEventParams = T extends "PoolCreated" // ============================================================================= export type PoolCreatedParams = { contractAddress: Address; + poolId: bigint; + profileId: Address; + strategyId: Address; + token: Address; + amount: bigint; + metadata: [pointer: string, protocol: bigint]; }; diff --git a/packages/shared/src/types/events/common.ts b/packages/shared/src/types/events/common.ts index 3b4921b..ad2a38d 100644 --- a/packages/shared/src/types/events/common.ts +++ b/packages/shared/src/types/events/common.ts @@ -1,9 +1,17 @@ +import { Hex } from "viem"; + import { Address } from "../../internal.js"; import { AlloEvent, AlloEventParams, StrategyEvent, StrategyEventParams } from "./index.js"; export type ContractName = "Strategy" | "Allo"; export type AnyEvent = StrategyEvent | AlloEvent; +type TransactionFields = { + hash: Hex; + transactionIndex: number; + from?: Address; +}; + /** * This type is used to map contract names to their respective event names. */ @@ -39,6 +47,7 @@ export type ProtocolEvent; srcAddress: Address; + transactionFields: TransactionFields; }; export type AnyProtocolEvent = ProtocolEvent>; diff --git a/packages/shared/src/utils/testing.ts b/packages/shared/src/utils/testing.ts new file mode 100644 index 0000000..ca9e678 --- /dev/null +++ b/packages/shared/src/utils/testing.ts @@ -0,0 +1,43 @@ +// Define a type for objects +type ObjectType = Record; + +// Helper type to create a deep partial type +export type DeepPartial = T extends ObjectType ? { [P in keyof T]?: DeepPartial } : T; + +/** + * Deeply merges a partial source object into a target object. + * @param target The target object to merge into + * @param source The source object to merge from (can be partial) + * @returns A new object with the merged properties + */ +export function mergeDeep(target: T, source: DeepPartial): T { + const output = { ...target }; + + if (isObject(target) && isObject(source)) { + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key as keyof T] = mergeDeep( + target[key as keyof T] as ObjectType, + source[key] as DeepPartial, + ) as T[keyof T]; + } + } else { + Object.assign(output, { [key]: source[key] }); + } + }); + } + + return output; +} + +/** + * Type guard to check if a value is an object + * @param item The value to check + * @returns True if the value is an object, false otherwise + */ +function isObject(item: unknown): item is ObjectType { + return item !== null && typeof item === "object" && !Array.isArray(item); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0051848..36e40e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,9 @@ importers: packages/processors: dependencies: + "@grants-stack-indexer/chain-providers": + specifier: workspace:* + version: link:../chain-providers "@grants-stack-indexer/metadata": specifier: workspace:* version: link:../metadata @@ -175,6 +178,9 @@ importers: viem: specifier: 2.21.19 version: 2.21.19(typescript@5.5.4)(zod@3.23.8) + zod: + specifier: 3.23.8 + version: 3.23.8 packages/repository: dependencies: @@ -194,6 +200,9 @@ importers: packages/shared: dependencies: + bignumber.js: + specifier: 9.1.2 + version: 9.1.2 viem: specifier: 2.21.19 version: 2.21.19(typescript@5.5.4)(zod@3.23.8) @@ -1518,6 +1527,12 @@ packages: integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, } + bignumber.js@9.1.2: + resolution: + { + integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==, + } + binary-extensions@2.3.0: resolution: { @@ -5354,6 +5369,8 @@ snapshots: base64-js@1.5.1: {} + bignumber.js@9.1.2: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: