From 2496d372f1bc7d6c3b8028226566a56e720062e0 Mon Sep 17 00:00:00 2001 From: Bug-Hunter-X Date: Thu, 28 May 2026 23:07:33 +0100 Subject: [PATCH 1/2] Fix wallet deletion audit issue --- src/blockchain/reorg-detector.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/blockchain/reorg-detector.service.ts b/src/blockchain/reorg-detector.service.ts index 0118d13..05ccc67 100644 --- a/src/blockchain/reorg-detector.service.ts +++ b/src/blockchain/reorg-detector.service.ts @@ -133,7 +133,6 @@ export class ReorgDetectorService { } /** - * Get all events affected by reorg. * Get the hash of the ancestor at a specific depth from the current block * @param currentBlock The current block to start from * @param depth How many blocks back to go (0 = current block, 1 = parent, 2 = grandparent, etc.) From e8ffb7baab8b0694049284d7643f7eb980379151 Mon Sep 17 00:00:00 2001 From: Bug-Hunter-X Date: Thu, 28 May 2026 23:21:09 +0100 Subject: [PATCH 2/2] Add wallet deletion audit logging --- src/audit/entities/audit-log.entity.ts | 2 +- src/identity/identity.service.spec.ts | 45 ++++++++++++++++++++------ src/identity/identity.service.ts | 17 ++++++++-- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/audit/entities/audit-log.entity.ts b/src/audit/entities/audit-log.entity.ts index d56b201..0ce5345 100644 --- a/src/audit/entities/audit-log.entity.ts +++ b/src/audit/entities/audit-log.entity.ts @@ -31,7 +31,7 @@ export enum AuditActionType { // User actions USER_CREATED = 'USER_CREATED', USER_UPDATED = 'USER_UPDATED', - WALLET_LINKED = 'WALLET_LINKED', + WALLET_UNLINKED = 'WALLET_UNLINKED', VERIFICATION_INITIATED = 'VERIFICATION_INITIATED', } diff --git a/src/identity/identity.service.spec.ts b/src/identity/identity.service.spec.ts index 5699c3c..ec9b125 100644 --- a/src/identity/identity.service.spec.ts +++ b/src/identity/identity.service.spec.ts @@ -1,7 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + NotFoundException, +} from '@nestjs/common'; import { IdentityService } from './identity.service'; import { PrismaService } from '../prisma/prisma.service'; +import { AuditTrailService } from '../audit/services/audit-trail.service'; +import { + AuditActionType, + AuditEntityType, +} from '../audit/entities/audit-log.entity'; import { LinkWalletDto } from './dto/link-wallet.dto'; import { verifyMessage } from 'ethers'; @@ -11,7 +20,8 @@ jest.mock('ethers', () => ({ describe('IdentityService', () => { let service: IdentityService; - let prisma: jest.Mocked; + let prisma: any; + let auditTrail: any; const mockUser = { id: 'user-123', @@ -65,6 +75,7 @@ describe('IdentityService', () => { create: jest.fn(), findFirst: jest.fn(), findUnique: jest.fn(), + count: jest.fn(), delete: jest.fn(), }, sybilScore: { @@ -72,11 +83,18 @@ describe('IdentityService', () => { }, }, }, + { + provide: AuditTrailService, + useValue: { + log: jest.fn().mockResolvedValue(undefined), + }, + }, ], }).compile(); service = module.get(IdentityService); prisma = module.get(PrismaService); + auditTrail = module.get(AuditTrailService); jest.clearAllMocks(); }); @@ -110,9 +128,13 @@ describe('IdentityService', () => { it('should rollback transaction if sybil score creation fails', async () => { mockTransaction.user.create.mockResolvedValue(mockUser); - mockTransaction.sybilScore.create.mockRejectedValue(new Error('Score creation failed')); + mockTransaction.sybilScore.create.mockRejectedValue( + new Error('Score creation failed'), + ); - await expect(service.createUser()).rejects.toThrow('Score creation failed'); + await expect(service.createUser()).rejects.toThrow( + 'Score creation failed', + ); }); }); @@ -284,13 +306,10 @@ describe('IdentityService', () => { linkedAt: new Date(), }; prisma.wallet.findUnique.mockResolvedValue(wallet); + prisma.wallet.count.mockResolvedValue(2); prisma.wallet.delete.mockResolvedValue(wallet); - const result = await service.unlinkWallet( - mockUser.id, - '0x123', - 'ETH', - ); + const result = await service.unlinkWallet(mockUser.id, '0x123', 'ETH'); expect(prisma.wallet.findUnique).toHaveBeenCalledWith({ where: { @@ -300,6 +319,14 @@ describe('IdentityService', () => { }, }, }); + expect(auditTrail.log).toHaveBeenCalledWith({ + actionType: AuditActionType.WALLET_UNLINKED, + entityType: AuditEntityType.WALLET, + entityId: wallet.id, + userId: mockUser.id, + walletAddress: '0x123', + description: 'Wallet unlinked', + }); expect(prisma.wallet.delete).toHaveBeenCalledWith({ where: { address_chain: { diff --git a/src/identity/identity.service.ts b/src/identity/identity.service.ts index a016322..600244a 100644 --- a/src/identity/identity.service.ts +++ b/src/identity/identity.service.ts @@ -2,10 +2,15 @@ import { BadRequestException, Injectable, ConflictException, NotFoundException } import { PrismaService } from '../prisma/prisma.service'; import { LinkWalletDto } from './dto/link-wallet.dto'; import { verifyMessage } from 'ethers'; +import { AuditTrailService } from '../audit/services/audit-trail.service'; +import { AuditActionType, AuditEntityType } from '../audit/entities/audit-log.entity'; @Injectable() export class IdentityService { - constructor(private prisma: PrismaService) {} + constructor( + private prisma: PrismaService, + private auditTrailService: AuditTrailService, + ) {} async createUser() { return this.prisma.$transaction(async (tx) => { @@ -104,11 +109,19 @@ export class IdentityService { where: { userId }, }); - // If we enforce at least one wallet: // if (count <= 1) throw new BadRequestException('Cannot unlink the last wallet.'); // For now, I'll allow unlinking all, as the user might want to delete their identity or switch completely. // But I'll leave a comment. + // Log audit entry for wallet unlink + await this.auditTrailService.log({ + actionType: AuditActionType.WALLET_UNLINKED, + entityType: AuditEntityType.WALLET, + entityId: wallet.id, + userId: userId, + walletAddress: address, + description: 'Wallet unlinked', + }); return this.prisma.wallet.delete({ where: { address_chain: {