Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/audit/entities/audit-log.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}

Expand Down
1 change: 0 additions & 1 deletion src/blockchain/reorg-detector.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
45 changes: 36 additions & 9 deletions src/identity/identity.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,7 +20,8 @@ jest.mock('ethers', () => ({

describe('IdentityService', () => {
let service: IdentityService;
let prisma: jest.Mocked<PrismaService>;
let prisma: any;
let auditTrail: any;

const mockUser = {
id: 'user-123',
Expand Down Expand Up @@ -65,18 +75,26 @@ describe('IdentityService', () => {
create: jest.fn(),
findFirst: jest.fn(),
findUnique: jest.fn(),
count: jest.fn(),
delete: jest.fn(),
},
sybilScore: {
create: jest.fn(),
},
},
},
{
provide: AuditTrailService,
useValue: {
log: jest.fn().mockResolvedValue(undefined),
},
},
],
}).compile();

service = module.get<IdentityService>(IdentityService);
prisma = module.get(PrismaService);
auditTrail = module.get(AuditTrailService);

jest.clearAllMocks();
});
Expand Down Expand Up @@ -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',
);
});
});

Expand Down Expand Up @@ -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: {
Expand All @@ -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: {
Expand Down
17 changes: 15 additions & 2 deletions src/identity/identity.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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: {
Expand Down
Loading