From 9f48dd1ba5a3d727b8c40298673144f50906b49a Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Wed, 20 Nov 2024 01:20:02 +0700 Subject: [PATCH] Handle `*_DELEGATE` Transaction Service events (#2106) * Handle `*_DELEGATE` Transaction Service events * Add E2E tests * Rename address field * Rename remaining fields * Make `safeAddress` optional * Add payload --- src/datasources/cache/cache.router.ts | 9 +- .../transaction-api.service.ts | 8 ++ .../v2/delegates.v2.repository.interface.ts | 5 + .../delegate/v2/delegates.v2.repository.ts | 10 ++ .../helpers/event-cache.helper.module.ts | 2 + .../hooks/helpers/event-cache.helper.ts | 65 ++++++++- .../helpers/event-notifications.helper.ts | 9 +- src/domain/hooks/hooks.repository.spec.ts | 7 + .../interfaces/transaction-api.interface.ts | 2 + .../__tests__/event-hooks-queue.e2e-spec.ts | 33 +++++ .../__tests__/delegate-events.builder.ts | 45 +++++++ .../hooks/entities/event-type.entity.ts | 3 + .../__tests__/delegate-events.schema.spec.ts | 123 ++++++++++++++++++ .../schemas/__tests__/event.schema.spec.ts | 13 +- .../schemas/delegate-events.schema.ts | 31 +++++ .../hooks/entities/schemas/event.schema.ts | 8 ++ .../hooks/hooks-cache.controller.spec.ts | 1 + 17 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 src/routes/hooks/entities/__tests__/delegate-events.builder.ts create mode 100644 src/routes/hooks/entities/schemas/__tests__/delegate-events.schema.spec.ts create mode 100644 src/routes/hooks/entities/schemas/delegate-events.schema.ts diff --git a/src/datasources/cache/cache.router.ts b/src/datasources/cache/cache.router.ts index 17c781d58e..952f7d30ff 100644 --- a/src/datasources/cache/cache.router.ts +++ b/src/datasources/cache/cache.router.ts @@ -199,6 +199,13 @@ export class CacheRouter { return `${args.chainId}_${CacheRouter.SAFE_COLLECTIBLES_KEY}_${args.safeAddress}`; } + static getDelegatesCacheKey(args: { + chainId: string; + safeAddress?: `0x${string}`; + }): string { + return `${args.chainId}_${CacheRouter.DELEGATES_KEY}_${args.safeAddress}`; + } + static getDelegatesCacheDir(args: { chainId: string; safeAddress?: `0x${string}`; @@ -209,7 +216,7 @@ export class CacheRouter { offset?: number; }): CacheDir { return new CacheDir( - `${args.chainId}_${CacheRouter.DELEGATES_KEY}_${args.safeAddress}`, + CacheRouter.getDelegatesCacheKey(args), `${args.delegate}_${args.delegator}_${args.label}_${args.limit}_${args.offset}`, ); } diff --git a/src/datasources/transaction-api/transaction-api.service.ts b/src/datasources/transaction-api/transaction-api.service.ts index 54245f97c0..2b8da1890c 100644 --- a/src/datasources/transaction-api/transaction-api.service.ts +++ b/src/datasources/transaction-api/transaction-api.service.ts @@ -342,6 +342,14 @@ export class TransactionApi implements ITransactionApi { } } + async clearDelegates(safeAddress?: `0x${string}`): Promise { + const cacheKey = CacheRouter.getDelegatesCacheKey({ + chainId: this.chainId, + safeAddress, + }); + await this.cacheService.deleteByKey(cacheKey); + } + async postDelegate(args: { safeAddress: `0x${string}` | null; delegate: `0x${string}`; diff --git a/src/domain/delegate/v2/delegates.v2.repository.interface.ts b/src/domain/delegate/v2/delegates.v2.repository.interface.ts index b8e26b15c5..3564ecbb18 100644 --- a/src/domain/delegate/v2/delegates.v2.repository.interface.ts +++ b/src/domain/delegate/v2/delegates.v2.repository.interface.ts @@ -17,6 +17,11 @@ export interface IDelegatesV2Repository { offset?: number; }): Promise>; + clearDelegates(args: { + chainId: string; + safeAddress?: string; + }): Promise; + postDelegate(args: { chainId: string; safeAddress: `0x${string}` | null; diff --git a/src/domain/delegate/v2/delegates.v2.repository.ts b/src/domain/delegate/v2/delegates.v2.repository.ts index ee7d0d866c..9d5c719391 100644 --- a/src/domain/delegate/v2/delegates.v2.repository.ts +++ b/src/domain/delegate/v2/delegates.v2.repository.ts @@ -36,6 +36,16 @@ export class DelegatesV2Repository implements IDelegatesV2Repository { return DelegatePageSchema.parse(page); } + async clearDelegates(args: { + chainId: string; + safeAddress?: `0x${string}`; + }): Promise { + const transactionService = await this.transactionApiManager.getApi( + args.chainId, + ); + await transactionService.clearDelegates(args.safeAddress); + } + async postDelegate(args: { chainId: string; safeAddress: `0x${string}` | null; diff --git a/src/domain/hooks/helpers/event-cache.helper.module.ts b/src/domain/hooks/helpers/event-cache.helper.module.ts index f6cb8a29e4..6330b3b173 100644 --- a/src/domain/hooks/helpers/event-cache.helper.module.ts +++ b/src/domain/hooks/helpers/event-cache.helper.module.ts @@ -9,6 +9,7 @@ import { SafeAppsRepositoryModule } from '@/domain/safe-apps/safe-apps.repositor import { SafeRepositoryModule } from '@/domain/safe/safe.repository.interface'; import { StakingRepositoryModule } from '@/domain/staking/staking.repository.module'; import { TransactionsRepositoryModule } from '@/domain/transactions/transactions.repository.interface'; +import { DelegatesV2RepositoryModule } from '@/domain/delegate/v2/delegates.v2.repository.interface'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { TransactionsRepositoryModule } from '@/domain/transactions/transactions BlockchainRepositoryModule, ChainsRepositoryModule, CollectiblesRepositoryModule, + DelegatesV2RepositoryModule, MessagesRepositoryModule, SafeAppsRepositoryModule, SafeRepositoryModule, diff --git a/src/domain/hooks/helpers/event-cache.helper.ts b/src/domain/hooks/helpers/event-cache.helper.ts index eec7e517c2..2ca27982a5 100644 --- a/src/domain/hooks/helpers/event-cache.helper.ts +++ b/src/domain/hooks/helpers/event-cache.helper.ts @@ -8,6 +8,7 @@ import { IBalancesRepository } from '@/domain/balances/balances.repository.inter import { IBlockchainRepository } from '@/domain/blockchain/blockchain.repository.interface'; import { IChainsRepository } from '@/domain/chains/chains.repository.interface'; import { ICollectiblesRepository } from '@/domain/collectibles/collectibles.repository.interface'; +import { IDelegatesV2Repository } from '@/domain/delegate/v2/delegates.v2.repository.interface'; import { IMessagesRepository } from '@/domain/messages/messages.repository.interface'; import { ISafeAppsRepository } from '@/domain/safe-apps/safe-apps.repository.interface'; import { ISafeRepository } from '@/domain/safe/safe.repository.interface'; @@ -40,6 +41,8 @@ export class EventCacheHelper { private readonly chainsRepository: IChainsRepository, @Inject(ICollectiblesRepository) private readonly collectiblesRepository: ICollectiblesRepository, + @Inject(IDelegatesV2Repository) + private readonly delegatesRepository: IDelegatesV2Repository, @Inject(IMessagesRepository) private readonly messagesRepository: IMessagesRepository, @Inject(ISafeAppsRepository) @@ -91,6 +94,12 @@ export class EventCacheHelper { [TransactionEventType.REORG_DETECTED]: () => [], [TransactionEventType.SAFE_CREATED]: this.onTransactionEventSafeCreated.bind(this), + [TransactionEventType.NEW_DELEGATE]: + this.onTransactionEventDelegate.bind(this), + [TransactionEventType.DELETED_DELEGATE]: + this.onTransactionEventDelegate.bind(this), + [TransactionEventType.UPDATED_DELEGATE]: + this.onTransactionEventDelegate.bind(this), [ConfigEventType.CHAIN_UPDATE]: this.onConfigEventChainUpdate.bind(this), [ConfigEventType.SAFE_APPS_UPDATE]: this.onConfigEventSafeAppsUpdate.bind(this), @@ -124,6 +133,9 @@ export class EventCacheHelper { case TransactionEventType.MESSAGE_CONFIRMATION: this._logMessageEvent(event); break; + case TransactionEventType.NEW_DELEGATE: + case TransactionEventType.UPDATED_DELEGATE: + case TransactionEventType.DELETED_DELEGATE: case ConfigEventType.CHAIN_UPDATE: case ConfigEventType.SAFE_APPS_UPDATE: this._logEvent(event); @@ -508,8 +520,38 @@ export class EventCacheHelper { return [this.safeRepository.clearIsSafe(event)]; } + private onTransactionEventDelegate( + event: Extract< + Event, + { + type: + | TransactionEventType.NEW_DELEGATE + | TransactionEventType.UPDATED_DELEGATE + | TransactionEventType.DELETED_DELEGATE; + } + >, + ): Array> { + // A delegate change affects: + // - the delegates associated to the Safe + return [ + this.delegatesRepository.clearDelegates({ + chainId: event.chainId, + safeAddress: event.address ?? undefined, + }), + ]; + } + private _logSafeTxEvent( - event: Event & { address: string; safeTxHash: string }, + event: Extract< + Event, + { + type: + | TransactionEventType.PENDING_MULTISIG_TRANSACTION + | TransactionEventType.DELETED_MULTISIG_TRANSACTION + | TransactionEventType.EXECUTED_MULTISIG_TRANSACTION + | TransactionEventType.NEW_CONFIRMATION; + } + >, ): void { this.loggingService.info({ type: EventCacheHelper.HOOK_TYPE, @@ -521,7 +563,17 @@ export class EventCacheHelper { } private _logTxEvent( - event: Event & { address: string; txHash: string }, + event: Extract< + Event, + { + type: + | TransactionEventType.MODULE_TRANSACTION + | TransactionEventType.INCOMING_ETHER + | TransactionEventType.OUTGOING_ETHER + | TransactionEventType.INCOMING_TOKEN + | TransactionEventType.OUTGOING_TOKEN; + } + >, ): void { this.loggingService.info({ type: EventCacheHelper.HOOK_TYPE, @@ -533,7 +585,14 @@ export class EventCacheHelper { } private _logMessageEvent( - event: Event & { address: string; messageHash: string }, + event: Extract< + Event, + { + type: + | TransactionEventType.MESSAGE_CREATED + | TransactionEventType.MESSAGE_CONFIRMATION; + } + >, ): void { this.loggingService.info({ type: EventCacheHelper.HOOK_TYPE, diff --git a/src/domain/hooks/helpers/event-notifications.helper.ts b/src/domain/hooks/helpers/event-notifications.helper.ts index 5ce7853ebb..2355d52676 100644 --- a/src/domain/hooks/helpers/event-notifications.helper.ts +++ b/src/domain/hooks/helpers/event-notifications.helper.ts @@ -112,6 +112,7 @@ export class EventNotificationsHelper { * @param event - {@link Event} to check */ private isEventToNotify(event: Event): event is EventToNotify { + // TODO: Simplify this by inverting the logic and/or refactor mapEventNotification to explicitly handle types return ( // Don't notify about Config events event.type !== ConfigEventType.CHAIN_UPDATE && @@ -124,7 +125,13 @@ export class EventNotificationsHelper { // We only notify required confirmations on required - see MESSAGE_CREATED event.type !== TransactionEventType.MESSAGE_CONFIRMATION && // You cannot subscribe to Safes-to-be-created - event.type !== TransactionEventType.SAFE_CREATED + event.type !== TransactionEventType.SAFE_CREATED && + // We don't notify about reorgs + event.type !== TransactionEventType.REORG_DETECTED && + // We don't notify about delegate events + event.type !== TransactionEventType.NEW_DELEGATE && + event.type !== TransactionEventType.UPDATED_DELEGATE && + event.type !== TransactionEventType.DELETED_DELEGATE ); } diff --git a/src/domain/hooks/hooks.repository.spec.ts b/src/domain/hooks/hooks.repository.spec.ts index 98ee41d944..0797180d41 100644 --- a/src/domain/hooks/hooks.repository.spec.ts +++ b/src/domain/hooks/hooks.repository.spec.ts @@ -5,6 +5,7 @@ import type { BlockchainRepository } from '@/domain/blockchain/blockchain.reposi import type { ChainsRepository } from '@/domain/chains/chains.repository'; import { chainBuilder } from '@/domain/chains/entities/__tests__/chain.builder'; import type { CollectiblesRepository } from '@/domain/collectibles/collectibles.repository'; +import type { DelegatesV2Repository } from '@/domain/delegate/v2/delegates.v2.repository'; import { pageBuilder } from '@/domain/entities/__tests__/page.builder'; import { EventCacheHelper } from '@/domain/hooks/helpers/event-cache.helper'; import type { EventNotificationsHelper } from '@/domain/hooks/helpers/event-notifications.helper'; @@ -42,6 +43,10 @@ const mockCollectiblesRepository = jest.mocked({ clearCollectibles: jest.fn(), } as jest.MockedObjectDeep); +const mockDelegatesRepository = jest.mocked({ + clearDelegates: jest.fn(), +} as jest.MockedObjectDeep); + const mockMessagesRepository = jest.mocked({ clearMessages: jest.fn(), } as unknown as jest.MockedObjectDeep); @@ -100,6 +105,7 @@ describe('HooksRepository (Unit)', () => { mockBlockchainRepository, mockChainsRepository, mockCollectiblesRepository, + mockDelegatesRepository, mockMessagesRepository, mockSafeAppsRepository, mockSafeRepository, @@ -278,6 +284,7 @@ describe('HooksRepositoryWithNotifications (Unit)', () => { mockBlockchainRepository, mockChainsRepository, mockCollectiblesRepository, + mockDelegatesRepository, mockMessagesRepository, mockSafeAppsRepository, mockSafeRepository, diff --git a/src/domain/interfaces/transaction-api.interface.ts b/src/domain/interfaces/transaction-api.interface.ts index 1585cc86e4..1088647573 100644 --- a/src/domain/interfaces/transaction-api.interface.ts +++ b/src/domain/interfaces/transaction-api.interface.ts @@ -61,6 +61,8 @@ export interface ITransactionApi { offset?: number; }): Promise>>; + clearDelegates(safeAddress?: `0x${string}`): Promise; + postDelegate(args: { safeAddress: `0x${string}` | null; delegate: `0x${string}`; diff --git a/src/routes/hooks/__tests__/event-hooks-queue.e2e-spec.ts b/src/routes/hooks/__tests__/event-hooks-queue.e2e-spec.ts index 73a55526cc..640bf9668d 100644 --- a/src/routes/hooks/__tests__/event-hooks-queue.e2e-spec.ts +++ b/src/routes/hooks/__tests__/event-hooks-queue.e2e-spec.ts @@ -544,4 +544,37 @@ describe('Events queue processing e2e tests', () => { expect(cacheContent).toBeNull(); }); }); + + it.each(['NEW_DELEGATE', 'UPDATED_DELEGATE', 'DELETED_DELEGATE'])( + '%s clears delegates', + async (type) => { + const cacheDir = new CacheDir( + `${TEST_SAFE.chainId}_delegates_${TEST_SAFE.address}`, + '', + ); + await redisClient.hSet( + `${cacheKeyPrefix}-${cacheDir.key}`, + cacheDir.field, + faker.string.alpha(), + ); + const data = { + type, + chainId: TEST_SAFE.chainId, + address: TEST_SAFE.address, + delegate: faker.finance.ethereumAddress(), + delegator: faker.finance.ethereumAddress(), + label: faker.lorem.word(), + }; + + await channel.sendToQueue(queueName, data); + + await retry(async () => { + const cacheContent = await redisClient.hGet( + `${cacheKeyPrefix}-${cacheDir.key}`, + cacheDir.field, + ); + expect(cacheContent).toBeNull(); + }); + }, + ); }); diff --git a/src/routes/hooks/entities/__tests__/delegate-events.builder.ts b/src/routes/hooks/entities/__tests__/delegate-events.builder.ts new file mode 100644 index 0000000000..05258bd1fe --- /dev/null +++ b/src/routes/hooks/entities/__tests__/delegate-events.builder.ts @@ -0,0 +1,45 @@ +import { faker } from '@faker-js/faker'; +import { getAddress } from 'viem'; +import type { z } from 'zod'; +import { TransactionEventType } from '@/routes/hooks/entities/event-type.entity'; +import { Builder } from '@/__tests__/builder'; +import type { IBuilder } from '@/__tests__/builder'; +import type { + DelegateEventPayloadSchema, + DeletedDelegateEvent, + NewDelegateEvent, + UpdatedDelegateEvent, +} from '@/routes/hooks/entities/schemas/delegate-events.schema'; + +type DelegateEventPayload = z.infer; + +function delegateEventBuilder(): IBuilder { + return new Builder() + .with('chainId', faker.string.numeric()) + .with('address', getAddress(faker.finance.ethereumAddress())) + .with('delegate', getAddress(faker.finance.ethereumAddress())) + .with('delegator', getAddress(faker.finance.ethereumAddress())) + .with('label', faker.lorem.word()) + .with('expiryDateSeconds', faker.number.int()); +} + +export function newDelegateEventBuilder(): IBuilder { + return (delegateEventBuilder() as IBuilder).with( + 'type', + TransactionEventType.NEW_DELEGATE, + ); +} + +export function updatedDelegateEventBuilder(): IBuilder { + return (delegateEventBuilder() as IBuilder).with( + 'type', + TransactionEventType.UPDATED_DELEGATE, + ); +} + +export function deletedDelegateEventBuilder(): IBuilder { + return (delegateEventBuilder() as IBuilder).with( + 'type', + TransactionEventType.DELETED_DELEGATE, + ); +} diff --git a/src/routes/hooks/entities/event-type.entity.ts b/src/routes/hooks/entities/event-type.entity.ts index 377abb7d60..35dfee4ca4 100644 --- a/src/routes/hooks/entities/event-type.entity.ts +++ b/src/routes/hooks/entities/event-type.entity.ts @@ -1,4 +1,5 @@ export enum TransactionEventType { + DELETED_DELEGATE = 'DELETED_DELEGATE', DELETED_MULTISIG_TRANSACTION = 'DELETED_MULTISIG_TRANSACTION', EXECUTED_MULTISIG_TRANSACTION = 'EXECUTED_MULTISIG_TRANSACTION', INCOMING_ETHER = 'INCOMING_ETHER', @@ -7,11 +8,13 @@ export enum TransactionEventType { MESSAGE_CREATED = 'MESSAGE_CREATED', MODULE_TRANSACTION = 'MODULE_TRANSACTION', NEW_CONFIRMATION = 'NEW_CONFIRMATION', + NEW_DELEGATE = 'NEW_DELEGATE', OUTGOING_ETHER = 'OUTGOING_ETHER', OUTGOING_TOKEN = 'OUTGOING_TOKEN', PENDING_MULTISIG_TRANSACTION = 'PENDING_MULTISIG_TRANSACTION', REORG_DETECTED = 'REORG_DETECTED', SAFE_CREATED = 'SAFE_CREATED', + UPDATED_DELEGATE = 'UPDATED_DELEGATE', } export enum ConfigEventType { diff --git a/src/routes/hooks/entities/schemas/__tests__/delegate-events.schema.spec.ts b/src/routes/hooks/entities/schemas/__tests__/delegate-events.schema.spec.ts new file mode 100644 index 0000000000..017937913d --- /dev/null +++ b/src/routes/hooks/entities/schemas/__tests__/delegate-events.schema.spec.ts @@ -0,0 +1,123 @@ +import { faker } from '@faker-js/faker'; +import { + deletedDelegateEventBuilder, + newDelegateEventBuilder, + updatedDelegateEventBuilder, +} from '@/routes/hooks/entities/__tests__/delegate-events.builder'; +import { + DeletedDelegateEventSchema, + NewDelegateEventSchema, + UpdatedDelegateEventSchema, +} from '@/routes/hooks/entities/schemas/delegate-events.schema'; +import { getAddress } from 'viem'; + +describe.each([ + ['NewDelegateEventSchema', NewDelegateEventSchema, newDelegateEventBuilder], + [ + 'UpdatedDelegateEventSchema', + UpdatedDelegateEventSchema, + updatedDelegateEventBuilder, + ], + [ + 'DeletedDelegateEventSchema', + DeletedDelegateEventSchema, + deletedDelegateEventBuilder, + ], +])('%s', (schemaName, Schema, builder) => { + it(`should validate a ${schemaName}`, () => { + const event = builder().build(); + + const result = Schema.safeParse(event); + + expect(result.success).toBe(true); + }); + + it('should throw if chainId is not a numeric string', () => { + const event = builder().with('chainId', faker.string.alpha()).build(); + + const result = Schema.safeParse(event); + + expect(!result.success && result.error.issues.length).toBe(1); + expect(!result.success && result.error.issues).toStrictEqual([ + { + code: 'custom', + message: 'Invalid base-10 numeric string', + path: ['chainId'], + }, + ]); + }); + + it.each(['address' as const, 'delegate' as const, 'delegator' as const])( + 'should checksum %s', + (field) => { + const nonChecksummedAddress = faker.finance.ethereumAddress(); + const event = builder() + .with(field, nonChecksummedAddress as `0x${string}`) + .build(); + + const result = Schema.safeParse(event); + + expect(result.success && result.data[field]).toBe( + getAddress(nonChecksummedAddress), + ); + }, + ); + + it.each(['address' as const, 'expiryDateSeconds' as const])( + 'should allow nullish %s, defaulting to null', + (field) => { + const event = builder().build(); + delete event[field]; + + const result = Schema.safeParse(event); + + expect(result.success && result.data[field]).toBe(null); + }, + ); + + it('should throw if the event is invalid', () => { + const event = { + invalid: 'event', + }; + + const result = Schema.safeParse(event); + + expect(!result.success && result.error.issues).toStrictEqual([ + { + code: 'invalid_type', + expected: 'string', + message: 'Required', + path: ['chainId'], + received: 'undefined', + }, + { + code: 'invalid_type', + expected: 'string', + message: 'Required', + path: ['delegate'], + received: 'undefined', + }, + { + code: 'invalid_type', + expected: 'string', + message: 'Required', + path: ['delegator'], + received: 'undefined', + }, + { + code: 'invalid_type', + expected: 'string', + message: 'Required', + path: ['label'], + received: 'undefined', + }, + { + code: 'invalid_literal', + expected: Schema.shape.type.value, + message: `Invalid literal value, expected "${Schema.shape.type.value}"`, + path: ['type'], + received: undefined, + }, + ]); + }); +}); diff --git a/src/routes/hooks/entities/schemas/__tests__/event.schema.spec.ts b/src/routes/hooks/entities/schemas/__tests__/event.schema.spec.ts index 0bdda70745..3c0a49f040 100644 --- a/src/routes/hooks/entities/schemas/__tests__/event.schema.spec.ts +++ b/src/routes/hooks/entities/schemas/__tests__/event.schema.spec.ts @@ -1,4 +1,9 @@ import { chainUpdateEventBuilder } from '@/routes/hooks/entities/__tests__/chain-update.builder'; +import { + deletedDelegateEventBuilder, + newDelegateEventBuilder, + updatedDelegateEventBuilder, +} from '@/routes/hooks/entities/__tests__/delegate-events.builder'; import { deletedMultisigTransactionEventBuilder } from '@/routes/hooks/entities/__tests__/deleted-multisig-transaction.builder'; import { executedTransactionEventBuilder } from '@/routes/hooks/entities/__tests__/executed-transaction.builder'; import { incomingEtherEventBuilder } from '@/routes/hooks/entities/__tests__/incoming-ether.builder'; @@ -18,6 +23,7 @@ import { ZodError } from 'zod'; describe('EventSchema', () => { [ chainUpdateEventBuilder, + deletedDelegateEventBuilder, deletedMultisigTransactionEventBuilder, executedTransactionEventBuilder, incomingEtherEventBuilder, @@ -25,12 +31,14 @@ describe('EventSchema', () => { messageCreatedEventBuilder, moduleTransactionEventBuilder, newConfirmationEventBuilder, + newDelegateEventBuilder, newMessageConfirmationEventBuilder, outgoingEtherEventBuilder, outgoingTokenEventBuilder, pendingTransactionEventBuilder, reorgDetectedEventBuilder, safeAppsEventBuilder, + updatedDelegateEventBuilder, ].forEach((builder) => { const event = builder().build(); @@ -56,10 +64,12 @@ describe('EventSchema', () => { 'CHAIN_UPDATE', 'DELETED_MULTISIG_TRANSACTION', 'EXECUTED_MULTISIG_TRANSACTION', + 'DELETED_DELEGATE', 'INCOMING_ETHER', 'INCOMING_TOKEN', 'MESSAGE_CREATED', 'MODULE_TRANSACTION', + 'NEW_DELEGATE', 'NEW_CONFIRMATION', 'MESSAGE_CONFIRMATION', 'OUTGOING_ETHER', @@ -68,10 +78,11 @@ describe('EventSchema', () => { 'REORG_DETECTED', 'SAFE_APPS_UPDATE', 'SAFE_CREATED', + 'UPDATED_DELEGATE', ], path: ['type'], message: - "Invalid discriminator value. Expected 'CHAIN_UPDATE' | 'DELETED_MULTISIG_TRANSACTION' | 'EXECUTED_MULTISIG_TRANSACTION' | 'INCOMING_ETHER' | 'INCOMING_TOKEN' | 'MESSAGE_CREATED' | 'MODULE_TRANSACTION' | 'NEW_CONFIRMATION' | 'MESSAGE_CONFIRMATION' | 'OUTGOING_ETHER' | 'OUTGOING_TOKEN' | 'PENDING_MULTISIG_TRANSACTION' | 'REORG_DETECTED' | 'SAFE_APPS_UPDATE' | 'SAFE_CREATED'", + "Invalid discriminator value. Expected 'CHAIN_UPDATE' | 'DELETED_MULTISIG_TRANSACTION' | 'EXECUTED_MULTISIG_TRANSACTION' | 'DELETED_DELEGATE' | 'INCOMING_ETHER' | 'INCOMING_TOKEN' | 'MESSAGE_CREATED' | 'MODULE_TRANSACTION' | 'NEW_DELEGATE' | 'NEW_CONFIRMATION' | 'MESSAGE_CONFIRMATION' | 'OUTGOING_ETHER' | 'OUTGOING_TOKEN' | 'PENDING_MULTISIG_TRANSACTION' | 'REORG_DETECTED' | 'SAFE_APPS_UPDATE' | 'SAFE_CREATED' | 'UPDATED_DELEGATE'", }, ]), ); diff --git a/src/routes/hooks/entities/schemas/delegate-events.schema.ts b/src/routes/hooks/entities/schemas/delegate-events.schema.ts new file mode 100644 index 0000000000..b7c2aa72d6 --- /dev/null +++ b/src/routes/hooks/entities/schemas/delegate-events.schema.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; +import { TransactionEventType } from '@/routes/hooks/entities/event-type.entity'; +import { AddressSchema } from '@/validation/entities/schemas/address.schema'; +import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema'; + +export const DelegateEventPayloadSchema = z.object({ + chainId: NumericStringSchema, + address: AddressSchema.nullish().default(null), + delegate: AddressSchema, + delegator: AddressSchema, + label: z.string(), + expiryDateSeconds: z.number().nullish().default(null), +}); + +export const NewDelegateEventSchema = DelegateEventPayloadSchema.extend({ + type: z.literal(TransactionEventType.NEW_DELEGATE), +}); + +export const UpdatedDelegateEventSchema = DelegateEventPayloadSchema.extend({ + type: z.literal(TransactionEventType.UPDATED_DELEGATE), +}); + +export const DeletedDelegateEventSchema = DelegateEventPayloadSchema.extend({ + type: z.literal(TransactionEventType.DELETED_DELEGATE), +}); + +export type NewDelegateEvent = z.infer; + +export type UpdatedDelegateEvent = z.infer; + +export type DeletedDelegateEvent = z.infer; diff --git a/src/routes/hooks/entities/schemas/event.schema.ts b/src/routes/hooks/entities/schemas/event.schema.ts index c5b8b2182f..10f5b31fbe 100644 --- a/src/routes/hooks/entities/schemas/event.schema.ts +++ b/src/routes/hooks/entities/schemas/event.schema.ts @@ -14,15 +14,22 @@ import { PendingTransactionEventSchema } from '@/routes/hooks/entities/schemas/p import { ReorgDetectedEventSchema } from '@/routes/hooks/entities/schemas/reorg-detected.schema'; import { SafeAppsUpdateEventSchema } from '@/routes/hooks/entities/schemas/safe-apps-update.schema'; import { SafeCreatedEventSchema } from '@/routes/hooks/entities/schemas/safe-created.schema'; +import { + DeletedDelegateEventSchema, + NewDelegateEventSchema, + UpdatedDelegateEventSchema, +} from '@/routes/hooks/entities/schemas/delegate-events.schema'; export const EventSchema = z.discriminatedUnion('type', [ ChainUpdateEventSchema, DeletedMultisigTransactionEventSchema, ExecutedTransactionEventSchema, + DeletedDelegateEventSchema, IncomingEtherEventSchema, IncomingTokenEventSchema, MessageCreatedEventSchema, ModuleTransactionEventSchema, + NewDelegateEventSchema, NewConfirmationEventSchema, NewMessageConfirmationEventSchema, OutgoingEtherEventSchema, @@ -31,4 +38,5 @@ export const EventSchema = z.discriminatedUnion('type', [ ReorgDetectedEventSchema, SafeAppsUpdateEventSchema, SafeCreatedEventSchema, + UpdatedDelegateEventSchema, ]); diff --git a/src/routes/hooks/hooks-cache.controller.spec.ts b/src/routes/hooks/hooks-cache.controller.spec.ts index df00507d48..082d211c5c 100644 --- a/src/routes/hooks/hooks-cache.controller.spec.ts +++ b/src/routes/hooks/hooks-cache.controller.spec.ts @@ -38,6 +38,7 @@ import { TestTargetedMessagingDatasourceModule } from '@/datasources/targeted-me import { TargetedMessagingDatasourceModule } from '@/datasources/targeted-messaging/targeted-messaging.datasource.module'; // TODO: Migrate to E2E tests as TransactionEventType events are already being received via queue. +// Add *_DELEGATE event tests here if we unskip this describe.skip('Post Hook Events for Cache (Unit)', () => { let app: INestApplication; let authToken: string;