From 6adc7847239ac28bdbcfec43b891629c344909b3 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Mon, 14 Oct 2024 13:39:08 -0300 Subject: [PATCH 1/7] feat(envio): add tx fields to config.yaml --- apps/indexer/config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) 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 # ########################## From a89704cdcd0dedc26981e3f52b05427f4f372493 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:49:09 -0300 Subject: [PATCH 2/7] feat: implement PoolCreated event handling --- packages/processors/package.json | 3 +- .../allo-v2/v1/DirectGrantsLiteStrategy.ts | 857 ++++++++++++ .../allo-v2/v1/DirectGrantsSimpleStrategy.ts | 1167 ++++++++++++++++ ...erkleDistributionDirectTransferStrategy.ts | 1172 +++++++++++++++++ .../processors/src/allo/allo.processor.ts | 33 +- .../processors/src/allo/handlers/index.ts | 1 + .../src/allo/handlers/poolCreated.handler.ts | 272 ++++ packages/processors/src/helpers/schemas.ts | 11 + packages/processors/src/helpers/strategy.ts | 320 +++++ packages/processors/src/helpers/tokenMath.ts | 34 + packages/processors/src/helpers/utils.ts | 10 + .../src/interfaces/eventHandler.interface.ts | 24 + packages/processors/src/interfaces/index.ts | 1 + .../src/interfaces/processor.interface.ts | 2 +- .../src/strategy/strategy.processor.ts | 2 +- packages/shared/src/constants/address.ts | 6 + packages/shared/src/external.ts | 7 +- packages/shared/src/types/events/allo.ts | 9 + packages/shared/src/types/events/common.ts | 9 + pnpm-lock.yaml | 3 + 20 files changed, 3934 insertions(+), 9 deletions(-) create mode 100644 packages/processors/src/abis/allo-v2/v1/DirectGrantsLiteStrategy.ts create mode 100644 packages/processors/src/abis/allo-v2/v1/DirectGrantsSimpleStrategy.ts create mode 100644 packages/processors/src/abis/allo-v2/v1/DonationVotingMerkleDistributionDirectTransferStrategy.ts create mode 100644 packages/processors/src/allo/handlers/index.ts create mode 100644 packages/processors/src/allo/handlers/poolCreated.handler.ts create mode 100644 packages/processors/src/helpers/schemas.ts create mode 100644 packages/processors/src/helpers/strategy.ts create mode 100644 packages/processors/src/helpers/tokenMath.ts create mode 100644 packages/processors/src/helpers/utils.ts create mode 100644 packages/processors/src/interfaces/eventHandler.interface.ts diff --git a/packages/processors/package.json b/packages/processors/package.json index 5fec498..95643e1 100644 --- a/packages/processors/package.json +++ b/packages/processors/package.json @@ -32,6 +32,7 @@ "@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..692e386 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -1,11 +1,34 @@ -import { Changeset } from "@grants-stack-indexer/repository"; -import { AlloEvent, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { Chain, PublicClient, Transport } from "viem"; + +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { Changeset, IRoundReadRepository } from "@grants-stack-indexer/repository"; +import { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; import type { IProcessor } 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, + //TODO: replace with provider abstraction + private readonly viemClient: PublicClient, + private readonly pricingProvider: IPricingProvider, + private readonly metadataProvider: IMetadataProvider, + private readonly roundRepository: IRoundReadRepository, + ) {} + + process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { + switch (event.eventName) { + case "PoolCreated": + return new PoolCreatedHandler(event, this.chainId, { + viemClient: this.viemClient, + pricingProvider: this.pricingProvider, + metadataProvider: this.metadataProvider, + roundRepository: this.roundRepository, + }).handle(); + default: + throw new Error("Unknown event name"); + } } } 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..a71834e --- /dev/null +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -0,0 +1,272 @@ +import { + Address, + encodePacked, + getAddress, + keccak256, + pad, + parseUnits, + PublicClient, + zeroAddress, +} from "viem"; + +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { Changeset, IRoundReadRepository, NewRound } from "@grants-stack-indexer/repository"; +import type { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { isAlloNativeToken } from "@grants-stack-indexer/shared/"; + +import type { IEventHandler } from "../../internal.js"; +import { RoundMetadataSchema } from "../../helpers/schemas.js"; +import { extractStrategyFromId, getStrategyTimings } from "../../helpers/strategy.js"; +import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; + +/** + /** + * 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"> { + private readonly viemClient: PublicClient; + private readonly pricingProvider: IPricingProvider; + private readonly metadataProvider: IMetadataProvider; + private readonly roundRepository: IRoundReadRepository; + + constructor( + readonly event: ProtocolEvent<"Allo", "PoolCreated">, + private readonly chainId: ChainId, + dependencies: { + viemClient: PublicClient; + pricingProvider: IPricingProvider; + metadataProvider: IMetadataProvider; + roundRepository: IRoundReadRepository; + }, + ) { + this.viemClient = dependencies.viemClient; + this.pricingProvider = dependencies.pricingProvider; + this.metadataProvider = dependencies.metadataProvider; + this.roundRepository = dependencies.roundRepository; + } + + async handle(): Promise { + const { pointer: 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; + + try { + const metadata = await this.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: { + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; + } = { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }; + + let matchAmount = 0n; + let matchAmountInUsd = 0; + + if (strategy) { + strategyTimings = await getStrategyTimings( + this.viemClient, + strategy, + strategyAddress, + ); + + //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 this.viemClient.getTransaction({ hash: txHash })).from; + + const roundRoles = this.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; + } catch (error: unknown) { + console.error( + `An error occurred while processing the PoolCreated event. Event: ${this.event} - Error: ${error}`, + ); + return []; + } + } + + /** + * 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. + */ + private 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 }; + } + + /** + * 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 changes: Changeset[] = []; + + for (const roleName of ["admin", "manager"] as const) { + const pendingRoles = await this.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, + }, + }, + }); + } + + if (pendingRoles.length > 0) { + changes.push({ + type: "DeletePendingRoundRoles", + args: { ids: pendingRoles.map((r) => r.id!) }, + }); + } + } + + return changes; + } + + private async getTokenAmountInUsd( + token: { address: Address; decimals: number }, + amount: bigint, + timestamp: number, + ): Promise { + const tokenPrice = await this.pricingProvider.getTokenPrice( + this.chainId, + token.address, + timestamp, + timestamp + 1200000, + ); + + if (!tokenPrice) { + throw new Error("Token price not found"); + } + + return calculateAmountInUsd(amount, tokenPrice, token.decimals); + } +} 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..bf0ef57 --- /dev/null +++ b/packages/processors/src/helpers/strategy.ts @@ -0,0 +1,320 @@ +import { Address, PublicClient } from "viem"; + +import { 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 { getDateFromTimestamp } from "./utils.js"; + +type SanitizedStrategyId = Branded; +type Strategy = { + id: SanitizedStrategyId; + name: string | null; + // TODO: check if groups are required + groups: string[]; +}; + +/* + * 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 +export const getStrategyTimings = async ( + viemClient: PublicClient, + strategy: Strategy, + strategyAddress: Address, +): Promise<{ + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; +}> => { + switch (strategy.name) { + case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy": + return getDonationVotingMerkleDistributionDirectTransferStrategyTimings( + viemClient, + strategyAddress, + ); + case "allov2.DirectGrantsSimpleStrategy": + case "allov2.DirectGrantsLiteStrategy": + return getDirectGrantsStrategyTimings(viemClient, strategyAddress); + default: + return { + applicationsStartTime: null, + applicationsEndTime: null, + donationsStartTime: null, + donationsEndTime: null, + }; + } +}; + +/** + * Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy + * @param viemClient - The viem client + * @param strategyId - The address of the strategy + * @returns The strategy data + */ +export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async ( + viemClient: PublicClient, + strategyId: Address, +): Promise<{ + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; +}> => { + let registrationStartTimeResolved: bigint; + let registrationEndTimeResolved: bigint; + let allocationStartTimeResolved: bigint; + let allocationEndTimeResolved: bigint; + + if (viemClient.chain?.contracts?.multicall3) { + const results = await viemClient.multicall({ + contracts: [ + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationEndTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationStartTime", + address: strategyId, + }, + { + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationEndTime", + address: strategyId, + }, + ], + allowFailure: false, + }); + registrationStartTimeResolved = results[0]; + registrationEndTimeResolved = results[1]; + allocationStartTimeResolved = results[2]; + allocationEndTimeResolved = results[3]; + } else { + const results = await Promise.all([ + viemClient.readContract({ + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationStartTime", + address: strategyId, + }), + viemClient.readContract({ + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "registrationEndTime", + address: strategyId, + }), + viemClient.readContract({ + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationStartTime", + address: strategyId, + }), + viemClient.readContract({ + abi: DonationVotingMerkleDistributionDirectTransferStrategy, + functionName: "allocationEndTime", + address: strategyId, + }), + ]); + registrationStartTimeResolved = results[0]; + registrationEndTimeResolved = results[1]; + allocationStartTimeResolved = results[2]; + allocationEndTimeResolved = results[3]; + } + + return { + applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), + applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), + donationsStartTime: getDateFromTimestamp(allocationStartTimeResolved), + donationsEndTime: getDateFromTimestamp(allocationEndTimeResolved), + }; +}; + +/** + * Gets the strategy data for the DirectGrantsStrategy + * @param viemClient - The viem client + * @param strategyAddress - The address of the strategy + * @returns The strategy data + */ +export const getDirectGrantsStrategyTimings = async ( + viemClient: PublicClient, + strategyAddress: Address, +): Promise<{ + applicationsStartTime: Date | null; + applicationsEndTime: Date | null; + donationsStartTime: Date | null; + donationsEndTime: Date | null; +}> => { + let registrationStartTimeResolved: bigint; + let registrationEndTimeResolved: bigint; + + if (viemClient.chain?.contracts?.multicall3) { + const results = await viemClient.multicall({ + contracts: [ + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyAddress, + }, + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyAddress, + }, + ], + allowFailure: false, + }); + registrationStartTimeResolved = results[0]; + registrationEndTimeResolved = results[1]; + } else { + const results = await Promise.all([ + viemClient.readContract({ + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyAddress, + }), + viemClient.readContract({ + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyAddress, + }), + ]); + registrationStartTimeResolved = results[0]; + registrationEndTimeResolved = results[1]; + } + + return { + applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), + applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), + 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..c0fed42 --- /dev/null +++ b/packages/processors/src/helpers/tokenMath.ts @@ -0,0 +1,34 @@ +import { formatUnits, parseUnits } from "viem"; + +import { TokenPrice } from "@grants-stack-indexer/pricing"; + +/** + * 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. + * @returns The amount in USD + */ +export const calculateAmountInUsd = ( + amount: bigint, + tokenPrice: TokenPrice, + tokenDecimals: number, + truncateDecimals?: number, +): number => { + const amountInUsd = Number( + formatUnits( + amount * parseUnits(tokenPrice.priceUsd.toString(), tokenDecimals), + tokenDecimals * 2, + ), + ); + + if (truncateDecimals) { + if (truncateDecimals < 0 || truncateDecimals > 18) { + throw new Error("Truncate decimals must be between 0 and 18"); + } + return Number(amountInUsd.toFixed(truncateDecimals)); + } + + return amountInUsd; +}; 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/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/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 6547286..80fefd6 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -1,3 +1,8 @@ export type * from "./types/index.js"; export type { Address } from "./internal.js"; -export { NATIVE_TOKEN_ADDRESS, isNativeToken } from "./constants/index.js"; +export { + NATIVE_TOKEN_ADDRESS, + isNativeToken, + ALLO_NATIVE_TOKEN, + isAlloNativeToken, +} from "./constants/index.js"; diff --git a/packages/shared/src/types/events/allo.ts b/packages/shared/src/types/events/allo.ts index 8e5a39d..353d39d 100644 --- a/packages/shared/src/types/events/allo.ts +++ b/packages/shared/src/types/events/allo.ts @@ -17,4 +17,13 @@ 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/pnpm-lock.yaml b/pnpm-lock.yaml index 442ab68..c5ec297 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -163,6 +163,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: From 40efd368b3ccce351769e872912107500ed29558 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 16 Oct 2024 15:09:12 -0300 Subject: [PATCH 3/7] test: add pool created handler tests --- .../processors/src/allo/allo.processor.ts | 4 +- .../src/allo/handlers/poolCreated.handler.ts | 24 +- packages/processors/src/helpers/tokenMath.ts | 6 +- .../test/allo/allo.processor.spec.ts | 76 ++++ .../allo/handlers/poolCreated.handler.spec.ts | 430 ++++++++++++++++++ .../processors/test/helpers/tokenMath.spec.ts | 98 ++++ .../processors/test/helpers/utils.spec.ts | 38 ++ packages/processors/vitest.config.ts | 10 +- packages/shared/src/external.ts | 3 + packages/shared/src/internal.ts | 1 + packages/shared/src/utils/testing.ts | 43 ++ 11 files changed, 718 insertions(+), 15 deletions(-) create mode 100644 packages/processors/test/allo/allo.processor.spec.ts create mode 100644 packages/processors/test/allo/handlers/poolCreated.handler.spec.ts create mode 100644 packages/processors/test/helpers/tokenMath.spec.ts create mode 100644 packages/processors/test/helpers/utils.spec.ts create mode 100644 packages/shared/src/utils/testing.ts diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 692e386..3735c46 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -18,7 +18,7 @@ export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { private readonly roundRepository: IRoundReadRepository, ) {} - process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { + async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { switch (event.eventName) { case "PoolCreated": return new PoolCreatedHandler(event, this.chainId, { @@ -28,7 +28,7 @@ export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { roundRepository: this.roundRepository, }).handle(); default: - throw new Error("Unknown event name"); + throw new Error(`Unknown event name: ${event.eventName}`); } } } diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index a71834e..fb9317d 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -11,7 +11,12 @@ import { import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import type { Changeset, IRoundReadRepository, NewRound } from "@grants-stack-indexer/repository"; +import type { + Changeset, + IRoundReadRepository, + NewRound, + PendingRoundRole, +} from "@grants-stack-indexer/repository"; import type { ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; import { isAlloNativeToken } from "@grants-stack-indexer/shared/"; @@ -222,6 +227,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> */ private async handlePendingRoles(chainId: ChainId, roundId: string): Promise { const changes: Changeset[] = []; + const allPendingRoles: PendingRoundRole[] = []; for (const roleName of ["admin", "manager"] as const) { const pendingRoles = await this.roundRepository.getPendingRoundRoles(chainId, roleName); @@ -239,13 +245,15 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> }, }); } + allPendingRoles.push(...pendingRoles); + } - if (pendingRoles.length > 0) { - changes.push({ - type: "DeletePendingRoundRoles", - args: { ids: pendingRoles.map((r) => r.id!) }, - }); - } + const pendingRoleIds = [...new Set(allPendingRoles.map((r) => r.id!))]; + if (pendingRoleIds.length > 0) { + changes.push({ + type: "DeletePendingRoundRoles", + args: { ids: pendingRoleIds }, + }); } return changes; @@ -267,6 +275,6 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> throw new Error("Token price not found"); } - return calculateAmountInUsd(amount, tokenPrice, token.decimals); + return calculateAmountInUsd(amount, tokenPrice.priceUsd, token.decimals); } } diff --git a/packages/processors/src/helpers/tokenMath.ts b/packages/processors/src/helpers/tokenMath.ts index c0fed42..4d32849 100644 --- a/packages/processors/src/helpers/tokenMath.ts +++ b/packages/processors/src/helpers/tokenMath.ts @@ -1,7 +1,5 @@ import { formatUnits, parseUnits } from "viem"; -import { TokenPrice } from "@grants-stack-indexer/pricing"; - /** * Calculates the amount in USD * @param amount - The amount to convert to USD @@ -12,13 +10,13 @@ import { TokenPrice } from "@grants-stack-indexer/pricing"; */ export const calculateAmountInUsd = ( amount: bigint, - tokenPrice: TokenPrice, + tokenPriceInUsd: number, tokenDecimals: number, truncateDecimals?: number, ): number => { const amountInUsd = Number( formatUnits( - amount * parseUnits(tokenPrice.priceUsd.toString(), tokenDecimals), + amount * parseUnits(tokenPriceInUsd.toString(), tokenDecimals), tokenDecimals * 2, ), ); 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..8e7119b --- /dev/null +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -0,0 +1,76 @@ +import { Chain, PublicClient, Transport } from "viem"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import type { IPricingProvider } from "@grants-stack-indexer/pricing"; +import type { 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"; + +// 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 mockViemClient: PublicClient; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + + beforeEach(() => { + mockViemClient = {} as PublicClient; + mockPricingProvider = {} as IPricingProvider; + mockMetadataProvider = {} as IMetadataProvider; + mockRoundRepository = {} as IRoundReadRepository; + + processor = new AlloProcessor( + mockChainId, + mockViemClient, + mockPricingProvider, + mockMetadataProvider, + mockRoundRepository, + ); + + // 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, { + viemClient: mockViemClient, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + 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( + "Unknown event name: UnknownEvent", + ); + }); +}); 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..a20e2cf --- /dev/null +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -0,0 +1,430 @@ +import { Chain, GetTransactionReturnType, parseUnits, PublicClient, Transport } from "viem"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { ChainId, DeepPartial, ProtocolEvent } from "@grants-stack-indexer/shared"; +import { IMetadataProvider } from "@grants-stack-indexer/metadata"; +import { IPricingProvider } from "@grants-stack-indexer/pricing"; +import { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; +import { mergeDeep } from "@grants-stack-indexer/shared"; + +import { PoolCreatedHandler } from "../../../src/allo/handlers/poolCreated.handler.js"; +import * as strategy from "../../../src/helpers/strategy.js"; + +vi.mock("../../../src/helpers/strategy.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + getDonationVotingMerkleDistributionDirectTransferStrategyTimings: vi.fn(), + getDirectGrantsStrategyTimings: vi.fn(), + }; +}); + +// 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: { + pointer: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", + protocol: 1n, + }, + }, + transactionFields: { + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + transactionIndex: 6, + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + }, + }; + + return mergeDeep(defaultEvent, overrides) as ProtocolEvent<"Allo", "PoolCreated">; +} + +describe("PoolCreatedHandler", () => { + let mockViemClient: PublicClient; + let mockPricingProvider: IPricingProvider; + let mockMetadataProvider: IMetadataProvider; + let mockRoundRepository: IRoundReadRepository; + + beforeEach(() => { + mockViemClient = { + readContract: vi.fn(), + getTransaction: vi.fn(), + } as unknown as PublicClient; + 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, { + viemClient: mockViemClient, + 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, { + viemClient: mockViemClient, + 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(strategy.getDirectGrantsStrategyTimings).not.toHaveBeenCalled(); + expect( + strategy.getDonationVotingMerkleDistributionDirectTransferStrategyTimings, + ).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( + strategy, + "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", + ).mockResolvedValue({ + applicationsStartTime: new Date(), + applicationsEndTime: new Date(), + donationsStartTime: new Date(), + donationsEndTime: new Date(), + }); + + vi.spyOn(mockRoundRepository, "getPendingRoundRoles") + .mockResolvedValueOnce([ + { + chainId: 10 as ChainId, + role: "admin", + address: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", + createdAtBlock: 116385567n, + }, + ]) + .mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + viemClient: mockViemClient, + 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, + }, + }, + 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(); + }); + + 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(mockViemClient, "getTransaction").mockResolvedValue({ + from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", + } as unknown as GetTransactionReturnType); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + viemClient: mockViemClient, + 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(mockViemClient.getTransaction).toHaveBeenCalledWith({ + hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + }); + }); + + it("handles an undefined metadata", async () => { + const mockEvent = createMockEvent(); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + vi.spyOn( + strategy, + "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", + ).mockResolvedValue({ + applicationsStartTime: new Date(), + applicationsEndTime: new Date(), + donationsStartTime: new Date(), + donationsEndTime: new Date(), + }); + vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + viemClient: mockViemClient, + 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("returns empty changeset if token price fetch fails", async () => { + const mockEvent = createMockEvent({ params: { amount: 1n } }); + + vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); + + vi.spyOn(mockPricingProvider, "getTokenPrice").mockResolvedValue(undefined); + + const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { + viemClient: mockViemClient, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + }); + + const result = await handler.handle(); + expect(result).toHaveLength(0); + }); + + 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( + strategy, + "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", + ).mockResolvedValue({ + applicationsStartTime: new Date(), + applicationsEndTime: new Date(), + donationsStartTime: new Date(), + donationsEndTime: new Date(), + }); + + 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, { + viemClient: mockViemClient, + 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..5702e52 --- /dev/null +++ b/packages/processors/test/helpers/tokenMath.spec.ts @@ -0,0 +1,98 @@ +import { parseGwei } from "viem"; +import { describe, expect, it, test } from "vitest"; + +import { calculateAmountInUsd } from "../../src/helpers/tokenMath.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).toBeCloseTo(41.025, 5); + }); + + 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).toBeCloseTo(19.125, 5); + }); + + 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("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(); + expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 19)).toThrow(); + }); + + 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..cdc570f --- /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.ts"; + +describe("utils", () => { + describe("getDateFromTimestamp", () => { + it("should convert 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("should handle 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("should handle the maximum valid timestamp", () => { + const maxTimestamp = 18446744073709551615n - 1n; + const result = getDateFromTimestamp(maxTimestamp); + expect(result).toBeInstanceOf(Date); + }); + + it("should return null for timestamps equal to or greater than UINT64_MAX", () => { + const maxTimestamp = 18446744073709551615n; + expect(getDateFromTimestamp(maxTimestamp)).toBeNull(); + expect(getDateFromTimestamp(maxTimestamp + 1n)).toBeNull(); + }); + + it("should return 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/shared/src/external.ts b/packages/shared/src/external.ts index 80fefd6..d3be43d 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -6,3 +6,6 @@ export { ALLO_NATIVE_TOKEN, isAlloNativeToken, } from "./constants/index.js"; + +export type { DeepPartial } from "./utils/testing.js"; +export { mergeDeep } from "./utils/testing.js"; diff --git a/packages/shared/src/internal.ts b/packages/shared/src/internal.ts index 7dc65a3..e48d62e 100644 --- a/packages/shared/src/internal.ts +++ b/packages/shared/src/internal.ts @@ -1,3 +1,4 @@ export type { Address } from "viem"; export * from "./types/index.js"; export * from "./constants/index.js"; +export * from "./utils/testing.js"; 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); +} From afba646dd8decb6179ee5e9e9961378d3c40ecae Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Wed, 16 Oct 2024 17:40:53 -0300 Subject: [PATCH 4/7] fix: pr comments --- .../src/providers/evmProvider.ts | 5 + .../providers/evmProvider.service.spec.ts | 15 + packages/processors/package.json | 1 + .../processors/src/allo/allo.processor.ts | 22 +- .../src/allo/handlers/poolCreated.handler.ts | 289 +++++++----------- packages/processors/src/exceptions/index.ts | 1 + .../tokenPriceNotFound.exception.ts | 7 + packages/processors/src/helpers/roles.ts | 18 ++ packages/processors/src/helpers/strategy.ts | 193 +++++------- packages/processors/src/internal.ts | 2 + packages/processors/src/types/index.ts | 2 + .../processors/src/types/processor.types.ts | 15 + .../processors/src/types/strategy.types.ts | 9 + .../test/allo/allo.processor.spec.ts | 27 +- .../allo/handlers/poolCreated.handler.spec.ts | 116 +++---- .../processors/test/helpers/utils.spec.ts | 10 +- packages/shared/src/types/events/allo.ts | 5 +- pnpm-lock.yaml | 3 + 18 files changed, 344 insertions(+), 396 deletions(-) create mode 100644 packages/processors/src/exceptions/index.ts create mode 100644 packages/processors/src/exceptions/tokenPriceNotFound.exception.ts create mode 100644 packages/processors/src/helpers/roles.ts create mode 100644 packages/processors/src/types/index.ts create mode 100644 packages/processors/src/types/processor.types.ts create mode 100644 packages/processors/src/types/strategy.types.ts 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/processors/package.json b/packages/processors/package.json index 95643e1..97af619 100644 --- a/packages/processors/package.json +++ b/packages/processors/package.json @@ -28,6 +28,7 @@ "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:*", diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 3735c46..88e93a1 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -1,31 +1,23 @@ -import { Chain, PublicClient, Transport } from "viem"; - -import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import { Changeset, IRoundReadRepository } from "@grants-stack-indexer/repository"; +import { Changeset } from "@grants-stack-indexer/repository"; import { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; -import type { IProcessor } from "../internal.js"; +import type { IProcessor, ProcessorDependencies } from "../internal.js"; import { PoolCreatedHandler } from "./handlers/index.js"; export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { constructor( private readonly chainId: ChainId, - //TODO: replace with provider abstraction - private readonly viemClient: PublicClient, - private readonly pricingProvider: IPricingProvider, - private readonly metadataProvider: IMetadataProvider, - private readonly roundRepository: IRoundReadRepository, + private readonly dependencies: ProcessorDependencies, ) {} async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { switch (event.eventName) { case "PoolCreated": return new PoolCreatedHandler(event, this.chainId, { - viemClient: this.viemClient, - pricingProvider: this.pricingProvider, - metadataProvider: this.metadataProvider, - roundRepository: this.roundRepository, + evmProvider: this.dependencies.evmProvider, + pricingProvider: this.dependencies.pricingProvider, + metadataProvider: this.dependencies.metadataProvider, + roundRepository: this.dependencies.roundRepository, }).handle(); default: throw new Error(`Unknown event name: ${event.eventName}`); diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index fb9317d..5315716 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -1,29 +1,23 @@ -import { - Address, - encodePacked, - getAddress, - keccak256, - pad, - parseUnits, - PublicClient, - zeroAddress, -} from "viem"; +import { Address, getAddress, parseUnits, zeroAddress } from "viem"; -import type { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import type { IPricingProvider } from "@grants-stack-indexer/pricing"; -import type { - Changeset, - IRoundReadRepository, - NewRound, - PendingRoundRole, -} from "@grants-stack-indexer/repository"; +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 } from "../../internal.js"; +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; /** /** @@ -37,29 +31,15 @@ import { calculateAmountInUsd } from "../../helpers/tokenMath.js"; * - Creates a new round object */ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> { - private readonly viemClient: PublicClient; - private readonly pricingProvider: IPricingProvider; - private readonly metadataProvider: IMetadataProvider; - private readonly roundRepository: IRoundReadRepository; - constructor( readonly event: ProtocolEvent<"Allo", "PoolCreated">, private readonly chainId: ChainId, - dependencies: { - viemClient: PublicClient; - pricingProvider: IPricingProvider; - metadataProvider: IMetadataProvider; - roundRepository: IRoundReadRepository; - }, - ) { - this.viemClient = dependencies.viemClient; - this.pricingProvider = dependencies.pricingProvider; - this.metadataProvider = dependencies.metadataProvider; - this.roundRepository = dependencies.roundRepository; - } + private readonly dependencies: Dependencies, + ) {} async handle(): Promise { - const { pointer: metadataPointer } = this.event.params.metadata; + const { metadataProvider, evmProvider } = this.dependencies; + const [metadataPointer] = this.event.params.metadata; const { poolId, strategyId, @@ -69,149 +49,114 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> } = this.event.params; const { hash: txHash, from: txFrom } = this.event.transactionFields; - try { - const metadata = await this.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: { - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; - } = { - applicationsStartTime: null, - applicationsEndTime: null, - donationsStartTime: null, - donationsEndTime: null, - }; - - let matchAmount = 0n; - let matchAmountInUsd = 0; - - if (strategy) { - strategyTimings = await getStrategyTimings( - this.viemClient, - strategy, - strategyAddress, + 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, ); - //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( + matchAmountInUsd = await this.getTokenAmountInUsd( token, - fundedAmount, + matchAmount, this.event.blockTimestamp, ); } + } - // transaction sender - const createdBy = - txFrom ?? (await this.viemClient.getTransaction({ hash: txHash })).from; - - const roundRoles = this.getRoundRoles(poolId); + let fundedAmountInUsd = 0; - 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, + if (token !== null && fundedAmount > 0n) { + fundedAmountInUsd = await this.getTokenAmountInUsd( + token, 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; - } catch (error: unknown) { - console.error( - `An error occurred while processing the PoolCreated event. Event: ${this.event} - Error: ${error}`, + this.event.blockTimestamp, ); - return []; } - } - /** - * 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. - */ - private getRoundRoles(poolId: bigint): { managerRole: string; adminRole: string } { - // POOL_MANAGER_ROLE = bytes32(poolId); - const managerRole = pad(`0x${poolId.toString(16)}`); + // 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()))); - // POOL_ADMIN_ROLE = keccak256(abi.encodePacked(poolId, "admin")); - const adminRawRole = encodePacked(["uint256", "string"], [poolId, "admin"]); - const adminRole = keccak256(adminRawRole); - return { managerRole, adminRole }; + return changes; } /** @@ -226,11 +171,12 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> * 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 this.roundRepository.getPendingRoundRoles(chainId, roleName); + const pendingRoles = await roundRepository.getPendingRoundRoles(chainId, roleName); for (const pr of pendingRoles) { changes.push({ type: "InsertRoundRole", @@ -264,15 +210,16 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> amount: bigint, timestamp: number, ): Promise { - const tokenPrice = await this.pricingProvider.getTokenPrice( + const { pricingProvider } = this.dependencies; + const tokenPrice = await pricingProvider.getTokenPrice( this.chainId, token.address, timestamp, - timestamp + 1200000, + timestamp + TIMESTAMP_DELTA_RANGE, ); if (!tokenPrice) { - throw new Error("Token price not found"); + 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..4bb65ad --- /dev/null +++ b/packages/processors/src/exceptions/index.ts @@ -0,0 +1 @@ +export * from "./tokenPriceNotFound.exception.js"; 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/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/strategy.ts b/packages/processors/src/helpers/strategy.ts index bf0ef57..76192e7 100644 --- a/packages/processors/src/helpers/strategy.ts +++ b/packages/processors/src/helpers/strategy.ts @@ -1,9 +1,9 @@ -import { Address, PublicClient } from "viem"; - -import { Branded } from "@grants-stack-indexer/shared"; +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; @@ -143,24 +143,19 @@ export function extractStrategyFromId(_id: Address): Strategy | undefined { //TODO: refactor this into the StrategyHandler when implemented export const getStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategy: Strategy, strategyAddress: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { +): Promise => { switch (strategy.name) { case "allov2.DonationVotingMerkleDistributionDirectTransferStrategy": return getDonationVotingMerkleDistributionDirectTransferStrategyTimings( - viemClient, + evmProvider, strategyAddress, ); case "allov2.DirectGrantsSimpleStrategy": case "allov2.DirectGrantsLiteStrategy": - return getDirectGrantsStrategyTimings(viemClient, strategyAddress); + return getDirectGrantsStrategyTimings(evmProvider, strategyAddress); default: return { applicationsStartTime: null, @@ -178,83 +173,52 @@ export const getStrategyTimings = async ( * @returns The strategy data */ export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategyId: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { - let registrationStartTimeResolved: bigint; - let registrationEndTimeResolved: bigint; - let allocationStartTimeResolved: bigint; - let allocationEndTimeResolved: bigint; +): 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 (viemClient.chain?.contracts?.multicall3) { - const results = await viemClient.multicall({ - contracts: [ - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationEndTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationStartTime", - address: strategyId, - }, - { - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationEndTime", - address: strategyId, - }, - ], + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, allowFailure: false, }); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; - allocationStartTimeResolved = results[2]; - allocationEndTimeResolved = results[3]; } else { - const results = await Promise.all([ - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationStartTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "registrationEndTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationStartTime", - address: strategyId, - }), - viemClient.readContract({ - abi: DonationVotingMerkleDistributionDirectTransferStrategy, - functionName: "allocationEndTime", - address: strategyId, - }), - ]); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; - allocationStartTimeResolved = results[2]; - allocationEndTimeResolved = results[3]; + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint, bigint, bigint]; } return { - applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), - applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), - donationsStartTime: getDateFromTimestamp(allocationStartTimeResolved), - donationsEndTime: getDateFromTimestamp(allocationEndTimeResolved), + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), + donationsStartTime: getDateFromTimestamp(results[2]), + donationsEndTime: getDateFromTimestamp(results[3]), }; }; @@ -265,55 +229,40 @@ export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = * @returns The strategy data */ export const getDirectGrantsStrategyTimings = async ( - viemClient: PublicClient, + evmProvider: EvmProvider, strategyAddress: Address, -): Promise<{ - applicationsStartTime: Date | null; - applicationsEndTime: Date | null; - donationsStartTime: Date | null; - donationsEndTime: Date | null; -}> => { - let registrationStartTimeResolved: bigint; - let registrationEndTimeResolved: bigint; +): Promise => { + let results: [bigint, bigint] = [0n, 0n]; + + const contractCalls = [ + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationStartTime", + address: strategyAddress, + }, + { + abi: DirectGrantsLiteStrategy, + functionName: "registrationEndTime", + address: strategyAddress, + }, + ] as const; - if (viemClient.chain?.contracts?.multicall3) { - const results = await viemClient.multicall({ - contracts: [ - { - abi: DirectGrantsLiteStrategy, - functionName: "registrationStartTime", - address: strategyAddress, - }, - { - abi: DirectGrantsLiteStrategy, - functionName: "registrationEndTime", - address: strategyAddress, - }, - ], + if (evmProvider.getMulticall3Address()) { + results = await evmProvider.multicall({ + contracts: contractCalls, allowFailure: false, }); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; } else { - const results = await Promise.all([ - viemClient.readContract({ - abi: DirectGrantsLiteStrategy, - functionName: "registrationStartTime", - address: strategyAddress, - }), - viemClient.readContract({ - abi: DirectGrantsLiteStrategy, - functionName: "registrationEndTime", - address: strategyAddress, - }), - ]); - registrationStartTimeResolved = results[0]; - registrationEndTimeResolved = results[1]; + results = (await Promise.all( + contractCalls.map((call) => + evmProvider.readContract(call.address, call.abi, call.functionName), + ), + )) as [bigint, bigint]; } return { - applicationsStartTime: getDateFromTimestamp(registrationStartTimeResolved), - applicationsEndTime: getDateFromTimestamp(registrationEndTimeResolved), + applicationsStartTime: getDateFromTimestamp(results[0]), + applicationsEndTime: getDateFromTimestamp(results[1]), donationsStartTime: null, donationsEndTime: null, }; 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/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 index 8e7119b..7d0967b 100644 --- a/packages/processors/test/allo/allo.processor.spec.ts +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -1,9 +1,12 @@ -import { Chain, PublicClient, Transport } from "viem"; 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 { IRoundReadRepository } from "@grants-stack-indexer/repository"; +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"; @@ -22,24 +25,24 @@ vi.mock("../../src/allo/handlers/poolCreated.handler.js", () => { describe("AlloProcessor", () => { const mockChainId = 10 as ChainId; let processor: AlloProcessor; - let mockViemClient: PublicClient; + let mockEvmProvider: EvmProvider; let mockPricingProvider: IPricingProvider; let mockMetadataProvider: IMetadataProvider; let mockRoundRepository: IRoundReadRepository; beforeEach(() => { - mockViemClient = {} as PublicClient; + mockEvmProvider = {} as EvmProvider; mockPricingProvider = {} as IPricingProvider; mockMetadataProvider = {} as IMetadataProvider; mockRoundRepository = {} as IRoundReadRepository; - processor = new AlloProcessor( - mockChainId, - mockViemClient, - mockPricingProvider, - mockMetadataProvider, - mockRoundRepository, - ); + processor = new AlloProcessor(mockChainId, { + evmProvider: mockEvmProvider, + pricingProvider: mockPricingProvider, + metadataProvider: mockMetadataProvider, + roundRepository: mockRoundRepository, + projectRepository: {} as IProjectReadRepository, + }); // Reset mocks before each test vi.clearAllMocks(); @@ -56,7 +59,7 @@ describe("AlloProcessor", () => { await processor.process(mockEvent); expect(PoolCreatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, diff --git a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts index 5fc396d..1332cbd 100644 --- a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -1,23 +1,14 @@ -import { Chain, GetTransactionReturnType, parseUnits, PublicClient, Transport } from "viem"; +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 { IMetadataProvider } from "@grants-stack-indexer/metadata"; -import { IPricingProvider } from "@grants-stack-indexer/pricing"; -import { IRoundReadRepository, Round } from "@grants-stack-indexer/repository"; import { mergeDeep } from "@grants-stack-indexer/shared"; import { PoolCreatedHandler } from "../../../src/allo/handlers/poolCreated.handler.js"; -import * as strategy from "../../../src/helpers/strategy.js"; - -vi.mock("../../../src/helpers/strategy.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - getDonationVotingMerkleDistributionDirectTransferStrategyTimings: vi.fn(), - getDirectGrantsStrategyTimings: vi.fn(), - }; -}); // Function to create a mock event with optional overrides function createMockEvent( @@ -38,10 +29,7 @@ function createMockEvent( strategyId: "0x9fa6890423649187b1f0e8bf4265f0305ce99523c3d11aa36b35a54617bb0ec0", amount: 0n, token: "0x4200000000000000000000000000000000000042", - metadata: { - pointer: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", - protocol: 1n, - }, + metadata: ["bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", 1n], }, transactionFields: { hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", @@ -54,16 +42,18 @@ function createMockEvent( } describe("PoolCreatedHandler", () => { - let mockViemClient: PublicClient; + let mockEvmProvider: EvmProvider; let mockPricingProvider: IPricingProvider; let mockMetadataProvider: IMetadataProvider; let mockRoundRepository: IRoundReadRepository; beforeEach(() => { - mockViemClient = { + mockEvmProvider = { readContract: vi.fn(), getTransaction: vi.fn(), - } as unknown as PublicClient; + multicall: vi.fn(), + getMulticall3Address: vi.fn().mockRejectedValue("0xmulticall3"), + } as unknown as EvmProvider; mockPricingProvider = { getTokenPrice: vi.fn(), }; @@ -92,7 +82,7 @@ describe("PoolCreatedHandler", () => { vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -119,7 +109,7 @@ describe("PoolCreatedHandler", () => { vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -139,10 +129,7 @@ describe("PoolCreatedHandler", () => { createdByAddress: mockEvent.transactionFields.from, }); expect(mockPricingProvider.getTokenPrice).not.toHaveBeenCalled(); - expect(strategy.getDirectGrantsStrategyTimings).not.toHaveBeenCalled(); - expect( - strategy.getDonationVotingMerkleDistributionDirectTransferStrategyTimings, - ).not.toHaveBeenCalled(); + expect(mockEvmProvider.multicall).not.toHaveBeenCalled(); }); it("process a DonationVotingMerkleDistributionDirectTransferStrategy", async () => { @@ -165,15 +152,12 @@ describe("PoolCreatedHandler", () => { priceUsd: 100, timestampMs: 1708369911, }); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles") .mockResolvedValueOnce([ @@ -187,7 +171,7 @@ describe("PoolCreatedHandler", () => { .mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -223,6 +207,10 @@ describe("PoolCreatedHandler", () => { 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", @@ -236,6 +224,7 @@ describe("PoolCreatedHandler", () => { }); expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); + expect(mockEvmProvider.multicall).toHaveBeenCalled(); }); it("fetches transaction sender if not present in event", async () => { @@ -252,12 +241,12 @@ describe("PoolCreatedHandler", () => { timestampMs: 1708369911, }); vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); - vi.spyOn(mockViemClient, "getTransaction").mockResolvedValue({ + vi.spyOn(mockEvmProvider, "getTransaction").mockResolvedValue({ from: "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", } as unknown as GetTransactionReturnType); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -269,28 +258,25 @@ describe("PoolCreatedHandler", () => { expect(changeset.args.round.createdByAddress).toBe( "0xcBf407C33d68a55CB594Ffc8f4fD1416Bba39DA5", ); - expect(mockViemClient.getTransaction).toHaveBeenCalledWith({ - hash: "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", - }); + expect(mockEvmProvider.getTransaction).toHaveBeenCalledWith( + "0xd2352acdcd59e312370831ea927d51a1917654697a72434cd905a60897a5bb8b", + ); }); it("handles an undefined metadata", async () => { const mockEvent = createMockEvent(); vi.spyOn(mockMetadataProvider, "getMetadata").mockResolvedValue(undefined); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles").mockResolvedValue([]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, @@ -320,22 +306,21 @@ describe("PoolCreatedHandler", () => { expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); }); - it("returns empty changeset if token price fetch fails", async () => { - const mockEvent = createMockEvent({ params: { amount: 1n } }); + 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, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, }); - const result = await handler.handle(); - expect(result).toHaveLength(0); + await expect(() => handler.handle()).rejects.toThrow("Token price not found"); }); it("handles pending round roles", async () => { @@ -346,15 +331,12 @@ describe("PoolCreatedHandler", () => { priceUsd: 100, timestampMs: 1708369911, }); - vi.spyOn( - strategy, - "getDonationVotingMerkleDistributionDirectTransferStrategyTimings", - ).mockResolvedValue({ - applicationsStartTime: new Date(), - applicationsEndTime: new Date(), - donationsStartTime: new Date(), - donationsEndTime: new Date(), - }); + vi.spyOn(mockEvmProvider, "multicall").mockResolvedValue([ + 1609459200n, + 1609459200n, + 1609459200n, + 1609459200n, + ]); vi.spyOn(mockRoundRepository, "getPendingRoundRoles") .mockResolvedValueOnce([ @@ -384,7 +366,7 @@ describe("PoolCreatedHandler", () => { ]); const handler = new PoolCreatedHandler(mockEvent, 10 as ChainId, { - viemClient: mockViemClient, + evmProvider: mockEvmProvider, pricingProvider: mockPricingProvider, metadataProvider: mockMetadataProvider, roundRepository: mockRoundRepository, diff --git a/packages/processors/test/helpers/utils.spec.ts b/packages/processors/test/helpers/utils.spec.ts index 1bbdb3e..eccc86b 100644 --- a/packages/processors/test/helpers/utils.spec.ts +++ b/packages/processors/test/helpers/utils.spec.ts @@ -4,33 +4,33 @@ import { getDateFromTimestamp } from "../../src/helpers/utils.js"; describe("utils", () => { describe("getDateFromTimestamp", () => { - it("should convert a valid timestamp to a Date object", () => { + 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("should handle the minimum valid timestamp (0)", () => { + 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("should handle the maximum valid timestamp", () => { + it("handles the maximum valid timestamp", () => { const maxTimestamp = 18446744073709551615n - 1n; const result = getDateFromTimestamp(maxTimestamp); expect(result).toBeInstanceOf(Date); }); - it("should return null for timestamps equal to or greater than UINT64_MAX", () => { + 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("should return null for negative timestamps", () => { + it("returns null for negative timestamps", () => { expect(getDateFromTimestamp(-1n)).toBeNull(); expect(getDateFromTimestamp(-1000000n)).toBeNull(); }); diff --git a/packages/shared/src/types/events/allo.ts b/packages/shared/src/types/events/allo.ts index 353d39d..32f5c44 100644 --- a/packages/shared/src/types/events/allo.ts +++ b/packages/shared/src/types/events/allo.ts @@ -22,8 +22,5 @@ export type PoolCreatedParams = { strategyId: Address; token: Address; amount: bigint; - metadata: { - pointer: string; - protocol: bigint; - }; + metadata: [pointer: string, protocol: bigint]; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51930fb..50f9985 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 From 7fc6f049c4c24d7ca1c9287d4db4d5c2f25c79c8 Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 17 Oct 2024 09:54:37 -0300 Subject: [PATCH 5/7] fix: add todo and fix typo --- packages/processors/src/allo/handlers/poolCreated.handler.ts | 2 +- packages/processors/src/helpers/strategy.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index 5315716..a07a449 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -2,7 +2,7 @@ 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 { isAlloNativeToken } from "@grants-stack-indexer/shared"; import type { IEventHandler, ProcessorDependencies, StrategyTimings } from "../../internal.js"; import { getRoundRoles } from "../../helpers/roles.js"; diff --git a/packages/processors/src/helpers/strategy.ts b/packages/processors/src/helpers/strategy.ts index 76192e7..75bdd68 100644 --- a/packages/processors/src/helpers/strategy.ts +++ b/packages/processors/src/helpers/strategy.ts @@ -14,6 +14,7 @@ type Strategy = { 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. From 11c9bbe2b6a4894c0811112e13b6e79351e14b4c Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 17 Oct 2024 16:53:51 -0300 Subject: [PATCH 6/7] feat: add bignumberjs for money ops --- .../processors/src/allo/allo.processor.ts | 7 +-- .../src/allo/handlers/poolCreated.handler.ts | 8 ++-- packages/processors/src/helpers/tokenMath.ts | 23 +++++----- .../test/allo/allo.processor.spec.ts | 11 +++-- .../allo/handlers/poolCreated.handler.spec.ts | 12 ++--- .../processors/test/helpers/tokenMath.spec.ts | 44 ++++++++++++++----- .../interfaces/roundRepository.interface.ts | 2 +- .../repositories/kysely/round.repository.ts | 2 +- .../repository/src/types/changeset.types.ts | 4 +- packages/repository/src/types/round.types.ts | 6 +-- packages/shared/package.json | 1 + packages/shared/src/external.ts | 3 ++ packages/shared/src/internal.ts | 1 + packages/shared/src/math/bignumber.ts | 4 ++ pnpm-lock.yaml | 11 +++++ 15 files changed, 86 insertions(+), 53 deletions(-) create mode 100644 packages/shared/src/math/bignumber.ts diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 88e93a1..76f3e41 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -13,12 +13,7 @@ export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { async process(event: ProtocolEvent<"Allo", AlloEvent>): Promise { switch (event.eventName) { case "PoolCreated": - return new PoolCreatedHandler(event, this.chainId, { - evmProvider: this.dependencies.evmProvider, - pricingProvider: this.dependencies.pricingProvider, - metadataProvider: this.dependencies.metadataProvider, - roundRepository: this.dependencies.roundRepository, - }).handle(); + return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle(); default: throw new Error(`Unknown event name: ${event.eventName}`); } diff --git a/packages/processors/src/allo/handlers/poolCreated.handler.ts b/packages/processors/src/allo/handlers/poolCreated.handler.ts index a07a449..392b587 100644 --- a/packages/processors/src/allo/handlers/poolCreated.handler.ts +++ b/packages/processors/src/allo/handlers/poolCreated.handler.ts @@ -78,7 +78,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> }; let matchAmount = 0n; - let matchAmountInUsd = 0; + let matchAmountInUsd = "0"; if (strategy) { strategyTimings = await getStrategyTimings(evmProvider, strategy, strategyAddress); @@ -102,7 +102,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> } } - let fundedAmountInUsd = 0; + let fundedAmountInUsd = "0"; if (token !== null && fundedAmount > 0n) { fundedAmountInUsd = await this.getTokenAmountInUsd( @@ -122,7 +122,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> id: poolId.toString(), tags: ["allo-v2", ...(parsedRoundMetadata.success ? ["grants-stack"] : [])], totalDonationsCount: 0, - totalAmountDonatedInUsd: 0, + totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress, matchAmount, @@ -209,7 +209,7 @@ export class PoolCreatedHandler implements IEventHandler<"Allo", "PoolCreated"> token: { address: Address; decimals: number }, amount: bigint, timestamp: number, - ): Promise { + ): Promise { const { pricingProvider } = this.dependencies; const tokenPrice = await pricingProvider.getTokenPrice( this.chainId, diff --git a/packages/processors/src/helpers/tokenMath.ts b/packages/processors/src/helpers/tokenMath.ts index 4d32849..037e1c2 100644 --- a/packages/processors/src/helpers/tokenMath.ts +++ b/packages/processors/src/helpers/tokenMath.ts @@ -1,4 +1,4 @@ -import { formatUnits, parseUnits } from "viem"; +import { BigNumber } from "@grants-stack-indexer/shared"; /** * Calculates the amount in USD @@ -10,23 +10,22 @@ import { formatUnits, parseUnits } from "viem"; */ export const calculateAmountInUsd = ( amount: bigint, - tokenPriceInUsd: number, + tokenPriceInUsd: string | number, tokenDecimals: number, truncateDecimals?: number, -): number => { - const amountInUsd = Number( - formatUnits( - amount * parseUnits(tokenPriceInUsd.toString(), tokenDecimals), - tokenDecimals * 2, - ), - ); +): string => { + const amountBN = new BigNumber(amount.toString()); + const tokenPriceBN = new BigNumber(tokenPriceInUsd.toString()); + const scaleFactor = new BigNumber(10).pow(tokenDecimals); - if (truncateDecimals) { + let amountInUsd = amountBN.multipliedBy(tokenPriceBN).dividedBy(scaleFactor); + + if (truncateDecimals !== undefined) { if (truncateDecimals < 0 || truncateDecimals > 18) { throw new Error("Truncate decimals must be between 0 and 18"); } - return Number(amountInUsd.toFixed(truncateDecimals)); + amountInUsd = amountInUsd.decimalPlaces(truncateDecimals); } - return amountInUsd; + return amountInUsd.toString(); }; diff --git a/packages/processors/test/allo/allo.processor.spec.ts b/packages/processors/test/allo/allo.processor.spec.ts index 7d0967b..5650eeb 100644 --- a/packages/processors/test/allo/allo.processor.spec.ts +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -58,12 +58,11 @@ describe("AlloProcessor", () => { await processor.process(mockEvent); - expect(PoolCreatedHandler).toHaveBeenCalledWith(mockEvent, mockChainId, { - evmProvider: mockEvmProvider, - pricingProvider: mockPricingProvider, - metadataProvider: mockMetadataProvider, - roundRepository: mockRoundRepository, - }); + expect(PoolCreatedHandler).toHaveBeenCalledWith( + mockEvent, + mockChainId, + processor["dependencies"], + ); expect(PoolCreatedHandler.prototype.handle).toHaveBeenCalled(); }); diff --git a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts index 1332cbd..49b3d43 100644 --- a/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts +++ b/packages/processors/test/allo/handlers/poolCreated.handler.spec.ts @@ -94,7 +94,7 @@ describe("PoolCreatedHandler", () => { expect(changeset.type).toBe("InsertRound"); expect(changeset.args.round).toMatchObject({ fundedAmount: fundedAmount, - fundedAmountInUsd: 1000, + fundedAmountInUsd: "1000", }); expect(mockPricingProvider.getTokenPrice).toHaveBeenCalled(); expect(mockMetadataProvider.getMetadata).toHaveBeenCalled(); @@ -188,13 +188,13 @@ describe("PoolCreatedHandler", () => { id: "10", tags: ["allo-v2", "grants-stack"], totalDonationsCount: 0, - totalAmountDonatedInUsd: 0, + totalAmountDonatedInUsd: "0", uniqueDonorsCount: 0, matchTokenAddress: mockEvent.params.token, matchAmount: parseUnits("1", 18), - matchAmountInUsd: 100, + matchAmountInUsd: "100", fundedAmount: 0n, - fundedAmountInUsd: 0, + fundedAmountInUsd: "0", applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", applicationMetadata: { version: "1.0.0", @@ -291,9 +291,9 @@ describe("PoolCreatedHandler", () => { id: "10", tags: ["allo-v2"], matchAmount: 0n, - matchAmountInUsd: 0, + matchAmountInUsd: "0", fundedAmount: 0n, - fundedAmountInUsd: 0, + fundedAmountInUsd: "0", applicationMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", applicationMetadata: {}, roundMetadataCid: "bafkreihrjyu5tney6wia2hmkertc74nzfpsgxw2epvnxm72bxj6ifnd4ku", diff --git a/packages/processors/test/helpers/tokenMath.spec.ts b/packages/processors/test/helpers/tokenMath.spec.ts index 5702e52..f15500d 100644 --- a/packages/processors/test/helpers/tokenMath.spec.ts +++ b/packages/processors/test/helpers/tokenMath.spec.ts @@ -10,7 +10,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(100); + expect(result).toBe("100"); }); it("calculate USD amount for 18 decimal token with float price", () => { @@ -19,7 +19,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBeCloseTo(41.025, 5); + expect(result).toBe("41.025"); }); it("calculate USD amount for 8 decimal token with integer price", () => { @@ -28,7 +28,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 8; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(50); + expect(result).toBe("50"); }); // Test case for 8 decimal token with float price @@ -38,7 +38,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 8; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBeCloseTo(19.125, 5); + expect(result).toBe("19.125"); }); it("correctly calculate USD amount for 1gwei token amount", () => { @@ -47,7 +47,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0.000001); + expect(result).toBe("0.000001"); }); it("correctly truncate decimals when specified", () => { @@ -56,7 +56,27 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 4); - expect(result).toBe(1.5185); + 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", () => { @@ -65,7 +85,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0); + expect(result).toBe("0"); }); it("should return zero for zero token price", () => { @@ -74,7 +94,7 @@ describe("calculateAmountInUsd", () => { const tokenDecimals = 18; const result = calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals); - expect(result).toBe(0); + expect(result).toBe("0"); }); it("throw an error for invalid truncate decimals", () => { @@ -87,12 +107,12 @@ describe("calculateAmountInUsd", () => { }); test("migrated cases", () => { - expect(calculateAmountInUsd(3400000000000000000n, 1, 18, 8)).toBe(3.4); + expect(calculateAmountInUsd(3400000000000000000n, 1, 18, 8)).toBe("3.4"); - expect(calculateAmountInUsd(50000000000n, 1, 18, 8)).toBe(0.00000005); + expect(calculateAmountInUsd(50000000000n, 1, 18, 8)).toBe("0.00000005"); - expect(calculateAmountInUsd(3400000000000000000n, 0.5, 18, 8)).toBe(1.7); + expect(calculateAmountInUsd(3400000000000000000n, 0.5, 18, 8)).toBe("1.7"); - expect(calculateAmountInUsd(3400000000000000000n, 2, 18, 8)).toBe(6.8); + expect(calculateAmountInUsd(3400000000000000000n, 2, 18, 8)).toBe("6.8"); }); }); 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/external.ts b/packages/shared/src/external.ts index d2aa32f..9b1b0af 100644 --- a/packages/shared/src/external.ts +++ b/packages/shared/src/external.ts @@ -10,3 +10,6 @@ export { export type { DeepPartial } from "./utils/testing.js"; export { mergeDeep } from "./utils/testing.js"; export type { ILogger, Logger } from "./internal.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 6462239..a6a96fb 100644 --- a/packages/shared/src/internal.ts +++ b/packages/shared/src/internal.ts @@ -1,4 +1,5 @@ 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"; 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/pnpm-lock.yaml b/pnpm-lock.yaml index 50f9985..36e40e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -200,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) @@ -1524,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: { @@ -5360,6 +5369,8 @@ snapshots: base64-js@1.5.1: {} + bignumber.js@9.1.2: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: From 89d2a63d8d0e15ee0ab49db85fa5bc9ac8ddeead Mon Sep 17 00:00:00 2001 From: nigiri <168690269+0xnigir1@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:39:24 -0300 Subject: [PATCH 7/7] fix: add specific exceptions --- packages/processors/src/allo/allo.processor.ts | 3 ++- packages/processors/src/exceptions/index.ts | 2 ++ .../src/exceptions/invalidArgument.exception.ts | 5 +++++ .../src/exceptions/unsupportedEvent.exception.ts | 10 ++++++++++ packages/processors/src/helpers/strategy.ts | 7 +++++-- packages/processors/src/helpers/tokenMath.ts | 5 ++++- packages/processors/test/allo/allo.processor.spec.ts | 5 ++--- packages/processors/test/helpers/tokenMath.spec.ts | 9 +++++++-- 8 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 packages/processors/src/exceptions/invalidArgument.exception.ts create mode 100644 packages/processors/src/exceptions/unsupportedEvent.exception.ts diff --git a/packages/processors/src/allo/allo.processor.ts b/packages/processors/src/allo/allo.processor.ts index 76f3e41..2a5ffed 100644 --- a/packages/processors/src/allo/allo.processor.ts +++ b/packages/processors/src/allo/allo.processor.ts @@ -2,6 +2,7 @@ import { Changeset } from "@grants-stack-indexer/repository"; import { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/shared"; 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> { @@ -15,7 +16,7 @@ export class AlloProcessor implements IProcessor<"Allo", AlloEvent> { case "PoolCreated": return new PoolCreatedHandler(event, this.chainId, this.dependencies).handle(); default: - throw new Error(`Unknown event name: ${event.eventName}`); + throw new UnsupportedEventException("Allo", event.eventName); } } } diff --git a/packages/processors/src/exceptions/index.ts b/packages/processors/src/exceptions/index.ts index 4bb65ad..746c664 100644 --- a/packages/processors/src/exceptions/index.ts +++ b/packages/processors/src/exceptions/index.ts @@ -1 +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/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/strategy.ts b/packages/processors/src/helpers/strategy.ts index 75bdd68..a71937c 100644 --- a/packages/processors/src/helpers/strategy.ts +++ b/packages/processors/src/helpers/strategy.ts @@ -143,6 +143,9 @@ export function extractStrategyFromId(_id: Address): Strategy | 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, @@ -169,7 +172,7 @@ export const getStrategyTimings = async ( /** * Gets the strategy data for the DonationVotingMerkleDistributionDirectTransferStrategy - * @param viemClient - The viem client + * @param evmProvider - The evm provider * @param strategyId - The address of the strategy * @returns The strategy data */ @@ -225,7 +228,7 @@ export const getDonationVotingMerkleDistributionDirectTransferStrategyTimings = /** * Gets the strategy data for the DirectGrantsStrategy - * @param viemClient - The viem client + * @param evmProvider - The evm provider * @param strategyAddress - The address of the strategy * @returns The strategy data */ diff --git a/packages/processors/src/helpers/tokenMath.ts b/packages/processors/src/helpers/tokenMath.ts index 037e1c2..e48da9b 100644 --- a/packages/processors/src/helpers/tokenMath.ts +++ b/packages/processors/src/helpers/tokenMath.ts @@ -1,11 +1,14 @@ 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 = ( @@ -22,7 +25,7 @@ export const calculateAmountInUsd = ( if (truncateDecimals !== undefined) { if (truncateDecimals < 0 || truncateDecimals > 18) { - throw new Error("Truncate decimals must be between 0 and 18"); + throw new InvalidArgument("Truncate decimals must be between 0 and 18"); } amountInUsd = amountInUsd.decimalPlaces(truncateDecimals); } diff --git a/packages/processors/test/allo/allo.processor.spec.ts b/packages/processors/test/allo/allo.processor.spec.ts index 5650eeb..173a2b3 100644 --- a/packages/processors/test/allo/allo.processor.spec.ts +++ b/packages/processors/test/allo/allo.processor.spec.ts @@ -11,6 +11,7 @@ import type { AlloEvent, ChainId, ProtocolEvent } from "@grants-stack-indexer/sh 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", () => { @@ -71,8 +72,6 @@ describe("AlloProcessor", () => { eventName: "UnknownEvent", } as unknown as ProtocolEvent<"Allo", AlloEvent>; - await expect(() => processor.process(mockEvent)).rejects.toThrow( - "Unknown event name: UnknownEvent", - ); + await expect(() => processor.process(mockEvent)).rejects.toThrow(UnsupportedEventException); }); }); diff --git a/packages/processors/test/helpers/tokenMath.spec.ts b/packages/processors/test/helpers/tokenMath.spec.ts index f15500d..3aee76c 100644 --- a/packages/processors/test/helpers/tokenMath.spec.ts +++ b/packages/processors/test/helpers/tokenMath.spec.ts @@ -2,6 +2,7 @@ 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", () => { @@ -102,8 +103,12 @@ describe("calculateAmountInUsd", () => { const tokenPriceInUsd = 100; const tokenDecimals = 18; - expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, -1)).toThrow(); - expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 19)).toThrow(); + expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, -1)).toThrow( + InvalidArgument, + ); + expect(() => calculateAmountInUsd(amount, tokenPriceInUsd, tokenDecimals, 19)).toThrow( + InvalidArgument, + ); }); test("migrated cases", () => {