diff --git a/src/config.ts b/src/config.ts index deb63e01..7a7f63c5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -21,7 +21,7 @@ type CoingeckoSupportedChainId = | 42220 | 1088; -const CHAIN_DATA_VERSION = "83"; +const CHAIN_DATA_VERSION = "86"; const IPFS_DATA_VERSION = "1"; const PRICE_DATA_VERSION = "1"; @@ -368,6 +368,12 @@ const CHAINS: Chain[] = [ address: "0xCd5AbD09ee34BA604795F7f69413caf20ee0Ab60", fromBlock: 5100681, }, + // Gitcoin Attestation Network + { + contractName: "GitcoinAttestationNetwork/GitcoinGrantsResolver", + address: "0xBAa70bbAB3C4a7265f879bb3658336DE893b8582", + fromBlock: 6746370, + }, ], }, { @@ -711,6 +717,12 @@ const CHAINS: Chain[] = [ address: "0x1133eA7Af70876e64665ecD07C0A0476d09465a1", fromBlock: 146498081, }, + // Gitcoin Attestation Network + // { + // contractName: "GitcoinAttestationNetwork/GitcoinGrantsResolver", + // address: "0x0", + // fromBlock: 0, + // }, ], }, { diff --git a/src/database/changeset.ts b/src/database/changeset.ts index c53d2a14..3fb89864 100644 --- a/src/database/changeset.ts +++ b/src/database/changeset.ts @@ -17,6 +17,7 @@ import { NewLegacyProject, NewApplicationPayout, NewIpfsData, + NewAttestationData, } from "./schema.js"; export type DataChange = @@ -145,4 +146,8 @@ export type DataChange = | { type: "InsertIpfsData"; ipfs: NewIpfsData; + } + | { + type: "InsertAttestation"; + attestation: NewAttestationData; }; diff --git a/src/database/index.ts b/src/database/index.ts index e767b354..c403cd48 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -15,6 +15,8 @@ import { LegacyProjectTable, ApplicationPayout, IpfsDataTable, + AttestationTable, + AttestationTxnTable, } from "./schema.js"; import { migrate, migrateDataFetcher, migratePriceFetcher } from "./migrate.js"; import { encodeJsonWithBigInts } from "../utils/index.js"; @@ -39,6 +41,8 @@ interface Tables { legacyProjects: LegacyProjectTable; applicationsPayouts: ApplicationPayout; ipfsData: IpfsDataTable; + attestations: AttestationTable; + attestationTxns: AttestationTxnTable; } type KyselyDb = Kysely; @@ -617,6 +621,36 @@ export class Database { break; } + case "InsertAttestation": { + const attestationData = change.attestation.attestationData; + const transactionsData = change.attestation.transactionsData; + + // Insert into attestations + await this.#db + .withSchema(this.chainDataSchemaName) + .insertInto("attestations") + .values(attestationData) + .execute(); + + // Insert into attestation transactions + const attestationTxns: AttestationTxnTable[] = []; + for (let i = 0; i < transactionsData.length; i++) { + // Link transaction to attestation + attestationTxns.push({ + chainId: transactionsData[i].chainId, + txnHash: transactionsData[i].txnHash, + attestationUid: attestationData.uid, + attestationChainId: attestationData.chainId, + }); + } + await this.#db + .withSchema(this.chainDataSchemaName) + .insertInto("attestationTxns") + .values(attestationTxns) + .execute(); + break; + } + default: throw new Error(`Unknown changeset type`); } diff --git a/src/database/migrate.ts b/src/database/migrate.ts index db6e2b88..2c828393 100644 --- a/src/database/migrate.ts +++ b/src/database/migrate.ts @@ -297,6 +297,48 @@ export async function migrate(db: Kysely, schemaName: string) { .columns(["chainId", "roundId", "applicationId"]) .execute(); + await schema + .createTable("attestations") + .addColumn("uid", "text") + .addColumn("chainId", CHAIN_ID_TYPE) + .addColumn("fee", BIGINT_TYPE) + .addColumn("recipient", ADDRESS_TYPE) + .addColumn("refUID", "text") + .addColumn("projectsContributed", BIGINT_TYPE) + .addColumn("roundsContributed", BIGINT_TYPE) + .addColumn("chainIdsContributed", BIGINT_TYPE) + .addColumn("totalUSDAmount", BIGINT_TYPE) + .addColumn("timestamp", "timestamptz") + .addColumn("metadataCid", "text") + .addColumn("metadata", "jsonb") + .addUniqueConstraint("unique_uid_chainId", ["uid", "chainId"]) + .execute(); + + await schema + .createTable("attestation_txns") + .addColumn("txnHash", "text") + .addColumn("chainId", CHAIN_ID_TYPE) + + // Add the foreign key columns + .addColumn("attestationUid", "text") + .addColumn("attestationChainId", CHAIN_ID_TYPE) + + // Add Constraints + .addUniqueConstraint("unique_txnHash_chainId_attestationUid", [ + "txnHash", + "chainId", + "attestationUid", + ]) + + .addForeignKeyConstraint( + "attestation_txns_attestations_fkey", + ["attestationUid", "attestationChainId"], + "attestations", + ["uid", "chainId"], + (cb) => cb.onDelete("cascade") + ) + .execute(); + await schema .createTable("legacy_projects") .addColumn("id", "serial", (col) => col.primaryKey()) @@ -338,6 +380,11 @@ export async function migrate(db: Kysely, schemaName: string) { "applications" )}(id, chain_id)|@fieldNme application'; + comment on table ${ref("attestation_txns")} is + E'@foreignKey ("txn_hash", "chain_id") references ${ref( + "donations" + )}(transaction_hash, chain_id)|@fieldName donations'; + create function ${ref("applications_canonical_project")}(a ${ref( "applications" )} ) returns ${ref("projects")} as $$ @@ -356,6 +403,11 @@ export async function migrate(db: Kysely, schemaName: string) { )}(id, round_id, chain_id)|@fieldName application|@foreignFieldName donations '; + comment on constraint "attestation_txns_attestations_fkey" on ${ref( + "attestation_txns" + )} is + E'@foreignFieldName attestationTxns\n@fieldName attestation'; + comment on constraint "round_roles_rounds_fkey" on ${ref("round_roles")} is E'@foreignFieldName roles\n@fieldName round'; diff --git a/src/database/schema.ts b/src/database/schema.ts index 95051343..cd4c377b 100644 --- a/src/database/schema.ts +++ b/src/database/schema.ts @@ -8,6 +8,7 @@ import { import { Address, Hex, ChainId } from "../types.js"; import { z } from "zod"; +import { AttestationTxnData } from "../indexer/types.js"; export type MatchingDistribution = z.infer; @@ -267,3 +268,31 @@ export type NewIpfsData = { cid: string; data: unknown; }; + +// Attestations +export type AttestationTable = { + uid: string; + chainId: ChainId; + fee: bigint; + recipient: Address; + refUID: string; + projectsContributed: bigint; + roundsContributed: bigint; + chainIdsContributed: bigint; + totalUSDAmount: bigint; + timestamp: Date | null; + metadataCid: string; + metadata: unknown; +}; + +export type AttestationTxnTable = { + chainId: ChainId; + txnHash: string; + attestationUid: string; + attestationChainId: ChainId; +}; + +export type NewAttestationData = { + attestationData: AttestationTable; + transactionsData: AttestationTxnData[]; +}; diff --git a/src/index.ts b/src/index.ts index 880d930c..e73e99a2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,7 @@ import abis from "./indexer/abis/index.js"; import type { EventHandlerContext } from "./indexer/indexer.js"; import { handleEvent as handleAlloV1Event } from "./indexer/allo/v1/handleEvent.js"; import { handleEvent as handleAlloV2Event } from "./indexer/allo/v2/handleEvent.js"; +import { handleEvent as handleGitcoinAttestationNetworkEvent } from "./indexer/gitcoin-attestation-network/handleEvent.js"; import { Database } from "./database/index.js"; import { decodeJsonWithBigInts, getExternalIP } from "./utils/index.js"; import { Block } from "chainsauce/dist/cache.js"; @@ -650,9 +651,21 @@ async function catchupAndWatchChain( }); }); } else { - const handler = args.event.contractName.startsWith("AlloV1") - ? handleAlloV1Event - : handleAlloV2Event; + let handler; + + if (args.event.contractName.startsWith("GitcoinAttestationNetwork")) { + handler = handleGitcoinAttestationNetworkEvent; + } else if (args.event.contractName.startsWith("AlloV1")) { + handler = handleAlloV1Event; + } else if (args.event.contractName.startsWith("AlloV2")) { + handler = handleAlloV2Event; + } else { + indexerLogger.warn({ + msg: "skipping event due to unknown contract", + event: args.event, + }); + return; + } const changesets = await handler(args); diff --git a/src/indexer/abis/gitcoin-attestation-network/GitcoinGrantsResolver.ts b/src/indexer/abis/gitcoin-attestation-network/GitcoinGrantsResolver.ts new file mode 100644 index 00000000..ee850862 --- /dev/null +++ b/src/indexer/abis/gitcoin-attestation-network/GitcoinGrantsResolver.ts @@ -0,0 +1,829 @@ +export default [ + { + inputs: [ + { + internalType: "contract IEAS", + name: "_eas", + type: "address", + }, + { + internalType: "address", + name: "_admin", + type: "address", + }, + { + internalType: "address", + name: "_manager", + type: "address", + }, + { + internalType: "address[]", + name: "_delegators", + type: "address[]", + }, + { + internalType: "address", + name: "_treasury", + type: "address", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [], + name: "AccessControlBadConfirmation", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + { + internalType: "bytes32", + name: "neededRole", + type: "bytes32", + }, + ], + name: "AccessControlUnauthorizedAccount", + type: "error", + }, + { + inputs: [], + name: "AccessDenied", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "target", + type: "address", + }, + ], + name: "AddressEmptyCode", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "AddressInsufficientBalance", + type: "error", + }, + { + inputs: [], + name: "FailedInnerCall", + type: "error", + }, + { + inputs: [], + name: "InsufficientValue", + type: "error", + }, + { + inputs: [], + name: "InvalidEAS", + type: "error", + }, + { + inputs: [], + name: "InvalidLength", + type: "error", + }, + { + inputs: [], + name: "NotAdmin", + type: "error", + }, + { + inputs: [], + name: "NotDelegatorsManager", + type: "error", + }, + { + inputs: [], + name: "NotPayable", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address", + }, + ], + name: "SafeERC20FailedOperation", + type: "error", + }, + { + inputs: [], + name: "UnauthorizedAttester", + type: "error", + }, + { + inputs: [], + name: "ZeroAddress", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + { + indexed: true, + internalType: "address", + name: "recipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "fee", + type: "uint256", + }, + { + indexed: false, + internalType: "bytes", + name: "data", + type: "bytes", + }, + { + indexed: false, + internalType: "bytes32", + name: "refUID", + type: "bytes32", + }, + ], + name: "OnAttested", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "previousAdminRole", + type: "bytes32", + }, + { + indexed: true, + internalType: "bytes32", + name: "newAdminRole", + type: "bytes32", + }, + ], + name: "RoleAdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + indexed: true, + internalType: "address", + name: "account", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "RoleRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "treasury", + type: "address", + }, + ], + name: "TreasuryUpdated", + type: "event", + }, + { + inputs: [], + name: "DEFAULT_ADMIN_ROLE", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DELEGATORS_MANAGER_ROLE", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "DELEGATOR_ROLE", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "NATIVE", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_delegators", + type: "address[]", + }, + ], + name: "addDelegators", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "schema", + type: "bytes32", + }, + { + internalType: "uint64", + name: "time", + type: "uint64", + }, + { + internalType: "uint64", + name: "expirationTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "revocationTime", + type: "uint64", + }, + { + internalType: "bytes32", + name: "refUID", + type: "bytes32", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "attester", + type: "address", + }, + { + internalType: "bool", + name: "revocable", + type: "bool", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Attestation", + name: "attestation", + type: "tuple", + }, + ], + name: "attest", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + ], + name: "getRoleAdmin", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "grantRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "hasRole", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "isPayable", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "schema", + type: "bytes32", + }, + { + internalType: "uint64", + name: "time", + type: "uint64", + }, + { + internalType: "uint64", + name: "expirationTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "revocationTime", + type: "uint64", + }, + { + internalType: "bytes32", + name: "refUID", + type: "bytes32", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "attester", + type: "address", + }, + { + internalType: "bool", + name: "revocable", + type: "bool", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Attestation[]", + name: "attestations", + type: "tuple[]", + }, + { + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + ], + name: "multiAttest", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "schema", + type: "bytes32", + }, + { + internalType: "uint64", + name: "time", + type: "uint64", + }, + { + internalType: "uint64", + name: "expirationTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "revocationTime", + type: "uint64", + }, + { + internalType: "bytes32", + name: "refUID", + type: "bytes32", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "attester", + type: "address", + }, + { + internalType: "bool", + name: "revocable", + type: "bool", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Attestation[]", + name: "attestations", + type: "tuple[]", + }, + { + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + ], + name: "multiRevoke", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "address[]", + name: "_delegators", + type: "address[]", + }, + ], + name: "removeDelegators", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "callerConfirmation", + type: "address", + }, + ], + name: "renounceRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "bytes32", + name: "uid", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "schema", + type: "bytes32", + }, + { + internalType: "uint64", + name: "time", + type: "uint64", + }, + { + internalType: "uint64", + name: "expirationTime", + type: "uint64", + }, + { + internalType: "uint64", + name: "revocationTime", + type: "uint64", + }, + { + internalType: "bytes32", + name: "refUID", + type: "bytes32", + }, + { + internalType: "address", + name: "recipient", + type: "address", + }, + { + internalType: "address", + name: "attester", + type: "address", + }, + { + internalType: "bool", + name: "revocable", + type: "bool", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct Attestation", + name: "attestation", + type: "tuple", + }, + ], + name: "revoke", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes32", + name: "role", + type: "bytes32", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "revokeRole", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "treasury", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_treasury", + type: "address", + }, + ], + name: "updateTreasury", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "version", + outputs: [ + { + internalType: "string", + name: "", + type: "string", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_token", + type: "address", + }, + { + internalType: "address", + name: "_to", + type: "address", + }, + { + internalType: "uint256", + name: "_amount", + type: "uint256", + }, + ], + name: "withdraw", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + stateMutability: "payable", + type: "receive", + }, +] as const; diff --git a/src/indexer/abis/index.ts b/src/indexer/abis/index.ts index 87caba85..730840d8 100644 --- a/src/indexer/abis/index.ts +++ b/src/indexer/abis/index.ts @@ -29,7 +29,11 @@ import AlloV2DirectGrantsLiteStrategy from "./allo-v2/v1/DirectGrantsLiteStrateg import AlloV2EasyRPGFStrategy from "./allo-v2/v1/EasyRPGFStrategy.js"; import AlloV2DirectAllocationStrategy from "./allo-v2/v1/DirectAllocationStrategy.js"; +// Gitcoin Attestation Network +import GitcoinAttestationNetwork from "./gitcoin-attestation-network/GitcoinGrantsResolver.js"; + const abis = { + // Allo V1 "AlloV1/ProjectRegistry/V1": ProjectRegistryV1, "AlloV1/ProjectRegistry/V2": ProjectRegistryV2, "AlloV1/ProgramFactory/V1": ProgramFactoryV1, @@ -53,10 +57,14 @@ const abis = { "AlloV1/MerklePayoutStrategyImplementation/V2": MerklePayoutStrategyImplementation, + // Allo V2 Profile Migration "AlloV2/AlloV1ToV2ProfileMigration": AlloV1ToV2ProfileMigration, - "AlloV2/Allo/V1": AlloV2, + // Allo V2 Registry "AlloV2/Registry/V1": AlloV2Registry, + + // Allo V2 Core + "AlloV2/Allo/V1": AlloV2, "AlloV2/IStrategy/V1": AlloV2IStrategy, "AlloV2/DonationVotingMerkleDistributionDirectTransferStrategy/V1": AlloV2DonationVotingMerkleDistributionDirectTransferStrategy, @@ -64,6 +72,9 @@ const abis = { "AlloV2/DirectGrantsLiteStrategy/V1": AlloV2DirectGrantsLiteStrategy, "AlloV2/EasyRPGFStrategy/V1": AlloV2EasyRPGFStrategy, "AlloV2/DirectAllocationStrategy/V1": AlloV2DirectAllocationStrategy, + + // Gitcoin Attestation Network + "GitcoinAttestationNetwork/GitcoinGrantsResolver": GitcoinAttestationNetwork, } as const; export default abis; diff --git a/src/indexer/allo/v1/handleEvent.ts b/src/indexer/allo/v1/handleEvent.ts index 95ddf7db..7cbfd71e 100644 --- a/src/indexer/allo/v1/handleEvent.ts +++ b/src/indexer/allo/v1/handleEvent.ts @@ -135,7 +135,7 @@ export async function handleEvent( msg: `MetadataUpdated: Failed to parse metadata for project ${projectId}`, event, metadataCid, - metadata, + // metadata, }); return []; } @@ -255,7 +255,7 @@ export async function handleEvent( msg: `ProgramCreated: Failed to parse metadata for program ${programAddress}`, event, metadataCid, - metadata, + // metadata, }); return []; } diff --git a/src/indexer/allo/v2/handleEvent.ts b/src/indexer/allo/v2/handleEvent.ts index c1cd3d1b..7db6631f 100644 --- a/src/indexer/allo/v2/handleEvent.ts +++ b/src/indexer/allo/v2/handleEvent.ts @@ -200,7 +200,7 @@ export async function handleEvent( msg: `ProfileCreated: Failed to parse metadata for profile ${profileId}`, event, metadataCid, - metadata, + // metadata, }); } diff --git a/src/indexer/gitcoin-attestation-network/handleEvent.ts b/src/indexer/gitcoin-attestation-network/handleEvent.ts new file mode 100644 index 00000000..a159a9f1 --- /dev/null +++ b/src/indexer/gitcoin-attestation-network/handleEvent.ts @@ -0,0 +1,109 @@ +import { EventHandlerArgs } from "chainsauce"; +import { Hex, decodeAbiParameters } from "viem"; +import { parseAddress } from "../../address.js"; +import { Changeset } from "../../database/index.js"; +import type { Indexer } from "../indexer.js"; +import { + AttestationMetadata, + AttestationTxnData, + GitcoinAttestedData, +} from "../types.js"; +import { getDateFromTimestamp } from "../../utils/index.js"; +import { AttestationTable } from "../../database/schema.js"; + +function decodeAttestedData(encodedData: Hex) { + const decodedData = decodeAbiParameters( + [ + { name: "projectsContributed", type: "uint64" }, + { name: "roundsCountributed", type: "uint64" }, + { name: "chainIdsContributed", type: "uint64" }, + { name: "totalUSDAmount", type: "uint128" }, + { name: "timestamp", type: "uint64" }, + { name: "metadataCid", type: "string" }, + ], + encodedData + ); + + const results: GitcoinAttestedData = { + projectsContributed: decodedData[0], + roundsCountributed: decodedData[1], + chainIdsContributed: decodedData[2], + totalUSDAmount: decodedData[3], + timestamp: decodedData[4], + metadataCid: decodedData[5], + }; + + return results; +} + +export async function handleEvent( + args: EventHandlerArgs +): Promise { + const { + chainId, + event, + context: { ipfsGet, logger }, + } = args; + + switch (event.name) { + case "OnAttested": { + const attestationId = event.params.uid; + const recipient = parseAddress(event.params.recipient); + const fee = event.params.fee; + const refUID = event.params.refUID; + + const decodedAttestationData = decodeAttestedData(event.params.data); + + let data: AttestationMetadata[] = []; + try { + data = (await ipfsGet(decodedAttestationData.metadataCid)) ?? []; + } catch (e) { + logger.warn({ + msg: `OnAttested: Failed to fetch metadata for attestation ${attestationId}`, + event, + decodedAttestationData, + }); + return []; + } + + const transactionsData: AttestationTxnData[] = []; + for (let i = 0; i < data.length; i++) { + const metadata = data[i]; + + transactionsData.push({ + chainId: metadata.chainId, + txnHash: metadata.txnHash, + }); + } + + const attestationData: AttestationTable = { + uid: attestationId, + chainId: chainId, + recipient: recipient, + fee: fee, + refUID: refUID, + projectsContributed: decodedAttestationData.projectsContributed, + roundsContributed: decodedAttestationData.roundsCountributed, + chainIdsContributed: decodedAttestationData.chainIdsContributed, + totalUSDAmount: decodedAttestationData.totalUSDAmount, + timestamp: getDateFromTimestamp(decodedAttestationData.timestamp), + metadataCid: decodedAttestationData.metadataCid, + metadata: JSON.stringify(data), + }; + + const changes: Changeset[] = [ + { + type: "InsertAttestation", + attestation: { + attestationData, + transactionsData, + }, + }, + ]; + + return changes; + } + } + + return []; +} diff --git a/src/indexer/types.ts b/src/indexer/types.ts index b8a8bd5a..f3b9b7cb 100644 --- a/src/indexer/types.ts +++ b/src/indexer/types.ts @@ -32,3 +32,36 @@ export type DGTimeStampUpdatedData = { registrationStartTime: bigint; registrationEndTime: bigint; }; + +export type GitcoinAttestedData = { + projectsContributed: bigint; + roundsCountributed: bigint; + chainIdsContributed: bigint; + totalUSDAmount: bigint; + timestamp: bigint; + metadataCid: string; +}; + +export type AttestationTxnData = { + chainId: number; + txnHash: string; + impactImage?: string; +}; + +export type AttestationProjectData = { + id: string; + title: string; + anchor: string; + applicationId: string; + applicationCId: string; + payoutAddress: string; + roundId: number; + strategy: string; + amountInUSD: bigint; + amount: bigint; + token: string; +}; + +export type AttestationMetadata = AttestationTxnData & { + projects: AttestationProjectData[]; +};