From 8c56e2fa1f978f7844b3b1ce4a7dad2fb623d16e Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 12 Aug 2025 16:47:10 +0200 Subject: [PATCH 01/16] feat: introduce DeadLine model --- src/core/splitRules.ts | 22 ++- .../20250812000000-add-deadlines.ts | 128 ++++++++++++++++++ src/db/modelRegistration.ts | 2 + src/models/DeadlineModel.ts | 102 ++++++++++++++ src/models/index.ts | 1 + ...ts => processLinkedIdentitySplits.test.ts} | 5 +- 6 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/db/migrations/20250812000000-add-deadlines.ts create mode 100644 src/models/DeadlineModel.ts rename tests/eventHandlers/SplitsSetEvent/{setLinkedIdentityFlag.test.ts => processLinkedIdentitySplits.test.ts} (98%) diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts index e1010e3..dcb8982 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', + }, // Drip List Rules { @@ -45,6 +50,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'project', relationshipType: 'drip_list_receiver', }, + { + senderAccountType: 'drip_list', + receiverAccountType: 'deadline', + relationshipType: 'drip_list_receiver', + }, // Ecosystem Main Account Rules { @@ -52,6 +62,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: 'sub_list', @@ -74,6 +89,11 @@ const SPLIT_RULES = Object.freeze([ receiverAccountType: 'project', relationshipType: 'sub_list_link', }, + { + senderAccountType: 'sub_list', + receiverAccountType: 'deadline', + relationshipType: 'sub_list_link', + }, { senderAccountType: 'sub_list', receiverAccountType: 'sub_list', @@ -139,7 +159,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 populated `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..c783b44 --- /dev/null +++ b/src/db/migrations/20250812000000-add-deadlines.ts @@ -0,0 +1,128 @@ +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); +} + +async function createDeadlinesTable( + queryInterface: QueryInterface, + schema: DbSchema, +) { + await queryInterface.createTable( + { + schema, + tableName: 'deadlines', + }, + transformFieldNamesToSnakeCase({ + accountId: { + primaryKey: true, + type: DataTypes.STRING, + }, + receivingAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receivingAccountType: { + 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(['receivingAccountId']), + { + 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', + }, + ); +} + +export async function down({ context: sequelize }: any): Promise { + const schema = getSchema(); + const queryInterface: QueryInterface = sequelize.getQueryInterface(); + + // Drop table + 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..c167f9e 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, @@ -34,6 +35,7 @@ export function registerModels(): void { registerModel(SubListModel); registerModel(ProjectModel); registerModel(DripListModel); + registerModel(DeadlineModel); registerModel(GivenEventModel); registerModel(SplitEventModel); registerModel(TransferEventModel); diff --git a/src/models/DeadlineModel.ts b/src/models/DeadlineModel.ts new file mode 100644 index 0000000..5b56abe --- /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 receivingAccountId: AccountId; + declare public receivingAccountType: 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, + }, + receivingAccountId: { + allowNull: false, + type: DataTypes.STRING, + }, + receivingAccountType: { + 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: ['receivingAccountId'], + 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..b2d00dd 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,6 +1,7 @@ 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'; diff --git a/tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts similarity index 98% rename from tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts rename to tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts index 68884af..55f235f 100644 --- a/tests/eventHandlers/SplitsSetEvent/setLinkedIdentityFlag.test.ts +++ b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts @@ -153,8 +153,9 @@ describe('processLinkedIdentitySplits', () => { expect(mockLinkedIdentity.save).toHaveBeenCalledWith({ transaction: mockTransaction, }); - expect(mockScopedLogger.bufferMessage).toHaveBeenCalledWith( - expect.stringContaining('WARNING: ORCID account'), + expect(mockScopedLogger.log).toHaveBeenCalledWith( + expect.stringContaining('not properly linked'), + 'warn', ); }); From e8f35331aded0d7b142a2a881bff2881ccae2fe3 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 12 Aug 2025 16:59:25 +0200 Subject: [PATCH 02/16] feat: implement RepoDeadlineDriver account ID utils --- src/core/constants.ts | 1 + src/utils/accountIdUtils.ts | 49 ++++++++++++- tests/utils/accountIdUtils.test.ts | 109 +++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) 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/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 7dacfbc..6113940 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 isNaN = Number.isNaN(Number(idString)); + const isAccountIdOfRepoDeadlineDriver = + getContractNameFromAccountId(idString) === 'repoDeadlineDriver'; + + if (isNaN || !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/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(); + }); + }); }); From e581e6972692da1c4df7d47193a86231d0c7fc60 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 12 Aug 2025 17:16:50 +0200 Subject: [PATCH 03/16] build: generate RepoDeadlineDriver client --- scripts/codegen-any-chain-types.ts | 10 +- src/abi/filecoin/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/goerli/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/localtestnet/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/mainnet/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/metis/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/optimism/RepoDeadlineDriver.json | 390 ++++++++++++++++++ .../optimism_sepolia/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/polygon_amoy/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/abi/sepolia/RepoDeadlineDriver.json | 390 ++++++++++++++++++ src/config/chainConfigs/filecoin.json | 3 + src/config/chainConfigs/localtestnet.json | 3 + src/config/chainConfigs/metis.json | 3 + src/config/chainConfigs/optimism.json | 3 + src/config/chainConfigs/optimism_sepolia.json | 3 + src/config/chainConfigs/sepolia.json | 3 + .../chainConfigs/zksync_era_sepolia.json | 3 + src/core/contractClients.ts | 16 +- 18 files changed, 3554 insertions(+), 3 deletions(-) create mode 100644 src/abi/filecoin/RepoDeadlineDriver.json create mode 100644 src/abi/goerli/RepoDeadlineDriver.json create mode 100644 src/abi/localtestnet/RepoDeadlineDriver.json create mode 100644 src/abi/mainnet/RepoDeadlineDriver.json create mode 100644 src/abi/metis/RepoDeadlineDriver.json create mode 100644 src/abi/optimism/RepoDeadlineDriver.json create mode 100644 src/abi/optimism_sepolia/RepoDeadlineDriver.json create mode 100644 src/abi/polygon_amoy/RepoDeadlineDriver.json create mode 100644 src/abi/sepolia/RepoDeadlineDriver.json 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/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/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/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/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, From 42ca356aaa159af1966c6192e265d0d2494db315 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 13 Aug 2025 12:34:11 +0200 Subject: [PATCH 04/16] feat: implement AccountSeen event handler for deadline management --- .../20250812000000-add-deadlines.ts | 74 ++- src/eventHandlers/AccountSeenEventHandler.ts | 145 ++++++ src/eventHandlers/index.ts | 3 + src/events/registrations.ts | 9 +- src/events/types.ts | 4 + src/models/AccountSeenEventModel.ts | 72 +++ src/models/DeadlineModel.ts | 10 +- src/models/index.ts | 3 +- src/utils/getAccountType.ts | 108 ++++ .../AccountSeenEventHandler.test.ts | 247 +++++++++ tests/utils/getAccountType.test.ts | 481 ++++++++++++++++++ 11 files changed, 1144 insertions(+), 12 deletions(-) create mode 100644 src/eventHandlers/AccountSeenEventHandler.ts create mode 100644 src/models/AccountSeenEventModel.ts create mode 100644 src/utils/getAccountType.ts create mode 100644 tests/eventHandlers/AccountSeenEventHandler.test.ts create mode 100644 tests/utils/getAccountType.test.ts diff --git a/src/db/migrations/20250812000000-add-deadlines.ts b/src/db/migrations/20250812000000-add-deadlines.ts index c783b44..c705f87 100644 --- a/src/db/migrations/20250812000000-add-deadlines.ts +++ b/src/db/migrations/20250812000000-add-deadlines.ts @@ -17,6 +17,7 @@ export async function up({ context: sequelize }: any): Promise { `); await createDeadlinesTable(queryInterface, schema); + await createAccountSeenEventsTable(queryInterface, schema); } async function createDeadlinesTable( @@ -33,11 +34,11 @@ async function createDeadlinesTable( primaryKey: true, type: DataTypes.STRING, }, - receivingAccountId: { + receiverAccountId: { allowNull: false, type: DataTypes.STRING, }, - receivingAccountType: { + receiverAccountType: { allowNull: false, type: literal(`${schema}.account_type`).val as unknown as DataType, }, @@ -73,7 +74,7 @@ async function createDeadlinesTable( schema, tableName: 'deadlines', }, - transformFieldArrayToSnakeCase(['receivingAccountId']), + transformFieldArrayToSnakeCase(['receiverAccountId']), { name: 'idx_deadlines_receiving_account_id', }, @@ -113,11 +114,76 @@ async function createDeadlinesTable( ); } +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 table + // Drop tables + await queryInterface.dropTable({ + schema, + tableName: 'account_seen_events', + }); + await queryInterface.dropTable({ schema, tableName: 'deadlines', diff --git a/src/eventHandlers/AccountSeenEventHandler.ts b/src/eventHandlers/AccountSeenEventHandler.ts new file mode 100644 index 0000000..faa0a6c --- /dev/null +++ b/src/eventHandlers/AccountSeenEventHandler.ts @@ -0,0 +1,145 @@ +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); + + await dbConnection.transaction(async (transaction) => { + const [receiverAccountType, refundAccountType] = await Promise.all([ + getAccountType(receiverAccountId, transaction), + 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/index.ts b/src/eventHandlers/index.ts index e2f5eff..ad34e6e 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'; +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/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 index 5b56abe..2647de7 100644 --- a/src/models/DeadlineModel.ts +++ b/src/models/DeadlineModel.ts @@ -19,8 +19,8 @@ export default class DeadlineModel extends Model< > { declare public accountId: RepoDeadlineDriverId; - declare public receivingAccountId: AccountId; - declare public receivingAccountType: AccountType; + declare public receiverAccountId: AccountId; + declare public receiverAccountType: AccountType; declare public claimableProjectId: RepoDriverId; @@ -39,11 +39,11 @@ export default class DeadlineModel extends Model< primaryKey: true, type: DataTypes.STRING, }, - receivingAccountId: { + receiverAccountId: { allowNull: false, type: DataTypes.STRING, }, - receivingAccountType: { + receiverAccountType: { allowNull: false, type: DataTypes.ENUM(...ACCOUNT_TYPES), }, @@ -80,7 +80,7 @@ export default class DeadlineModel extends Model< timestamps: true, indexes: [ { - fields: ['receivingAccountId'], + fields: ['receiverAccountId'], name: 'idx_deadlines_receiving_account_id', }, { diff --git a/src/models/index.ts b/src/models/index.ts index b2d00dd..9a22657 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -4,12 +4,13 @@ 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/getAccountType.ts b/src/utils/getAccountType.ts new file mode 100644 index 0000000..7dde4a5 --- /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({ + 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, dripList] = await Promise.all([ + EcosystemMainAccountModel.findByPk(nftDriverId, { + transaction, + attributes: ['accountId'], + }), + 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/tests/eventHandlers/AccountSeenEventHandler.test.ts b/tests/eventHandlers/AccountSeenEventHandler.test.ts new file mode 100644 index 0000000..4ca0a7a --- /dev/null +++ b/tests/eventHandlers/AccountSeenEventHandler.test.ts @@ -0,0 +1,247 @@ +/* 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'; +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'; + +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, // accountId + 80920745289880686872077472087501508459438916877610571750365932290049n, // repoAccountId + 80920745289880686872077472087501508459438916877610571750365932290050n, // recipientAccountId + 80920745289880686872077472087501508459438916877610571750365932290051n, // refundAccountId + 1704067200, // deadline (unix timestamp) + ], + logIndex: 1, + blockNumber: 1, + blockTimestamp: new Date(), + 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.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: new Date(1704067200 * 1000), + 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: new Date(1704067200 * 1000), + 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: new Date(1704067200 * 1000), + 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: new Date(1704067200 * 1000), + 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( + new Date(1704067200 * 1000), + ); + 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, + ); + }); + }); +}); diff --git a/tests/utils/getAccountType.test.ts b/tests/utils/getAccountType.test.ts new file mode 100644 index 0000000..f14b9b9 --- /dev/null +++ b/tests/utils/getAccountType.test.ts @@ -0,0 +1,481 @@ +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 = {} 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); + + // 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); + + // Assert. + expect(result).toBe('linked_identity'); + expect(getContractNameFromAccountId).toHaveBeenCalledWith(accountId); + expect(isOrcidAccount).toHaveBeenCalledWith(accountId); + expect(convertToRepoDriverId).toHaveBeenCalledWith(accountId); + expect(LinkedIdentityModel.findOne).toHaveBeenCalledWith({ + where: { + accountId: convertedId, + identityType: 'orcid', + }, + transaction: undefined, + 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({ + 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)).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); + + // 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); + + // Assert. + expect(result).toBe('sub_list'); + expect(convertToImmutableSplitsDriverId).toHaveBeenCalledWith(accountId); + expect(SubListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: undefined, + 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)).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); + + // Assert. + expect(result).toBe('deadline'); + expect(convertToRepoDeadlineDriverId).toHaveBeenCalledWith(accountId); + expect(DeadlineModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: undefined, + 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)).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); + + // 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); + + // Assert. + expect(result).toBe('ecosystem_main_account'); + expect(convertToNftDriverId).toHaveBeenCalledWith(accountId); + expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( + convertedId, + { + transaction: undefined, + attributes: ['accountId'], + }, + ); + expect(DripListModel.findByPk).toHaveBeenCalledWith(convertedId, { + transaction: undefined, + 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); + + // 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); + + // 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)).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)).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)).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)).rejects.toThrow(dbError); + }); + }); +}); From 68fbe122f4756e83e868fd26e513cab3deecfefe Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 18 Aug 2025 13:48:07 +0200 Subject: [PATCH 05/16] refactor: isValid & isValid flag updates --- src/db/modelRegistration.ts | 2 + .../handlers/handleSubListMetadata.ts | 2 +- .../AccountSeenEventHandler.ts | 52 +- .../findAffectedAccounts.ts | 94 ++++ .../recalculateValidationFlags.ts | 227 ++++++++ src/eventHandlers/OwnerUpdatedEventHandler.ts | 8 +- .../processLinkedIdentitySplits.ts | 1 + .../SplitsSetEvent/setIsValidFlag.ts | 313 +++++++---- src/eventHandlers/index.ts | 2 +- src/utils/checkIncompleteDeadlineReceivers.ts | 45 ++ src/utils/getAccountType.ts | 22 +- src/utils/validateLinkedIdentity.ts | 18 +- .../AccountSeenEventHandler.test.ts | 54 +- .../processLinkedIdentitySplits.test.ts | 2 + .../checkIncompleteDeadlineReceivers.test.ts | 100 ++++ tests/utils/findAffectedAccounts.test.ts | 249 +++++++++ tests/utils/getAccountType.test.ts | 64 ++- .../utils/recalculateValidationFlags.test.ts | 523 ++++++++++++++++++ tests/utils/validateLinkedIdentity.test.ts | 70 +++ 19 files changed, 1689 insertions(+), 159 deletions(-) rename src/eventHandlers/{ => AccountSeenEventHandler}/AccountSeenEventHandler.ts (70%) create mode 100644 src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts create mode 100644 src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts create mode 100644 src/utils/checkIncompleteDeadlineReceivers.ts create mode 100644 tests/utils/checkIncompleteDeadlineReceivers.test.ts create mode 100644 tests/utils/findAffectedAccounts.test.ts create mode 100644 tests/utils/recalculateValidationFlags.test.ts diff --git a/src/db/modelRegistration.ts b/src/db/modelRegistration.ts index c167f9e..483d200 100644 --- a/src/db/modelRegistration.ts +++ b/src/db/modelRegistration.ts @@ -16,6 +16,7 @@ import { SplitsReceiverModel, OwnerUpdatedEventModel, LinkedIdentityModel, + AccountSeenEventModel, } from '../models'; import SplitsSetEventModel from '../models/SplitsSetEventModel'; @@ -43,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/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 6fa8328..bc02688 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -213,7 +213,7 @@ async function createNewSplitReceivers({ senderAccountType: 'sub_list', receiverAccountId: repoDriverId, receiverAccountType: 'linked_identity', - relationshipType: 'sub_list_link', + relationshipType: 'sub_list_receiver', weight: receiver.weight, blockTimestamp, splitsToRepoDriverSubAccount: diff --git a/src/eventHandlers/AccountSeenEventHandler.ts b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts similarity index 70% rename from src/eventHandlers/AccountSeenEventHandler.ts rename to src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts index faa0a6c..81f41d5 100644 --- a/src/eventHandlers/AccountSeenEventHandler.ts +++ b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts @@ -1,17 +1,19 @@ -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 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'; +} from '../../utils/accountIdUtils'; +import { getAccountType } from '../../utils/getAccountType'; +import { isLatestEvent } from '../../utils/isLatestEvent'; +import { findAffectedAccounts } from './findAffectedAccounts'; +import { recalculateValidationFlags } from './recalculateValidationFlags'; export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'> { public eventSignatures = [ @@ -47,10 +49,14 @@ export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSe const scopedLogger = new ScopedLogger(this.name, requestId); await dbConnection.transaction(async (transaction) => { - const [receiverAccountType, refundAccountType] = await Promise.all([ - getAccountType(receiverAccountId, transaction), - getAccountType(refundAccountId, transaction), - ]); + const receiverAccountType = await getAccountType( + receiverAccountId, + transaction, + ); + const refundAccountType = await getAccountType( + refundAccountId, + transaction, + ); scopedLogger.log( [ @@ -139,6 +145,24 @@ export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSe await deadlineEntry.save({ transaction }); } + // Recalculate validation flags for accounts affected by this deadline becoming "seen" + const affectedAccounts = await findAffectedAccounts( + accountId, + transaction, + ); + + if (affectedAccounts.length > 0) { + scopedLogger.bufferMessage( + `Found ${affectedAccounts.length} accounts with splits pointing to deadline ${accountId}. Recalculating validation flags.`, + ); + + await recalculateValidationFlags( + affectedAccounts, + scopedLogger, + transaction, + ); + } + scopedLogger.flush(); }); } diff --git a/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts b/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts new file mode 100644 index 0000000..6f4fe70 --- /dev/null +++ b/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts @@ -0,0 +1,94 @@ +import type { Transaction } from 'sequelize'; +import SplitsReceiverModel from '../../models/SplitsReceiverModel'; +import ProjectModel from '../../models/ProjectModel'; +import DripListModel from '../../models/DripListModel'; +import SubListModel from '../../models/SubListModel'; +import EcosystemMainAccountModel from '../../models/EcosystemMainAccountModel'; +import LinkedIdentityModel from '../../models/LinkedIdentityModel'; +import type { AccountId } from '../../core/types'; + +export interface AffectedAccount { + accountId: AccountId; + type: + | 'Project' + | 'DripList' + | 'SubList' + | 'EcosystemMainAccount' + | 'LinkedIdentity'; +} + +/** + * Finds all accounts that have splits pointing to the specified deadline account. + * These accounts may need their isValid/isLinked flags recalculated when the deadline account becomes "seen". + */ +export async function findAffectedAccounts( + deadlineAccountId: AccountId, + transaction: Transaction, +): Promise { + const splitsReceivers = await SplitsReceiverModel.findAll({ + where: { + receiverAccountId: deadlineAccountId, + }, + attributes: ['senderAccountId'], + transaction, + }); + + const affectedAccountIds = [ + ...new Set(splitsReceivers.map((receiver) => receiver.senderAccountId)), + ]; + + const affectedAccounts: AffectedAccount[] = []; + + for (const accountId of affectedAccountIds) { + const project = await ProjectModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const dripList = await DripListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const subList = await SubListModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk( + accountId, + { + transaction, + lock: transaction.LOCK.UPDATE, + }, + ); + + const linkedIdentity = await LinkedIdentityModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const foundEntities = [ + project && 'Project', + dripList && 'DripList', + subList && 'SubList', + ecosystemMainAccount && 'EcosystemMainAccount', + linkedIdentity && 'LinkedIdentity', + ].filter(Boolean); + + if (foundEntities.length > 1) { + throw new Error( + `CRITICAL BUG: Account ${accountId} exists in multiple entity tables: ${foundEntities.join(', ')}`, + ); + } + + if (foundEntities.length === 1) { + affectedAccounts.push({ + accountId, + type: foundEntities[0] as AffectedAccount['type'], + }); + } + } + + return affectedAccounts; +} diff --git a/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts b/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts new file mode 100644 index 0000000..53cfe95 --- /dev/null +++ b/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts @@ -0,0 +1,227 @@ +import type { Transaction, Model } from 'sequelize'; +import type { AccountId } from '../../core/types'; +import type ScopedLogger from '../../core/ScopedLogger'; +import { + ProjectModel, + DripListModel, + EcosystemMainAccountModel, + SubListModel, + LinkedIdentityModel, + SplitsReceiverModel, +} from '../../models'; +import { + dripsContract, + nftDriverContract, + repoDriverContract, +} from '../../core/contractClients'; +import { calcSubRepoDriverId } from '../../utils/accountIdUtils'; +import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; +import { checkIncompleteDeadlineReceivers } from '../../utils/checkIncompleteDeadlineReceivers'; +import { validateLinkedIdentity } from '../../utils/validateLinkedIdentity'; +import RecoverableError from '../../utils/recoverableError'; +import type { AffectedAccount } from './findAffectedAccounts'; + +/** + * Recalculates and updates validation flags (isValid/isLinked). + */ +export async function recalculateValidationFlags( + affectedAccounts: AffectedAccount[], + scopedLogger: ScopedLogger, + transaction: Transaction, +): Promise { + for (const { accountId, type } of affectedAccounts) { + if (type === 'LinkedIdentity') { + await recalculateLinkedIdentityFlag(accountId, scopedLogger, transaction); + } else { + await recalculateIsValidFlag(accountId, type, scopedLogger, transaction); + } + } +} + +async function recalculateLinkedIdentityFlag( + accountId: AccountId, + scopedLogger: ScopedLogger, + transaction: Transaction, +): Promise { + const linkedIdentity = await LinkedIdentityModel.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!linkedIdentity) { + throw new RecoverableError( + `LinkedIdentity ${accountId} not found during recalculation. Waiting for entity creation.`, + ); + } + + const previousIsLinked = linkedIdentity.isLinked; + const newIsLinked = await validateLinkedIdentity( + accountId, + linkedIdentity.ownerAccountId, + transaction, + ); + + if (previousIsLinked !== newIsLinked) { + linkedIdentity.isLinked = newIsLinked; + + scopedLogger.bufferUpdate({ + id: accountId, + type: LinkedIdentityModel, + input: linkedIdentity, + }); + + await linkedIdentity.save({ transaction }); + + scopedLogger.bufferMessage( + `Recalculated LinkedIdentity ${accountId} isLinked flag: ${previousIsLinked} → ${newIsLinked}`, + ); + } +} + +type ValidatableAccountType = + | 'Project' + | 'DripList' + | 'SubList' + | 'EcosystemMainAccount'; + +type ValidatableAccount = Model & { + isValid: boolean; + [key: string]: any; +}; + +type ValidatableModelStatic = { + findByPk: ( + accountId: AccountId, + options: { transaction: Transaction; lock: any }, + ) => Promise; +} & (abstract new (...args: any[]) => ValidatableAccount); + +type ContractClient = { + ownerOf: (accountId: AccountId) => Promise; +}; + +type ValidationConfig = { + model: ValidatableModelStatic; + contractClient?: ContractClient | null; + ownerField: string; + skipOwnerCheck?: boolean; +}; + +const VALIDATION_CONFIGS: Record = { + Project: { + model: ProjectModel, + contractClient: repoDriverContract, + ownerField: 'ownerAddress', + }, + DripList: { + model: DripListModel, + contractClient: nftDriverContract, + ownerField: 'ownerAddress', + }, + EcosystemMainAccount: { + model: EcosystemMainAccountModel, + contractClient: nftDriverContract, + ownerField: 'ownerAddress', + }, + SubList: { + model: SubListModel, + contractClient: null, + ownerField: 'ownerAddress', + skipOwnerCheck: true, + }, +}; + +async function recalculateAccountIsValid( + accountId: AccountId, + type: ValidatableAccountType, + scopedLogger: ScopedLogger, + transaction: Transaction, +): Promise { + const config = VALIDATION_CONFIGS[type]; + const account = await config.model.findByPk(accountId, { + transaction, + lock: transaction.LOCK.UPDATE, + }); + + if (!account) { + throw new RecoverableError( + `${type} ${accountId} not found during recalculation. Waiting for entity creation.`, + ); + } + + const onChainReceiversHash = await dripsContract.splitsHash(accountId); + + let onChainOwner: string | null = null; + if (!config.skipOwnerCheck && config.contractClient) { + onChainOwner = await config.contractClient.ownerOf(accountId); + } + + const dbReceiversHash = await hashDbSplits(accountId, transaction); + + // Skip if owner mismatch (temporary state). + if (!config.skipOwnerCheck && onChainOwner !== account[config.ownerField]) { + throw new RecoverableError( + `Owner mismatch for ${type} ${accountId}: on-chain ${onChainOwner} vs DB ${account[config.ownerField]}. Waiting for owner update.`, + ); + } + + const hashValid = dbReceiversHash === onChainReceiversHash; + const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( + accountId, + transaction, + ); + + const previousIsValid = account.isValid; + const newIsValid = hashValid && !hasIncompleteDeadlines; + + if (previousIsValid !== newIsValid) { + account.isValid = newIsValid; + + scopedLogger.bufferUpdate({ + id: accountId, + type: config.model, + input: account, + }); + + await account.save({ transaction }); + + scopedLogger.bufferMessage( + `Recalculated ${type} ${accountId} isValid flag: ${previousIsValid} → ${newIsValid}`, + ); + } +} + +async function recalculateIsValidFlag( + accountId: AccountId, + type: ValidatableAccountType, + scopedLogger: ScopedLogger, + transaction: Transaction, +): Promise { + await recalculateAccountIsValid(accountId, type, scopedLogger, transaction); +} + +async function hashDbSplits( + accountId: AccountId, + transaction: Transaction, +): Promise { + const rows = await SplitsReceiverModel.findAll({ + transaction, + lock: transaction.LOCK.UPDATE, + where: { senderAccountId: accountId }, + }); + + const receivers = []; + for (const s of rows) { + let receiverId = s.receiverAccountId; + if (s.splitsToRepoDriverSubAccount) { + receiverId = await calcSubRepoDriverId(s.receiverAccountId); + } + + receivers.push({ + accountId: receiverId, + weight: s.weight, + }); + } + + return dripsContract.hashSplits(formatSplitReceivers(receivers)); +} diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts index e34b158..920106d 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; @@ -174,7 +175,11 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd identityType: 'orcid', ownerAddress: owner, ownerAccountId, - isLinked: await validateLinkedIdentity(accountId, ownerAccountId), + isLinked: await validateLinkedIdentity( + accountId, + ownerAccountId, + transaction, + ), lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }, }, @@ -193,6 +198,7 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd linkedIdentity.isLinked = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, + transaction, ); linkedIdentity.lastProcessedVersion = makeVersion( blockNumber, diff --git a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts index 1944d48..7239c37 100644 --- a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts +++ b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts @@ -44,6 +44,7 @@ export async function processLinkedIdentitySplits( const isLinked = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, + transaction, ); assertIsRepoDriverId(accountId); diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index a216b0f..815f052 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -23,6 +23,7 @@ import { import type SplitsSetEventModel from '../../models/SplitsSetEventModel'; import type ScopedLogger from '../../core/ScopedLogger'; import unreachableError from '../../utils/unreachableError'; +import { checkIncompleteDeadlineReceivers } from '../../utils/checkIncompleteDeadlineReceivers'; export default async function setIsValidFlag( { accountId, receiversHash: eventReceiversHash }: SplitsSetEventModel, @@ -31,139 +32,237 @@ 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!; + 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}'.`, + ); + } + + 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.`, - ); - } + 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(); - if (dripList && ecosystemMain) { - unreachableError( - `Invariant violation: both Drip List and Ecosystem Main Account found for token '${accountId}'.`, - ); - } + if (entityFinder.validateOwnership) { + await entityFinder.validateOwnership(entity); + } - 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 { hashValid } = await validateSplitsHash( + accountId, + onChainReceiversHash, + transaction, + scopedLogger, + entityType, + ); - const dbReceiversHash = await hashDbSplits(accountId, transaction); - const isValid = dbReceiversHash === onChainReceiversHash; + const { hasIncompleteDeadlines } = await validateDeadlineReceivers( + accountId, + transaction, + scopedLogger, + entityType, + ); - entity.isValid = isValid; + const isValid = hashValid && !hasIncompleteDeadlines; - scopedLogger.bufferUpdate({ - id: entity.accountId, - type: Model, - input: entity, - }); + entity.isValid = isValid; - await entity.save({ transaction }); + scopedLogger.bufferUpdate({ + id: entity.accountId, + type: Model, + input: entity as any, + }); - 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, - }); + await entity.save({ transaction }); - if (!subList) { - throw new RecoverableError( - `Failed to set 'isValid' flag for SubList: SubList '${accountId}' not found.`, - ); - } + if (!isValid) { + const reasons = []; + if (!hashValid) reasons.push('splits hash mismatch'); + if (hasIncompleteDeadlines) reasons.push('incomplete deadline receivers'); - const dbReceiversHash = await hashDbSplits(accountId, transaction); - const isValid = dbReceiversHash === onChainReceiversHash; + 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.`, + ); + } +} - subList.isValid = isValid; +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}'`, + ); + } - scopedLogger.bufferUpdate({ - id: subList.accountId, - type: SubListModel, - input: subList, - }); + return { hashValid, dbReceiversHash }; +} - await subList.save({ transaction }); +async function validateDeadlineReceivers( + accountId: AccountId, + transaction: Transaction, + scopedLogger: ScopedLogger, + entityType: string, +): Promise<{ + hasIncompleteDeadlines: boolean; +}> { + const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( + accountId, + 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 SubList '${accountId}'. Likely waiting on another event to be processed. Retrying, but if this persists, it is a real error.`, - ); - } + if (hasIncompleteDeadlines) { + scopedLogger.bufferMessage( + `${entityType} ${accountId} has splits pointing to incomplete deadline accounts`, + ); } + + return { hasIncompleteDeadlines }; } async function hashDbSplits( diff --git a/src/eventHandlers/index.ts b/src/eventHandlers/index.ts index ad34e6e..418ecc9 100644 --- a/src/eventHandlers/index.ts +++ b/src/eventHandlers/index.ts @@ -2,7 +2,7 @@ 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'; +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'; diff --git a/src/utils/checkIncompleteDeadlineReceivers.ts b/src/utils/checkIncompleteDeadlineReceivers.ts new file mode 100644 index 0000000..5f07f0d --- /dev/null +++ b/src/utils/checkIncompleteDeadlineReceivers.ts @@ -0,0 +1,45 @@ +import type { Transaction } from 'sequelize'; +import { Op } from 'sequelize'; +import AccountSeenEventModel from '../models/AccountSeenEventModel'; +import SplitsReceiverModel from '../models/SplitsReceiverModel'; +import { isRepoDeadlineDriverId } from './accountIdUtils'; +import type { AccountId, RepoDeadlineDriverId } from '../core/types'; + +export async function checkIncompleteDeadlineReceivers( + senderAccountId: AccountId, + transaction: Transaction, +): Promise { + const splitsReceivers = await SplitsReceiverModel.findAll({ + where: { senderAccountId }, + transaction, + lock: transaction.LOCK.UPDATE, + }); + + const deadlineReceivers = splitsReceivers.filter((receiver) => + isRepoDeadlineDriverId(receiver.receiverAccountId), + ); + + if (deadlineReceivers.length === 0) { + return false; + } + + const receiverAccountIds = deadlineReceivers.map( + (r) => r.receiverAccountId as RepoDeadlineDriverId, + ); + + const existingAccountSeenEvents = await AccountSeenEventModel.findAll({ + where: { + accountId: { + [Op.in]: receiverAccountIds, + }, + }, + transaction, + lock: transaction.LOCK.UPDATE, + }); + + // Check if any receivers are missing + const existingAccountIds = new Set( + existingAccountSeenEvents.map((e) => e.accountId), + ); + return receiverAccountIds.some((id) => !existingAccountIds.has(id)); +} diff --git a/src/utils/getAccountType.ts b/src/utils/getAccountType.ts index 7dde4a5..a4a5f6d 100644 --- a/src/utils/getAccountType.ts +++ b/src/utils/getAccountType.ts @@ -18,12 +18,13 @@ import RecoverableError from './recoverableError'; export async function getAccountType( accountId: AccountId, - transaction?: Transaction, + 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', @@ -83,16 +84,15 @@ export async function getAccountType( case 'nftDriver': { const nftDriverId = convertToNftDriverId(accountId); - const [ecosystem, dripList] = await Promise.all([ - EcosystemMainAccountModel.findByPk(nftDriverId, { - transaction, - attributes: ['accountId'], - }), - DripListModel.findByPk(nftDriverId, { - transaction, - attributes: ['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'; diff --git a/src/utils/validateLinkedIdentity.ts b/src/utils/validateLinkedIdentity.ts index 10f652d..09408ce 100644 --- a/src/utils/validateLinkedIdentity.ts +++ b/src/utils/validateLinkedIdentity.ts @@ -1,11 +1,14 @@ +import type { Transaction } from 'sequelize'; import { dripsContract } from '../core/contractClients'; import type { SplitsReceiverStruct } from '../../contracts/CURRENT_NETWORK/Drips'; import type { AccountId, AddressDriverId } from '../core/types'; import logger from '../core/logger'; +import { checkIncompleteDeadlineReceivers } from './checkIncompleteDeadlineReceivers'; export async function validateLinkedIdentity( accountId: AccountId, expectedOwnerAccountId: AddressDriverId, + transaction: Transaction, ): Promise { try { const onChainHash = await dripsContract.splitsHash(accountId); @@ -20,7 +23,20 @@ export async function validateLinkedIdentity( const expectedHash = await dripsContract.hashSplits(expectedReceivers); - return onChainHash === expectedHash; + const isHashValid = onChainHash === expectedHash; + + const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( + accountId, + transaction, + ); + + if (hasIncompleteDeadlines) { + logger.warn( + `LinkedIdentity ${accountId} has splits pointing to incomplete deadline accounts`, + ); + } + + return isHashValid && !hasIncompleteDeadlines; } 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 index 4ca0a7a..887ac06 100644 --- a/tests/eventHandlers/AccountSeenEventHandler.test.ts +++ b/tests/eventHandlers/AccountSeenEventHandler.test.ts @@ -6,10 +6,13 @@ 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'; +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 * as findAffectedAccounts from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; +import * as recalculateValidationFlags from '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags'; +import type { AffectedAccount } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; import type { AccountId, RepoDeadlineDriverId, @@ -23,6 +26,12 @@ jest.mock('bee-queue'); jest.mock('../../src/core/ScopedLogger'); jest.mock('../../src/utils/getAccountType'); jest.mock('../../src/utils/isLatestEvent'); +jest.mock( + '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts', +); +jest.mock( + '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags', +); describe('AccountSeenEventHandler', () => { let mockDbTransaction: any; @@ -81,9 +90,17 @@ describe('AccountSeenEventHandler', () => { jest.mocked(isLatestEvent.isLatestEvent).mockResolvedValue(true); + jest + .mocked(findAffectedAccounts.findAffectedAccounts) + .mockResolvedValue([]); + jest + .mocked(recalculateValidationFlags.recalculateValidationFlags) + .mockResolvedValue(); + 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(); }); @@ -243,5 +260,40 @@ describe('AccountSeenEventHandler', () => { mockDbTransaction, ); }); + + test('should call recalculateValidationFlags when affected accounts exist', async () => { + // Arrange + const accountSeenEvent = { + accountId: 'deadline-account-id', + }; + const affectedAccounts: AffectedAccount[] = [ + { accountId: 'affected-account-1' as AccountId, type: 'Project' }, + { accountId: 'affected-account-2' as AccountId, type: 'DripList' }, + ]; + + AccountSeenEventModel.create = jest + .fn() + .mockResolvedValue(accountSeenEvent); + DeadlineModel.findOrCreate = jest.fn().mockResolvedValue([{}, true]); + jest + .mocked(findAffectedAccounts.findAffectedAccounts) + .mockResolvedValue(affectedAccounts); + + // Act + await handler['_handle'](mockRequest); + + // Assert + expect(findAffectedAccounts.findAffectedAccounts).toHaveBeenCalledWith( + 'deadline-account-id', + mockDbTransaction, + ); + expect( + recalculateValidationFlags.recalculateValidationFlags, + ).toHaveBeenCalledWith( + affectedAccounts, + expect.any(ScopedLogger), + mockDbTransaction, + ); + }); }); }); diff --git a/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts index 920bc33..51bba08 100644 --- a/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts +++ b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts @@ -91,6 +91,7 @@ describe('processLinkedIdentitySplits', () => { expect(validateLinkedIdentity).toHaveBeenCalledWith( mockAccountId, mockOwnerAccountId, + mockTransaction, ); expect(SplitsReceiverModel.destroy).toHaveBeenCalledWith({ @@ -140,6 +141,7 @@ describe('processLinkedIdentitySplits', () => { expect(validateLinkedIdentity).toHaveBeenCalledWith( mockAccountId, mockOwnerAccountId, + mockTransaction, ); expect(SplitsReceiverModel.destroy).toHaveBeenCalledWith({ diff --git a/tests/utils/checkIncompleteDeadlineReceivers.test.ts b/tests/utils/checkIncompleteDeadlineReceivers.test.ts new file mode 100644 index 0000000..1dfe55f --- /dev/null +++ b/tests/utils/checkIncompleteDeadlineReceivers.test.ts @@ -0,0 +1,100 @@ +import type { Transaction } from 'sequelize'; +import { Op } from 'sequelize'; +import { checkIncompleteDeadlineReceivers } from '../../src/utils/checkIncompleteDeadlineReceivers'; +import SplitsReceiverModel from '../../src/models/SplitsReceiverModel'; +import AccountSeenEventModel from '../../src/models/AccountSeenEventModel'; +import { isRepoDeadlineDriverId } from '../../src/utils/accountIdUtils'; +import type { AccountId } from '../../src/core/types'; + +jest.mock('../../src/models/SplitsReceiverModel'); +jest.mock('../../src/models/AccountSeenEventModel'); +jest.mock('../../src/utils/accountIdUtils'); + +describe('checkIncompleteDeadlineReceivers', () => { + const mockSenderAccountId: AccountId = '123456789' as AccountId; + const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return false when deadlineReceivers.length === 0', async () => { + const mockSplitsReceivers = [ + { receiverAccountId: 'regular-account-1' }, + { receiverAccountId: 'regular-account-2' }, + ]; + + jest + .mocked(SplitsReceiverModel.findAll) + .mockResolvedValue(mockSplitsReceivers as any); + jest.mocked(isRepoDeadlineDriverId).mockReturnValue(false); + + const result = await checkIncompleteDeadlineReceivers( + mockSenderAccountId, + mockTransaction, + ); + + expect(result).toBe(false); + expect(SplitsReceiverModel.findAll).toHaveBeenCalledWith({ + where: { senderAccountId: mockSenderAccountId }, + transaction: mockTransaction, + lock: mockTransaction.LOCK.UPDATE, + }); + expect(AccountSeenEventModel.findAll).not.toHaveBeenCalled(); + }); + + it('should return true when deadlineReceivers.length > 0 and AccountSeenEvent not found', async () => { + const mockDeadlineReceiverId = 'deadline-receiver-id'; + const mockSplitsReceivers = [ + { receiverAccountId: 'regular-account' }, + { receiverAccountId: mockDeadlineReceiverId }, + ]; + + jest + .mocked(SplitsReceiverModel.findAll) + .mockResolvedValue(mockSplitsReceivers as any); + jest + .mocked(isRepoDeadlineDriverId) + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + jest.mocked(AccountSeenEventModel.findAll).mockResolvedValue([]); + + const result = await checkIncompleteDeadlineReceivers( + mockSenderAccountId, + mockTransaction, + ); + + expect(result).toBe(true); + expect(AccountSeenEventModel.findAll).toHaveBeenCalledWith({ + where: { accountId: { [Op.in]: [mockDeadlineReceiverId] } }, + transaction: mockTransaction, + lock: mockTransaction.LOCK.UPDATE, + }); + }); + + it('should return false when deadlineReceivers.length > 0 but all have AccountSeenEvents', async () => { + const mockDeadlineReceiverId = 'deadline-receiver-id'; + const mockSplitsReceivers = [{ receiverAccountId: mockDeadlineReceiverId }]; + const mockAccountSeenEvent = { accountId: mockDeadlineReceiverId }; + + jest + .mocked(SplitsReceiverModel.findAll) + .mockResolvedValue(mockSplitsReceivers as any); + jest.mocked(isRepoDeadlineDriverId).mockReturnValue(true); + jest + .mocked(AccountSeenEventModel.findAll) + .mockResolvedValue([mockAccountSeenEvent as any]); + + const result = await checkIncompleteDeadlineReceivers( + mockSenderAccountId, + mockTransaction, + ); + + expect(result).toBe(false); + expect(AccountSeenEventModel.findAll).toHaveBeenCalledWith({ + where: { accountId: { [Op.in]: [mockDeadlineReceiverId] } }, + transaction: mockTransaction, + lock: mockTransaction.LOCK.UPDATE, + }); + }); +}); diff --git a/tests/utils/findAffectedAccounts.test.ts b/tests/utils/findAffectedAccounts.test.ts new file mode 100644 index 0000000..4dd639a --- /dev/null +++ b/tests/utils/findAffectedAccounts.test.ts @@ -0,0 +1,249 @@ +import type { Transaction } from 'sequelize'; +import { findAffectedAccounts } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; +import SplitsReceiverModel from '../../src/models/SplitsReceiverModel'; +import ProjectModel from '../../src/models/ProjectModel'; +import DripListModel from '../../src/models/DripListModel'; +import SubListModel from '../../src/models/SubListModel'; +import EcosystemMainAccountModel from '../../src/models/EcosystemMainAccountModel'; +import LinkedIdentityModel from '../../src/models/LinkedIdentityModel'; +import type { AccountId } from '../../src/core/types'; + +jest.mock('../../src/models/SplitsReceiverModel'); +jest.mock('../../src/models/ProjectModel'); +jest.mock('../../src/models/DripListModel'); +jest.mock('../../src/models/SubListModel'); +jest.mock('../../src/models/EcosystemMainAccountModel'); +jest.mock('../../src/models/LinkedIdentityModel'); + +describe('findAffectedAccounts', () => { + const mockDeadlineAccountId: AccountId = 'deadline-123' as AccountId; + const mockSenderAccountId1: AccountId = 'sender-1' as AccountId; + const mockSenderAccountId2: AccountId = 'sender-2' as AccountId; + const mockTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + } as Transaction; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const setupSplitsReceivers = (senderAccountIds: AccountId[]) => { + const mockSplitsReceivers = senderAccountIds.map((id) => ({ + senderAccountId: id, + })); + jest + .mocked(SplitsReceiverModel.findAll) + .mockResolvedValue(mockSplitsReceivers as any); + }; + + const setupEntityMocks = ( + project?: any, + dripList?: any, + subList?: any, + ecosystemMainAccount?: any, + linkedIdentity?: any, + ) => { + jest.mocked(ProjectModel.findByPk).mockResolvedValue(project || null); + jest.mocked(DripListModel.findByPk).mockResolvedValue(dripList || null); + jest.mocked(SubListModel.findByPk).mockResolvedValue(subList || null); + jest + .mocked(EcosystemMainAccountModel.findByPk) + .mockResolvedValue(ecosystemMainAccount || null); + jest + .mocked(LinkedIdentityModel.findByPk) + .mockResolvedValue(linkedIdentity || null); + }; + + it('should return Project type for project entity', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks({ accountId: mockSenderAccountId1 }); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'Project', + }, + ]); + expect(SplitsReceiverModel.findAll).toHaveBeenCalledWith({ + where: { receiverAccountId: mockDeadlineAccountId }, + attributes: ['senderAccountId'], + transaction: mockTransaction, + }); + }); + + it('should return DripList type for drip list entity', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks(null, { accountId: mockSenderAccountId1 }); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'DripList', + }, + ]); + }); + + it('should return SubList type for sub list entity', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks(null, null, { accountId: mockSenderAccountId1 }); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'SubList', + }, + ]); + }); + + it('should return EcosystemMainAccount type for ecosystem main account entity', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks(null, null, null, { accountId: mockSenderAccountId1 }); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'EcosystemMainAccount', + }, + ]); + }); + + it('should return LinkedIdentity type for linked identity entity', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks(null, null, null, null, { + accountId: mockSenderAccountId1, + }); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'LinkedIdentity', + }, + ]); + }); + + it('should return multiple affected accounts of different types', async () => { + setupSplitsReceivers([mockSenderAccountId1, mockSenderAccountId2]); + + jest + .mocked(ProjectModel.findByPk) + .mockResolvedValueOnce({ accountId: mockSenderAccountId1 } as any) + .mockResolvedValueOnce(null); + + jest + .mocked(DripListModel.findByPk) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ accountId: mockSenderAccountId2 } as any); + + jest.mocked(SubListModel.findByPk).mockResolvedValue(null); + jest.mocked(EcosystemMainAccountModel.findByPk).mockResolvedValue(null); + jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'Project', + }, + { + accountId: mockSenderAccountId2, + type: 'DripList', + }, + ]); + }); + + it('should skip accounts that do not exist in any entity table', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks(); // All null + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([]); + }); + + it('should handle duplicate sender account IDs', async () => { + const mockSplitsReceivers = [ + { senderAccountId: mockSenderAccountId1 }, + { senderAccountId: mockSenderAccountId1 }, // Duplicate + { senderAccountId: mockSenderAccountId2 }, + ]; + jest + .mocked(SplitsReceiverModel.findAll) + .mockResolvedValue(mockSplitsReceivers as any); + + jest + .mocked(ProjectModel.findByPk) + .mockResolvedValueOnce({ accountId: mockSenderAccountId1 } as any) + .mockResolvedValueOnce({ accountId: mockSenderAccountId2 } as any); + + jest.mocked(DripListModel.findByPk).mockResolvedValue(null); + jest.mocked(SubListModel.findByPk).mockResolvedValue(null); + jest.mocked(EcosystemMainAccountModel.findByPk).mockResolvedValue(null); + jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); + + const result = await findAffectedAccounts( + mockDeadlineAccountId, + mockTransaction, + ); + + expect(result).toEqual([ + { + accountId: mockSenderAccountId1, + type: 'Project', + }, + { + accountId: mockSenderAccountId2, + type: 'Project', + }, + ]); + // Should only call findByPk twice due to Set deduplication + expect(ProjectModel.findByPk).toHaveBeenCalledTimes(2); + }); + + it('should throw critical bug error when account exists in multiple entity tables', async () => { + setupSplitsReceivers([mockSenderAccountId1]); + setupEntityMocks( + { accountId: mockSenderAccountId1 }, // Project + { accountId: mockSenderAccountId1 }, // DripList + ); + + await expect( + findAffectedAccounts(mockDeadlineAccountId, mockTransaction), + ).rejects.toThrow( + `CRITICAL BUG: Account ${mockSenderAccountId1} exists in multiple entity tables: Project, DripList`, + ); + }); +}); diff --git a/tests/utils/getAccountType.test.ts b/tests/utils/getAccountType.test.ts index f14b9b9..9a92e49 100644 --- a/tests/utils/getAccountType.test.ts +++ b/tests/utils/getAccountType.test.ts @@ -27,7 +27,11 @@ describe('getAccountType', () => { beforeEach(() => { jest.clearAllMocks(); - mockTransaction = {} as Transaction; + mockTransaction = { + LOCK: { + UPDATE: 'UPDATE', + }, + } as Transaction; }); describe('repoDriver (project and linked_identity)', () => { @@ -38,7 +42,7 @@ describe('getAccountType', () => { (isOrcidAccount as jest.Mock).mockReturnValue(false); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('project'); @@ -73,7 +77,7 @@ describe('getAccountType', () => { }); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('linked_identity'); @@ -81,11 +85,12 @@ describe('getAccountType', () => { expect(isOrcidAccount).toHaveBeenCalledWith(accountId); expect(convertToRepoDriverId).toHaveBeenCalledWith(accountId); expect(LinkedIdentityModel.findOne).toHaveBeenCalledWith({ + lock: mockTransaction.LOCK.UPDATE, where: { accountId: convertedId, identityType: 'orcid', }, - transaction: undefined, + transaction: mockTransaction, attributes: ['accountId'], }); }); @@ -107,6 +112,7 @@ describe('getAccountType', () => { // Assert. expect(result).toBe('linked_identity'); expect(LinkedIdentityModel.findOne).toHaveBeenCalledWith({ + lock: mockTransaction.LOCK.UPDATE, where: { accountId: convertedId, identityType: 'orcid', @@ -126,7 +132,9 @@ describe('getAccountType', () => { (LinkedIdentityModel.findOne as jest.Mock).mockResolvedValue(null); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); }); @@ -139,7 +147,7 @@ describe('getAccountType', () => { ); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('address'); @@ -178,13 +186,13 @@ describe('getAccountType', () => { }); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('sub_list'); expect(convertToImmutableSplitsDriverId).toHaveBeenCalledWith(accountId); expect(SubListModel.findByPk).toHaveBeenCalledWith(convertedId, { - transaction: undefined, + transaction: mockTransaction, attributes: ['accountId'], }); }); @@ -227,7 +235,9 @@ describe('getAccountType', () => { (SubListModel.findByPk as jest.Mock).mockResolvedValue(null); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); }); @@ -245,13 +255,13 @@ describe('getAccountType', () => { }); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('deadline'); expect(convertToRepoDeadlineDriverId).toHaveBeenCalledWith(accountId); expect(DeadlineModel.findByPk).toHaveBeenCalledWith(convertedId, { - transaction: undefined, + transaction: mockTransaction, attributes: ['accountId'], }); }); @@ -290,7 +300,9 @@ describe('getAccountType', () => { (DeadlineModel.findByPk as jest.Mock).mockResolvedValue(null); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); }); @@ -303,7 +315,7 @@ describe('getAccountType', () => { ); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('project'); @@ -339,7 +351,7 @@ describe('getAccountType', () => { (DripListModel.findByPk as jest.Mock).mockResolvedValue(null); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('ecosystem_main_account'); @@ -347,12 +359,12 @@ describe('getAccountType', () => { expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( convertedId, { - transaction: undefined, + transaction: mockTransaction, attributes: ['accountId'], }, ); expect(DripListModel.findByPk).toHaveBeenCalledWith(convertedId, { - transaction: undefined, + transaction: mockTransaction, attributes: ['accountId'], }); }); @@ -369,7 +381,7 @@ describe('getAccountType', () => { }); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('drip_list'); @@ -389,7 +401,7 @@ describe('getAccountType', () => { }); // Act. - const result = await getAccountType(accountId); + const result = await getAccountType(accountId, mockTransaction); // Assert. expect(result).toBe('ecosystem_main_account'); @@ -434,7 +446,9 @@ describe('getAccountType', () => { (DripListModel.findByPk as jest.Mock).mockResolvedValue(null); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); }); @@ -447,7 +461,9 @@ describe('getAccountType', () => { ); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); it('should throw error for invalid contract name', async () => { @@ -458,7 +474,9 @@ describe('getAccountType', () => { ); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(); + await expect( + getAccountType(accountId, mockTransaction), + ).rejects.toThrow(); }); it('should handle database errors gracefully', async () => { @@ -475,7 +493,9 @@ describe('getAccountType', () => { (SubListModel.findByPk as jest.Mock).mockRejectedValue(dbError); // Act & Assert. - await expect(getAccountType(accountId)).rejects.toThrow(dbError); + await expect(getAccountType(accountId, mockTransaction)).rejects.toThrow( + dbError, + ); }); }); }); diff --git a/tests/utils/recalculateValidationFlags.test.ts b/tests/utils/recalculateValidationFlags.test.ts new file mode 100644 index 0000000..38a3ea5 --- /dev/null +++ b/tests/utils/recalculateValidationFlags.test.ts @@ -0,0 +1,523 @@ +import type { Transaction } from 'sequelize'; +import { recalculateValidationFlags } from '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags'; +import type { AffectedAccount } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; +import type { AccountId, AddressDriverId } from '../../src/core/types'; +import type ScopedLogger from '../../src/core/ScopedLogger'; +import { + ProjectModel, + DripListModel, + EcosystemMainAccountModel, + SubListModel, + LinkedIdentityModel, + SplitsReceiverModel, +} from '../../src/models'; +import { + dripsContract, + nftDriverContract, + repoDriverContract, +} from '../../src/core/contractClients'; +import { formatSplitReceivers } from '../../src/utils/formatSplitReceivers'; +import { checkIncompleteDeadlineReceivers } from '../../src/utils/checkIncompleteDeadlineReceivers'; +import { validateLinkedIdentity } from '../../src/utils/validateLinkedIdentity'; + +jest.mock('../../src/models', () => ({ + ProjectModel: { + findByPk: jest.fn(), + }, + DripListModel: { + findByPk: jest.fn(), + }, + EcosystemMainAccountModel: { + findByPk: jest.fn(), + }, + SubListModel: { + findByPk: jest.fn(), + }, + LinkedIdentityModel: { + findByPk: jest.fn(), + }, + SplitsReceiverModel: { + findAll: jest.fn(), + }, +})); +jest.mock('../../src/core/contractClients', () => ({ + dripsContract: { + splitsHash: jest.fn(), + hashSplits: jest.fn(), + }, + nftDriverContract: { + ownerOf: jest.fn(), + }, + repoDriverContract: { + ownerOf: jest.fn(), + }, +})); +jest.mock('../../src/utils/accountIdUtils'); +jest.mock('../../src/utils/formatSplitReceivers'); +jest.mock('../../src/utils/checkIncompleteDeadlineReceivers'); +jest.mock('../../src/utils/validateLinkedIdentity'); + +describe('recalculateValidationFlags', () => { + const mockAccountId = '123456789' as AccountId; + const mockOwnerAccountId = '987654321' as AddressDriverId; + const mockOwnerAddress = '0x1234567890abcdef'; + const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; + const mockScopedLogger = { + bufferMessage: jest.fn(), + bufferUpdate: jest.fn(), + } as unknown as ScopedLogger; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('LinkedIdentity type recalculation', () => { + it('should recalculate LinkedIdentity isLinked flag when changed', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'LinkedIdentity' }, + ]; + + const mockLinkedIdentity = { + isLinked: false, + ownerAccountId: mockOwnerAccountId, + save: jest.fn(), + }; + + jest + .mocked(LinkedIdentityModel.findByPk) + .mockResolvedValue(mockLinkedIdentity as any); + jest.mocked(validateLinkedIdentity).mockResolvedValue(true); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(LinkedIdentityModel.findByPk).toHaveBeenCalledWith(mockAccountId, { + transaction: mockTransaction, + lock: 'UPDATE', + }); + expect(validateLinkedIdentity).toHaveBeenCalledWith( + mockAccountId, + mockOwnerAccountId, + mockTransaction, + ); + expect(mockLinkedIdentity.isLinked).toBe(true); + expect(mockLinkedIdentity.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + expect(mockScopedLogger.bufferUpdate).toHaveBeenCalledWith({ + id: mockAccountId, + type: LinkedIdentityModel, + input: mockLinkedIdentity, + }); + expect(mockScopedLogger.bufferMessage).toHaveBeenCalledWith( + `Recalculated LinkedIdentity ${mockAccountId} isLinked flag: false → true`, + ); + }); + + it('should not save LinkedIdentity when isLinked flag unchanged', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'LinkedIdentity' }, + ]; + + const mockLinkedIdentity = { + isLinked: true, + ownerAccountId: mockOwnerAccountId, + save: jest.fn(), + }; + + jest + .mocked(LinkedIdentityModel.findByPk) + .mockResolvedValue(mockLinkedIdentity as any); + jest.mocked(validateLinkedIdentity).mockResolvedValue(true); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(mockLinkedIdentity.save).not.toHaveBeenCalled(); + expect(mockScopedLogger.bufferUpdate).not.toHaveBeenCalled(); + }); + + it('should throw RecoverableError when LinkedIdentity not found', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'LinkedIdentity' }, + ]; + + jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); + + await expect( + recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ), + ).rejects.toThrow( + `LinkedIdentity ${mockAccountId} not found during recalculation. Waiting for entity creation.`, + ); + }); + }); + + describe('Project type recalculation', () => { + it('should recalculate Project isValid flag when changed', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'Project' }, + ]; + + const mockProject = { + isValid: false, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xabcdef123456'; + + jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest + .mocked(repoDriverContract.ownerOf) + .mockResolvedValue(mockOwnerAddress); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); + + // Mock hashDbSplits via SplitsReceiverModel and related functions + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(ProjectModel.findByPk).toHaveBeenCalledWith(mockAccountId, { + transaction: mockTransaction, + lock: 'UPDATE', + }); + expect(dripsContract.splitsHash).toHaveBeenCalledWith(mockAccountId); + expect(repoDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); + expect(checkIncompleteDeadlineReceivers).toHaveBeenCalledWith( + mockAccountId, + mockTransaction, + ); + expect(mockProject.isValid).toBe(true); + expect(mockProject.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + }); + + it('should skip Project recalculation when owner mismatch', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'Project' }, + ]; + + const mockProject = { + isValid: false, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const differentOwner = '0xdifferentowner'; + + jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); + jest.mocked(dripsContract.splitsHash).mockResolvedValue('0xhash'); + jest.mocked(repoDriverContract.ownerOf).mockResolvedValue(differentOwner); + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest.mocked(dripsContract.hashSplits).mockResolvedValue('0xhash'); + + await expect( + recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ), + ).rejects.toThrow( + `Owner mismatch for Project ${mockAccountId}: on-chain ${differentOwner} vs DB ${mockOwnerAddress}. Waiting for owner update.`, + ); + + expect(mockProject.save).not.toHaveBeenCalled(); + }); + }); + + describe('DripList type recalculation', () => { + it('should recalculate DripList isValid flag correctly', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'DripList' }, + ]; + + const mockDripList = { + isValid: false, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xabcdef123456'; + + jest + .mocked(DripListModel.findByPk) + .mockResolvedValue(mockDripList as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest + .mocked(nftDriverContract.ownerOf) + .mockResolvedValue(mockOwnerAddress); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); + + // Mock hashDbSplits + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(DripListModel.findByPk).toHaveBeenCalledWith(mockAccountId, { + transaction: mockTransaction, + lock: 'UPDATE', + }); + expect(nftDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); + expect(mockDripList.isValid).toBe(true); + expect(mockDripList.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + }); + }); + + describe('SubList type recalculation', () => { + it('should recalculate SubList isValid flag correctly', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'SubList' }, + ]; + + const mockSubList = { + isValid: false, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xabcdef123456'; + + jest.mocked(SubListModel.findByPk).mockResolvedValue(mockSubList as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); + + // Mock hashDbSplits + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(SubListModel.findByPk).toHaveBeenCalledWith(mockAccountId, { + transaction: mockTransaction, + lock: 'UPDATE', + }); + expect(mockSubList.isValid).toBe(true); + expect(mockSubList.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + }); + }); + + describe('EcosystemMainAccount type recalculation', () => { + it('should recalculate EcosystemMainAccount isValid flag correctly', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'EcosystemMainAccount' }, + ]; + + const mockEcosystemMainAccount = { + isValid: false, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xabcdef123456'; + + jest + .mocked(EcosystemMainAccountModel.findByPk) + .mockResolvedValue(mockEcosystemMainAccount as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest + .mocked(nftDriverContract.ownerOf) + .mockResolvedValue(mockOwnerAddress); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); + + // Mock hashDbSplits + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( + mockAccountId, + { + transaction: mockTransaction, + lock: 'UPDATE', + }, + ); + expect(nftDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); + expect(mockEcosystemMainAccount.isValid).toBe(true); + expect(mockEcosystemMainAccount.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + }); + }); + + describe('Error handling', () => { + it('should propagate error and stop processing when one account fails', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'Project' }, + { accountId: '987654321' as AccountId, type: 'DripList' }, + ]; + + // First account (Project) will throw error + jest + .mocked(ProjectModel.findByPk) + .mockRejectedValue(new Error('Database error')); + + await expect( + recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ), + ).rejects.toThrow('Database error'); + + // Second account should not be processed since error stops processing + expect(DripListModel.findByPk).not.toHaveBeenCalled(); + }); + }); + + describe('Edge cases', () => { + it('should handle empty affectedAccounts array', async () => { + const affectedAccounts: AffectedAccount[] = []; + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(mockScopedLogger.bufferMessage).not.toHaveBeenCalled(); + expect(mockScopedLogger.bufferUpdate).not.toHaveBeenCalled(); + }); + + it('should handle hash mismatch making account invalid', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'Project' }, + ]; + + const mockProject = { + isValid: true, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xdifferenthash'; + + jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest + .mocked(repoDriverContract.ownerOf) + .mockResolvedValue(mockOwnerAddress); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(mockProject.isValid).toBe(false); + expect(mockProject.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + expect(mockScopedLogger.bufferMessage).toHaveBeenCalledWith( + `Recalculated Project ${mockAccountId} isValid flag: true → false`, + ); + }); + + it('should handle incomplete deadline receivers making account invalid', async () => { + const affectedAccounts: AffectedAccount[] = [ + { accountId: mockAccountId, type: 'Project' }, + ]; + + const mockProject = { + isValid: true, + ownerAddress: mockOwnerAddress, + save: jest.fn(), + }; + + const mockOnChainReceiversHash = '0xabcdef123456'; + const mockDbReceiversHash = '0xabcdef123456'; + + jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); + jest + .mocked(dripsContract.splitsHash) + .mockResolvedValue(mockOnChainReceiversHash); + jest + .mocked(repoDriverContract.ownerOf) + .mockResolvedValue(mockOwnerAddress); + jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(true); + jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); + jest.mocked(formatSplitReceivers).mockReturnValue([]); + jest + .mocked(dripsContract.hashSplits) + .mockResolvedValue(mockDbReceiversHash); + + await recalculateValidationFlags( + affectedAccounts, + mockScopedLogger, + mockTransaction, + ); + + expect(mockProject.isValid).toBe(false); + expect(mockProject.save).toHaveBeenCalledWith({ + transaction: mockTransaction, + }); + }); + }); +}); diff --git a/tests/utils/validateLinkedIdentity.test.ts b/tests/utils/validateLinkedIdentity.test.ts index 2f6ef1e..5e552d8 100644 --- a/tests/utils/validateLinkedIdentity.test.ts +++ b/tests/utils/validateLinkedIdentity.test.ts @@ -1,8 +1,11 @@ +import type { Transaction } from 'sequelize'; import { validateLinkedIdentity } from '../../src/utils/validateLinkedIdentity'; import { dripsContract } from '../../src/core/contractClients'; import type { AccountId, AddressDriverId } from '../../src/core/types'; +import * as checkIncompleteDeadlineReceiversModule from '../../src/utils/checkIncompleteDeadlineReceivers'; jest.mock('../../src/core/contractClients'); +jest.mock('../../src/utils/checkIncompleteDeadlineReceivers'); describe('validateLinkedIdentity', () => { const mockAccountId = @@ -10,9 +13,16 @@ describe('validateLinkedIdentity', () => { const mockOwnerAccountId = '123456789' as AddressDriverId; const mockOnChainHash = '0xabcdef123456'; const mockExpectedHash = '0xabcdef123456'; + const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; beforeEach(() => { jest.resetAllMocks(); + + jest + .mocked( + checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, + ) + .mockResolvedValue(false); }); it('should return true when on-chain hash matches expected hash', async () => { @@ -28,6 +38,7 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, + mockTransaction, ); // Assert @@ -55,6 +66,7 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, + mockTransaction, ); // Assert @@ -71,6 +83,7 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, + mockTransaction, ); // Assert @@ -90,9 +103,66 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, + mockTransaction, + ); + + // Assert + expect(result).toBe(false); + }); + + test('should return false when account has incomplete deadline receivers', async () => { + // Arrange + (dripsContract as any).splitsHash = jest + .fn() + .mockResolvedValue(mockOnChainHash); + (dripsContract as any).hashSplits = jest + .fn() + .mockResolvedValue(mockExpectedHash); + jest + .mocked( + checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, + ) + .mockResolvedValue(true); + + // Act + const result = await validateLinkedIdentity( + mockAccountId, + mockOwnerAccountId, + mockTransaction, ); // Assert expect(result).toBe(false); + expect( + checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, + ).toHaveBeenCalledWith(mockAccountId, mockTransaction); + }); + + test('should return true when hash is valid and no incomplete deadline receivers', async () => { + // Arrange + (dripsContract as any).splitsHash = jest + .fn() + .mockResolvedValue(mockOnChainHash); + (dripsContract as any).hashSplits = jest + .fn() + .mockResolvedValue(mockExpectedHash); + jest + .mocked( + checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, + ) + .mockResolvedValue(false); + + // Act + const result = await validateLinkedIdentity( + mockAccountId, + mockOwnerAccountId, + mockTransaction, + ); + + // Assert + expect(result).toBe(true); + expect( + checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, + ).toHaveBeenCalledWith(mockAccountId, mockTransaction); }); }); From ab9dba133c70aaa1b354611b71a463009be7e3ae Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 18 Aug 2025 13:54:48 +0200 Subject: [PATCH 06/16] refactor: apply PR comments --- src/core/splitRules.ts | 2 +- src/utils/accountIdUtils.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/splitRules.ts b/src/core/splitRules.ts index cbfeda8..4e73900 100644 --- a/src/core/splitRules.ts +++ b/src/core/splitRules.ts @@ -179,7 +179,7 @@ export const RELATIONSHIP_TYPES = Array.from( ) as (typeof SPLIT_RULES)[number]['relationshipType'][]; export const ACCOUNT_TYPE_TO_METADATA_RECEIVER_TYPE: Record< - Exclude, // We don't populated `RepoDeadlineDriver` metadata. + Exclude, // We don't populate `RepoDeadlineDriver` metadata. string > = { project: 'repoDriver', diff --git a/src/utils/accountIdUtils.ts b/src/utils/accountIdUtils.ts index 6113940..fda91d4 100644 --- a/src/utils/accountIdUtils.ts +++ b/src/utils/accountIdUtils.ts @@ -215,11 +215,11 @@ export function isRepoDeadlineDriverId( id: string | bigint, ): id is RepoDeadlineDriverId { const idString = typeof id === 'bigint' ? id.toString() : id; - const isNaN = Number.isNaN(Number(idString)); + const isNotANum = Number.isNaN(Number(idString)); const isAccountIdOfRepoDeadlineDriver = getContractNameFromAccountId(idString) === 'repoDeadlineDriver'; - if (isNaN || !isAccountIdOfRepoDeadlineDriver) { + if (isNotANum || !isAccountIdOfRepoDeadlineDriver) { return false; } From b9feb4b388aabb05926e48679da7c433d3f069db Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 24 Sep 2025 12:20:53 +0300 Subject: [PATCH 07/16] refactor: await async call --- .../handlers/handleDripListMetadata.ts | 2 +- .../handlers/handleEcosystemMainAccountMetadata.ts | 2 +- .../handlers/handleProjectMetadata.ts | 2 +- .../handlers/handleSubListMetadata.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 47d8606..55eb3ce 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -110,7 +110,7 @@ export default async function handleDripListMetadata({ transaction, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ metadata, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 79572d0..bf4c894 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -105,7 +105,7 @@ export default async function handleEcosystemMainAccountMetadata({ transaction, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ logIndex, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 84153d3..b5dffa4 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -139,7 +139,7 @@ export default async function handleProjectMetadata({ scopedLogger, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ logIndex, diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 4554c9e..955a177 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -108,7 +108,7 @@ export default async function handleSubListMetadata({ emitterAccountId, }); - deleteExistingSplitReceivers(emitterAccountId, transaction); + await deleteExistingSplitReceivers(emitterAccountId, transaction); await createNewSplitReceivers({ subList, From fdc1a1a5ce82122287e88fc7138d12831b7bdad6 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 24 Sep 2025 12:29:23 +0300 Subject: [PATCH 08/16] refactor: upgrade metadata to include deadlines --- src/metadata/schemas/index.ts | 2 ++ src/metadata/schemas/nft-driver/v7.ts | 49 +++++++++++++++++++++----- src/metadata/schemas/repo-driver/v6.ts | 40 +++++++++++++++++++-- 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/metadata/schemas/index.ts b/src/metadata/schemas/index.ts index 49b8d2f..7838ef8 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -13,6 +13,7 @@ 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'; export const nftDriverAccountMetadataParser = createVersionedParser([ nftDriverAccountMetadataSchemaV7.parse, @@ -29,6 +30,7 @@ export const addressDriverAccountMetadataParser = createVersionedParser([ ]); export const repoDriverAccountMetadataParser = createVersionedParser([ + repoDriverAccountMetadataSchemaV6.parse, repoDriverAccountMetadataSchemaV5.parse, repoDriverAccountMetadataSchemaV4.parse, repoDriverAccountMetadataSchemaV3.parse, diff --git a/src/metadata/schemas/nft-driver/v7.ts b/src/metadata/schemas/nft-driver/v7.ts index 6870e98..2f69e8a 100644 --- a/src/metadata/schemas/nft-driver/v7.ts +++ b/src/metadata/schemas/nft-driver/v7.ts @@ -1,20 +1,51 @@ 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, +}); + +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..7df1537 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,33 @@ 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.date(), +}); + +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, + }); From 93ad45cdc83b88614f77387ca947fca00fe604e7 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Wed, 24 Sep 2025 13:15:53 +0300 Subject: [PATCH 09/16] refactor: improve project validation utils --- .../handlers/handleDripListMetadata.ts | 6 +- .../handleEcosystemMainAccountMetadata.ts | 6 +- .../handlers/handleProjectMetadata.ts | 26 +----- .../handlers/handleSubListMetadata.ts | 6 +- src/utils/projectUtils.ts | 92 +++++++++++-------- 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index ae57e88..64dda7d 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts @@ -85,7 +85,7 @@ export default async function handleDripListMetadata({ return; } - const { areProjectsValid, message } = await verifyProjectSources( + const verificationResult = await verifyProjectSources( splitReceivers.filter( ( splitReceiver, @@ -94,9 +94,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; diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index bf4c894..46b89a4 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts @@ -81,16 +81,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}`, ); } diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index b5dffa4..56df38b 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts @@ -93,40 +93,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({ diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 955a177..b7730ae 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -81,16 +81,16 @@ export default async function handleSubListMetadata({ 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 Sub-List metadata processing: ${message}`, + `🚨🕵️‍♂️ Skipped Sub-List metadata processing: ${verificationResult.message}`, ); return; diff --git a/src/utils/projectUtils.ts b/src/utils/projectUtils.ts index 3a4e512..ab2aebe 100644 --- a/src/utils/projectUtils.ts +++ b/src/utils/projectUtils.ts @@ -59,55 +59,71 @@ 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 }; } From 30c515e1b35235e6e954bb9fbf0f6d0118d4968d Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 25 Sep 2025 17:33:30 +0300 Subject: [PATCH 10/16] feat: create deadlines from project metadata --- .eslintrc | 27 ++-- .../handlers/handleProjectMetadata.ts | 90 ++++++++++- src/utils/deadlineUtils.ts | 152 ++++++++++++++++++ src/utils/projectUtils.ts | 45 +++++- 4 files changed, 292 insertions(+), 22 deletions(-) create mode 100644 src/utils/deadlineUtils.ts 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/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleProjectMetadata.ts index 56df38b..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; @@ -131,7 +138,10 @@ export default async function handleProjectMetadata({ scopedLogger, transaction, blockTimestamp, - emitterAccountId, + emitter: { + accountId: emitterAccountId, + source: metadata.source, + }, splitReceivers: metadata.splits, }); } @@ -191,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; @@ -216,7 +229,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: maintainer.accountId, receiverAccountType: 'address', @@ -228,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( @@ -247,7 +261,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'linked_identity', @@ -280,7 +294,7 @@ async function createNewSplitReceivers({ scopedLogger, transaction, splitReceiverShape: { - senderAccountId: emitterAccountId, + senderAccountId: emitter.accountId, senderAccountType: 'project', receiverAccountId: dependency.accountId, receiverAccountType: 'project', @@ -291,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', @@ -307,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/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/projectUtils.ts b/src/utils/projectUtils.ts index ab2aebe..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) { @@ -127,3 +131,42 @@ export async function verifyProjectSources( 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, + }); + } +} From c33c7e907d07fae5e8571837e26b7979ad1ea8ee Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 26 Sep 2025 14:01:35 +0300 Subject: [PATCH 11/16] feat: create deadlines from drip list metadata --- .../handlers/handleDripListMetadata.ts | 152 +++++++++++++++--- 1 file changed, 128 insertions(+), 24 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleDripListMetadata.ts index 64dda7d..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,10 +83,13 @@ export default async function handleDripListMetadata({ return; } - const splitReceivers = - ('projects' in metadata ? metadata.projects : undefined) ?? - ('recipients' in metadata ? metadata.recipients : undefined) ?? - []; + 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, @@ -226,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) { @@ -286,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, @@ -350,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}`, @@ -370,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; +} From 11d7f65779505b159058bac25a240f4b46f22d40 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Fri, 26 Sep 2025 14:25:01 +0300 Subject: [PATCH 12/16] feat: create deadlines from ecosystem and sublist metadata --- .../handleEcosystemMainAccountMetadata.ts | 65 ++++++++++++++++++- .../handlers/handleSubListMetadata.ts | 65 ++++++++++++++++++- 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleEcosystemMainAccountMetadata.ts index 46b89a4..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; @@ -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/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index b7730ae..42cda3b 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; @@ -198,6 +208,7 @@ async function createNewSplitReceivers({ | z.infer | z.infer | z.infer + | z.infer )[]; }) { const receiverPromises = receivers.map(async (receiver) => { @@ -269,6 +280,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({ From 41428eb0cdca649443593677344ac483ac4c4e4c Mon Sep 17 00:00:00 2001 From: jtourkos Date: Mon, 29 Sep 2025 12:52:07 +0300 Subject: [PATCH 13/16] refactor: simplify Deadline creation --- .../AccountSeenEventHandler.ts | 36 +- .../findAffectedAccounts.ts | 94 ---- .../recalculateValidationFlags.ts | 229 -------- src/eventHandlers/OwnerUpdatedEventHandler.ts | 2 - .../processLinkedIdentitySplits.ts | 1 - .../SplitsSetEvent/setIsValidFlag.ts | 33 +- src/utils/checkIncompleteDeadlineReceivers.ts | 45 -- src/utils/validateLinkedIdentity.ts | 16 +- .../AccountSeenEventHandler.test.ts | 91 ++- .../processLinkedIdentitySplits.test.ts | 44 -- .../checkIncompleteDeadlineReceivers.test.ts | 100 ---- tests/utils/findAffectedAccounts.test.ts | 249 --------- .../utils/recalculateValidationFlags.test.ts | 523 ------------------ tests/utils/validateLinkedIdentity.test.ts | 53 +- 14 files changed, 50 insertions(+), 1466 deletions(-) delete mode 100644 src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts delete mode 100644 src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts delete mode 100644 src/utils/checkIncompleteDeadlineReceivers.ts delete mode 100644 tests/utils/checkIncompleteDeadlineReceivers.test.ts delete mode 100644 tests/utils/findAffectedAccounts.test.ts delete mode 100644 tests/utils/recalculateValidationFlags.test.ts diff --git a/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts index 81f41d5..5873909 100644 --- a/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts +++ b/src/eventHandlers/AccountSeenEventHandler/AccountSeenEventHandler.ts @@ -12,8 +12,6 @@ import { } from '../../utils/accountIdUtils'; import { getAccountType } from '../../utils/getAccountType'; import { isLatestEvent } from '../../utils/isLatestEvent'; -import { findAffectedAccounts } from './findAffectedAccounts'; -import { recalculateValidationFlags } from './recalculateValidationFlags'; export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'> { public eventSignatures = [ @@ -48,6 +46,22 @@ export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSe 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, @@ -145,24 +159,6 @@ export default class AccountSeenEventHandler extends EventHandlerBase<'AccountSe await deadlineEntry.save({ transaction }); } - // Recalculate validation flags for accounts affected by this deadline becoming "seen" - const affectedAccounts = await findAffectedAccounts( - accountId, - transaction, - ); - - if (affectedAccounts.length > 0) { - scopedLogger.bufferMessage( - `Found ${affectedAccounts.length} accounts with splits pointing to deadline ${accountId}. Recalculating validation flags.`, - ); - - await recalculateValidationFlags( - affectedAccounts, - scopedLogger, - transaction, - ); - } - scopedLogger.flush(); }); } diff --git a/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts b/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts deleted file mode 100644 index faf3a64..0000000 --- a/src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { Transaction } from 'sequelize'; -import SplitsReceiverModel from '../../models/SplitsReceiverModel'; -import ProjectModel from '../../models/ProjectModel'; -import DripListModel from '../../models/DripListModel'; -import SubListModel from '../../models/SubListModel'; -import EcosystemMainAccountModel from '../../models/EcosystemMainAccountModel'; -import LinkedIdentityModel from '../../models/LinkedIdentityModel'; -import type { AccountId } from '../../core/types'; - -export interface AffectedAccount { - accountId: AccountId; - type: - | 'Project' - | 'DripList' - | 'SubList' - | 'EcosystemMainAccount' - | 'LinkedIdentity'; -} - -/** - * Finds all accounts that have splits pointing to the specified deadline account. - * These accounts may need their isValid/areSplitsValid flags recalculated when the deadline account becomes "seen". - */ -export async function findAffectedAccounts( - deadlineAccountId: AccountId, - transaction: Transaction, -): Promise { - const splitsReceivers = await SplitsReceiverModel.findAll({ - where: { - receiverAccountId: deadlineAccountId, - }, - attributes: ['senderAccountId'], - transaction, - }); - - const affectedAccountIds = [ - ...new Set(splitsReceivers.map((receiver) => receiver.senderAccountId)), - ]; - - const affectedAccounts: AffectedAccount[] = []; - - for (const accountId of affectedAccountIds) { - const project = await ProjectModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const dripList = await DripListModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const subList = await SubListModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk( - accountId, - { - transaction, - lock: transaction.LOCK.UPDATE, - }, - ); - - const linkedIdentity = await LinkedIdentityModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const foundEntities = [ - project && 'Project', - dripList && 'DripList', - subList && 'SubList', - ecosystemMainAccount && 'EcosystemMainAccount', - linkedIdentity && 'LinkedIdentity', - ].filter(Boolean); - - if (foundEntities.length > 1) { - throw new Error( - `CRITICAL BUG: Account ${accountId} exists in multiple entity tables: ${foundEntities.join(', ')}`, - ); - } - - if (foundEntities.length === 1) { - affectedAccounts.push({ - accountId, - type: foundEntities[0] as AffectedAccount['type'], - }); - } - } - - return affectedAccounts; -} diff --git a/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts b/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts deleted file mode 100644 index 4c3601a..0000000 --- a/src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags.ts +++ /dev/null @@ -1,229 +0,0 @@ -import type { Transaction, Model } from 'sequelize'; -import type { AccountId } from '../../core/types'; -import type ScopedLogger from '../../core/ScopedLogger'; -import { - ProjectModel, - DripListModel, - EcosystemMainAccountModel, - SubListModel, - LinkedIdentityModel, - SplitsReceiverModel, -} from '../../models'; -import { - dripsContract, - nftDriverContract, - repoDriverContract, -} from '../../core/contractClients'; -import { calcSubRepoDriverId } from '../../utils/accountIdUtils'; -import { formatSplitReceivers } from '../../utils/formatSplitReceivers'; -import { checkIncompleteDeadlineReceivers } from '../../utils/checkIncompleteDeadlineReceivers'; -import { validateLinkedIdentity } from '../../utils/validateLinkedIdentity'; -import RecoverableError from '../../utils/recoverableError'; -import type { AffectedAccount } from './findAffectedAccounts'; - -/** - * Recalculates and updates validation flags (isValid/areSplitsValid). - */ -export async function recalculateValidationFlags( - affectedAccounts: AffectedAccount[], - scopedLogger: ScopedLogger, - transaction: Transaction, -): Promise { - for (const { accountId, type } of affectedAccounts) { - if (type === 'LinkedIdentity') { - await recalculateLinkedIdentityFlag(accountId, scopedLogger, transaction); - } else { - await recalculateIsValidFlag(accountId, type, scopedLogger, transaction); - } - } -} - -async function recalculateLinkedIdentityFlag( - accountId: AccountId, - scopedLogger: ScopedLogger, - transaction: Transaction, -): Promise { - const linkedIdentity = await LinkedIdentityModel.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!linkedIdentity) { - throw new RecoverableError( - `LinkedIdentity ${accountId} not found during recalculation. Waiting for entity creation.`, - ); - } - - const previousAreSplitsValid = linkedIdentity.areSplitsValid; - const newAreSplitsValid = linkedIdentity.ownerAccountId - ? await validateLinkedIdentity( - accountId, - linkedIdentity.ownerAccountId, - transaction, - ) - : false; - - if (previousAreSplitsValid !== newAreSplitsValid) { - linkedIdentity.areSplitsValid = newAreSplitsValid; - - scopedLogger.bufferUpdate({ - id: accountId, - type: LinkedIdentityModel, - input: linkedIdentity, - }); - - await linkedIdentity.save({ transaction }); - - scopedLogger.bufferMessage( - `Recalculated LinkedIdentity ${accountId} areSplitsValid flag: ${previousAreSplitsValid} → ${newAreSplitsValid}`, - ); - } -} - -type ValidatableAccountType = - | 'Project' - | 'DripList' - | 'SubList' - | 'EcosystemMainAccount'; - -type ValidatableAccount = Model & { - isValid: boolean; - [key: string]: any; -}; - -type ValidatableModelStatic = { - findByPk: ( - accountId: AccountId, - options: { transaction: Transaction; lock: any }, - ) => Promise; -} & (abstract new (...args: any[]) => ValidatableAccount); - -type ContractClient = { - ownerOf: (accountId: AccountId) => Promise; -}; - -type ValidationConfig = { - model: ValidatableModelStatic; - contractClient?: ContractClient | null; - ownerField: string; - skipOwnerCheck?: boolean; -}; - -const VALIDATION_CONFIGS: Record = { - Project: { - model: ProjectModel, - contractClient: repoDriverContract, - ownerField: 'ownerAddress', - }, - DripList: { - model: DripListModel, - contractClient: nftDriverContract, - ownerField: 'ownerAddress', - }, - EcosystemMainAccount: { - model: EcosystemMainAccountModel, - contractClient: nftDriverContract, - ownerField: 'ownerAddress', - }, - SubList: { - model: SubListModel, - contractClient: null, - ownerField: 'ownerAddress', - skipOwnerCheck: true, - }, -}; - -async function recalculateAccountIsValid( - accountId: AccountId, - type: ValidatableAccountType, - scopedLogger: ScopedLogger, - transaction: Transaction, -): Promise { - const config = VALIDATION_CONFIGS[type]; - const account = await config.model.findByPk(accountId, { - transaction, - lock: transaction.LOCK.UPDATE, - }); - - if (!account) { - throw new RecoverableError( - `${type} ${accountId} not found during recalculation. Waiting for entity creation.`, - ); - } - - const onChainReceiversHash = await dripsContract.splitsHash(accountId); - - let onChainOwner: string | null = null; - if (!config.skipOwnerCheck && config.contractClient) { - onChainOwner = await config.contractClient.ownerOf(accountId); - } - - const dbReceiversHash = await hashDbSplits(accountId, transaction); - - // Skip if owner mismatch (temporary state). - if (!config.skipOwnerCheck && onChainOwner !== account[config.ownerField]) { - throw new RecoverableError( - `Owner mismatch for ${type} ${accountId}: on-chain ${onChainOwner} vs DB ${account[config.ownerField]}. Waiting for owner update.`, - ); - } - - const hashValid = dbReceiversHash === onChainReceiversHash; - const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( - accountId, - transaction, - ); - - const previousIsValid = account.isValid; - const newIsValid = hashValid && !hasIncompleteDeadlines; - - if (previousIsValid !== newIsValid) { - account.isValid = newIsValid; - - scopedLogger.bufferUpdate({ - id: accountId, - type: config.model, - input: account, - }); - - await account.save({ transaction }); - - scopedLogger.bufferMessage( - `Recalculated ${type} ${accountId} isValid flag: ${previousIsValid} → ${newIsValid}`, - ); - } -} - -async function recalculateIsValidFlag( - accountId: AccountId, - type: ValidatableAccountType, - scopedLogger: ScopedLogger, - transaction: Transaction, -): Promise { - await recalculateAccountIsValid(accountId, type, scopedLogger, transaction); -} - -async function hashDbSplits( - accountId: AccountId, - transaction: Transaction, -): Promise { - const rows = await SplitsReceiverModel.findAll({ - transaction, - lock: transaction.LOCK.UPDATE, - where: { senderAccountId: accountId }, - }); - - const receivers = []; - for (const s of rows) { - let receiverId = s.receiverAccountId; - if (s.splitsToRepoDriverSubAccount) { - receiverId = await calcSubRepoDriverId(s.receiverAccountId); - } - - receivers.push({ - accountId: receiverId, - weight: s.weight, - }); - } - - return dripsContract.hashSplits(formatSplitReceivers(receivers)); -} diff --git a/src/eventHandlers/OwnerUpdatedEventHandler.ts b/src/eventHandlers/OwnerUpdatedEventHandler.ts index 8e20298..dac5bde 100644 --- a/src/eventHandlers/OwnerUpdatedEventHandler.ts +++ b/src/eventHandlers/OwnerUpdatedEventHandler.ts @@ -177,7 +177,6 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd areSplitsValid: await validateLinkedIdentity( accountId, ownerAccountId, - transaction, ), lastProcessedVersion: makeVersion(blockNumber, logIndex).toString(), }, @@ -197,7 +196,6 @@ export default class OwnerUpdatedEventHandler extends EventHandlerBase<'OwnerUpd linkedIdentity.areSplitsValid = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, - transaction, ); linkedIdentity.lastProcessedVersion = makeVersion( blockNumber, diff --git a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts index 7bbd595..8d49601 100644 --- a/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts +++ b/src/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.ts @@ -62,7 +62,6 @@ export async function processLinkedIdentitySplits( const areSplitsValid = await validateLinkedIdentity( accountId, linkedIdentity.ownerAccountId, - transaction, ); assertIsRepoDriverId(accountId); diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index 815f052..e7152dc 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -23,7 +23,6 @@ import { import type SplitsSetEventModel from '../../models/SplitsSetEventModel'; import type ScopedLogger from '../../core/ScopedLogger'; import unreachableError from '../../utils/unreachableError'; -import { checkIncompleteDeadlineReceivers } from '../../utils/checkIncompleteDeadlineReceivers'; export default async function setIsValidFlag( { accountId, receiversHash: eventReceiversHash }: SplitsSetEventModel, @@ -191,14 +190,7 @@ async function handleEntityValidation( entityType, ); - const { hasIncompleteDeadlines } = await validateDeadlineReceivers( - accountId, - transaction, - scopedLogger, - entityType, - ); - - const isValid = hashValid && !hasIncompleteDeadlines; + const isValid = hashValid; entity.isValid = isValid; @@ -213,7 +205,6 @@ async function handleEntityValidation( if (!isValid) { const reasons = []; if (!hashValid) reasons.push('splits hash mismatch'); - if (hasIncompleteDeadlines) reasons.push('incomplete deadline receivers'); 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.`, @@ -243,28 +234,6 @@ async function validateSplitsHash( return { hashValid, dbReceiversHash }; } -async function validateDeadlineReceivers( - accountId: AccountId, - transaction: Transaction, - scopedLogger: ScopedLogger, - entityType: string, -): Promise<{ - hasIncompleteDeadlines: boolean; -}> { - const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( - accountId, - transaction, - ); - - if (hasIncompleteDeadlines) { - scopedLogger.bufferMessage( - `${entityType} ${accountId} has splits pointing to incomplete deadline accounts`, - ); - } - - return { hasIncompleteDeadlines }; -} - async function hashDbSplits( accountId: AccountId, transaction: Transaction, diff --git a/src/utils/checkIncompleteDeadlineReceivers.ts b/src/utils/checkIncompleteDeadlineReceivers.ts deleted file mode 100644 index 5f07f0d..0000000 --- a/src/utils/checkIncompleteDeadlineReceivers.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { Transaction } from 'sequelize'; -import { Op } from 'sequelize'; -import AccountSeenEventModel from '../models/AccountSeenEventModel'; -import SplitsReceiverModel from '../models/SplitsReceiverModel'; -import { isRepoDeadlineDriverId } from './accountIdUtils'; -import type { AccountId, RepoDeadlineDriverId } from '../core/types'; - -export async function checkIncompleteDeadlineReceivers( - senderAccountId: AccountId, - transaction: Transaction, -): Promise { - const splitsReceivers = await SplitsReceiverModel.findAll({ - where: { senderAccountId }, - transaction, - lock: transaction.LOCK.UPDATE, - }); - - const deadlineReceivers = splitsReceivers.filter((receiver) => - isRepoDeadlineDriverId(receiver.receiverAccountId), - ); - - if (deadlineReceivers.length === 0) { - return false; - } - - const receiverAccountIds = deadlineReceivers.map( - (r) => r.receiverAccountId as RepoDeadlineDriverId, - ); - - const existingAccountSeenEvents = await AccountSeenEventModel.findAll({ - where: { - accountId: { - [Op.in]: receiverAccountIds, - }, - }, - transaction, - lock: transaction.LOCK.UPDATE, - }); - - // Check if any receivers are missing - const existingAccountIds = new Set( - existingAccountSeenEvents.map((e) => e.accountId), - ); - return receiverAccountIds.some((id) => !existingAccountIds.has(id)); -} diff --git a/src/utils/validateLinkedIdentity.ts b/src/utils/validateLinkedIdentity.ts index 09408ce..78f653f 100644 --- a/src/utils/validateLinkedIdentity.ts +++ b/src/utils/validateLinkedIdentity.ts @@ -1,14 +1,11 @@ -import type { Transaction } from 'sequelize'; import { dripsContract } from '../core/contractClients'; import type { SplitsReceiverStruct } from '../../contracts/CURRENT_NETWORK/Drips'; import type { AccountId, AddressDriverId } from '../core/types'; import logger from '../core/logger'; -import { checkIncompleteDeadlineReceivers } from './checkIncompleteDeadlineReceivers'; export async function validateLinkedIdentity( accountId: AccountId, expectedOwnerAccountId: AddressDriverId, - transaction: Transaction, ): Promise { try { const onChainHash = await dripsContract.splitsHash(accountId); @@ -25,18 +22,7 @@ export async function validateLinkedIdentity( const isHashValid = onChainHash === expectedHash; - const hasIncompleteDeadlines = await checkIncompleteDeadlineReceivers( - accountId, - transaction, - ); - - if (hasIncompleteDeadlines) { - logger.warn( - `LinkedIdentity ${accountId} has splits pointing to incomplete deadline accounts`, - ); - } - - return isHashValid && !hasIncompleteDeadlines; + 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 index 887ac06..da6183a 100644 --- a/tests/eventHandlers/AccountSeenEventHandler.test.ts +++ b/tests/eventHandlers/AccountSeenEventHandler.test.ts @@ -10,15 +10,18 @@ import AccountSeenEventHandler from '../../src/eventHandlers/AccountSeenEventHan import * as accountIdUtils from '../../src/utils/accountIdUtils'; import * as getAccountType from '../../src/utils/getAccountType'; import * as isLatestEvent from '../../src/utils/isLatestEvent'; -import * as findAffectedAccounts from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; -import * as recalculateValidationFlags from '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags'; -import type { AffectedAccount } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; 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'); @@ -26,12 +29,6 @@ jest.mock('bee-queue'); jest.mock('../../src/core/ScopedLogger'); jest.mock('../../src/utils/getAccountType'); jest.mock('../../src/utils/isLatestEvent'); -jest.mock( - '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts', -); -jest.mock( - '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags', -); describe('AccountSeenEventHandler', () => { let mockDbTransaction: any; @@ -47,15 +44,15 @@ describe('AccountSeenEventHandler', () => { id: randomUUID(), event: { args: [ - 80920745289880686872077472087501508459438916877610571750365932290048n, // accountId - 80920745289880686872077472087501508459438916877610571750365932290049n, // repoAccountId - 80920745289880686872077472087501508459438916877610571750365932290050n, // recipientAccountId - 80920745289880686872077472087501508459438916877610571750365932290051n, // refundAccountId - 1704067200, // deadline (unix timestamp) + 80920745289880686872077472087501508459438916877610571750365932290048n, + 80920745289880686872077472087501508459438916877610571750365932290049n, + 80920745289880686872077472087501508459438916877610571750365932290050n, + 80920745289880686872077472087501508459438916877610571750365932290051n, + accountSeenDeadlineUnix, ], logIndex: 1, blockNumber: 1, - blockTimestamp: new Date(), + blockTimestamp: accountSeenBlockTimestamp, transactionHash: 'requestTransactionHash', eventSignature: 'AccountSeen(uint256,uint256,uint256,uint256,uint32)', } as EventData<'AccountSeen(uint256,uint256,uint256,uint256,uint32)'>, @@ -90,13 +87,6 @@ describe('AccountSeenEventHandler', () => { jest.mocked(isLatestEvent.isLatestEvent).mockResolvedValue(true); - jest - .mocked(findAffectedAccounts.findAffectedAccounts) - .mockResolvedValue([]); - jest - .mocked(recalculateValidationFlags.recalculateValidationFlags) - .mockResolvedValue(); - ScopedLogger.prototype.log = jest.fn(); ScopedLogger.prototype.bufferCreation = jest.fn(); ScopedLogger.prototype.bufferUpdate = jest.fn(); @@ -112,7 +102,7 @@ describe('AccountSeenEventHandler', () => { repoAccountId: 'repo-account-id', receiverAccountId: 'receiver-account-id', refundAccountId: 'refund-account-id', - deadline: new Date(1704067200 * 1000), + deadline: accountSeenDeadlineDate, logIndex: 1, blockNumber: 1, blockTimestamp: mockRequest.event.blockTimestamp, @@ -124,7 +114,7 @@ describe('AccountSeenEventHandler', () => { receiverAccountId: 'receiver-account-id', receiverAccountType: 'project', claimableProjectId: 'repo-account-id', - deadline: new Date(1704067200 * 1000), + deadline: accountSeenDeadlineDate, refundAccountId: 'refund-account-id', refundAccountType: 'address', }; @@ -146,7 +136,7 @@ describe('AccountSeenEventHandler', () => { repoAccountId: 'repo-account-id', receiverAccountId: 'receiver-account-id', refundAccountId: 'refund-account-id', - deadline: new Date(1704067200 * 1000), + deadline: accountSeenDeadlineDate, logIndex: 1, blockNumber: 1, blockTimestamp: mockRequest.event.blockTimestamp, @@ -166,7 +156,7 @@ describe('AccountSeenEventHandler', () => { receiverAccountId: 'receiver-account-id', receiverAccountType: 'project', claimableProjectId: 'repo-account-id', - deadline: new Date(1704067200 * 1000), + deadline: accountSeenDeadlineDate, refundAccountId: 'refund-account-id', refundAccountType: 'address', }, @@ -206,9 +196,7 @@ describe('AccountSeenEventHandler', () => { ); expect(existingDeadlineEntry.receiverAccountType).toBe('project'); expect(existingDeadlineEntry.claimableProjectId).toBe('repo-account-id'); - expect(existingDeadlineEntry.deadline).toEqual( - new Date(1704067200 * 1000), - ); + expect(existingDeadlineEntry.deadline).toEqual(accountSeenDeadlineDate); expect(existingDeadlineEntry.refundAccountId).toBe('refund-account-id'); expect(existingDeadlineEntry.refundAccountType).toBe('address'); expect(existingDeadlineEntry.save).toHaveBeenCalledWith({ @@ -261,39 +249,22 @@ describe('AccountSeenEventHandler', () => { ); }); - test('should call recalculateValidationFlags when affected accounts exist', async () => { - // Arrange - const accountSeenEvent = { - accountId: 'deadline-account-id', - }; - const affectedAccounts: AffectedAccount[] = [ - { accountId: 'affected-account-1' as AccountId, type: 'Project' }, - { accountId: 'affected-account-2' as AccountId, type: 'DripList' }, - ]; - - AccountSeenEventModel.create = jest - .fn() - .mockResolvedValue(accountSeenEvent); - DeadlineModel.findOrCreate = jest.fn().mockResolvedValue([{}, true]); - jest - .mocked(findAffectedAccounts.findAffectedAccounts) - .mockResolvedValue(affectedAccounts); - - // Act - await handler['_handle'](mockRequest); + 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)'>; - // Assert - expect(findAffectedAccounts.findAffectedAccounts).toHaveBeenCalledWith( - 'deadline-account-id', - mockDbTransaction, - ); - expect( - recalculateValidationFlags.recalculateValidationFlags, - ).toHaveBeenCalledWith( - affectedAccounts, - expect.any(ScopedLogger), - mockDbTransaction, + 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/processLinkedIdentitySplits.test.ts b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts index b208370..72af918 100644 --- a/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts +++ b/tests/eventHandlers/SplitsSetEvent/processLinkedIdentitySplits.test.ts @@ -91,7 +91,6 @@ describe('processLinkedIdentitySplits', () => { expect(validateLinkedIdentity).toHaveBeenCalledWith( mockAccountId, mockOwnerAccountId, - mockTransaction, ); expect(SplitsReceiverModel.destroy).toHaveBeenCalledWith({ @@ -118,49 +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, - mockTransaction, - ); - - 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/checkIncompleteDeadlineReceivers.test.ts b/tests/utils/checkIncompleteDeadlineReceivers.test.ts deleted file mode 100644 index 1dfe55f..0000000 --- a/tests/utils/checkIncompleteDeadlineReceivers.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import type { Transaction } from 'sequelize'; -import { Op } from 'sequelize'; -import { checkIncompleteDeadlineReceivers } from '../../src/utils/checkIncompleteDeadlineReceivers'; -import SplitsReceiverModel from '../../src/models/SplitsReceiverModel'; -import AccountSeenEventModel from '../../src/models/AccountSeenEventModel'; -import { isRepoDeadlineDriverId } from '../../src/utils/accountIdUtils'; -import type { AccountId } from '../../src/core/types'; - -jest.mock('../../src/models/SplitsReceiverModel'); -jest.mock('../../src/models/AccountSeenEventModel'); -jest.mock('../../src/utils/accountIdUtils'); - -describe('checkIncompleteDeadlineReceivers', () => { - const mockSenderAccountId: AccountId = '123456789' as AccountId; - const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should return false when deadlineReceivers.length === 0', async () => { - const mockSplitsReceivers = [ - { receiverAccountId: 'regular-account-1' }, - { receiverAccountId: 'regular-account-2' }, - ]; - - jest - .mocked(SplitsReceiverModel.findAll) - .mockResolvedValue(mockSplitsReceivers as any); - jest.mocked(isRepoDeadlineDriverId).mockReturnValue(false); - - const result = await checkIncompleteDeadlineReceivers( - mockSenderAccountId, - mockTransaction, - ); - - expect(result).toBe(false); - expect(SplitsReceiverModel.findAll).toHaveBeenCalledWith({ - where: { senderAccountId: mockSenderAccountId }, - transaction: mockTransaction, - lock: mockTransaction.LOCK.UPDATE, - }); - expect(AccountSeenEventModel.findAll).not.toHaveBeenCalled(); - }); - - it('should return true when deadlineReceivers.length > 0 and AccountSeenEvent not found', async () => { - const mockDeadlineReceiverId = 'deadline-receiver-id'; - const mockSplitsReceivers = [ - { receiverAccountId: 'regular-account' }, - { receiverAccountId: mockDeadlineReceiverId }, - ]; - - jest - .mocked(SplitsReceiverModel.findAll) - .mockResolvedValue(mockSplitsReceivers as any); - jest - .mocked(isRepoDeadlineDriverId) - .mockReturnValueOnce(false) - .mockReturnValueOnce(true); - jest.mocked(AccountSeenEventModel.findAll).mockResolvedValue([]); - - const result = await checkIncompleteDeadlineReceivers( - mockSenderAccountId, - mockTransaction, - ); - - expect(result).toBe(true); - expect(AccountSeenEventModel.findAll).toHaveBeenCalledWith({ - where: { accountId: { [Op.in]: [mockDeadlineReceiverId] } }, - transaction: mockTransaction, - lock: mockTransaction.LOCK.UPDATE, - }); - }); - - it('should return false when deadlineReceivers.length > 0 but all have AccountSeenEvents', async () => { - const mockDeadlineReceiverId = 'deadline-receiver-id'; - const mockSplitsReceivers = [{ receiverAccountId: mockDeadlineReceiverId }]; - const mockAccountSeenEvent = { accountId: mockDeadlineReceiverId }; - - jest - .mocked(SplitsReceiverModel.findAll) - .mockResolvedValue(mockSplitsReceivers as any); - jest.mocked(isRepoDeadlineDriverId).mockReturnValue(true); - jest - .mocked(AccountSeenEventModel.findAll) - .mockResolvedValue([mockAccountSeenEvent as any]); - - const result = await checkIncompleteDeadlineReceivers( - mockSenderAccountId, - mockTransaction, - ); - - expect(result).toBe(false); - expect(AccountSeenEventModel.findAll).toHaveBeenCalledWith({ - where: { accountId: { [Op.in]: [mockDeadlineReceiverId] } }, - transaction: mockTransaction, - lock: mockTransaction.LOCK.UPDATE, - }); - }); -}); diff --git a/tests/utils/findAffectedAccounts.test.ts b/tests/utils/findAffectedAccounts.test.ts deleted file mode 100644 index 4dd639a..0000000 --- a/tests/utils/findAffectedAccounts.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import type { Transaction } from 'sequelize'; -import { findAffectedAccounts } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; -import SplitsReceiverModel from '../../src/models/SplitsReceiverModel'; -import ProjectModel from '../../src/models/ProjectModel'; -import DripListModel from '../../src/models/DripListModel'; -import SubListModel from '../../src/models/SubListModel'; -import EcosystemMainAccountModel from '../../src/models/EcosystemMainAccountModel'; -import LinkedIdentityModel from '../../src/models/LinkedIdentityModel'; -import type { AccountId } from '../../src/core/types'; - -jest.mock('../../src/models/SplitsReceiverModel'); -jest.mock('../../src/models/ProjectModel'); -jest.mock('../../src/models/DripListModel'); -jest.mock('../../src/models/SubListModel'); -jest.mock('../../src/models/EcosystemMainAccountModel'); -jest.mock('../../src/models/LinkedIdentityModel'); - -describe('findAffectedAccounts', () => { - const mockDeadlineAccountId: AccountId = 'deadline-123' as AccountId; - const mockSenderAccountId1: AccountId = 'sender-1' as AccountId; - const mockSenderAccountId2: AccountId = 'sender-2' as AccountId; - const mockTransaction = { - LOCK: { - UPDATE: 'UPDATE', - }, - } as Transaction; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - const setupSplitsReceivers = (senderAccountIds: AccountId[]) => { - const mockSplitsReceivers = senderAccountIds.map((id) => ({ - senderAccountId: id, - })); - jest - .mocked(SplitsReceiverModel.findAll) - .mockResolvedValue(mockSplitsReceivers as any); - }; - - const setupEntityMocks = ( - project?: any, - dripList?: any, - subList?: any, - ecosystemMainAccount?: any, - linkedIdentity?: any, - ) => { - jest.mocked(ProjectModel.findByPk).mockResolvedValue(project || null); - jest.mocked(DripListModel.findByPk).mockResolvedValue(dripList || null); - jest.mocked(SubListModel.findByPk).mockResolvedValue(subList || null); - jest - .mocked(EcosystemMainAccountModel.findByPk) - .mockResolvedValue(ecosystemMainAccount || null); - jest - .mocked(LinkedIdentityModel.findByPk) - .mockResolvedValue(linkedIdentity || null); - }; - - it('should return Project type for project entity', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks({ accountId: mockSenderAccountId1 }); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'Project', - }, - ]); - expect(SplitsReceiverModel.findAll).toHaveBeenCalledWith({ - where: { receiverAccountId: mockDeadlineAccountId }, - attributes: ['senderAccountId'], - transaction: mockTransaction, - }); - }); - - it('should return DripList type for drip list entity', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks(null, { accountId: mockSenderAccountId1 }); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'DripList', - }, - ]); - }); - - it('should return SubList type for sub list entity', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks(null, null, { accountId: mockSenderAccountId1 }); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'SubList', - }, - ]); - }); - - it('should return EcosystemMainAccount type for ecosystem main account entity', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks(null, null, null, { accountId: mockSenderAccountId1 }); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'EcosystemMainAccount', - }, - ]); - }); - - it('should return LinkedIdentity type for linked identity entity', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks(null, null, null, null, { - accountId: mockSenderAccountId1, - }); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'LinkedIdentity', - }, - ]); - }); - - it('should return multiple affected accounts of different types', async () => { - setupSplitsReceivers([mockSenderAccountId1, mockSenderAccountId2]); - - jest - .mocked(ProjectModel.findByPk) - .mockResolvedValueOnce({ accountId: mockSenderAccountId1 } as any) - .mockResolvedValueOnce(null); - - jest - .mocked(DripListModel.findByPk) - .mockResolvedValueOnce(null) - .mockResolvedValueOnce({ accountId: mockSenderAccountId2 } as any); - - jest.mocked(SubListModel.findByPk).mockResolvedValue(null); - jest.mocked(EcosystemMainAccountModel.findByPk).mockResolvedValue(null); - jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'Project', - }, - { - accountId: mockSenderAccountId2, - type: 'DripList', - }, - ]); - }); - - it('should skip accounts that do not exist in any entity table', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks(); // All null - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([]); - }); - - it('should handle duplicate sender account IDs', async () => { - const mockSplitsReceivers = [ - { senderAccountId: mockSenderAccountId1 }, - { senderAccountId: mockSenderAccountId1 }, // Duplicate - { senderAccountId: mockSenderAccountId2 }, - ]; - jest - .mocked(SplitsReceiverModel.findAll) - .mockResolvedValue(mockSplitsReceivers as any); - - jest - .mocked(ProjectModel.findByPk) - .mockResolvedValueOnce({ accountId: mockSenderAccountId1 } as any) - .mockResolvedValueOnce({ accountId: mockSenderAccountId2 } as any); - - jest.mocked(DripListModel.findByPk).mockResolvedValue(null); - jest.mocked(SubListModel.findByPk).mockResolvedValue(null); - jest.mocked(EcosystemMainAccountModel.findByPk).mockResolvedValue(null); - jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); - - const result = await findAffectedAccounts( - mockDeadlineAccountId, - mockTransaction, - ); - - expect(result).toEqual([ - { - accountId: mockSenderAccountId1, - type: 'Project', - }, - { - accountId: mockSenderAccountId2, - type: 'Project', - }, - ]); - // Should only call findByPk twice due to Set deduplication - expect(ProjectModel.findByPk).toHaveBeenCalledTimes(2); - }); - - it('should throw critical bug error when account exists in multiple entity tables', async () => { - setupSplitsReceivers([mockSenderAccountId1]); - setupEntityMocks( - { accountId: mockSenderAccountId1 }, // Project - { accountId: mockSenderAccountId1 }, // DripList - ); - - await expect( - findAffectedAccounts(mockDeadlineAccountId, mockTransaction), - ).rejects.toThrow( - `CRITICAL BUG: Account ${mockSenderAccountId1} exists in multiple entity tables: Project, DripList`, - ); - }); -}); diff --git a/tests/utils/recalculateValidationFlags.test.ts b/tests/utils/recalculateValidationFlags.test.ts deleted file mode 100644 index e0ecd7c..0000000 --- a/tests/utils/recalculateValidationFlags.test.ts +++ /dev/null @@ -1,523 +0,0 @@ -import type { Transaction } from 'sequelize'; -import { recalculateValidationFlags } from '../../src/eventHandlers/AccountSeenEventHandler/recalculateValidationFlags'; -import type { AffectedAccount } from '../../src/eventHandlers/AccountSeenEventHandler/findAffectedAccounts'; -import type { AccountId, AddressDriverId } from '../../src/core/types'; -import type ScopedLogger from '../../src/core/ScopedLogger'; -import { - ProjectModel, - DripListModel, - EcosystemMainAccountModel, - SubListModel, - LinkedIdentityModel, - SplitsReceiverModel, -} from '../../src/models'; -import { - dripsContract, - nftDriverContract, - repoDriverContract, -} from '../../src/core/contractClients'; -import { formatSplitReceivers } from '../../src/utils/formatSplitReceivers'; -import { checkIncompleteDeadlineReceivers } from '../../src/utils/checkIncompleteDeadlineReceivers'; -import { validateLinkedIdentity } from '../../src/utils/validateLinkedIdentity'; - -jest.mock('../../src/models', () => ({ - ProjectModel: { - findByPk: jest.fn(), - }, - DripListModel: { - findByPk: jest.fn(), - }, - EcosystemMainAccountModel: { - findByPk: jest.fn(), - }, - SubListModel: { - findByPk: jest.fn(), - }, - LinkedIdentityModel: { - findByPk: jest.fn(), - }, - SplitsReceiverModel: { - findAll: jest.fn(), - }, -})); -jest.mock('../../src/core/contractClients', () => ({ - dripsContract: { - splitsHash: jest.fn(), - hashSplits: jest.fn(), - }, - nftDriverContract: { - ownerOf: jest.fn(), - }, - repoDriverContract: { - ownerOf: jest.fn(), - }, -})); -jest.mock('../../src/utils/accountIdUtils'); -jest.mock('../../src/utils/formatSplitReceivers'); -jest.mock('../../src/utils/checkIncompleteDeadlineReceivers'); -jest.mock('../../src/utils/validateLinkedIdentity'); - -describe('recalculateValidationFlags', () => { - const mockAccountId = '123456789' as AccountId; - const mockOwnerAccountId = '987654321' as AddressDriverId; - const mockOwnerAddress = '0x1234567890abcdef'; - const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; - const mockScopedLogger = { - bufferMessage: jest.fn(), - bufferUpdate: jest.fn(), - } as unknown as ScopedLogger; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - describe('LinkedIdentity type recalculation', () => { - it('should recalculate LinkedIdentity areSplitsValid flag when changed', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'LinkedIdentity' }, - ]; - - const mockLinkedIdentity = { - areSplitsValid: false, - ownerAccountId: mockOwnerAccountId, - save: jest.fn(), - }; - - jest - .mocked(LinkedIdentityModel.findByPk) - .mockResolvedValue(mockLinkedIdentity as any); - jest.mocked(validateLinkedIdentity).mockResolvedValue(true); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(LinkedIdentityModel.findByPk).toHaveBeenCalledWith(mockAccountId, { - transaction: mockTransaction, - lock: 'UPDATE', - }); - expect(validateLinkedIdentity).toHaveBeenCalledWith( - mockAccountId, - mockOwnerAccountId, - mockTransaction, - ); - expect(mockLinkedIdentity.areSplitsValid).toBe(true); - expect(mockLinkedIdentity.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - expect(mockScopedLogger.bufferUpdate).toHaveBeenCalledWith({ - id: mockAccountId, - type: LinkedIdentityModel, - input: mockLinkedIdentity, - }); - expect(mockScopedLogger.bufferMessage).toHaveBeenCalledWith( - `Recalculated LinkedIdentity ${mockAccountId} areSplitsValid flag: false → true`, - ); - }); - - it('should not save LinkedIdentity when areSplitsValid flag unchanged', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'LinkedIdentity' }, - ]; - - const mockLinkedIdentity = { - areSplitsValid: true, - ownerAccountId: mockOwnerAccountId, - save: jest.fn(), - }; - - jest - .mocked(LinkedIdentityModel.findByPk) - .mockResolvedValue(mockLinkedIdentity as any); - jest.mocked(validateLinkedIdentity).mockResolvedValue(true); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(mockLinkedIdentity.save).not.toHaveBeenCalled(); - expect(mockScopedLogger.bufferUpdate).not.toHaveBeenCalled(); - }); - - it('should throw RecoverableError when LinkedIdentity not found', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'LinkedIdentity' }, - ]; - - jest.mocked(LinkedIdentityModel.findByPk).mockResolvedValue(null); - - await expect( - recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ), - ).rejects.toThrow( - `LinkedIdentity ${mockAccountId} not found during recalculation. Waiting for entity creation.`, - ); - }); - }); - - describe('Project type recalculation', () => { - it('should recalculate Project isValid flag when changed', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'Project' }, - ]; - - const mockProject = { - isValid: false, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xabcdef123456'; - - jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest - .mocked(repoDriverContract.ownerOf) - .mockResolvedValue(mockOwnerAddress); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); - - // Mock hashDbSplits via SplitsReceiverModel and related functions - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(ProjectModel.findByPk).toHaveBeenCalledWith(mockAccountId, { - transaction: mockTransaction, - lock: 'UPDATE', - }); - expect(dripsContract.splitsHash).toHaveBeenCalledWith(mockAccountId); - expect(repoDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); - expect(checkIncompleteDeadlineReceivers).toHaveBeenCalledWith( - mockAccountId, - mockTransaction, - ); - expect(mockProject.isValid).toBe(true); - expect(mockProject.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - }); - - it('should skip Project recalculation when owner mismatch', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'Project' }, - ]; - - const mockProject = { - isValid: false, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const differentOwner = '0xdifferentowner'; - - jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); - jest.mocked(dripsContract.splitsHash).mockResolvedValue('0xhash'); - jest.mocked(repoDriverContract.ownerOf).mockResolvedValue(differentOwner); - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest.mocked(dripsContract.hashSplits).mockResolvedValue('0xhash'); - - await expect( - recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ), - ).rejects.toThrow( - `Owner mismatch for Project ${mockAccountId}: on-chain ${differentOwner} vs DB ${mockOwnerAddress}. Waiting for owner update.`, - ); - - expect(mockProject.save).not.toHaveBeenCalled(); - }); - }); - - describe('DripList type recalculation', () => { - it('should recalculate DripList isValid flag correctly', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'DripList' }, - ]; - - const mockDripList = { - isValid: false, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xabcdef123456'; - - jest - .mocked(DripListModel.findByPk) - .mockResolvedValue(mockDripList as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest - .mocked(nftDriverContract.ownerOf) - .mockResolvedValue(mockOwnerAddress); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); - - // Mock hashDbSplits - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(DripListModel.findByPk).toHaveBeenCalledWith(mockAccountId, { - transaction: mockTransaction, - lock: 'UPDATE', - }); - expect(nftDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); - expect(mockDripList.isValid).toBe(true); - expect(mockDripList.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - }); - }); - - describe('SubList type recalculation', () => { - it('should recalculate SubList isValid flag correctly', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'SubList' }, - ]; - - const mockSubList = { - isValid: false, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xabcdef123456'; - - jest.mocked(SubListModel.findByPk).mockResolvedValue(mockSubList as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); - - // Mock hashDbSplits - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(SubListModel.findByPk).toHaveBeenCalledWith(mockAccountId, { - transaction: mockTransaction, - lock: 'UPDATE', - }); - expect(mockSubList.isValid).toBe(true); - expect(mockSubList.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - }); - }); - - describe('EcosystemMainAccount type recalculation', () => { - it('should recalculate EcosystemMainAccount isValid flag correctly', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'EcosystemMainAccount' }, - ]; - - const mockEcosystemMainAccount = { - isValid: false, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xabcdef123456'; - - jest - .mocked(EcosystemMainAccountModel.findByPk) - .mockResolvedValue(mockEcosystemMainAccount as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest - .mocked(nftDriverContract.ownerOf) - .mockResolvedValue(mockOwnerAddress); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); - - // Mock hashDbSplits - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(EcosystemMainAccountModel.findByPk).toHaveBeenCalledWith( - mockAccountId, - { - transaction: mockTransaction, - lock: 'UPDATE', - }, - ); - expect(nftDriverContract.ownerOf).toHaveBeenCalledWith(mockAccountId); - expect(mockEcosystemMainAccount.isValid).toBe(true); - expect(mockEcosystemMainAccount.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - }); - }); - - describe('Error handling', () => { - it('should propagate error and stop processing when one account fails', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'Project' }, - { accountId: '987654321' as AccountId, type: 'DripList' }, - ]; - - // First account (Project) will throw error - jest - .mocked(ProjectModel.findByPk) - .mockRejectedValue(new Error('Database error')); - - await expect( - recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ), - ).rejects.toThrow('Database error'); - - // Second account should not be processed since error stops processing - expect(DripListModel.findByPk).not.toHaveBeenCalled(); - }); - }); - - describe('Edge cases', () => { - it('should handle empty affectedAccounts array', async () => { - const affectedAccounts: AffectedAccount[] = []; - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(mockScopedLogger.bufferMessage).not.toHaveBeenCalled(); - expect(mockScopedLogger.bufferUpdate).not.toHaveBeenCalled(); - }); - - it('should handle hash mismatch making account invalid', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'Project' }, - ]; - - const mockProject = { - isValid: true, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xdifferenthash'; - - jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest - .mocked(repoDriverContract.ownerOf) - .mockResolvedValue(mockOwnerAddress); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(false); - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(mockProject.isValid).toBe(false); - expect(mockProject.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - expect(mockScopedLogger.bufferMessage).toHaveBeenCalledWith( - `Recalculated Project ${mockAccountId} isValid flag: true → false`, - ); - }); - - it('should handle incomplete deadline receivers making account invalid', async () => { - const affectedAccounts: AffectedAccount[] = [ - { accountId: mockAccountId, type: 'Project' }, - ]; - - const mockProject = { - isValid: true, - ownerAddress: mockOwnerAddress, - save: jest.fn(), - }; - - const mockOnChainReceiversHash = '0xabcdef123456'; - const mockDbReceiversHash = '0xabcdef123456'; - - jest.mocked(ProjectModel.findByPk).mockResolvedValue(mockProject as any); - jest - .mocked(dripsContract.splitsHash) - .mockResolvedValue(mockOnChainReceiversHash); - jest - .mocked(repoDriverContract.ownerOf) - .mockResolvedValue(mockOwnerAddress); - jest.mocked(checkIncompleteDeadlineReceivers).mockResolvedValue(true); - jest.mocked(SplitsReceiverModel.findAll).mockResolvedValue([]); - jest.mocked(formatSplitReceivers).mockReturnValue([]); - jest - .mocked(dripsContract.hashSplits) - .mockResolvedValue(mockDbReceiversHash); - - await recalculateValidationFlags( - affectedAccounts, - mockScopedLogger, - mockTransaction, - ); - - expect(mockProject.isValid).toBe(false); - expect(mockProject.save).toHaveBeenCalledWith({ - transaction: mockTransaction, - }); - }); - }); -}); diff --git a/tests/utils/validateLinkedIdentity.test.ts b/tests/utils/validateLinkedIdentity.test.ts index 5e552d8..0d077b8 100644 --- a/tests/utils/validateLinkedIdentity.test.ts +++ b/tests/utils/validateLinkedIdentity.test.ts @@ -1,11 +1,8 @@ -import type { Transaction } from 'sequelize'; import { validateLinkedIdentity } from '../../src/utils/validateLinkedIdentity'; import { dripsContract } from '../../src/core/contractClients'; import type { AccountId, AddressDriverId } from '../../src/core/types'; -import * as checkIncompleteDeadlineReceiversModule from '../../src/utils/checkIncompleteDeadlineReceivers'; jest.mock('../../src/core/contractClients'); -jest.mock('../../src/utils/checkIncompleteDeadlineReceivers'); describe('validateLinkedIdentity', () => { const mockAccountId = @@ -13,16 +10,9 @@ describe('validateLinkedIdentity', () => { const mockOwnerAccountId = '123456789' as AddressDriverId; const mockOnChainHash = '0xabcdef123456'; const mockExpectedHash = '0xabcdef123456'; - const mockTransaction = { LOCK: { UPDATE: 'UPDATE' } } as Transaction; beforeEach(() => { jest.resetAllMocks(); - - jest - .mocked( - checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, - ) - .mockResolvedValue(false); }); it('should return true when on-chain hash matches expected hash', async () => { @@ -38,7 +28,6 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, - mockTransaction, ); // Assert @@ -66,7 +55,6 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, - mockTransaction, ); // Assert @@ -83,7 +71,6 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, - mockTransaction, ); // Assert @@ -103,42 +90,13 @@ describe('validateLinkedIdentity', () => { const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, - mockTransaction, - ); - - // Assert - expect(result).toBe(false); - }); - - test('should return false when account has incomplete deadline receivers', async () => { - // Arrange - (dripsContract as any).splitsHash = jest - .fn() - .mockResolvedValue(mockOnChainHash); - (dripsContract as any).hashSplits = jest - .fn() - .mockResolvedValue(mockExpectedHash); - jest - .mocked( - checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, - ) - .mockResolvedValue(true); - - // Act - const result = await validateLinkedIdentity( - mockAccountId, - mockOwnerAccountId, - mockTransaction, ); // Assert expect(result).toBe(false); - expect( - checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, - ).toHaveBeenCalledWith(mockAccountId, mockTransaction); }); - test('should return true when hash is valid and no incomplete deadline receivers', async () => { + test('should return true when hash is valid', async () => { // Arrange (dripsContract as any).splitsHash = jest .fn() @@ -146,23 +104,14 @@ describe('validateLinkedIdentity', () => { (dripsContract as any).hashSplits = jest .fn() .mockResolvedValue(mockExpectedHash); - jest - .mocked( - checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, - ) - .mockResolvedValue(false); // Act const result = await validateLinkedIdentity( mockAccountId, mockOwnerAccountId, - mockTransaction, ); // Assert expect(result).toBe(true); - expect( - checkIncompleteDeadlineReceiversModule.checkIncompleteDeadlineReceivers, - ).toHaveBeenCalledWith(mockAccountId, mockTransaction); }); }); From f25cee74f716c191750526e04f439bb59bdd31e7 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Thu, 2 Oct 2025 14:35:55 +0300 Subject: [PATCH 14/16] refactor: support allowExternalDonations in metadata --- src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts | 13 ++++++++++--- src/metadata/schemas/nft-driver/v7.ts | 12 ++++++++---- src/metadata/schemas/repo-driver/v6.ts | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts index e7152dc..d278c03 100644 --- a/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts +++ b/src/eventHandlers/SplitsSetEvent/setIsValidFlag.ts @@ -95,15 +95,22 @@ export default async function setIsValidFlag( }, ); - const entity = dripList ?? ecosystemMain!; - const Model = dripList ? DripListModel : EcosystemMainAccountModel; + 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 ${Model.name}: ${Model.name} '${accountId}' not found.`, + `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}'.`, diff --git a/src/metadata/schemas/nft-driver/v7.ts b/src/metadata/schemas/nft-driver/v7.ts index 2f69e8a..c122f22 100644 --- a/src/metadata/schemas/nft-driver/v7.ts +++ b/src/metadata/schemas/nft-driver/v7.ts @@ -13,10 +13,14 @@ import { orcidSplitReceiverSchema, } from '../repo-driver/v6'; -const base = nftDriverAccountMetadataSchemaV5.omit({ - isDripList: true, - projects: true, -}); +const base = nftDriverAccountMetadataSchemaV5 + .omit({ + isDripList: true, + projects: true, + }) + .extend({ + allowExternalDonations: z.boolean().optional(), + }); const ecosystemVariant = base.extend({ type: z.literal('ecosystem'), diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts index 7df1537..ea4a33a 100644 --- a/src/metadata/schemas/repo-driver/v6.ts +++ b/src/metadata/schemas/repo-driver/v6.ts @@ -24,7 +24,7 @@ export const deadlineSplitReceiverSchema = z.object({ }), recipientAccountId: z.string(), refundAccountId: z.string(), - deadline: z.date(), + deadline: z.coerce.date(), }); const repoDriverAccountSplitsSchemaV6 = z.object({ From d887dbb66ce3be4a6fa9b3f9ccddabc3d7741fed Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 7 Oct 2025 13:44:37 +0300 Subject: [PATCH 15/16] refactor: update metadata and contract addresses --- src/config/chainConfigs/goerli.json | 3 +++ src/config/chainConfigs/mainnet.json | 3 +++ src/config/chainConfigs/polygon_amoy.json | 3 +++ .../schemas/immutable-splits-driver/v2.ts | 19 +++++++++++++++++++ src/metadata/schemas/index.ts | 2 ++ src/metadata/schemas/repo-driver/v6.ts | 9 ++++++++- 6 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/metadata/schemas/immutable-splits-driver/v2.ts 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/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/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/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 7838ef8..bd09cb9 100644 --- a/src/metadata/schemas/index.ts +++ b/src/metadata/schemas/index.ts @@ -14,6 +14,7 @@ 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, @@ -39,5 +40,6 @@ export const repoDriverAccountMetadataParser = createVersionedParser([ ]); export const immutableSplitsDriverMetadataParser = createVersionedParser([ + subListMetadataSchemaV2.parse, subListMetadataSchemaV1.parse, ]); diff --git a/src/metadata/schemas/repo-driver/v6.ts b/src/metadata/schemas/repo-driver/v6.ts index ea4a33a..6a46793 100644 --- a/src/metadata/schemas/repo-driver/v6.ts +++ b/src/metadata/schemas/repo-driver/v6.ts @@ -24,7 +24,14 @@ export const deadlineSplitReceiverSchema = z.object({ }), recipientAccountId: z.string(), refundAccountId: z.string(), - deadline: z.coerce.date(), + 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({ From 9641aefd5ebb48ea82e3b2cc5aa2f3927e63fd59 Mon Sep 17 00:00:00 2001 From: jtourkos Date: Tue, 7 Oct 2025 13:51:58 +0300 Subject: [PATCH 16/16] fix: compilation error --- .../handlers/handleSubListMetadata.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts index 42cda3b..590e3fb 100644 --- a/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts +++ b/src/eventHandlers/AccountMetadataEmittedEvent/handlers/handleSubListMetadata.ts @@ -91,12 +91,17 @@ export default async function handleSubListMetadata({ return; } - const verificationResult = 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; + }>; + + const verificationResult = await verifyProjectSources(projectReceivers); if (!verificationResult.isValid) { scopedLogger.bufferMessage(