diff --git a/.eslintrc b/.eslintrc index 71ec25e..d6f3d63 100644 --- a/.eslintrc +++ b/.eslintrc @@ -2,25 +2,26 @@ "env": { "es2021": true, "node": true, - "jest": true + "jest": true, }, "globals": { - "NodeJS": true + "NodeJS": true, }, "extends": ["airbnb-base", "plugin:import/typescript", "prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { - "ecmaVersion": "latest" + "ecmaVersion": "latest", }, "plugins": ["@typescript-eslint"], "rules": { + "object-shorthand": ["error", "always"], "no-await-in-loop": "off", "no-restricted-syntax": [ "off", { "selector": "ForOfStatement", - "message": "for...of statements are allowed" - } + "message": "for...of statements are allowed", + }, ], "max-classes-per-file": "off", "camelcase": "off", @@ -33,8 +34,8 @@ "no-empty-function": [ "error", { - "allow": ["constructors"] - } + "allow": ["constructors"], + }, ], "padding-line-between-statements": "off", "lines-between-class-members": "off", @@ -43,17 +44,17 @@ "@typescript-eslint/no-use-before-define": [ "error", { - "functions": false - } + "functions": false, + }, ], "@typescript-eslint/no-unused-vars": [ "error", { "args": "after-used", - "argsIgnorePattern": "^_" - } + "argsIgnorePattern": "^_", + }, ], "no-plusplus": "off", - "@typescript-eslint/consistent-type-imports": "error" - } + "@typescript-eslint/consistent-type-imports": "error", + }, } diff --git a/scripts/codegen-any-chain-types.ts b/scripts/codegen-any-chain-types.ts index 08a43be..63739d5 100644 --- a/scripts/codegen-any-chain-types.ts +++ b/scripts/codegen-any-chain-types.ts @@ -40,6 +40,7 @@ function generateTypeImports(chainNames: string[]) { import { Drips as ${chainName}Drips } from './${chainName}/'; import { NftDriver as ${chainName}NftDriver } from './${chainName}/'; import { RepoDriver as ${chainName}RepoDriver } from './${chainName}/'; +import { RepoDeadlineDriver as ${chainName}RepoDeadlineDriver } from './${chainName}/'; import { RepoSubAccountDriver as ${chainName}RepoSubAccountDriver } from './${chainName}/'; import { AddressDriver as ${chainName}AddressDriver } from './${chainName}/'; import { ImmutableSplitsDriver as ${chainName}ImmutableSplitsDriver } from './${chainName}/'; @@ -55,6 +56,7 @@ function generateAnyChainTypes(chainNames: string[]) { export type AnyChainDrips = ${chainNames.map((name) => `${name}Drips`).join(' | ')}; export type AnyChainNftDriver = ${chainNames.map((name) => `${name}NftDriver`).join(' | ')}; export type AnyChainRepoDriver = ${chainNames.map((name) => `${name}RepoDriver`).join(' | ')}; +export type AnyChainRepoDeadlineDriver = ${chainNames.map((name) => `${name}RepoDeadlineDriver`).join(' | ')}; export type AnyChainRepoSubAccountDriver = ${chainNames.map((name) => `${name}RepoSubAccountDriver`).join(' | ')}; export type AnyChainAddressDriver = ${chainNames.map((name) => `${name}AddressDriver`).join(' | ')}; export type AnyChainImmutableSplitsDriver = ${chainNames.map((name) => `${name}ImmutableSplitsDriver`).join(' | ')}; @@ -62,6 +64,7 @@ export type AnyChainImmutableSplitsDriver = ${chainNames.map((name) => `${name}I export type AnyChainDripsFilters = ${chainNames.map((name) => `${name}Drips['filters']`).join(' & ')}; export type AnyChainNftDriverFilters = ${chainNames.map((name) => `${name}NftDriver['filters']`).join(' & ')}; export type AnyChainRepoDriverFilters = ${chainNames.map((name) => `${name}RepoDriver['filters']`).join(' & ')}; +export type AnyChainRepoDeadlineDriverFilters = ${chainNames.map((name) => `${name}RepoDeadlineDriver['filters']`).join(' & ')}; export type AnyChainRepoSubAccountDriverFilters = ${chainNames.map((name) => `${name}RepoSubAccountDriver['filters']`).join(' & ')}; export type AnyChainAddressDriverFilters = ${chainNames.map((name) => `${name}AddressDriver['filters']`).join(' & ')}; export type AnyChainImmutableSplitsDriverFilters = ${chainNames.map((name) => `${name}ImmutableSplitsDriver['filters']`).join(' & ')}; @@ -76,7 +79,7 @@ function generateContractGetters() { return ` import type { Provider } from 'ethers'; -import { Drips__factory, NftDriver__factory, RepoDriver__factory, AddressDriver__factory, ImmutableSplitsDriver__factory, RepoSubAccountDriver__factory } from './${process.env.NETWORK}'; +import { Drips__factory, NftDriver__factory, RepoDriver__factory, RepoDeadlineDriver__factory, AddressDriver__factory, ImmutableSplitsDriver__factory, RepoSubAccountDriver__factory } from './${process.env.NETWORK}'; export const getDripsContract: (contractAddress: string, provider: Provider) => AnyChainDrips = (contractAddress, provider) => Drips__factory.connect( contractAddress, @@ -93,6 +96,11 @@ export const getRepoDriverContract: (contractAddress: string, provider: Provider provider ); +export const getRepoDeadlineDriverContract: (contractAddress: string, provider: Provider) => AnyChainRepoDeadlineDriver = (contractAddress, provider) => RepoDeadlineDriver__factory.connect( + contractAddress, + provider +); + export const getRepoSubAccountDriverContract: (contractAddress: string, provider: Provider) => AnyChainRepoSubAccountDriver = (contractAddress, provider) => RepoSubAccountDriver__factory.connect( contractAddress, provider diff --git a/src/abi/filecoin/RepoDeadlineDriver.json b/src/abi/filecoin/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/filecoin/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/goerli/RepoDeadlineDriver.json b/src/abi/goerli/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/goerli/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/localtestnet/RepoDeadlineDriver.json b/src/abi/localtestnet/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/localtestnet/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/mainnet/RepoDeadlineDriver.json b/src/abi/mainnet/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/mainnet/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/metis/RepoDeadlineDriver.json b/src/abi/metis/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/metis/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism/RepoDeadlineDriver.json b/src/abi/optimism/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/optimism/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/optimism_sepolia/RepoDeadlineDriver.json b/src/abi/optimism_sepolia/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/optimism_sepolia/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/polygon_amoy/RepoDeadlineDriver.json b/src/abi/polygon_amoy/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/polygon_amoy/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/abi/sepolia/RepoDeadlineDriver.json b/src/abi/sepolia/RepoDeadlineDriver.json new file mode 100644 index 0000000..f0ad264 --- /dev/null +++ b/src/abi/sepolia/RepoDeadlineDriver.json @@ -0,0 +1,390 @@ +[ + { + "inputs": [ + { + "internalType": "contract RepoDriver", + "name": "repoDriver_", + "type": "address" + }, + { "internalType": "uint32", "name": "driverId_", "type": "uint32" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "accountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "repoAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint32", + "name": "deadline", + "type": "uint32" + } + ], + "name": "AccountSeen", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "previousAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "AdminChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "beacon", + "type": "address" + } + ], + "name": "BeaconUpgraded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "currentAdmin", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newAdmin", + "type": "address" + } + ], + "name": "NewAdminProposed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserGranted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "admin", + "type": "address" + } + ], + "name": "PauserRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pauser", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "acceptAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "admin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "allPausers", + "outputs": [ + { + "internalType": "address[]", + "name": "pausersList", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" } + ], + "name": "calcAccountId", + "outputs": [ + { "internalType": "uint256", "name": "accountId", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "repoAccountId", "type": "uint256" }, + { + "internalType": "uint256", + "name": "recipientAccountId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "refundAccountId", + "type": "uint256" + }, + { "internalType": "uint32", "name": "deadline", "type": "uint32" }, + { "internalType": "contract IERC20", "name": "erc20", "type": "address" } + ], + "name": "collectAndGive", + "outputs": [ + { "internalType": "uint128", "name": "amt", "type": "uint128" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "drips", + "outputs": [ + { "internalType": "contract Drips", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "driverId", + "outputs": [{ "internalType": "uint32", "name": "", "type": "uint32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "grantPauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "implementation", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "isPaused", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "isPauser", + "outputs": [ + { "internalType": "bool", "name": "isAddrPauser", "type": "bool" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "newAdmin", "type": "address" } + ], + "name": "proposeNewAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "proposedAdmin", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceAdmin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "repoDriver", + "outputs": [ + { "internalType": "contract RepoDriver", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "pauser", "type": "address" } + ], + "name": "revokePauser", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + } + ], + "name": "upgradeTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/src/config/chainConfigs/filecoin.json b/src/config/chainConfigs/filecoin.json index bdedf46..c9bd8f0 100644 --- a/src/config/chainConfigs/filecoin.json +++ b/src/config/chainConfigs/filecoin.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0xe75f56B26857cAe06b455Bfc9481593Ae0FB4257" }, + "repoDeadlineDriver": { + "address": "0x0386b66e2b0106ff27ef26e84102ca78a5c0edef" + }, "repoSubAccountDriver": { "address": "0x925a69f6d07ee4c753df139bcc2a946e1d1ee92a" }, diff --git a/src/config/chainConfigs/goerli.json b/src/config/chainConfigs/goerli.json index 312a37d..dc2d189 100644 --- a/src/config/chainConfigs/goerli.json +++ b/src/config/chainConfigs/goerli.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0xa71bdf410D48d4AA9aE1517A69D7E1Ef0c179b2B" }, + "repoDeadlineDriver": { + "address": "0x0000000000000000000000000000000000000000" + }, "repoSubAccountDriver": { "address": "0x0000000000000000000000000000000000000000" }, diff --git a/src/config/chainConfigs/localtestnet.json b/src/config/chainConfigs/localtestnet.json index 12087ab..bda368c 100644 --- a/src/config/chainConfigs/localtestnet.json +++ b/src/config/chainConfigs/localtestnet.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0x971e08fc533d2A5f228c7944E511611dA3B56B24" }, + "repoDeadlineDriver": { + "address": "0xFD9Aa049A4f3dC1a2CD3355Ce52A943418Fa54e3" + }, "repoSubAccountDriver": { "address": "0xB8743C2bB8DF7399273aa7EE4cE8d4109Bec327F" }, diff --git a/src/config/chainConfigs/mainnet.json b/src/config/chainConfigs/mainnet.json index 542eb89..f4f2058 100644 --- a/src/config/chainConfigs/mainnet.json +++ b/src/config/chainConfigs/mainnet.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0x770023d55D09A9C110694827F1a6B32D5c2b373E" }, + "repoDeadlineDriver": { + "address": "0x8324ea3538f12895c941a625b7f15df2d7dbfdff" + }, "repoSubAccountDriver": { "address": "0xc219395880fa72e3ad9180b8878e0d39d144130b" }, diff --git a/src/config/chainConfigs/metis.json b/src/config/chainConfigs/metis.json index a084373..91894e8 100644 --- a/src/config/chainConfigs/metis.json +++ b/src/config/chainConfigs/metis.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0xe75f56B26857cAe06b455Bfc9481593Ae0FB4257" }, + "repoDeadlineDriver": { + "address": "0x0386b66e2b0106ff27ef26e84102ca78a5c0edef" + }, "repoSubAccountDriver": { "address": "0x925a69f6d07ee4c753df139bcc2a946e1d1ee92a" }, diff --git a/src/config/chainConfigs/optimism.json b/src/config/chainConfigs/optimism.json index 195ffbf..c0b56ff 100644 --- a/src/config/chainConfigs/optimism.json +++ b/src/config/chainConfigs/optimism.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0xe75f56B26857cAe06b455Bfc9481593Ae0FB4257" }, + "repoDeadlineDriver": { + "address": "0x0386b66e2b0106ff27ef26e84102ca78a5c0edef" + }, "repoSubAccountDriver": { "address": "0x925a69f6d07ee4c753df139bcc2a946e1d1ee92a" }, diff --git a/src/config/chainConfigs/optimism_sepolia.json b/src/config/chainConfigs/optimism_sepolia.json index 1772262..40f5f9b 100644 --- a/src/config/chainConfigs/optimism_sepolia.json +++ b/src/config/chainConfigs/optimism_sepolia.json @@ -14,6 +14,9 @@ "repoDriver": { "address": "0x808e5C413BB085284D18e17BDF9682A66A0097D5" }, + "repoDeadlineDriver": { + "address": "0xE57A3111414E0FaB39cc6e8fDe957b1f6471cd49" + }, "repoSubAccountDriver": { "address": "0xe077e0D50fB60b900467F4a44DF7b49deB41097d" }, diff --git a/src/config/chainConfigs/polygon_amoy.json b/src/config/chainConfigs/polygon_amoy.json index 82e64a9..eff7622 100644 --- a/src/config/chainConfigs/polygon_amoy.json +++ b/src/config/chainConfigs/polygon_amoy.json @@ -8,6 +8,9 @@ "nftDriver": { "address": "0xDafd9Ab96E62941808caa115D184D30A200FA777" }, + "repoDeadlineDriver": { + "address": "0x0000000000000000000000000000000000000000" + }, "repoSubAccountDriver": { "address": "0x0000000000000000000000000000000000000000" }, diff --git a/src/config/chainConfigs/sepolia.json b/src/config/chainConfigs/sepolia.json index a76afb4..5b0ca85 100644 --- a/src/config/chainConfigs/sepolia.json +++ b/src/config/chainConfigs/sepolia.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0xa71bdf410D48d4AA9aE1517A69D7E1Ef0c179b2B" }, + "repoDeadlineDriver": { + "address": "0x4e576318213e3c9b436d0758a021a485c5d8b929" + }, "repoSubAccountDriver": { "address": "0x317400fd9dfdad78d53a34455d89beb8f03f90ee" }, diff --git a/src/config/chainConfigs/zksync_era_sepolia.json b/src/config/chainConfigs/zksync_era_sepolia.json index 93a1520..174acf2 100644 --- a/src/config/chainConfigs/zksync_era_sepolia.json +++ b/src/config/chainConfigs/zksync_era_sepolia.json @@ -8,6 +8,9 @@ "repoDriver": { "address": "0x8bDC23877A23Ce59fEF1712A1486810d9A6E2B94" }, + "repoDeadlineDriver": { + "address": "0xe1139558bbBb20b6bE95b069402370c01250eE4e" + }, "repoSubAccountDriver": { "address": "0x0000000000000000000000000000000000000000" }, diff --git a/src/core/constants.ts b/src/core/constants.ts index 3fe5170..0c3d478 100644 --- a/src/core/constants.ts +++ b/src/core/constants.ts @@ -17,6 +17,7 @@ export const DRIPS_CONTRACTS = [ 'drips', 'nftDriver', 'repoDriver', + 'repoDeadlineDriver', 'repoSubAccountDriver', 'addressDriver', 'immutableSplitsDriver', diff --git a/src/core/contractClients.ts b/src/core/contractClients.ts index 1e0ff55..02bf67c 100644 --- a/src/core/contractClients.ts +++ b/src/core/contractClients.ts @@ -3,14 +3,21 @@ import { getAddressDriverContract, getNftDriverContract, getRepoDriverContract, + getRepoDeadlineDriverContract, getRepoSubAccountDriverContract, } from '../../contracts/contract-types'; import loadChainConfig from '../config/loadChainConfig'; import getProvider from './getProvider'; const { contracts } = loadChainConfig(); -const { drips, addressDriver, nftDriver, repoDriver, repoSubAccountDriver } = - contracts; +const { + drips, + addressDriver, + nftDriver, + repoDriver, + repoDeadlineDriver, + repoSubAccountDriver, +} = contracts; const provider = getProvider(); @@ -31,6 +38,11 @@ export const repoDriverContract = getRepoDriverContract( provider, ); +export const repoDeadlineDriverContract = getRepoDeadlineDriverContract( + repoDeadlineDriver.address, + provider, +); + export const repoSubAccountDriverContract = getRepoSubAccountDriverContract( repoSubAccountDriver.address, provider, diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts index a82bd97..4e73900 100644 --- a/src/core/splitRules.ts +++ b/src/core/splitRules.ts @@ -28,6 +28,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'drip_list', relationshipType: 'project_dependency', }, + { + senderAccountType: 'project', + receiverAccountType: 'deadline', + relationshipType: 'project_dependency', + }, { senderAccountType: 'project', receiverAccountType: 'linked_identity', @@ -50,6 +55,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'project', relationshipType: 'drip_list_receiver', }, + { + senderAccountType: 'drip_list', + receiverAccountType: 'deadline', + relationshipType: 'drip_list_receiver', + }, { senderAccountType: 'drip_list', receiverAccountType: 'linked_identity', @@ -62,6 +72,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'project', relationshipType: 'ecosystem_receiver', }, + { + senderAccountType: 'ecosystem_main_account', + receiverAccountType: 'deadline', + relationshipType: 'ecosystem_receiver', + }, { senderAccountType: 'ecosystem_main_account', receiverAccountType: 'linked_identity', @@ -89,6 +104,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'project', relationshipType: 'sub_list_receiver', }, + { + senderAccountType: 'sub_list', + receiverAccountType: 'deadline', + relationshipType: 'sub_list_link', + }, { senderAccountType: 'sub_list', receiverAccountType: 'sub_list', @@ -159,7 +179,7 @@ export const RELATIONSHIP_TYPES = Array.from( ) as (typeof SPLIT_RULES)[number]['relationshipType'][]; export const ACCOUNT_TYPE_TO_METADATA_RECEIVER_TYPE: Record< - AccountType, + Exclude, // We don't populate `RepoDeadlineDriver` metadata. string > = { project: 'repoDriver', diff --git a/src/db/migrations/20250812000000-add-deadlines.ts b/src/db/migrations/20250812000000-add-deadlines.ts new file mode 100644 index 0000000..c705f87 --- /dev/null +++ b/src/db/migrations/20250812000000-add-deadlines.ts @@ -0,0 +1,194 @@ +import { DataTypes, literal } from 'sequelize'; +import type { DataType, QueryInterface } from 'sequelize'; +import getSchema from '../../utils/getSchema'; +import type { DbSchema } from '../../core/types'; +import { + transformFieldNamesToSnakeCase, + transformFieldArrayToSnakeCase, +} from './20250414133746-initial_create'; + +export async function up({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + // Add new account type to enum + await queryInterface.sequelize.query(` + ALTER TYPE ${schema}.account_type ADD VALUE IF NOT EXISTS 'deadline'; + `); + + await createDeadlinesTable(queryInterface, schema); + await createAccountSeenEventsTable(queryInterface, schema); +} + +async function createDeadlinesTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: 'deadlines', + }, + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + claimableProjectId: { + allowNull: false, + type: DataTypes.STRING, + }, + deadline: { + allowNull: false, + type: DataTypes.DATE, + }, + refundAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + refundAccountType: { + allowNull: false, + type: literal(`${schema}.account_type`).val as unknown as DataType, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); + + await queryInterface.addIndex( + { + schema, + tableName: 'deadlines', + }, + transformFieldArrayToSnakeCase(['receiverAccountId']), + { + name: 'idx_deadlines_receiving_account_id', + }, + ); + + await queryInterface.addIndex( + { + schema, + tableName: 'deadlines', + }, + transformFieldArrayToSnakeCase(['claimableProjectId']), + { + name: 'idx_deadlines_claimable_project_id', + }, + ); + + await queryInterface.addIndex( + { + schema, + tableName: 'deadlines', + }, + transformFieldArrayToSnakeCase(['refundAccountId']), + { + name: 'idx_deadlines_refund_account_id', + }, + ); + + await queryInterface.addIndex( + { + schema, + tableName: 'deadlines', + }, + transformFieldArrayToSnakeCase(['deadline']), + { + name: 'idx_deadlines_deadline', + }, + ); +} + +async function createAccountSeenEventsTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: 'account_seen_events', + }, + transformFieldNamesToSnakeCase({ + transactionHash: { + primaryKey: true, + allowNull: false, + type: DataTypes.STRING, + }, + logIndex: { + primaryKey: true, + allowNull: false, + type: DataTypes.INTEGER, + }, + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + repoAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + refundAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + deadline: { + allowNull: false, + type: DataTypes.DATE, + }, + blockTimestamp: { + allowNull: false, + type: DataTypes.DATE, + }, + blockNumber: { + allowNull: false, + type: DataTypes.INTEGER, + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }), + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + // Drop tables + await queryInterface.dropTable({ + schema, + tableName: 'account_seen_events', + }); + + await queryInterface.dropTable({ + schema, + tableName: 'deadlines', + }); + + // Note: We cannot remove enum values from existing types in PostgreSQL. + // The 'deadline' value will remain in the account_type enum. +} diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index 6843bfa..483d200 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -2,6 +2,7 @@ import type { ModelStaticMembers } from '../core/types'; import { AccountMetadataEmittedEventModel, DripListModel, + DeadlineModel, ProjectModel, TransferEventModel, GivenEventModel, @@ -15,6 +16,7 @@ import { SplitsReceiverModel, OwnerUpdatedEventModel, LinkedIdentityModel, + AccountSeenEventModel, } from '../models'; import SplitsSetEventModel from '../models/SplitsSetEventModel'; @@ -34,6 +36,7 @@ export function registerModels(): void { registerModel(SubListModel); registerModel(ProjectModel); registerModel(DripListModel); + registerModel(DeadlineModel); registerModel(GivenEventModel); registerModel(SplitEventModel); registerModel(TransferEventModel); @@ -41,6 +44,7 @@ export function registerModels(): void { registerModel(SplitsReceiverModel); registerModel(SplitsSetEventModel); registerModel(StreamsSetEventModel); + registerModel(AccountSeenEventModel); registerModel(OwnerUpdatedEventModel); registerModel(EcosystemMainAccountModel); registerModel(SqueezedStreamsEventModel); diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts index 7ee62f7..8002468 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler.ts @@ -235,8 +235,8 @@ export default class AccountMetadataEmittedEventHandler extends EventHandlerBase metadata: AnyVersion, ): boolean { return ( - metadata.isDripList || - ('type' in metadata ? metadata.type === 'dripList' : false) + ('isDripList' in metadata && metadata.isDripList) || + ('type' in metadata && metadata.type === 'dripList') ); } } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 47d8606..60092ae 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -17,11 +17,15 @@ import { createSplitReceiver, deleteExistingSplitReceivers, } from '../receiversRepository'; -import { verifyProjectSources } from '../../../utils/projectUtils'; +import { + ensureProjectExists, + verifyProjectSources, +} from '../../../utils/projectUtils'; import DripListModel from '../../../models/DripListModel'; import { assertIsAddressDiverId, assertIsNftDriverId, + assertIsRepoDeadlineDriverId, assertIsRepoDriverId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; @@ -32,6 +36,11 @@ import { } from '../../../core/contractClients'; import { ProjectModel } from '../../../models'; import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; +import { + ensureDeadlineExists, + normalizeDeadlineReceiver, + verifyDeadlineReceiver, +} from '../../../utils/deadlineUtils'; type Params = { ipfsHash: IpfsHash; @@ -44,6 +53,16 @@ type Params = { metadata: AnyVersion; }; +type DripListReceiver = DripListMetadata['recipients'][number]; +type LegacyReceiver = LegacyDripListMetadata['projects'][number]; +type LegacyRepoReceiver = Extract; +type LegacyAddressReceiver = Exclude; + +type NormalizedSplitReceiver = + | DripListReceiver + | (LegacyRepoReceiver & { type: 'repoDriver' }) + | (LegacyAddressReceiver & { type: 'address' }); + export default async function handleDripListMetadata({ ipfsHash, logIndex, @@ -64,7 +83,13 @@ export default async function handleDripListMetadata({ return; } - const splitReceivers = metadata.projects ?? metadata.recipients; + const splitReceivers: ReadonlyArray = + // eslint-disable-next-line no-nested-ternary + isDripListMetadata(metadata) + ? metadata.recipients + : isLegacyDripListMetadata(metadata) + ? metadata.projects + : []; const { isMatch, actualHash, onChainHash } = await verifySplitsReceivers( emitterAccountId, @@ -82,7 +107,7 @@ export default async function handleDripListMetadata({ return; } - const { areProjectsValid, message } = await verifyProjectSources( + const verificationResult = await verifyProjectSources( splitReceivers.filter( ( splitReceiver, @@ -91,9 +116,9 @@ export default async function handleDripListMetadata({ ), ); - if (!areProjectsValid) { + if (!verificationResult.isValid) { scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: ${message}`, + `🚨🕵️‍♂️ Skipped Drip List ${emitterAccountId} metadata processing: ${verificationResult.message}`, ); return; @@ -110,7 +135,7 @@ export default async function handleDripListMetadata({ transaction, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ metadata, @@ -223,31 +248,32 @@ async function createNewSplitReceivers({ emitterAccountId: NftDriverId; metadata: AnyVersion; }) { - const rawReceivers = + const rawReceivers: ReadonlyArray = // eslint-disable-next-line no-nested-ternary - 'recipients' in metadata - ? (metadata.recipients ?? []) - : 'projects' in metadata - ? (metadata.projects ?? []) + isDripListMetadata(metadata) + ? metadata.recipients + : isLegacyDripListMetadata(metadata) + ? metadata.projects : []; // 2. Upgrade legacy payloads so that *every* receiver object has a `type`. // – v2+ entries already expose `type`. // – v1 repo receivers carry a `source` property. - const splitReceivers = rawReceivers.map((receiver: any) => { - if ('type' in receiver) { - return receiver; // v6 or v2–v5. - } - - // v1 without `type`. - if ('source' in receiver) { - // Legacy repo driver receiver. - return { ...receiver, type: 'repoDriver' } as const; - } - - // Legacy address receiver. - return { ...receiver, type: 'address' } as const; - }); + const splitReceivers: ReadonlyArray = + rawReceivers.map((receiver): NormalizedSplitReceiver => { + if ('type' in receiver) { + return receiver; // v6 or v2–v5. + } + + // v1 without `type`. + if ('source' in receiver) { + // Legacy repo driver receiver. + return { ...receiver, type: 'repoDriver' }; + } + + // Legacy address receiver. + return { ...receiver, type: 'address' }; + }); // Nothing to persist. if (splitReceivers.length === 0) { @@ -283,6 +309,13 @@ async function createNewSplitReceivers({ case 'repoDriver': assertIsRepoDriverId(receiver.accountId); + // Narrow down to project receiver. + if (!('source' in receiver && receiver.source.forge === 'github')) { + throw new Error( + `Project receiver ${receiver.accountId} has invalid metadata shape: ${JSON.stringify(receiver)}`, + ); + } + await ProjectModel.findOrCreate({ transaction, lock: transaction.LOCK.UPDATE, @@ -347,6 +380,58 @@ async function createNewSplitReceivers({ }, }); + case 'deadline': { + assertIsRepoDeadlineDriverId(receiver.accountId); + + if (receiver.deadline <= blockTimestamp) { + throw new Error( + `Deadline receiver ${receiver.accountId} has deadline in the past: ${receiver.deadline.toISOString()}`, + ); + } + + const normalizedDeadline = normalizeDeadlineReceiver(receiver); + + const verificationResult = + await verifyDeadlineReceiver(normalizedDeadline); + if (!verificationResult.isValid) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Cancelled Drip List ${emitterAccountId} metadata processing: ${verificationResult.message}`, + ); + + throw new Error( + `Cannot process Deadline receiver for Drip List ${emitterAccountId}: ${verificationResult.message}`, + ); + } + + await ensureProjectExists({ + project: normalizedDeadline.claimableProject, + blockNumber, + logIndex, + transaction, + scopedLogger, + }); + + await ensureDeadlineExists({ + deadline: normalizedDeadline, + transaction, + scopedLogger, + }); + + return createSplitReceiver({ + scopedLogger, + transaction, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'drip_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'deadline', + relationshipType: 'drip_list_receiver', + weight: receiver.weight, + blockTimestamp, + }, + }); + } + default: return unreachableError( `Unhandled Drip List Split Receiver type: ${(receiver as any).type}`, @@ -367,3 +452,25 @@ function validateMetadata( throw new Error('Invalid Drip List metadata schema.'); } } + +type DripListMetadata = Extract< + AnyVersion, + { type: 'dripList'; recipients: unknown } +>; + +type LegacyDripListMetadata = Extract< + AnyVersion, + { projects: unknown } +>; + +function isDripListMetadata( + metadata: AnyVersion, +): metadata is DripListMetadata { + return 'recipients' in metadata && metadata.type === 'dripList'; +} + +function isLegacyDripListMetadata( + metadata: AnyVersion, +): metadata is LegacyDripListMetadata { + return 'projects' in metadata; +} diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 79572d0..b920b1a 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -12,9 +12,13 @@ import type { import type { nftDriverAccountMetadataParser } from '../../../metadata/schemas'; import verifySplitsReceivers from '../verifySplitsReceivers'; import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immutable-splits-driver/v1'; -import { verifyProjectSources } from '../../../utils/projectUtils'; +import { + ensureProjectExists, + verifyProjectSources, +} from '../../../utils/projectUtils'; import { assertIsImmutableSplitsDriverId, + assertIsRepoDeadlineDriverId, calcParentRepoDriverId, convertToNftDriverId, } from '../../../utils/accountIdUtils'; @@ -34,8 +38,14 @@ import { makeVersion, } from '../../../utils/lastProcessedVersion'; import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; +import type { deadlineSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v6'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; +import { + ensureDeadlineExists, + normalizeDeadlineReceiver, + verifyDeadlineReceiver, +} from '../../../utils/deadlineUtils'; type Params = { ipfsHash: IpfsHash; @@ -81,16 +91,16 @@ export default async function handleEcosystemMainAccountMetadata({ return; } - const { areProjectsValid, message } = await verifyProjectSources( + const verificationResult = await verifyProjectSources( metadata.recipients.filter( (r): r is typeof r & { source: z.infer } => r.type === 'repoSubAccountDriver' && r.source.forge !== 'orcid', ), ); - if (!areProjectsValid) { + if (!verificationResult.isValid) { scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: ${message}`, + `🚨🕵️‍♂️ Skipped Ecosystem Main Account ${emitterAccountId} metadata processing: ${verificationResult.message}`, ); } @@ -105,7 +115,7 @@ export default async function handleEcosystemMainAccountMetadata({ transaction, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ logIndex, @@ -222,6 +232,7 @@ async function createNewSplitReceivers({ splitReceivers: ( | z.infer | z.infer + | z.infer )[]; }) { const receiverPromises = splitReceivers.map(async (receiver) => { @@ -287,6 +298,58 @@ async function createNewSplitReceivers({ }); } + case 'deadline': { + assertIsRepoDeadlineDriverId(receiver.accountId); + + if (receiver.deadline <= blockTimestamp) { + throw new Error( + `Deadline receiver ${receiver.accountId} has deadline in the past: ${receiver.deadline.toISOString()}`, + ); + } + + const normalizedDeadline = normalizeDeadlineReceiver(receiver); + + const verificationResult = + await verifyDeadlineReceiver(normalizedDeadline); + if (!verificationResult.isValid) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Cancelled Ecosystem Main Account ${emitterAccountId} metadata processing: ${verificationResult.message}`, + ); + + throw new Error( + `Cannot process Deadline receiver for Ecosystem Main Account ${emitterAccountId}: ${verificationResult.message}`, + ); + } + + await ensureProjectExists({ + project: normalizedDeadline.claimableProject, + blockNumber, + logIndex, + transaction, + scopedLogger, + }); + + await ensureDeadlineExists({ + deadline: normalizedDeadline, + transaction, + scopedLogger, + }); + + return createSplitReceiver({ + scopedLogger, + transaction, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'ecosystem_main_account', + receiverAccountId: receiver.accountId, + receiverAccountType: 'deadline', + relationshipType: 'ecosystem_receiver', + weight: receiver.weight, + blockTimestamp, + }, + }); + } + case 'subList': assertIsImmutableSplitsDriverId(receiver.accountId); return createSplitReceiver({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 84153d3..c55966e 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -7,6 +7,7 @@ import type { repoDriverAccountMetadataParser } from '../../../metadata/schemas' import type ScopedLogger from '../../../core/ScopedLogger'; import { calculateProjectStatus, + ensureProjectExists, verifyProjectSources, } from '../../../utils/projectUtils'; import type { IpfsHash, RepoDriverId } from '../../../core/types'; @@ -14,6 +15,7 @@ import { assertIsAddressDiverId, isAddressDriverId, isNftDriverId, + isRepoDeadlineDriverId, isRepoDriverId, } from '../../../utils/accountIdUtils'; import unreachableError from '../../../utils/unreachableError'; @@ -27,6 +29,11 @@ import { makeVersion } from '../../../utils/lastProcessedVersion'; import RecoverableError from '../../../utils/recoverableError'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; +import { + ensureDeadlineExists, + normalizeDeadlineReceiver, + verifyDeadlineReceiver, +} from '../../../utils/deadlineUtils'; type Params = { logIndex: number; @@ -93,40 +100,24 @@ export default async function handleProjectMetadata({ (dep) => 'source' in dep && dep.source.forge === 'github', ) as { accountId: string; source: z.infer }[]; - const { areProjectsValid, message } = await verifyProjectSources([ + const verificationResult = await verifyProjectSources([ ...projectReceivers, + // We'll store `source` information from metadata, not from the 'OwnerUpdatedRequested' event. + // Therefore, it's necessary to also verify the emitter project's source. { accountId: emitterAccountId, source: metadata.source, }, ]); - if (!areProjectsValid) { + if (!verificationResult.isValid) { scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${message}`, + `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${verificationResult.message}`, ); return; } - // We'll store `source` information the metadata, not from the 'OwnerUpdatedRequested' event. - // Therefore, it's necessary to also verify the project's source directly. - const { areProjectsValid: isProjectSourceValid } = await verifyProjectSources( - [ - { - accountId: emitterAccountId, - source: metadata.source, - }, - ], - ); - - if (!isProjectSourceValid) { - scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped ${metadata.source.ownerName}/${metadata.source.repoName} (${emitterAccountId}) metadata processing: ${message}`, - ); - return; - } - // ✅ All checks passed, we can proceed with the processing. await updateProject({ @@ -139,7 +130,7 @@ export default async function handleProjectMetadata({ scopedLogger, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ logIndex, @@ -147,7 +138,10 @@ export default async function handleProjectMetadata({ scopedLogger, transaction, blockTimestamp, - emitterAccountId, + emitter: { + accountId: emitterAccountId, + source: metadata.source, + }, splitReceivers: metadata.splits, }); } @@ -207,20 +201,23 @@ async function updateProject({ } async function createNewSplitReceivers({ + emitter, logIndex, blockNumber, transaction, scopedLogger, blockTimestamp, splitReceivers, - emitterAccountId, }: { logIndex: number; blockNumber: number; blockTimestamp: Date; scopedLogger: ScopedLogger; transaction: Transaction; - emitterAccountId: RepoDriverId; + emitter: { + accountId: RepoDriverId; + source: z.infer; + }; splitReceivers: AnyVersion['splits']; }) { const { dependencies, maintainers } = splitReceivers; @@ -232,7 +229,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: maintainer.accountId, receiverAccountType: 'address', @@ -244,6 +241,7 @@ async function createNewSplitReceivers({ }); const dependencyPromises = dependencies.map(async (dependency) => { + // Project or ORCID if (isRepoDriverId(dependency.accountId)) { if (!('source' in dependency)) { throw new Error( @@ -263,7 +261,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'linked_identity', @@ -296,7 +294,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'project', @@ -307,12 +305,71 @@ async function createNewSplitReceivers({ }); } + // Deadline + if (isRepoDeadlineDriverId(dependency.accountId)) { + // Narrow down to deadline receiver. + if (!('type' in dependency && dependency.type === 'deadline')) { + throw new Error( + `Deadline receiver ${dependency.accountId} has invalid metadata shape: ${JSON.stringify(dependency)}`, + ); + } + + if (dependency.deadline <= blockTimestamp) { + throw new Error( + `Deadline receiver ${dependency.accountId} has deadline in the past: ${dependency.deadline.toISOString()}`, + ); + } + + const normalizedDeadline = normalizeDeadlineReceiver(dependency); + + const verificationResult = + await verifyDeadlineReceiver(normalizedDeadline); + if (!verificationResult.isValid) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Cancelled ${emitter.source.ownerName}/${emitter.source.repoName} (${emitter.accountId}) metadata processing: ${verificationResult.message}`, + ); + + throw new Error( + `Cannot process Deadline receiver for ${emitter.source.ownerName}/${emitter.source.repoName} (${emitter.accountId}): ${verificationResult.message}`, + ); + } + + await ensureProjectExists({ + project: normalizedDeadline.claimableProject, + blockNumber, + logIndex, + transaction, + scopedLogger, + }); + + await ensureDeadlineExists({ + deadline: normalizedDeadline, + transaction, + scopedLogger, + }); + + return createSplitReceiver({ + scopedLogger, + transaction, + splitReceiverShape: { + senderAccountId: emitter.accountId, + senderAccountType: 'project', + receiverAccountId: dependency.accountId, + receiverAccountType: 'deadline', + relationshipType: 'project_dependency', + weight: dependency.weight, + blockTimestamp, + }, + }); + } + + // Address if (isAddressDriverId(dependency.accountId)) { return createSplitReceiver({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'address', @@ -323,12 +380,13 @@ async function createNewSplitReceivers({ }); } + // Drip List if (isNftDriverId(dependency.accountId)) { return createSplitReceiver({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'drip_list', diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 4554c9e..590e3fb 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -17,7 +17,10 @@ import type { subListSplitReceiverSchema } from '../../../metadata/schemas/immut import type { dripListSplitReceiverSchema } from '../../../metadata/schemas/nft-driver/v2'; import RecoverableError from '../../../utils/recoverableError'; import type { immutableSplitsDriverMetadataParser } from '../../../metadata/schemas'; -import { verifyProjectSources } from '../../../utils/projectUtils'; +import { + ensureProjectExists, + verifyProjectSources, +} from '../../../utils/projectUtils'; import { createSplitReceiver, deleteExistingSplitReceivers, @@ -27,6 +30,7 @@ import { assertIsAddressDiverId, assertIsImmutableSplitsDriverId, assertIsNftDriverId, + assertIsRepoDeadlineDriverId, calcParentRepoDriverId, convertToAccountId, convertToImmutableSplitsDriverId, @@ -35,6 +39,12 @@ import { makeVersion } from '../../../utils/lastProcessedVersion'; import type { repoSubAccountDriverSplitReceiverSchema } from '../../../metadata/schemas/common/repoSubAccountDriverSplitReceiverSchema'; import type { gitHubSourceSchema } from '../../../metadata/schemas/common/sources'; import { ensureLinkedIdentityExists } from '../../../utils/linkedIdentityUtils'; +import type { deadlineSplitReceiverSchema } from '../../../metadata/schemas/repo-driver/v6'; +import { + ensureDeadlineExists, + normalizeDeadlineReceiver, + verifyDeadlineReceiver, +} from '../../../utils/deadlineUtils'; type Params = { logIndex: number; @@ -81,16 +91,21 @@ export default async function handleSubListMetadata({ return; } - const { areProjectsValid, message } = await verifyProjectSources( - metadata.recipients.filter( - (r): r is typeof r & { source: z.infer } => - r.type === 'repoSubAccountDriver' && r.source.forge !== 'orcid', - ), - ); + const projectReceivers = metadata.recipients + .filter((r) => r.type === 'repoSubAccountDriver') + .filter((r) => { + if (r.type !== 'repoSubAccountDriver') return false; + return r.source.forge !== 'orcid'; + }) as Array<{ + accountId: string; + source: z.infer; + }>; - if (!areProjectsValid) { + const verificationResult = await verifyProjectSources(projectReceivers); + + if (!verificationResult.isValid) { scopedLogger.bufferMessage( - `🚨🕵️‍♂️ Skipped Sub-List metadata processing: ${message}`, + `🚨🕵️‍♂️ Skipped Sub-List metadata processing: ${verificationResult.message}`, ); return; @@ -108,7 +123,7 @@ export default async function handleSubListMetadata({ emitterAccountId, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ subList, @@ -198,6 +213,7 @@ async function createNewSplitReceivers({ | z.infer | z.infer | z.infer + | z.infer )[]; }) { const receiverPromises = receivers.map(async (receiver) => { @@ -269,6 +285,58 @@ async function createNewSplitReceivers({ }); } + case 'deadline': { + assertIsRepoDeadlineDriverId(receiver.accountId); + + if (receiver.deadline <= blockTimestamp) { + throw new Error( + `Deadline receiver ${receiver.accountId} has deadline in the past: ${receiver.deadline.toISOString()}`, + ); + } + + const normalizedDeadline = normalizeDeadlineReceiver(receiver); + + const verificationResult = + await verifyDeadlineReceiver(normalizedDeadline); + if (!verificationResult.isValid) { + scopedLogger.bufferMessage( + `🚨🕵️‍♂️ Cancelled Sub-List ${emitterAccountId} metadata processing: ${verificationResult.message}`, + ); + + throw new Error( + `Cannot process Deadline receiver for Sub-List ${emitterAccountId}: ${verificationResult.message}`, + ); + } + + await ensureProjectExists({ + project: normalizedDeadline.claimableProject, + blockNumber, + logIndex, + transaction, + scopedLogger, + }); + + await ensureDeadlineExists({ + deadline: normalizedDeadline, + transaction, + scopedLogger, + }); + + return createSplitReceiver({ + scopedLogger, + transaction, + splitReceiverShape: { + senderAccountId: emitterAccountId, + senderAccountType: 'sub_list', + receiverAccountId: receiver.accountId, + receiverAccountType: 'deadline', + relationshipType: 'sub_list_link', + weight: receiver.weight, + blockTimestamp, + }, + }); + } + case 'subList': assertIsImmutableSplitsDriverId(receiver.accountId); return createSplitReceiver({ diff --git a/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts new file mode 100644 index 0000000..5873909 --- /dev/null +++ b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts @@ -0,0 +1,165 @@ +import type { AccountSeenEvent } from '../../../contracts/CURRENT_NETWORK/RepoDeadlineDriver'; +import ScopedLogger from '../../core/ScopedLogger'; +import { dbConnection } from '../../db/database'; +import EventHandlerBase from '../../events/EventHandlerBase'; +import type EventHandlerRequest from '../../events/EventHandlerRequest'; +import DeadlineModel from '../../models/DeadlineModel'; +import AccountSeenEventModel from '../../models/AccountSeenEventModel'; +import { + convertToAccountId, + convertToRepoDeadlineDriverId, + convertToRepoDriverId, +} from '../../utils/accountIdUtils'; +import { getAccountType } from '../../utils/getAccountType'; +import { isLatestEvent } from '../../utils/isLatestEvent'; + +export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'> { + public eventSignatures = [ + 'AccountSeen(uint256,uint256,uint256,uint256,uint32)' as const, + ]; + + protected async _handle({ + id: requestId, + event: { + args, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + eventSignature, + }, + }: EventHandlerRequest<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>): Promise { + const [ + rawAccountId, + rawRepoAccountId, + rawRecipientAccountId, + rawRefundAccountId, + rawDeadline, + ] = args as AccountSeenEvent.OutputTuple; + + const accountId = convertToRepoDeadlineDriverId(rawAccountId); + const repoAccountId = convertToRepoDriverId(rawRepoAccountId); + const receiverAccountId = convertToAccountId(rawRecipientAccountId); + const refundAccountId = convertToAccountId(rawRefundAccountId); + + const deadline = new Date(Number(rawDeadline) * 1000); + + const scopedLogger = new ScopedLogger(this.name, requestId); + + if (deadline.getTime() <= blockTimestamp.getTime()) { + const message = [ + 'Cannot process AccountSeen event: deadline must be after block timestamp.', + ` - deadline: ${deadline.toISOString()}`, + ` - blockTime: ${blockTimestamp.toISOString()}`, + ` - txHash: ${transactionHash}`, + ` - logIndex: ${logIndex}`, + ].join('\n'); + + scopedLogger.log(message); + + throw new Error( + `AccountSeen deadline ${deadline.toISOString()} is not after block timestamp ${blockTimestamp.toISOString()}.`, + ); + } + + await dbConnection.transaction(async (transaction) => { + const receiverAccountType = await getAccountType( + receiverAccountId, + transaction, + ); + const refundAccountType = await getAccountType( + refundAccountId, + transaction, + ); + + scopedLogger.log( + [ + `📥 ${this.name} is processing ${eventSignature}:`, + ` - accountId: ${accountId}`, + ` - repoAccountId: ${repoAccountId}`, + ` - receiverAccountId: ${receiverAccountId} (${receiverAccountType})`, + ` - refundAccountId: ${refundAccountId} (${refundAccountType})`, + ` - deadline: ${deadline.toISOString()}`, + ` - logIndex: ${logIndex}`, + ` - txHash: ${transactionHash}`, + ].join('\n'), + ); + + const accountSeenEvent = await AccountSeenEventModel.create( + { + accountId, + repoAccountId, + receiverAccountId, + refundAccountId, + deadline, + logIndex, + blockNumber, + blockTimestamp, + transactionHash, + }, + { transaction }, + ); + + scopedLogger.bufferCreation({ + type: AccountSeenEventModel, + input: accountSeenEvent, + id: `${transactionHash}-${logIndex}`, + }); + + if ( + !(await isLatestEvent( + accountSeenEvent, + AccountSeenEventModel, + { accountId }, + transaction, + )) + ) { + scopedLogger.flush(); + return; + } + + const [deadlineEntry, isCreation] = await DeadlineModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId, + }, + defaults: { + accountId, + receiverAccountId, + receiverAccountType, + claimableProjectId: repoAccountId, + deadline, + refundAccountId, + refundAccountType, + }, + }); + + if (isCreation) { + scopedLogger.bufferCreation({ + type: DeadlineModel, + input: deadlineEntry, + id: accountId, + }); + } else { + // Update existing deadline + deadlineEntry.receiverAccountId = receiverAccountId; + deadlineEntry.receiverAccountType = receiverAccountType; + deadlineEntry.claimableProjectId = repoAccountId; + deadlineEntry.deadline = deadline; + deadlineEntry.refundAccountId = refundAccountId; + deadlineEntry.refundAccountType = refundAccountType; + + scopedLogger.bufferUpdate({ + type: DeadlineModel, + id: accountId, + input: deadlineEntry, + }); + + await deadlineEntry.save({ transaction }); + } + + scopedLogger.flush(); + }); + } +} diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts index 4e503a3..dac5bde 100644 --- a/src/eventHandlers/OwnerUpdatedEventHandler.ts +++ b/src/eventHandlers/OwnerUpdatedEventHandler.ts @@ -45,6 +45,7 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd ].join('\n'), ); + // Ensure we process the latest event. const onChainOwner = (await repoDriverContract.ownerOf( accountId, )) as Address; diff --git a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts index 6d46a0e..8d49601 100644 --- a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts +++ b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts @@ -23,7 +23,7 @@ export async function processLinkedIdentitySplits( // Only proceed if this event matches the latest on-chain hash. if (eventReceiversHash !== onChainReceiversHash) { scopedLogger.bufferMessage( - `Skipped setting 'isLinked' flag for ${accountId}: on-chain splits hash '${onChainReceiversHash}' does not match event hash '${eventReceiversHash}'.`, + `Skipped setting 'areSplitsValid' flag for ${accountId}: on-chain splits hash '${onChainReceiversHash}' does not match event hash '${eventReceiversHash}'.`, ); return; @@ -37,7 +37,7 @@ export async function processLinkedIdentitySplits( if (!linkedIdentity) { throw new RecoverableError( - `Failed to set 'isLinked' flag for LinkedIdentity: Linked Identity '${accountId}' not found. Likely waiting on 'OwnerUpdated' event to be processed. Retrying, but if this persists, it is a real error.`, + `Failed to set 'areSplitsValid' flag for LinkedIdentity: Linked Identity '${accountId}' not found. Likely waiting on 'OwnerUpdated' event to be processed. Retrying, but if this persists, it is a real error.`, ); } @@ -59,7 +59,7 @@ export async function processLinkedIdentitySplits( return; } - const isLinked = await validateLinkedIdentity( + const areSplitsValid = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, ); @@ -80,7 +80,7 @@ export async function processLinkedIdentitySplits( } // Only create new splits if identity is properly linked. - if (isLinked) { + if (areSplitsValid) { await createSplitReceiver({ scopedLogger, transaction, @@ -105,7 +105,7 @@ export async function processLinkedIdentitySplits( ); } - linkedIdentity.areSplitsValid = isLinked; + linkedIdentity.areSplitsValid = areSplitsValid; scopedLogger.bufferUpdate({ type: LinkedIdentityModel, diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index a216b0f..d278c03 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -31,139 +31,214 @@ export default async function setIsValidFlag( ): Promise { const onChainReceiversHash = await dripsContract.splitsHash(accountId); - // Only proceed if this event matches the latest on-chain hash. if (eventReceiversHash !== onChainReceiversHash) { scopedLogger.bufferMessage( `Skipped setting 'isValid' flag for ${accountId}: on-chain splits hash '${onChainReceiversHash}' does not match event hash '${eventReceiversHash}'.`, ); - return; } if (isRepoDriverId(accountId)) { - const project = await ProjectModel.findByPk(accountId, { + await handleEntityValidation( + accountId, + onChainReceiversHash, transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!project) { - throw new RecoverableError( - `Failed to set 'isValid' flag for Project: Project '${accountId}' not found. Likely waiting on 'AccountMetadataEmitted' event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const onChainOwner = await repoDriverContract.ownerOf(accountId); - const dbOwner = project.ownerAddress; // populated from metadata. - if (onChainOwner !== dbOwner) { - throw new RecoverableError( - `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const dbReceiversHash = await hashDbSplits(accountId, transaction); - const isValid = dbReceiversHash === onChainReceiversHash; - - project.isValid = isValid; - - scopedLogger.bufferUpdate({ - id: project.accountId, - type: ProjectModel, - input: project, - }); - - await project.save({ transaction }); - - if (!isValid) { - // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. - throw new RecoverableError( - `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for Project '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } + scopedLogger, + { + findEntity: async () => { + const project = await ProjectModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!project) { + throw new RecoverableError( + `Failed to set 'isValid' flag for Project: Project '${accountId}' not found. Likely waiting on 'AccountMetadataEmitted' event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + + return { + entity: project, + Model: ProjectModel, + entityType: 'Project', + }; + }, + validateOwnership: async (entity) => { + const onChainOwner = await repoDriverContract.ownerOf(accountId); + const dbOwner = entity.ownerAddress; + if (onChainOwner !== dbOwner) { + throw new RecoverableError( + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for Project '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + }, + }, + ); } else if (isNftDriverId(accountId)) { - const dripList = await DripListModel.findByPk(accountId, { + await handleEntityValidation( + accountId, + onChainReceiversHash, transaction, - lock: transaction.LOCK.UPDATE, - }); - - const ecosystemMain = await EcosystemMainAccountModel.findByPk(accountId, { + scopedLogger, + { + findEntity: async () => { + const dripList = await DripListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const ecosystemMain = await EcosystemMainAccountModel.findByPk( + accountId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, + ); + + const entity = dripList ?? ecosystemMain; + // eslint-disable-next-line no-nested-ternary + const resolvedModelName = dripList + ? DripListModel.name + : ecosystemMain !== null + ? EcosystemMainAccountModel.name + : 'DripList or EcosystemMainAccount'; + + if (!entity) { + throw new RecoverableError( + `Failed to set 'isValid' flag for ${resolvedModelName}: ${resolvedModelName} '${accountId}' not found.`, + ); + } + + const Model = dripList ? DripListModel : EcosystemMainAccountModel; + + if (dripList && ecosystemMain) { + unreachableError( + `Invariant violation: both Drip List and Ecosystem Main Account found for token '${accountId}'.`, + ); + } + + return { entity, Model, entityType: Model.name }; + }, + validateOwnership: async (entity) => { + const onChainOwner = await nftDriverContract.ownerOf(accountId); + const dbOwner = entity.ownerAddress; + if (onChainOwner !== dbOwner) { + const entityType = entity.constructor.name; + throw new RecoverableError( + `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for ${entityType} '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } + }, + }, + ); + } else if (isImmutableSplitsDriverId(accountId)) { + await handleEntityValidation( + accountId, + onChainReceiversHash, transaction, - lock: transaction.LOCK.UPDATE, - }); - - const entity = dripList ?? ecosystemMain!; - const Model = dripList ? DripListModel : EcosystemMainAccountModel; - - if (!entity) { - throw new RecoverableError( - `Failed to set 'isValid' flag for ${Model.name}: ${Model.name} '${accountId}' not found.`, - ); - } - - if (dripList && ecosystemMain) { - unreachableError( - `Invariant violation: both Drip List and Ecosystem Main Account found for token '${accountId}'.`, - ); - } - - const onChainOwner = await nftDriverContract.ownerOf(accountId); - const dbOwner = entity.ownerAddress; - if (onChainOwner !== dbOwner) { - throw new RecoverableError( - `On-chain owner ${onChainOwner} does not match DB owner ${dbOwner} for ${Model.name} '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - - const dbReceiversHash = await hashDbSplits(accountId, transaction); - const isValid = dbReceiversHash === onChainReceiversHash; - - entity.isValid = isValid; + scopedLogger, + { + findEntity: async () => { + const subList = await SubListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!subList) { + throw new RecoverableError( + `Failed to set 'isValid' flag for SubList: SubList '${accountId}' not found.`, + ); + } + + return { + entity: subList, + Model: SubListModel, + entityType: 'SubList', + }; + }, + }, + ); + } +} +type EntityWithValidation = { + isValid: boolean; + accountId: AccountId; + ownerAddress?: string | null; + save: (options: { transaction: Transaction }) => Promise; +}; + +type EntityFinder = { + findEntity: () => Promise<{ + entity: EntityWithValidation; + Model: any; + entityType: string; + }>; + validateOwnership?: (entity: EntityWithValidation) => Promise; +}; + +async function handleEntityValidation( + accountId: AccountId, + onChainReceiversHash: string, + transaction: Transaction, + scopedLogger: ScopedLogger, + entityFinder: EntityFinder, +): Promise { + const { entity, Model, entityType } = await entityFinder.findEntity(); - scopedLogger.bufferUpdate({ - id: entity.accountId, - type: Model, - input: entity, - }); + if (entityFinder.validateOwnership) { + await entityFinder.validateOwnership(entity); + } - await entity.save({ transaction }); + const { hashValid } = await validateSplitsHash( + accountId, + onChainReceiversHash, + transaction, + scopedLogger, + entityType, + ); - if (!isValid) { - // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. - throw new RecoverableError( - `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for ${Model.name} '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } - } else if (isImmutableSplitsDriverId(accountId)) { - const subList = await SubListModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); + const isValid = hashValid; - if (!subList) { - throw new RecoverableError( - `Failed to set 'isValid' flag for SubList: SubList '${accountId}' not found.`, - ); - } + entity.isValid = isValid; - const dbReceiversHash = await hashDbSplits(accountId, transaction); - const isValid = dbReceiversHash === onChainReceiversHash; + scopedLogger.bufferUpdate({ + id: entity.accountId, + type: Model, + input: entity as any, + }); - subList.isValid = isValid; + await entity.save({ transaction }); - scopedLogger.bufferUpdate({ - id: subList.accountId, - type: SubListModel, - input: subList, - }); + if (!isValid) { + const reasons = []; + if (!hashValid) reasons.push('splits hash mismatch'); - await subList.save({ transaction }); + throw new RecoverableError( + `${entityType} '${accountId}' validation failed: ${reasons.join(', ')}. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, + ); + } +} - if (!isValid) { - // Rethrow the error to trigger a retry. Eventually, the on-chain hash should match the DB hash. - throw new RecoverableError( - `On-chain splits hash '${onChainReceiversHash}' does not match DB splits hash '${dbReceiversHash}' for SubList '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } +async function validateSplitsHash( + accountId: AccountId, + onChainReceiversHash: string, + transaction: Transaction, + scopedLogger: ScopedLogger, + entityType: string, +): Promise<{ + hashValid: boolean; + dbReceiversHash: string; +}> { + const dbReceiversHash = await hashDbSplits(accountId, transaction); + const hashValid = dbReceiversHash === onChainReceiversHash; + + if (!hashValid) { + scopedLogger.bufferMessage( + `${entityType} ${accountId} splits hash mismatch: on-chain '${onChainReceiversHash}' vs DB '${dbReceiversHash}'`, + ); } + + return { hashValid, dbReceiversHash }; } async function hashDbSplits( diff --git a/src/eventHandlers/index.ts b/src/eventHandlers/index.ts index e2f5eff..418ecc9 100644 --- a/src/eventHandlers/index.ts +++ b/src/eventHandlers/index.ts @@ -2,6 +2,9 @@ export { default as GivenEventHandler } from './GivenEventHandler'; export { default as SplitEventHandler } from './SplitEventHandler'; export { default as TransferEventHandler } from './TransferEventHandler'; export { default as StreamsSetEventHandler } from './StreamsSetEventHandler'; +export { default as AccountSeenEventHandler } from './AccountSeenEventHandler/AccountSeenEventHandler'; +export { default as OwnerUpdatedEventHandler } from './OwnerUpdatedEventHandler'; export { default as SqueezedStreamsEventHandler } from './SqueezedStreamsEventHandler'; +export { default as SplitsSetEventHandler } from './SplitsSetEvent/SplitsSetEventHandler'; export { default as StreamReceiverSeenEventHandler } from './StreamReceiverSeenEventHandler'; export { default as AccountMetadataEmittedEventHandler } from './AccountMetadataEmittedEvent/AccountMetadataEmittedEventHandler'; diff --git a/src/events/registrations.ts b/src/events/registrations.ts index 7f45011..6a5629b 100644 --- a/src/events/registrations.ts +++ b/src/events/registrations.ts @@ -1,14 +1,15 @@ import { AccountMetadataEmittedEventHandler, + AccountSeenEventHandler, GivenEventHandler, SplitEventHandler, TransferEventHandler, StreamReceiverSeenEventHandler, StreamsSetEventHandler, SqueezedStreamsEventHandler, + OwnerUpdatedEventHandler, + SplitsSetEventHandler, } from '../eventHandlers'; -import OwnerUpdatedEventHandler from '../eventHandlers/OwnerUpdatedEventHandler'; -import SplitsSetEventHandler from '../eventHandlers/SplitsSetEvent/SplitsSetEventHandler'; import { registerEventHandler } from './eventHandlerUtils'; export function registerEventHandlers(): void { @@ -48,4 +49,8 @@ export function registerEventHandlers(): void { 'OwnerUpdated(uint256,address)', OwnerUpdatedEventHandler, ); + registerEventHandler<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>( + 'AccountSeen(uint256,uint256,uint256,uint256,uint32)', + AccountSeenEventHandler, + ); } diff --git a/src/events/types.ts b/src/events/types.ts index a7f9a5f..bfb0eac 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -5,6 +5,7 @@ import type { AnyChainImmutableSplitsDriverFilters, AnyChainNftDriverFilters, AnyChainRepoDriverFilters, + AnyChainRepoDeadlineDriverFilters, AnyChainTypedLogDescription, } from '../../contracts/contract-types'; @@ -12,11 +13,14 @@ import type { type AllFilters = AnyChainDripsFilters & AnyChainNftDriverFilters & AnyChainRepoDriverFilters & + AnyChainRepoDeadlineDriverFilters & AnyChainImmutableSplitsDriverFilters; export type DripsContractEvent = ValuesOf; export type NftDriverContractEvent = ValuesOf; export type RepoDriverContractEvent = ValuesOf; +export type RepoDeadlineDriverContractEvent = + ValuesOf; export type ImmutableSplitsDriverContractEvent = ValuesOf; diff --git a/src/metadata/schemas/immutable-splits-driver/v2.ts b/src/metadata/schemas/immutable-splits-driver/v2.ts new file mode 100644 index 0000000..f7ec32f --- /dev/null +++ b/src/metadata/schemas/immutable-splits-driver/v2.ts @@ -0,0 +1,19 @@ +import z from 'zod'; +import { addressDriverSplitReceiverSchema } from '../repo-driver/v2'; +import { dripListSplitReceiverSchema } from '../nft-driver/v2'; +import { repoSubAccountDriverSplitReceiverSchema } from '../common/repoSubAccountDriverSplitReceiverSchema'; +import { deadlineSplitReceiverSchema } from '../repo-driver/v6'; +import { subListSplitReceiverSchema, subListMetadataSchemaV1 } from './v1'; + +export const subListMetadataSchemaV2 = subListMetadataSchemaV1.extend({ + recipients: z.array( + z.union([ + addressDriverSplitReceiverSchema, + dripListSplitReceiverSchema, + repoSubAccountDriverSplitReceiverSchema, + subListSplitReceiverSchema, + deadlineSplitReceiverSchema, // New in v2 + ]), + ), + isVisible: z.boolean().optional(), +}); diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 49b8d2f..bd09cb9 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -13,6 +13,8 @@ import { nftDriverAccountMetadataSchemaV5 } from './nft-driver/v5'; import { subListMetadataSchemaV1 } from './immutable-splits-driver/v1'; import { nftDriverAccountMetadataSchemaV6 } from './nft-driver/v6'; import { nftDriverAccountMetadataSchemaV7 } from './nft-driver/v7'; +import { repoDriverAccountMetadataSchemaV6 } from './repo-driver/v6'; +import { subListMetadataSchemaV2 } from './immutable-splits-driver/v2'; export const nftDriverAccountMetadataParser = createVersionedParser([ nftDriverAccountMetadataSchemaV7.parse, @@ -29,6 +31,7 @@ export const addressDriverAccountMetadataParser = createVersionedParser([ ]); export const repoDriverAccountMetadataParser = createVersionedParser([ + repoDriverAccountMetadataSchemaV6.parse, repoDriverAccountMetadataSchemaV5.parse, repoDriverAccountMetadataSchemaV4.parse, repoDriverAccountMetadataSchemaV3.parse, @@ -37,5 +40,6 @@ export const repoDriverAccountMetadataParser = createVersionedParser([ ]); export const immutableSplitsDriverMetadataParser = createVersionedParser([ + subListMetadataSchemaV2.parse, subListMetadataSchemaV1.parse, ]); diff --git a/src/metadata/schemas/nft-driver/v7.ts b/src/metadata/schemas/nft-driver/v7.ts index 6870e98..c122f22 100644 --- a/src/metadata/schemas/nft-driver/v7.ts +++ b/src/metadata/schemas/nft-driver/v7.ts @@ -1,20 +1,55 @@ import { z } from 'zod'; +import { nftDriverAccountMetadataSchemaV5 } from './v5'; import { - dripListVariant as dripListVariantV6, - nftDriverAccountMetadataSchemaV6, -} from './v6'; -import { orcidSplitReceiverSchema } from '../repo-driver/v6'; + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from '../repo-driver/v2'; +import { subListSplitReceiverSchema } from '../immutable-splits-driver/v1'; +import { dripListSplitReceiverSchema } from './v2'; +import { repoSubAccountDriverSplitReceiverSchema } from '../common/repoSubAccountDriverSplitReceiverSchema'; +import { emojiAvatarSchema } from '../repo-driver/v4'; +import { + deadlineSplitReceiverSchema, + orcidSplitReceiverSchema, +} from '../repo-driver/v6'; + +const base = nftDriverAccountMetadataSchemaV5 + .omit({ + isDripList: true, + projects: true, + }) + .extend({ + allowExternalDonations: z.boolean().optional(), + }); + +const ecosystemVariant = base.extend({ + type: z.literal('ecosystem'), + recipients: z.array( + z.union([ + repoSubAccountDriverSplitReceiverSchema, + subListSplitReceiverSchema, + deadlineSplitReceiverSchema, // New in v7 + ]), + ), + color: z.string(), + avatar: emojiAvatarSchema, +}); -export const dripListVariantV7 = dripListVariantV6.extend({ +const dripListVariant = base.extend({ + type: z.literal('dripList'), recipients: z.array( z.union([ - ...dripListVariantV6.shape.recipients._def.type.options, - orcidSplitReceiverSchema, + repoDriverSplitReceiverSchema, + subListSplitReceiverSchema, + addressDriverSplitReceiverSchema, + dripListSplitReceiverSchema, + deadlineSplitReceiverSchema, // New in v7 + orcidSplitReceiverSchema, // New in v7 ]), ), }); export const nftDriverAccountMetadataSchemaV7 = z.discriminatedUnion('type', [ - nftDriverAccountMetadataSchemaV6._def.options[0], - dripListVariantV7, + ecosystemVariant, + dripListVariant, ]); diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts index b5acd88..6a46793 100644 --- a/src/metadata/schemas/repo-driver/v6.ts +++ b/src/metadata/schemas/repo-driver/v6.ts @@ -1,4 +1,11 @@ import z from 'zod'; +import { gitHubSourceSchema } from '../common/sources'; +import { + addressDriverSplitReceiverSchema, + repoDriverSplitReceiverSchema, +} from './v2'; +import { dripListSplitReceiverSchema } from '../nft-driver/v2'; +import { repoDriverAccountMetadataSchemaV5 } from './v5'; export const orcidSplitReceiverSchema = z.object({ type: z.literal('orcid'), @@ -7,6 +14,40 @@ export const orcidSplitReceiverSchema = z.object({ orcidId: z.string(), }); -// TODO: actually export new version -// should allow orcidSplitReceiverSchema as a dependency -// for repoDriverAccountSplitsSchema +export const deadlineSplitReceiverSchema = z.object({ + type: z.literal('deadline'), + weight: z.number(), + accountId: z.string(), + claimableProject: z.object({ + accountId: z.string(), + source: gitHubSourceSchema, + }), + recipientAccountId: z.string(), + refundAccountId: z.string(), + deadline: z.coerce + .date() + .refine((date) => !Number.isNaN(date.getTime()), 'Invalid date') + .refine((date) => date > new Date(), 'Deadline must be in the future') + .refine( + (date) => date < new Date(Date.now() + 365 * 24 * 60 * 60 * 1000), + 'Deadline cannot be more than 1 year in the future', + ), +}); + +const repoDriverAccountSplitsSchemaV6 = z.object({ + maintainers: z.array(addressDriverSplitReceiverSchema), + dependencies: z.array( + z.union([ + dripListSplitReceiverSchema, + repoDriverSplitReceiverSchema, + addressDriverSplitReceiverSchema, + deadlineSplitReceiverSchema, // New in v6 + orcidSplitReceiverSchema, // New in v6 + ]), + ), +}); + +export const repoDriverAccountMetadataSchemaV6 = + repoDriverAccountMetadataSchemaV5.extend({ + splits: repoDriverAccountSplitsSchemaV6, + }); diff --git a/src/models/AccountSeenEventModel.ts b/src/models/AccountSeenEventModel.ts new file mode 100644 index 0000000..514b70a --- /dev/null +++ b/src/models/AccountSeenEventModel.ts @@ -0,0 +1,72 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import getSchema from '../utils/getSchema'; +import type { IEventModel } from '../events/types'; +import type { + AccountId, + RepoDeadlineDriverId, + RepoDriverId, +} from '../core/types'; +import { COMMON_EVENT_INIT_ATTRIBUTES } from '../core/constants'; + +export default class AccountSeenEventModel + extends Model< + InferAttributes, + InferCreationAttributes + > + implements IEventModel +{ + declare public accountId: RepoDeadlineDriverId; + declare public repoAccountId: RepoDriverId; + declare public receiverAccountId: AccountId; + declare public refundAccountId: AccountId; + declare public deadline: Date; + + declare public logIndex: number; + declare public blockNumber: number; + declare public blockTimestamp: Date; + declare public transactionHash: string; + + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + accountId: { + allowNull: false, + type: DataTypes.STRING, + }, + repoAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + refundAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + deadline: { + allowNull: false, + type: DataTypes.DATE, + }, + ...COMMON_EVENT_INIT_ATTRIBUTES, + }, + { + sequelize, + schema: getSchema(), + tableName: 'account_seen_events', + underscored: true, + timestamps: true, + }, + ); + } +} diff --git a/src/models/DeadlineModel.ts b/src/models/DeadlineModel.ts new file mode 100644 index 0000000..2647de7 --- /dev/null +++ b/src/models/DeadlineModel.ts @@ -0,0 +1,102 @@ +import type { + CreationOptional, + InferAttributes, + InferCreationAttributes, + Sequelize, +} from 'sequelize'; +import { DataTypes, Model } from 'sequelize'; +import getSchema from '../utils/getSchema'; +import type { + AccountId, + RepoDeadlineDriverId, + RepoDriverId, +} from '../core/types'; +import { type AccountType, ACCOUNT_TYPES } from '../core/splitRules'; + +export default class DeadlineModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare public accountId: RepoDeadlineDriverId; + + declare public receiverAccountId: AccountId; + declare public receiverAccountType: AccountType; + + declare public claimableProjectId: RepoDriverId; + + declare public deadline: Date; + + declare public refundAccountId: AccountId; + declare public refundAccountType: AccountType; + + declare public createdAt: CreationOptional; + declare public updatedAt: CreationOptional; + + public static initialize(sequelize: Sequelize): void { + this.init( + { + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + receiverAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receiverAccountType: { + allowNull: false, + type: DataTypes.ENUM(...ACCOUNT_TYPES), + }, + claimableProjectId: { + allowNull: false, + type: DataTypes.STRING, + }, + deadline: { + allowNull: false, + type: DataTypes.DATE, + }, + refundAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + refundAccountType: { + allowNull: false, + type: DataTypes.ENUM(...ACCOUNT_TYPES), + }, + createdAt: { + allowNull: false, + type: DataTypes.DATE, + }, + updatedAt: { + allowNull: false, + type: DataTypes.DATE, + }, + }, + { + sequelize, + schema: getSchema(), + tableName: 'deadlines', + underscored: true, + timestamps: true, + indexes: [ + { + fields: ['receiverAccountId'], + name: 'idx_deadlines_receiving_account_id', + }, + { + fields: ['claimableProjectId'], + name: 'idx_deadlines_claimable_project_id', + }, + { + fields: ['refundAccountId'], + name: 'idx_deadlines_refund_account_id', + }, + { + fields: ['deadline'], + name: 'idx_deadlines_deadline', + }, + ], + }, + ); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 0816077..9a22657 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,14 +1,16 @@ export { default as SubListModel } from './SubListModel'; export { default as ProjectModel } from './ProjectModel'; export { default as DripListModel } from './DripListModel'; +export { default as DeadlineModel } from './DeadlineModel'; export { default as GivenEventModel } from './GivenEventModel'; export { default as SplitEventModel } from './SplitEventModel'; -export { default as SplitsReceiverModel } from './SplitsReceiverModel'; export { default as TransferEventModel } from './TransferEventModel'; +export { default as SplitsReceiverModel } from './SplitsReceiverModel'; export { default as SplitsSetEventModel } from './SplitsSetEventModel'; export { default as LinkedIdentityModel } from './LinkedIdentityModel'; export { default as StreamsSetEventModel } from './StreamsSetEventModel'; export { default as LastIndexedBlockModel } from './LastIndexedBlockModel'; +export { default as AccountSeenEventModel } from './AccountSeenEventModel'; export { default as OwnerUpdatedEventModel } from './OwnerUpdatedEventModel'; export { default as SqueezedStreamsEventModel } from './SqueezedStreamsEventModel'; export { default as EcosystemMainAccountModel } from './EcosystemMainAccountModel'; diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 7dacfbc..fda91d4 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -7,6 +7,7 @@ import type { ImmutableSplitsDriverId, NftDriverId, RepoDriverId, + RepoDeadlineDriverId, RepoSubAccountDriverId, } from '../core/types'; import { ORCID_FORGE_ID } from '../models/LinkedIdentityModel'; @@ -40,6 +41,8 @@ export function getContractNameFromAccountId(id: string): DripsContract { return 'repoDriver'; case 4n: return 'repoSubAccountDriver'; + case 5n: + return 'repoDeadlineDriver'; default: throw new Error(`Unknown driver for ID ${id}.`); } @@ -207,6 +210,46 @@ export function assertIsRepoSubAccountDriverId( } } +// RepoDeadlineDriver +export function isRepoDeadlineDriverId( + id: string | bigint, +): id is RepoDeadlineDriverId { + const idString = typeof id === 'bigint' ? id.toString() : id; + const isNotANum = Number.isNaN(Number(idString)); + const isAccountIdOfRepoDeadlineDriver = + getContractNameFromAccountId(idString) === 'repoDeadlineDriver'; + + if (isNotANum || !isAccountIdOfRepoDeadlineDriver) { + return false; + } + + return true; +} + +export function convertToRepoDeadlineDriverId( + id: bigint | string, +): RepoDeadlineDriverId { + const repoDeadlineDriverId = typeof id === 'bigint' ? id.toString() : id; + + if (!isRepoDeadlineDriverId(repoDeadlineDriverId)) { + throw new Error( + `Failed to convert: '${id}' is not a valid RepoDeadlineDriver ID.`, + ); + } + + return repoDeadlineDriverId as RepoDeadlineDriverId; +} + +export function assertIsRepoDeadlineDriverId( + id: string, +): asserts id is RepoDeadlineDriverId { + if (!isRepoDeadlineDriverId(id)) { + throw new Error( + `Failed to assert: '${id}' is not a valid RepoDeadlineDriver ID.`, + ); + } +} + export async function transformRepoDriverId( id: string, direction: 'toParent' | 'toSub', @@ -261,7 +304,8 @@ export function convertToAccountId(id: bigint | string): AccountId { isNftDriverId(accountIdAsString) || isAddressDriverId(accountIdAsString) || isImmutableSplitsDriverId(accountIdAsString) || - isRepoSubAccountDriverId(accountIdAsString) + isRepoSubAccountDriverId(accountIdAsString) || + isRepoDeadlineDriverId(accountIdAsString) ) { return accountIdAsString as AccountId; } @@ -279,7 +323,8 @@ export function assertIsAccountId( !isNftDriverId(accountId) && !isAddressDriverId(accountId) && !isImmutableSplitsDriverId(accountId) && - !isRepoSubAccountDriverId(accountId) + !isRepoSubAccountDriverId(accountId) && + !isRepoDeadlineDriverId(accountId) ) { throw new Error( `Failed to assert: '${accountId}' is not a valid account ID.`, diff --git a/src/utils/deadlineUtils.ts b/src/utils/deadlineUtils.ts new file mode 100644 index 0000000..e3f0525 --- /dev/null +++ b/src/utils/deadlineUtils.ts @@ -0,0 +1,152 @@ +import type { Transaction } from 'sequelize'; +import type { z } from 'zod'; +import type ScopedLogger from '../core/ScopedLogger'; +import type { + AccountId, + RepoDeadlineDriverId, + RepoDriverId, +} from '../core/types'; +import DeadlineModel from '../models/DeadlineModel'; +import { repoDeadlineDriverContract } from '../core/contractClients'; +import type { deadlineSplitReceiverSchema } from '../metadata/schemas/repo-driver/v6'; +import { verifyProjectSource } from './projectUtils'; +import { + convertToAccountId, + convertToRepoDeadlineDriverId, + convertToRepoDriverId, +} from './accountIdUtils'; +import { getAccountType } from './getAccountType'; +import type { gitHubSourceSchema } from '../metadata/schemas/common/sources'; + +async function calcDeadlineAccountId( + repoAccountId: RepoDriverId, + recipientAccountId: AccountId, + refundAccountId: AccountId, + deadline: Date, +): Promise { + const deadlineInSeconds = BigInt(Math.floor(deadline.getTime() / 1000)); + + const calculatedAccountId = await repoDeadlineDriverContract.calcAccountId( + repoAccountId, + recipientAccountId, + refundAccountId, + deadlineInSeconds, + ); + + return convertToRepoDeadlineDriverId(calculatedAccountId); +} + +type DeadlineReceiverVerificationResult = { + isValid: boolean; + message?: string; +}; + +export async function verifyDeadlineReceiver( + receiver: NormalizedDeadlineReceiver, +): Promise { + const { + accountId: deadlineAccountId, + recipientAccountId, + refundAccountId, + deadline, + claimableProject, + } = receiver; + + const { accountId: repoAccountId, source } = claimableProject; + + const expectedAccountId = await calcDeadlineAccountId( + repoAccountId, + recipientAccountId, + refundAccountId, + deadline, + ); + + if (expectedAccountId !== deadlineAccountId) { + return { + isValid: false, + message: `Metadata Deadline receiver ${deadlineAccountId} mismatches on-chain calculation ${expectedAccountId} for repo ${repoAccountId} (${source.url}), recipient ${recipientAccountId}, refund ${refundAccountId}, deadline ${deadline.toISOString()}.`, + }; + } + + return verifyProjectSource(repoAccountId, source); +} + +export async function ensureDeadlineExists(ctx: { + deadline: NormalizedDeadlineReceiver; + transaction: Transaction; + scopedLogger: ScopedLogger; +}): Promise { + const { + deadline: { + accountId, + claimableProject, + recipientAccountId, + refundAccountId, + deadline, + }, + transaction, + scopedLogger, + } = ctx; + + const receiverAccountType = await getAccountType( + recipientAccountId, + transaction, + ); + const refundAccountType = await getAccountType(refundAccountId, transaction); + + const [deadlineEntry, isCreation] = await DeadlineModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId, + }, + defaults: { + accountId, + receiverAccountId: recipientAccountId, + receiverAccountType, + claimableProjectId: claimableProject.accountId, + deadline, + refundAccountId, + refundAccountType, + }, + }); + + if (isCreation) { + scopedLogger.bufferCreation({ + type: DeadlineModel, + input: deadlineEntry, + id: accountId, + }); + } +} + +type NormalizedDeadlineReceiver = { + type: 'deadline'; + weight: number; + accountId: RepoDeadlineDriverId; + claimableProject: { + accountId: RepoDriverId; + source: z.infer; + }; + recipientAccountId: AccountId; + refundAccountId: AccountId; + deadline: Date; +}; + +export function normalizeDeadlineReceiver( + receiver: z.infer, +): NormalizedDeadlineReceiver { + const { accountId, refundAccountId, claimableProject, recipientAccountId } = + receiver; + + return { + ...receiver, + accountId: convertToRepoDeadlineDriverId(accountId), + recipientAccountId: convertToAccountId(recipientAccountId), + refundAccountId: convertToAccountId(refundAccountId), + claimableProject: { + accountId: convertToRepoDriverId(claimableProject.accountId), + source: claimableProject.source, + }, + }; +} diff --git a/src/utils/getAccountType.ts b/src/utils/getAccountType.ts new file mode 100644 index 0000000..a4a5f6d --- /dev/null +++ b/src/utils/getAccountType.ts @@ -0,0 +1,108 @@ +import type { Transaction } from 'sequelize'; +import type { AccountType } from '../core/splitRules'; +import DripListModel from '../models/DripListModel'; +import EcosystemMainAccountModel from '../models/EcosystemMainAccountModel'; +import SubListModel from '../models/SubListModel'; +import DeadlineModel from '../models/DeadlineModel'; +import LinkedIdentityModel from '../models/LinkedIdentityModel'; +import { + getContractNameFromAccountId, + convertToNftDriverId, + convertToImmutableSplitsDriverId, + convertToRepoDeadlineDriverId, + convertToRepoDriverId, + isOrcidAccount, +} from './accountIdUtils'; +import type { AccountId } from '../core/types'; +import RecoverableError from './recoverableError'; + +export async function getAccountType( + accountId: AccountId, + transaction: Transaction, +): Promise { + const contractName = getContractNameFromAccountId(accountId); + + if (contractName === 'repoDriver' && isOrcidAccount(accountId)) { + const linkedIdentity = await LinkedIdentityModel.findOne({ + lock: transaction.LOCK.UPDATE, + where: { + accountId: convertToRepoDriverId(accountId), + identityType: 'orcid', + }, + transaction, + attributes: ['accountId'], + }); + if (linkedIdentity) { + return 'linked_identity'; + } + throw new RecoverableError( + `LinkedIdentity ${accountId} not found in database (yet?)`, + ); + } + + // Projects don't need to exist in DB. + if ( + contractName === 'repoDriver' || + contractName === 'repoSubAccountDriver' + ) { + return 'project'; + } + + // Addresses don't need to exist in DB. + if (contractName === 'addressDriver') { + return 'address'; + } + + // All other types must exist in DB. + switch (contractName) { + case 'immutableSplitsDriver': { + const subList = await SubListModel.findByPk( + convertToImmutableSplitsDriverId(accountId), + { transaction, attributes: ['accountId'] }, + ); + if (!subList) { + throw new RecoverableError( + `SubList ${accountId} not found in database (yet?)`, + ); + } + return 'sub_list'; + } + + case 'repoDeadlineDriver': { + const deadline = await DeadlineModel.findByPk( + convertToRepoDeadlineDriverId(accountId), + { transaction, attributes: ['accountId'] }, + ); + if (!deadline) { + throw new RecoverableError( + `Deadline ${accountId} not found in database (yet?)`, + ); + } + return 'deadline'; + } + + case 'nftDriver': { + const nftDriverId = convertToNftDriverId(accountId); + + const ecosystem = await EcosystemMainAccountModel.findByPk(nftDriverId, { + transaction, + attributes: ['accountId'], + }); + + const dripList = await DripListModel.findByPk(nftDriverId, { + transaction, + attributes: ['accountId'], + }); + + if (ecosystem) return 'ecosystem_main_account'; + if (dripList) return 'drip_list'; + + throw new RecoverableError( + `NFTDriver entity ${accountId} not found in database (yet?)`, + ); + } + + default: + throw new Error(`Unknown contract name: ${contractName}`); + } +} diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index 3a4e512..a274524 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -1,7 +1,8 @@ import { hexlify, toUtf8Bytes } from 'ethers'; import type { z } from 'zod'; +import type { Transaction } from 'sequelize'; import unreachableError from './unreachableError'; -import type ProjectModel from '../models/ProjectModel'; +import ProjectModel from '../models/ProjectModel'; import type { Forge, ProjectVerificationStatus } from '../models/ProjectModel'; import { repoDriverContract } from '../core/contractClients'; import type { gitHubSourceSchema } from '../metadata/schemas/common/sources'; @@ -10,6 +11,9 @@ import { isRepoDriverId, isRepoSubAccountDriverId, } from './accountIdUtils'; +import type ScopedLogger from '../core/ScopedLogger'; +import type { RepoDriverId } from '../core/types'; +import { makeVersion } from './lastProcessedVersion'; export function convertForgeToNumber(forge: Forge) { switch (forge) { @@ -59,55 +63,110 @@ export async function calcProjectId( return accountId.toString(); } +export async function verifyProjectSource( + accountId: string, + source: z.infer, +): Promise<{ isValid: true } | { isValid: false; message: string }> { + const { forge, ownerName, repoName } = source; + const isSubAccount = isRepoSubAccountDriverId(accountId.toString()); + const isParentAccount = isRepoDriverId(accountId.toString()); + + if (!isSubAccount && !isParentAccount) { + unreachableError( + `Invalid account ID: '${accountId}' is not a valid RepoDriver or RepoSubAccount ID.`, + ); + } + + const calculatedParentAccountId = await calcProjectId( + forge, + ownerName, + repoName, + ); + + if (isSubAccount) { + const parentId = await calcParentRepoDriverId(accountId); + + if (parentId !== calculatedParentAccountId.toString()) { + return { + isValid: false, + message: `Mismatch for '${ownerName}/${repoName}' on '${forge}': for sub account '${accountId}', expected parent '${calculatedParentAccountId}', got '${parentId}'.`, + }; + } + } else if (accountId !== calculatedParentAccountId.toString()) { + return { + isValid: false, + message: `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected parent account '${calculatedParentAccountId}', got '${accountId}'.`, + }; + } + + return { isValid: true }; +} + export async function verifyProjectSources( projects: { accountId: string; source: z.infer; }[], -): Promise<{ - areProjectsValid: boolean; - message?: string; -}> { - const errors: string[] = []; - for (const { - accountId, - source: { forge, ownerName, repoName }, - } of projects) { - const isSubAccount = isRepoSubAccountDriverId(accountId.toString()); - const isParentAccount = isRepoDriverId(accountId.toString()); - - if (!isSubAccount && !isParentAccount) { - unreachableError( - `Invalid account ID: '${accountId}' is not a valid RepoDriver or RepoSubAccount ID.`, - ); - } - const calculatedParentAccountId = await calcProjectId( - forge, - ownerName, - repoName, - ); +): Promise<{ isValid: true } | { isValid: false; message: string }> { + // Parallelize all verification calls to fix N+1 problem and sequential processing + const verificationResults = await Promise.all( + projects.map(({ accountId, source }) => + verifyProjectSource(accountId, source), + ), + ); - if (isSubAccount) { - const parentId = await calcParentRepoDriverId(accountId); - - if (parentId !== calculatedParentAccountId.toString()) { - errors.push( - `Mismatch for '${ownerName}/${repoName}' on '${forge}': for sub account '${accountId}', expected parent '${calculatedParentAccountId}', got '${parentId}'.`, - ); - } - } else if (accountId !== calculatedParentAccountId.toString()) { - errors.push( - `Mismatch for '${ownerName}/${repoName}' on '${forge}': expected parent account '${calculatedParentAccountId}', got '${accountId}'.`, - ); + const errors: string[] = []; + for (const result of verificationResults) { + if (!result.isValid) { + errors.push(result.message); } } + if (errors.length > 0) { return { - areProjectsValid: false, + isValid: false, message: `Failed to verify project sources:\n${errors.join('\n')}`, }; } - return { - areProjectsValid: true, + + return { isValid: true }; +} + +export async function ensureProjectExists(ctx: { + project: { + accountId: RepoDriverId; + source: z.infer; }; + blockNumber: number; + logIndex: number; + transaction: Transaction; + scopedLogger: ScopedLogger; +}) { + const { project, blockNumber, logIndex, transaction, scopedLogger } = ctx; + + const [projectEntry, isCreation] = await ProjectModel.findOrCreate({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { + accountId: project.accountId, + }, + defaults: { + accountId: project.accountId, + verificationStatus: 'unclaimed', + isVisible: true, // Visible by default. Account metadata will set the final visibility. + isValid: true, // There are no receivers yet. Consider the project valid. + url: project.source.url, + forge: project.source.forge, + name: `${project.source.ownerName}/${project.source.repoName}`, + lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), + }, + }); + + if (isCreation) { + scopedLogger.bufferCreation({ + type: ProjectModel, + input: projectEntry, + id: project.accountId, + }); + } } diff --git a/src/utils/validateLinkedIdentity.ts b/src/utils/validateLinkedIdentity.ts index 10f652d..78f653f 100644 --- a/src/utils/validateLinkedIdentity.ts +++ b/src/utils/validateLinkedIdentity.ts @@ -20,7 +20,9 @@ export async function validateLinkedIdentity( const expectedHash = await dripsContract.hashSplits(expectedReceivers); - return onChainHash === expectedHash; + const isHashValid = onChainHash === expectedHash; + + return isHashValid; } catch (error) { logger.error('Error validating linked identity', error); return false; diff --git a/tests/eventHandlers/AccountSeenEventHandler.test.ts b/tests/eventHandlers/AccountSeenEventHandler.test.ts new file mode 100644 index 0000000..da6183a --- /dev/null +++ b/tests/eventHandlers/AccountSeenEventHandler.test.ts @@ -0,0 +1,270 @@ +/* eslint-disable dot-notation */ +import { randomUUID } from 'crypto'; +import type EventHandlerRequest from '../../src/events/EventHandlerRequest'; +import { dbConnection } from '../../src/db/database'; +import type { EventData } from '../../src/events/types'; +import AccountSeenEventModel from '../../src/models/AccountSeenEventModel'; +import DeadlineModel from '../../src/models/DeadlineModel'; +import ScopedLogger from '../../src/core/ScopedLogger'; +import AccountSeenEventHandler from '../../src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler'; +import * as accountIdUtils from '../../src/utils/accountIdUtils'; +import * as getAccountType from '../../src/utils/getAccountType'; +import * as isLatestEvent from '../../src/utils/isLatestEvent'; +import type { + AccountId, + RepoDeadlineDriverId, + RepoDriverId, +} from '../../src/core/types'; + +const accountSeenDeadlineUnix = 1704067200; +const accountSeenDeadlineDate = new Date(accountSeenDeadlineUnix * 1000); +const accountSeenBlockTimestamp = new Date( + (accountSeenDeadlineUnix - 60) * 1000, +); + +jest.mock('../../src/models/AccountSeenEventModel'); +jest.mock('../../src/models/DeadlineModel'); +jest.mock('../../src/db/database'); +jest.mock('bee-queue'); +jest.mock('../../src/core/ScopedLogger'); +jest.mock('../../src/utils/getAccountType'); +jest.mock('../../src/utils/isLatestEvent'); + +describe('AccountSeenEventHandler', () => { + let mockDbTransaction: any; + let handler: AccountSeenEventHandler; + let mockRequest: EventHandlerRequest<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>; + + beforeEach(() => { + jest.clearAllMocks(); + + handler = new AccountSeenEventHandler(); + + mockRequest = { + id: randomUUID(), + event: { + args: [ + 80920745289880686872077472087501508459438916877610571750365932290048n, + 80920745289880686872077472087501508459438916877610571750365932290049n, + 80920745289880686872077472087501508459438916877610571750365932290050n, + 80920745289880686872077472087501508459438916877610571750365932290051n, + accountSeenDeadlineUnix, + ], + logIndex: 1, + blockNumber: 1, + blockTimestamp: accountSeenBlockTimestamp, + transactionHash: 'requestTransactionHash', + eventSignature: 'AccountSeen(uint256,uint256,uint256,uint256,uint32)', + } as EventData<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>, + }; + + mockDbTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + }; + + dbConnection.transaction = jest + .fn() + .mockImplementation((callback) => callback(mockDbTransaction)); + + // Mock utility functions + jest + .spyOn(accountIdUtils, 'convertToRepoDeadlineDriverId') + .mockReturnValue('deadline-account-id' as RepoDeadlineDriverId); + jest + .spyOn(accountIdUtils, 'convertToRepoDriverId') + .mockReturnValue('repo-account-id' as RepoDriverId); + jest + .spyOn(accountIdUtils, 'convertToAccountId') + .mockReturnValueOnce('receiver-account-id' as AccountId) + .mockReturnValueOnce('refund-account-id' as AccountId); + + jest + .mocked(getAccountType.getAccountType) + .mockResolvedValueOnce('project') + .mockResolvedValueOnce('address'); + + jest.mocked(isLatestEvent.isLatestEvent).mockResolvedValue(true); + + ScopedLogger.prototype.log = jest.fn(); + ScopedLogger.prototype.bufferCreation = jest.fn(); + ScopedLogger.prototype.bufferUpdate = jest.fn(); + ScopedLogger.prototype.bufferMessage = jest.fn(); + ScopedLogger.prototype.flush = jest.fn(); + }); + + describe('_handle', () => { + test('should create AccountSeenEventModel and new DeadlineModel when deadline does not exist', async () => { + // Arrange + const accountSeenEvent = { + accountId: 'deadline-account-id', + repoAccountId: 'repo-account-id', + receiverAccountId: 'receiver-account-id', + refundAccountId: 'refund-account-id', + deadline: accountSeenDeadlineDate, + logIndex: 1, + blockNumber: 1, + blockTimestamp: mockRequest.event.blockTimestamp, + transactionHash: 'requestTransactionHash', + }; + + const deadlineEntry = { + accountId: 'deadline-account-id', + receiverAccountId: 'receiver-account-id', + receiverAccountType: 'project', + claimableProjectId: 'repo-account-id', + deadline: accountSeenDeadlineDate, + refundAccountId: 'refund-account-id', + refundAccountType: 'address', + }; + + AccountSeenEventModel.create = jest + .fn() + .mockResolvedValue(accountSeenEvent); + DeadlineModel.findOrCreate = jest + .fn() + .mockResolvedValue([deadlineEntry, true]); + + // Act + await handler['_handle'](mockRequest); + + // Assert + expect(AccountSeenEventModel.create).toHaveBeenCalledWith( + { + accountId: 'deadline-account-id', + repoAccountId: 'repo-account-id', + receiverAccountId: 'receiver-account-id', + refundAccountId: 'refund-account-id', + deadline: accountSeenDeadlineDate, + logIndex: 1, + blockNumber: 1, + blockTimestamp: mockRequest.event.blockTimestamp, + transactionHash: 'requestTransactionHash', + }, + { transaction: mockDbTransaction }, + ); + + expect(DeadlineModel.findOrCreate).toHaveBeenCalledWith({ + transaction: mockDbTransaction, + lock: mockDbTransaction.LOCK.UPDATE, + where: { + accountId: 'deadline-account-id', + }, + defaults: { + accountId: 'deadline-account-id', + receiverAccountId: 'receiver-account-id', + receiverAccountType: 'project', + claimableProjectId: 'repo-account-id', + deadline: accountSeenDeadlineDate, + refundAccountId: 'refund-account-id', + refundAccountType: 'address', + }, + }); + }); + + test('should update existing DeadlineModel when deadline already exists', async () => { + // Arrange + const accountSeenEvent = { + accountId: 'deadline-account-id', + }; + + const existingDeadlineEntry = { + accountId: 'deadline-account-id', + receiverAccountId: 'old-receiver-account-id', + receiverAccountType: 'old-type', + claimableProjectId: 'old-repo-account-id', + deadline: new Date(1234567890 * 1000), + refundAccountId: 'old-refund-account-id', + refundAccountType: 'old-refund-type', + save: jest.fn().mockResolvedValue(undefined), + }; + + AccountSeenEventModel.create = jest + .fn() + .mockResolvedValue(accountSeenEvent); + DeadlineModel.findOrCreate = jest + .fn() + .mockResolvedValue([existingDeadlineEntry, false]); + + // Act + await handler['_handle'](mockRequest); + + // Assert + expect(existingDeadlineEntry.receiverAccountId).toBe( + 'receiver-account-id', + ); + expect(existingDeadlineEntry.receiverAccountType).toBe('project'); + expect(existingDeadlineEntry.claimableProjectId).toBe('repo-account-id'); + expect(existingDeadlineEntry.deadline).toEqual(accountSeenDeadlineDate); + expect(existingDeadlineEntry.refundAccountId).toBe('refund-account-id'); + expect(existingDeadlineEntry.refundAccountType).toBe('address'); + expect(existingDeadlineEntry.save).toHaveBeenCalledWith({ + transaction: mockDbTransaction, + }); + }); + + test('should return early if event is not the latest', async () => { + // Arrange + const accountSeenEvent = { + accountId: 'deadline-account-id', + }; + + AccountSeenEventModel.create = jest + .fn() + .mockResolvedValue(accountSeenEvent); + jest.mocked(isLatestEvent.isLatestEvent).mockResolvedValue(false); + DeadlineModel.findOrCreate = jest.fn(); + + // Act + await handler['_handle'](mockRequest); + + // Assert + expect(DeadlineModel.findOrCreate).not.toHaveBeenCalled(); + expect(ScopedLogger.prototype.flush).toHaveBeenCalled(); + }); + + test('should call getAccountType for receiver and refund accounts', async () => { + // Arrange + const accountSeenEvent = { + accountId: 'deadline-account-id', + }; + + AccountSeenEventModel.create = jest + .fn() + .mockResolvedValue(accountSeenEvent); + DeadlineModel.findOrCreate = jest.fn().mockResolvedValue([{}, true]); + + // Act + await handler['_handle'](mockRequest); + + // Assert + expect(getAccountType.getAccountType).toHaveBeenCalledWith( + 'receiver-account-id', + mockDbTransaction, + ); + expect(getAccountType.getAccountType).toHaveBeenCalledWith( + 'refund-account-id', + mockDbTransaction, + ); + }); + + test('should reject deadlines at or before block timestamp', async () => { + const invalidRequest = { + ...mockRequest, + event: { + ...mockRequest.event, + blockTimestamp: new Date((accountSeenDeadlineUnix + 60) * 1000), + }, + } as EventHandlerRequest<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>; + + await expect(handler['_handle'](invalidRequest)).rejects.toThrow( + /deadline .* not after block timestamp/, + ); + + expect(dbConnection.transaction).not.toHaveBeenCalled(); + expect(AccountSeenEventModel.create).not.toHaveBeenCalled(); + expect(ScopedLogger.prototype.flush).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts similarity index 85% rename from tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts rename to tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts index 06990a7..72af918 100644 --- a/tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts +++ b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts @@ -51,7 +51,7 @@ describe('processLinkedIdentitySplits', () => { mockLinkedIdentity = { accountId: mockAccountId, ownerAccountId: mockOwnerAccountId, - isLinked: false, + areSplitsValid: false, save: jest.fn(), }; @@ -117,48 +117,6 @@ describe('processLinkedIdentitySplits', () => { }); }); - it('should update areSplitsValid flag when validation returns false and NOT create splits', async () => { - mockLinkedIdentity.areSplitsValid = true; - (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue( - mockLinkedIdentity, - ); - (validateLinkedIdentity as jest.Mock).mockResolvedValue(false); - jest.mocked(SplitsReceiverModel.destroy).mockResolvedValue(1); - - const mockEvent = { - accountId: mockAccountId, - receiversHash: mockReceiversHash, - blockTimestamp: new Date('2024-01-01'), - }; - - await processLinkedIdentitySplits( - mockEvent as any, - mockScopedLogger, - mockTransaction, - ); - - expect(validateLinkedIdentity).toHaveBeenCalledWith( - mockAccountId, - mockOwnerAccountId, - ); - - expect(SplitsReceiverModel.destroy).toHaveBeenCalledWith({ - where: { senderAccountId: mockAccountId }, - transaction: mockTransaction, - }); - - expect(receiversRepository.createSplitReceiver).not.toHaveBeenCalled(); - - expect(mockLinkedIdentity.areSplitsValid).toBe(false); - expect(mockLinkedIdentity.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - expect(mockScopedLogger.log).toHaveBeenCalledWith( - expect.stringContaining('ORCID account'), - 'warn', - ); - }); - it('should skip when on-chain hash does not match event hash', async () => { jest.mocked(dripsContract.splitsHash).mockResolvedValue('0xdifferent'); (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue( diff --git a/tests/utils/accountIdUtils.test.ts b/tests/utils/accountIdUtils.test.ts index bb632d2..75bcda6 100644 --- a/tests/utils/accountIdUtils.test.ts +++ b/tests/utils/accountIdUtils.test.ts @@ -1,9 +1,24 @@ import { + assertIsAccountId, + assertIsRepoDeadlineDriverId, + convertToAccountId, + convertToRepoDeadlineDriverId, extractForgeFromAccountId, + getContractNameFromAccountId, isOrcidAccount, + isRepoDeadlineDriverId, } from '../../src/utils/accountIdUtils'; describe('accountIdUtils', () => { + describe('getContractNameFromAccountId', () => { + it('should return repoDeadlineDriver for RepoDeadlineDriverId', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + const result = getContractNameFromAccountId(validRepoDeadlineDriverId); + expect(result).toBe('repoDeadlineDriver'); + }); + }); + describe('extractForgeFromAccountId', () => { it('should extract forge ID from different valid RepoDriver IDs', () => { const testCases = [ @@ -54,4 +69,98 @@ describe('accountIdUtils', () => { expect(result).toBe(false); }); }); + + describe('RepoDeadlineDriver functions', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + + describe('isRepoDeadlineDriverId', () => { + it('should return true for valid RepoDeadlineDriver ID as string', () => { + const result = isRepoDeadlineDriverId(validRepoDeadlineDriverId); + expect(result).toBe(true); + }); + + it('should return true for valid RepoDeadlineDriver ID as bigint', () => { + const result = isRepoDeadlineDriverId( + BigInt(validRepoDeadlineDriverId), + ); + expect(result).toBe(true); + }); + + it('should return false for invalid ID', () => { + const result = isRepoDeadlineDriverId('123456789'); + expect(result).toBe(false); + }); + + it('should return false for NaN string', () => { + expect(() => isRepoDeadlineDriverId('not-a-number')).toThrow(); + }); + }); + + describe('convertToRepoDeadlineDriverId', () => { + it('should convert valid string ID to RepoDeadlineDriverId', () => { + const result = convertToRepoDeadlineDriverId(validRepoDeadlineDriverId); + expect(result).toBe(validRepoDeadlineDriverId); + }); + + it('should convert valid bigint ID to RepoDeadlineDriverId', () => { + const result = convertToRepoDeadlineDriverId( + BigInt(validRepoDeadlineDriverId), + ); + expect(result).toBe(validRepoDeadlineDriverId); + }); + + it('should throw error for invalid ID', () => { + expect(() => convertToRepoDeadlineDriverId('123456789')).toThrow( + "Failed to convert: '123456789' is not a valid RepoDeadlineDriver ID.", + ); + }); + }); + + describe('assertIsRepoDeadlineDriverId', () => { + it('should not throw for valid RepoDeadlineDriver ID', () => { + expect(() => + assertIsRepoDeadlineDriverId(validRepoDeadlineDriverId), + ).not.toThrow(); + }); + + it('should throw error for invalid ID', () => { + expect(() => assertIsRepoDeadlineDriverId('123456789')).toThrow( + "Failed to assert: '123456789' is not a valid RepoDeadlineDriver ID.", + ); + }); + }); + }); + + describe('convertToAccountId', () => { + it('should convert valid RepoDeadlineDriverId to AccountId', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + const result = convertToAccountId(validRepoDeadlineDriverId); + expect(result).toBe(validRepoDeadlineDriverId); + }); + + it('should convert valid RepoDeadlineDriverId bigint to AccountId', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + const result = convertToAccountId(BigInt(validRepoDeadlineDriverId)); + expect(result).toBe(validRepoDeadlineDriverId); + }); + }); + + describe('assertIsAccountId', () => { + it('should not throw for valid RepoDeadlineDriverId', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + expect(() => assertIsAccountId(validRepoDeadlineDriverId)).not.toThrow(); + }); + + it('should not throw for valid RepoDeadlineDriverId bigint', () => { + const validRepoDeadlineDriverId = + '134824369987331688459978851430856029499523851846503058183003895555073'; + expect(() => + assertIsAccountId(BigInt(validRepoDeadlineDriverId)), + ).not.toThrow(); + }); + }); }); diff --git a/tests/utils/getAccountType.test.ts b/tests/utils/getAccountType.test.ts new file mode 100644 index 0000000..9a92e49 --- /dev/null +++ b/tests/utils/getAccountType.test.ts @@ -0,0 +1,501 @@ +import type { Transaction } from 'sequelize'; +import type { AccountId } from '../../src/core/types'; +import { getAccountType } from '../../src/utils/getAccountType'; +import DripListModel from '../../src/models/DripListModel'; +import EcosystemMainAccountModel from '../../src/models/EcosystemMainAccountModel'; +import SubListModel from '../../src/models/SubListModel'; +import DeadlineModel from '../../src/models/DeadlineModel'; +import LinkedIdentityModel from '../../src/models/LinkedIdentityModel'; +import { + getContractNameFromAccountId, + convertToNftDriverId, + convertToImmutableSplitsDriverId, + convertToRepoDeadlineDriverId, + convertToRepoDriverId, + isOrcidAccount, +} from '../../src/utils/accountIdUtils'; + +jest.mock('../../src/models/DripListModel'); +jest.mock('../../src/models/EcosystemMainAccountModel'); +jest.mock('../../src/models/SubListModel'); +jest.mock('../../src/models/DeadlineModel'); +jest.mock('../../src/models/LinkedIdentityModel'); +jest.mock('../../src/utils/accountIdUtils'); + +describe('getAccountType', () => { + let mockTransaction: Transaction; + + beforeEach(() => { + jest.clearAllMocks(); + mockTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + } as Transaction; + }); + + describe('repoDriver (project and linked_identity)', () => { + it('should return "project" for regular repoDriver accounts', async () => { + // Arrange. + const accountId = 'repoDriver:123' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('repoDriver'); + (isOrcidAccount as jest.Mock).mockReturnValue(false); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('project'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + expect(isOrcidAccount).toHaveBeenCalledWith(accountId); + }); + + it('should return "project" for regular repoDriver accounts with transaction', async () => { + // Arrange. + const accountId = 'repoDriver:456' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('repoDriver'); + (isOrcidAccount as jest.Mock).mockReturnValue(false); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('project'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + expect(isOrcidAccount).toHaveBeenCalledWith(accountId); + }); + + it('should return "linked_identity" for ORCID repoDriver accounts when exists in DB', async () => { + // Arrange. + const accountId = 'repoDriver:789' as AccountId; + const convertedId = 'converted:789'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('repoDriver'); + (isOrcidAccount as jest.Mock).mockReturnValue(true); + (convertToRepoDriverId as jest.Mock).mockReturnValue(convertedId); + (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('linked_identity'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + expect(isOrcidAccount).toHaveBeenCalledWith(accountId); + expect(convertToRepoDriverId).toHaveBeenCalledWith(accountId); + expect(LinkedIdentityModel.findOne).toHaveBeenCalledWith({ + lock: mockTransaction.LOCK.UPDATE, + where: { + accountId: convertedId, + identityType: 'orcid', + }, + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should return "linked_identity" for ORCID repoDriver accounts with transaction', async () => { + // Arrange. + const accountId = 'repoDriver:890' as AccountId; + const convertedId = 'converted:890'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('repoDriver'); + (isOrcidAccount as jest.Mock).mockReturnValue(true); + (convertToRepoDriverId as jest.Mock).mockReturnValue(convertedId); + (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('linked_identity'); + expect(LinkedIdentityModel.findOne).toHaveBeenCalledWith({ + lock: mockTransaction.LOCK.UPDATE, + where: { + accountId: convertedId, + identityType: 'orcid', + }, + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should throw error when ORCID LinkedIdentity not found in database', async () => { + // Arrange. + const accountId = 'repoDriver:999' as AccountId; + const convertedId = 'converted:999'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('repoDriver'); + (isOrcidAccount as jest.Mock).mockReturnValue(true); + (convertToRepoDriverId as jest.Mock).mockReturnValue(convertedId); + (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue(null); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + }); + + describe('addressDriver (address)', () => { + it('should return "address" for addressDriver accounts', async () => { + // Arrange. + const accountId = 'addressDriver:0x123' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'addressDriver', + ); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('address'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + }); + + it('should return "address" for addressDriver accounts with transaction', async () => { + // Arrange. + const accountId = 'addressDriver:0x456' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'addressDriver', + ); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('address'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + }); + }); + + describe('immutableSplitsDriver (sub_list)', () => { + it('should return "sub_list" when SubList exists in database', async () => { + // Arrange. + const accountId = 'immutableSplitsDriver:123' as AccountId; + const convertedId = 'converted:123'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'immutableSplitsDriver', + ); + (convertToImmutableSplitsDriverId as jest.Mock).mockReturnValue( + convertedId, + ); + (SubListModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('sub_list'); + expect(convertToImmutableSplitsDriverId).toHaveBeenCalledWith(accountId); + expect(SubListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should return "sub_list" with transaction when SubList exists', async () => { + // Arrange. + const accountId = 'immutableSplitsDriver:456' as AccountId; + const convertedId = 'converted:456'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'immutableSplitsDriver', + ); + (convertToImmutableSplitsDriverId as jest.Mock).mockReturnValue( + convertedId, + ); + (SubListModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('sub_list'); + expect(SubListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should throw error when SubList not found in database', async () => { + // Arrange. + const accountId = 'immutableSplitsDriver:789' as AccountId; + const convertedId = 'converted:789'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'immutableSplitsDriver', + ); + (convertToImmutableSplitsDriverId as jest.Mock).mockReturnValue( + convertedId, + ); + (SubListModel.findByPk as jest.Mock).mockResolvedValue(null); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + }); + + describe('repoDeadlineDriver (deadline)', () => { + it('should return "deadline" when Deadline exists in database', async () => { + // Arrange. + const accountId = 'repoDeadlineDriver:123' as AccountId; + const convertedId = 'converted:123'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'repoDeadlineDriver', + ); + (convertToRepoDeadlineDriverId as jest.Mock).mockReturnValue(convertedId); + (DeadlineModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('deadline'); + expect(convertToRepoDeadlineDriverId).toHaveBeenCalledWith(accountId); + expect(DeadlineModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should return "deadline" with transaction when Deadline exists', async () => { + // Arrange. + const accountId = 'repoDeadlineDriver:456' as AccountId; + const convertedId = 'converted:456'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'repoDeadlineDriver', + ); + (convertToRepoDeadlineDriverId as jest.Mock).mockReturnValue(convertedId); + (DeadlineModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('deadline'); + expect(DeadlineModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should throw error when Deadline not found in database', async () => { + // Arrange. + const accountId = 'repoDeadlineDriver:789' as AccountId; + const convertedId = 'converted:789'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'repoDeadlineDriver', + ); + (convertToRepoDeadlineDriverId as jest.Mock).mockReturnValue(convertedId); + (DeadlineModel.findByPk as jest.Mock).mockResolvedValue(null); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + }); + + describe('repoSubAccountDriver (project)', () => { + it('should return "project" for repoSubAccountDriver accounts', async () => { + // Arrange. + const accountId = 'repoSubAccountDriver:123' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'repoSubAccountDriver', + ); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('project'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + }); + + it('should return "project" for repoSubAccountDriver accounts with transaction', async () => { + // Arrange. + const accountId = 'repoSubAccountDriver:456' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'repoSubAccountDriver', + ); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('project'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + }); + }); + + describe('nftDriver types', () => { + it('should return "ecosystem_main_account" when EcosystemMainAccount exists', async () => { + // Arrange. + const accountId = 'nftDriver:123' as AccountId; + const convertedId = 'converted:123'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('nftDriver'); + (convertToNftDriverId as jest.Mock).mockReturnValue(convertedId); + (EcosystemMainAccountModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + (DripListModel.findByPk as jest.Mock).mockResolvedValue(null); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('ecosystem_main_account'); + expect(convertToNftDriverId).toHaveBeenCalledWith(accountId); + expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( + convertedId, + { + transaction: mockTransaction, + attributes: ['accountId'], + }, + ); + expect(DripListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should return "drip_list" when DripList exists but EcosystemMainAccount does not', async () => { + // Arrange. + const accountId = 'nftDriver:456' as AccountId; + const convertedId = 'converted:456'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('nftDriver'); + (convertToNftDriverId as jest.Mock).mockReturnValue(convertedId); + (EcosystemMainAccountModel.findByPk as jest.Mock).mockResolvedValue(null); + (DripListModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('drip_list'); + }); + + it('should return "ecosystem_main_account" when both exist (ecosystem takes priority)', async () => { + // Arrange. + const accountId = 'nftDriver:789' as AccountId; + const convertedId = 'converted:789'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('nftDriver'); + (convertToNftDriverId as jest.Mock).mockReturnValue(convertedId); + (EcosystemMainAccountModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + (DripListModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('ecosystem_main_account'); + }); + + it('should work with transaction parameter', async () => { + // Arrange. + const accountId = 'nftDriver:111' as AccountId; + const convertedId = 'converted:111'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('nftDriver'); + (convertToNftDriverId as jest.Mock).mockReturnValue(convertedId); + (EcosystemMainAccountModel.findByPk as jest.Mock).mockResolvedValue({ + accountId: convertedId, + }); + (DripListModel.findByPk as jest.Mock).mockResolvedValue(null); + + // Act. + const result = await getAccountType(accountId, mockTransaction); + + // Assert. + expect(result).toBe('ecosystem_main_account'); + expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( + convertedId, + { + transaction: mockTransaction, + attributes: ['accountId'], + }, + ); + expect(DripListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: mockTransaction, + attributes: ['accountId'], + }); + }); + + it('should throw error when neither EcosystemMainAccount nor DripList exist', async () => { + // Arrange. + const accountId = 'nftDriver:999' as AccountId; + const convertedId = 'converted:999'; + (getContractNameFromAccountId as jest.Mock).mockReturnValue('nftDriver'); + (convertToNftDriverId as jest.Mock).mockReturnValue(convertedId); + (EcosystemMainAccountModel.findByPk as jest.Mock).mockResolvedValue(null); + (DripListModel.findByPk as jest.Mock).mockResolvedValue(null); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + }); + + describe('error handling', () => { + it('should throw error for unknown contract name', async () => { + // Arrange. + const accountId = 'unknownDriver:123' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'unknownDriver', + ); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + + it('should throw error for invalid contract name', async () => { + // Arrange. + const accountId = 'someInvalidDriver:456' as AccountId; + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'someInvalidDriver', + ); + + // Act & Assert. + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); + }); + + it('should handle database errors gracefully', async () => { + // Arrange. + const accountId = 'immutableSplitsDriver:error' as AccountId; + const convertedId = 'converted:error'; + const dbError = new Error('Database connection failed'); + (getContractNameFromAccountId as jest.Mock).mockReturnValue( + 'immutableSplitsDriver', + ); + (convertToImmutableSplitsDriverId as jest.Mock).mockReturnValue( + convertedId, + ); + (SubListModel.findByPk as jest.Mock).mockRejectedValue(dbError); + + // Act & Assert. + await expect(getAccountType(accountId, mockTransaction)).rejects.toThrow( + dbError, + ); + }); + }); +}); diff --git a/tests/utils/validateLinkedIdentity.test.ts b/tests/utils/validateLinkedIdentity.test.ts index 2f6ef1e..0d077b8 100644 --- a/tests/utils/validateLinkedIdentity.test.ts +++ b/tests/utils/validateLinkedIdentity.test.ts @@ -95,4 +95,23 @@ describe('validateLinkedIdentity', () => { // Assert expect(result).toBe(false); }); + + test('should return true when hash is valid', async () => { + // Arrange + (dripsContract as any).splitsHash = jest + .fn() + .mockResolvedValue(mockOnChainHash); + (dripsContract as any).hashSplits = jest + .fn() + .mockResolvedValue(mockExpectedHash); + + // Act + const result = await validateLinkedIdentity( + mockAccountId, + mockOwnerAccountId, + ); + + // Assert + expect(result).toBe(true); + }); });