Skip to content

Commit

Permalink
Handle *_DELEGATE Transaction Service events (#2106)
Browse files Browse the repository at this point in the history
* Handle `*_DELEGATE` Transaction Service events

* Add E2E tests

* Rename address field

* Rename remaining fields

* Make `safeAddress` optional

* Add payload
  • Loading branch information
iamacook authored Nov 19, 2024
1 parent b0e9ad4 commit 9f48dd1
Show file tree
Hide file tree
Showing 17 changed files with 368 additions and 6 deletions.
9 changes: 8 additions & 1 deletion src/datasources/cache/cache.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;
Expand All @@ -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}`,
);
}
Expand Down
8 changes: 8 additions & 0 deletions src/datasources/transaction-api/transaction-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,14 @@ export class TransactionApi implements ITransactionApi {
}
}

async clearDelegates(safeAddress?: `0x${string}`): Promise<void> {
const cacheKey = CacheRouter.getDelegatesCacheKey({
chainId: this.chainId,
safeAddress,
});
await this.cacheService.deleteByKey(cacheKey);
}

async postDelegate(args: {
safeAddress: `0x${string}` | null;
delegate: `0x${string}`;
Expand Down
5 changes: 5 additions & 0 deletions src/domain/delegate/v2/delegates.v2.repository.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface IDelegatesV2Repository {
offset?: number;
}): Promise<Page<Delegate>>;

clearDelegates(args: {
chainId: string;
safeAddress?: string;
}): Promise<void>;

postDelegate(args: {
chainId: string;
safeAddress: `0x${string}` | null;
Expand Down
10 changes: 10 additions & 0 deletions src/domain/delegate/v2/delegates.v2.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export class DelegatesV2Repository implements IDelegatesV2Repository {
return DelegatePageSchema.parse(page);
}

async clearDelegates(args: {
chainId: string;
safeAddress?: `0x${string}`;
}): Promise<void> {
const transactionService = await this.transactionApiManager.getApi(
args.chainId,
);
await transactionService.clearDelegates(args.safeAddress);
}

async postDelegate(args: {
chainId: string;
safeAddress: `0x${string}` | null;
Expand Down
2 changes: 2 additions & 0 deletions src/domain/hooks/helpers/event-cache.helper.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ 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: [
BalancesRepositoryModule,
BlockchainRepositoryModule,
ChainsRepositoryModule,
CollectiblesRepositoryModule,
DelegatesV2RepositoryModule,
MessagesRepositoryModule,
SafeAppsRepositoryModule,
SafeRepositoryModule,
Expand Down
65 changes: 62 additions & 3 deletions src/domain/hooks/helpers/event-cache.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<Promise<void>> {
// 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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion src/domain/hooks/helpers/event-notifications.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand All @@ -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
);
}

Expand Down
7 changes: 7 additions & 0 deletions src/domain/hooks/hooks.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +43,10 @@ const mockCollectiblesRepository = jest.mocked({
clearCollectibles: jest.fn(),
} as jest.MockedObjectDeep<CollectiblesRepository>);

const mockDelegatesRepository = jest.mocked({
clearDelegates: jest.fn(),
} as jest.MockedObjectDeep<DelegatesV2Repository>);

const mockMessagesRepository = jest.mocked({
clearMessages: jest.fn(),
} as unknown as jest.MockedObjectDeep<MessagesRepository>);
Expand Down Expand Up @@ -100,6 +105,7 @@ describe('HooksRepository (Unit)', () => {
mockBlockchainRepository,
mockChainsRepository,
mockCollectiblesRepository,
mockDelegatesRepository,
mockMessagesRepository,
mockSafeAppsRepository,
mockSafeRepository,
Expand Down Expand Up @@ -278,6 +284,7 @@ describe('HooksRepositoryWithNotifications (Unit)', () => {
mockBlockchainRepository,
mockChainsRepository,
mockCollectiblesRepository,
mockDelegatesRepository,
mockMessagesRepository,
mockSafeAppsRepository,
mockSafeRepository,
Expand Down
2 changes: 2 additions & 0 deletions src/domain/interfaces/transaction-api.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ export interface ITransactionApi {
offset?: number;
}): Promise<Raw<Page<Delegate>>>;

clearDelegates(safeAddress?: `0x${string}`): Promise<void>;

postDelegate(args: {
safeAddress: `0x${string}` | null;
delegate: `0x${string}`;
Expand Down
33 changes: 33 additions & 0 deletions src/routes/hooks/__tests__/event-hooks-queue.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
},
);
});
45 changes: 45 additions & 0 deletions src/routes/hooks/entities/__tests__/delegate-events.builder.ts
Original file line number Diff line number Diff line change
@@ -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<typeof DelegateEventPayloadSchema>;

function delegateEventBuilder(): IBuilder<DelegateEventPayload> {
return new Builder<DelegateEventPayload>()
.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<NewDelegateEvent> {
return (delegateEventBuilder() as IBuilder<NewDelegateEvent>).with(
'type',
TransactionEventType.NEW_DELEGATE,
);
}

export function updatedDelegateEventBuilder(): IBuilder<UpdatedDelegateEvent> {
return (delegateEventBuilder() as IBuilder<UpdatedDelegateEvent>).with(
'type',
TransactionEventType.UPDATED_DELEGATE,
);
}

export function deletedDelegateEventBuilder(): IBuilder<DeletedDelegateEvent> {
return (delegateEventBuilder() as IBuilder<DeletedDelegateEvent>).with(
'type',
TransactionEventType.DELETED_DELEGATE,
);
}
3 changes: 3 additions & 0 deletions src/routes/hooks/entities/event-type.entity.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 9f48dd1

Please sign in to comment.