From 64ae8f975fa53da8b6050fd5bc0536dd7b1c7755 Mon Sep 17 00:00:00 2001 From: Maverick Date: Sun, 14 Jun 2026 19:58:04 +0100 Subject: [PATCH 1/5] feat: add comprehensive unit tests for core services - Added unit tests for IdentityService with full CRUD coverage - Added unit tests for VerificationService including approve/reject flows - Added unit tests for AuthService covering authentication logic - All tests include proper mocking of dependencies - Tests cover both happy paths and error cases (NotFoundException) - Achieves high code coverage for critical business logic --- backend/src/auth/auth.service.spec.ts | 91 +++++++++ backend/src/identity/identity.service.spec.ts | 192 ++++++++++++++++++ .../verification/verification.service.spec.ts | 191 +++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 backend/src/auth/auth.service.spec.ts create mode 100644 backend/src/identity/identity.service.spec.ts create mode 100644 backend/src/verification/verification.service.spec.ts diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts new file mode 100644 index 00000000..2a29b93f --- /dev/null +++ b/backend/src/auth/auth.service.spec.ts @@ -0,0 +1,91 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + let jwtService: JwtService; + + const mockJwtService = { + sign: jest.fn(), + verify: jest.fn(), + }; + + const mockConfigService = { + get: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthService, + { + provide: JwtService, + useValue: mockJwtService, + }, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + service = module.get(AuthService); + jwtService = module.get(JwtService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('validateWallet', () => { + it('should validate wallet signature', async () => { + const walletAddress = 'GABC123...'; + const signature = 'signature123'; + const message = 'message123'; + + const result = await service.validateWallet(walletAddress, signature, message); + + expect(result).toBe(true); + }); + + // Note: This is a placeholder implementation + // In a real implementation, you would test actual signature verification + }); + + describe('login', () => { + it('should return access token and wallet address', async () => { + const walletAddress = 'GABC123...'; + const mockToken = 'jwt-token-123'; + + mockJwtService.sign.mockReturnValue(mockToken); + + const result = await service.login(walletAddress); + + expect(jwtService.sign).toHaveBeenCalledWith({ walletAddress }); + expect(result).toEqual({ + access_token: mockToken, + walletAddress, + }); + }); + }); + + describe('verifyToken', () => { + it('should verify and decode token', async () => { + const token = 'jwt-token-123'; + const decodedPayload = { walletAddress: 'GABC123...' }; + + mockJwtService.verify.mockReturnValue(decodedPayload); + + const result = await service.verifyToken(token); + + expect(jwtService.verify).toHaveBeenCalledWith(token); + expect(result).toEqual(decodedPayload); + }); + }); +}); diff --git a/backend/src/identity/identity.service.spec.ts b/backend/src/identity/identity.service.spec.ts new file mode 100644 index 00000000..8a826d74 --- /dev/null +++ b/backend/src/identity/identity.service.spec.ts @@ -0,0 +1,192 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotFoundException } from '@nestjs/common'; +import { IdentityService } from './identity.service'; +import { Identity } from './identity.entity'; +import { CreateIdentityDto } from './dto/create-identity.dto'; +import { UpdateIdentityDto } from './dto/update-identity.dto'; + +describe('IdentityService', () => { + let service: IdentityService; + let repository: Repository; + + const mockIdentity: Identity = { + id: '123e4567-e89b-12d3-a456-426614174000', + walletAddress: 'GABC123...', + documentHash: 'QmHash123', + metadata: { name: 'Test User' }, + revoked: false, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockRepository = { + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + remove: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + IdentityService, + { + provide: getRepositoryToken(Identity), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(IdentityService); + repository = module.get>(getRepositoryToken(Identity)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create an identity', async () => { + const createDto: CreateIdentityDto = { + walletAddress: 'GABC123...', + documentHash: 'QmHash123', + metadata: { name: 'Test User' }, + }; + + mockRepository.create.mockReturnValue(mockIdentity); + mockRepository.save.mockResolvedValue(mockIdentity); + + const result = await service.create(createDto); + + expect(repository.create).toHaveBeenCalledWith(createDto); + expect(repository.save).toHaveBeenCalledWith(mockIdentity); + expect(result).toEqual(mockIdentity); + }); + }); + + describe('findAll', () => { + it('should return all identities', async () => { + const identities = [mockIdentity]; + mockRepository.find.mockResolvedValue(identities); + + const result = await service.findAll(); + + expect(repository.find).toHaveBeenCalledWith({ order: { createdAt: 'DESC' } }); + expect(result).toEqual(identities); + }); + + it('should return identities filtered by wallet address', async () => { + const identities = [mockIdentity]; + mockRepository.find.mockResolvedValue(identities); + + const result = await service.findAll('GABC123...'); + + expect(repository.find).toHaveBeenCalledWith({ + where: { walletAddress: 'GABC123...' }, + order: { createdAt: 'DESC' }, + }); + expect(result).toEqual(identities); + }); + }); + + describe('findOne', () => { + it('should return an identity by id', async () => { + mockRepository.findOne.mockResolvedValue(mockIdentity); + + const result = await service.findOne('123e4567-e89b-12d3-a456-426614174000'); + + expect(repository.findOne).toHaveBeenCalledWith({ + where: { id: '123e4567-e89b-12d3-a456-426614174000' }, + }); + expect(result).toEqual(mockIdentity); + }); + + it('should throw NotFoundException when identity not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.findOne('non-existent-id')).rejects.toThrow(NotFoundException); + }); + }); + + describe('update', () => { + it('should update an identity', async () => { + const updateDto: UpdateIdentityDto = { + metadata: { name: 'Updated User' }, + }; + const updatedIdentity = { ...mockIdentity, ...updateDto }; + + mockRepository.findOne.mockResolvedValue(mockIdentity); + mockRepository.save.mockResolvedValue(updatedIdentity); + + const result = await service.update('123e4567-e89b-12d3-a456-426614174000', updateDto); + + expect(repository.save).toHaveBeenCalled(); + expect(result.metadata).toEqual(updateDto.metadata); + }); + + it('should throw NotFoundException when updating non-existent identity', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect( + service.update('non-existent-id', { metadata: {} }), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('revoke', () => { + it('should revoke an identity', async () => { + const revokedIdentity = { ...mockIdentity, revoked: true }; + + mockRepository.findOne.mockResolvedValue(mockIdentity); + mockRepository.save.mockResolvedValue(revokedIdentity); + + const result = await service.revoke('123e4567-e89b-12d3-a456-426614174000'); + + expect(result.revoked).toBe(true); + expect(repository.save).toHaveBeenCalled(); + }); + + it('should throw NotFoundException when revoking non-existent identity', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.revoke('non-existent-id')).rejects.toThrow(NotFoundException); + }); + }); + + describe('remove', () => { + it('should remove an identity', async () => { + mockRepository.findOne.mockResolvedValue(mockIdentity); + mockRepository.remove.mockResolvedValue(mockIdentity); + + await service.remove('123e4567-e89b-12d3-a456-426614174000'); + + expect(repository.remove).toHaveBeenCalledWith(mockIdentity); + }); + + it('should throw NotFoundException when removing non-existent identity', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.remove('non-existent-id')).rejects.toThrow(NotFoundException); + }); + }); + + describe('findByDocumentHash', () => { + it('should return an identity by document hash', async () => { + mockRepository.findOne.mockResolvedValue(mockIdentity); + + const result = await service.findByDocumentHash('QmHash123'); + + expect(repository.findOne).toHaveBeenCalledWith({ + where: { documentHash: 'QmHash123' }, + }); + expect(result).toEqual(mockIdentity); + }); + }); +}); diff --git a/backend/src/verification/verification.service.spec.ts b/backend/src/verification/verification.service.spec.ts new file mode 100644 index 00000000..a8826b50 --- /dev/null +++ b/backend/src/verification/verification.service.spec.ts @@ -0,0 +1,191 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotFoundException } from '@nestjs/common'; +import { VerificationService } from './verification.service'; +import { Verification } from './verification.entity'; +import { CreateVerificationDto } from './dto/create-verification.dto'; +import { UpdateVerificationDto } from './dto/update-verification.dto'; + +describe('VerificationService', () => { + let service: VerificationService; + let repository: Repository; + + const mockVerification: Verification = { + id: '123e4567-e89b-12d3-a456-426614174000', + identityId: 'identity-123', + verifierAddress: 'GVERIFY123...', + status: 'pending', + reason: null, + metadata: {}, + createdAt: new Date(), + updatedAt: new Date(), + }; + + const mockRepository = { + create: jest.fn(), + save: jest.fn(), + find: jest.fn(), + findOne: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + VerificationService, + { + provide: getRepositoryToken(Verification), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(VerificationService); + repository = module.get>(getRepositoryToken(Verification)); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('create', () => { + it('should create a verification', async () => { + const createDto: CreateVerificationDto = { + identityId: 'identity-123', + verifierAddress: 'GVERIFY123...', + metadata: {}, + }; + + mockRepository.create.mockReturnValue(mockVerification); + mockRepository.save.mockResolvedValue(mockVerification); + + const result = await service.create(createDto); + + expect(repository.create).toHaveBeenCalledWith(createDto); + expect(repository.save).toHaveBeenCalledWith(mockVerification); + expect(result).toEqual(mockVerification); + }); + }); + + describe('findAll', () => { + it('should return all verifications', async () => { + const verifications = [mockVerification]; + mockRepository.find.mockResolvedValue(verifications); + + const result = await service.findAll(); + + expect(repository.find).toHaveBeenCalledWith({ order: { createdAt: 'DESC' } }); + expect(result).toEqual(verifications); + }); + }); + + describe('findOne', () => { + it('should return a verification by id', async () => { + mockRepository.findOne.mockResolvedValue(mockVerification); + + const result = await service.findOne('123e4567-e89b-12d3-a456-426614174000'); + + expect(repository.findOne).toHaveBeenCalledWith({ + where: { id: '123e4567-e89b-12d3-a456-426614174000' }, + }); + expect(result).toEqual(mockVerification); + }); + + it('should throw NotFoundException when verification not found', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.findOne('non-existent-id')).rejects.toThrow(NotFoundException); + }); + }); + + describe('update', () => { + it('should update a verification', async () => { + const updateDto: UpdateVerificationDto = { + status: 'approved', + }; + const updatedVerification = { ...mockVerification, status: 'approved' }; + + mockRepository.findOne.mockResolvedValue(mockVerification); + mockRepository.save.mockResolvedValue(updatedVerification); + + const result = await service.update('123e4567-e89b-12d3-a456-426614174000', updateDto); + + expect(repository.save).toHaveBeenCalled(); + expect(result.status).toEqual('approved'); + }); + + it('should throw NotFoundException when updating non-existent verification', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect( + service.update('non-existent-id', { status: 'approved' }), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('approve', () => { + it('should approve a verification', async () => { + const approvedVerification = { ...mockVerification, status: 'approved' }; + + mockRepository.findOne.mockResolvedValue(mockVerification); + mockRepository.save.mockResolvedValue(approvedVerification); + + const result = await service.approve('123e4567-e89b-12d3-a456-426614174000'); + + expect(result.status).toBe('approved'); + expect(repository.save).toHaveBeenCalled(); + }); + + it('should throw NotFoundException when approving non-existent verification', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.approve('non-existent-id')).rejects.toThrow(NotFoundException); + }); + }); + + describe('reject', () => { + it('should reject a verification with reason', async () => { + const rejectedVerification = { + ...mockVerification, + status: 'rejected', + reason: 'Invalid documents', + }; + + mockRepository.findOne.mockResolvedValue(mockVerification); + mockRepository.save.mockResolvedValue(rejectedVerification); + + const result = await service.reject('123e4567-e89b-12d3-a456-426614174000', 'Invalid documents'); + + expect(result.status).toBe('rejected'); + expect(result.reason).toBe('Invalid documents'); + expect(repository.save).toHaveBeenCalled(); + }); + + it('should throw NotFoundException when rejecting non-existent verification', async () => { + mockRepository.findOne.mockResolvedValue(null); + + await expect( + service.reject('non-existent-id', 'reason'), + ).rejects.toThrow(NotFoundException); + }); + }); + + describe('findByIdentityId', () => { + it('should return verifications by identity id', async () => { + const verifications = [mockVerification]; + mockRepository.find.mockResolvedValue(verifications); + + const result = await service.findByIdentityId('identity-123'); + + expect(repository.find).toHaveBeenCalledWith({ + where: { identityId: 'identity-123' }, + order: { createdAt: 'DESC' }, + }); + expect(result).toEqual(verifications); + }); + }); +}); From 4db6e07ad5f1869cd1e818227c479afadffa57b6 Mon Sep 17 00:00:00 2001 From: Maverick Date: Sun, 14 Jun 2026 20:53:51 +0100 Subject: [PATCH 2/5] style: apply cargo fmt to fix Rust formatting - Fixed formatting in access_control.rs - Fixed formatting in data_sharing.rs - Fixed formatting in identity_registry.rs - Fixed formatting in lib.rs - Fixed formatting in verification.rs - Ensures CI formatting checks pass --- contracts/src/access_control.rs | 92 ++++++++++++++--------- contracts/src/data_sharing.rs | 87 ++++++++++++++-------- contracts/src/identity_registry.rs | 96 ++++++++++++++++-------- contracts/src/lib.rs | 8 +- contracts/src/verification.rs | 116 ++++++++++++++++++----------- 5 files changed, 254 insertions(+), 145 deletions(-) diff --git a/contracts/src/access_control.rs b/contracts/src/access_control.rs index 6d9dd3ad..13706268 100644 --- a/contracts/src/access_control.rs +++ b/contracts/src/access_control.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec, Map, U256}; +use soroban_sdk::{contract, contractimpl, Address, Env, Map, String, Vec, U256}; #[derive(Clone)] pub struct AccessPermission { @@ -23,9 +23,14 @@ impl AccessControl { duration_seconds: u64, ) -> u64 { grantor.require_auth(); - - let permission_id = env.storage().instance().get(&("permission_counter")).unwrap_or(0u64) + 1; - + + let permission_id = env + .storage() + .instance() + .get(&("permission_counter")) + .unwrap_or(0u64) + + 1; + let permission = AccessPermission { grantor: grantor.clone(), grantee: grantee.clone(), @@ -34,77 +39,96 @@ impl AccessControl { is_active: true, granted_at: env.ledger().timestamp(), }; - - env.storage().instance().set(&("permission_counter"), &permission_id); - env.storage().instance().set(&(permission_id, "permission"), &permission); - env.storage().instance().set(&(grantee, resource_id), &permission_id); - + + env.storage() + .instance() + .set(&("permission_counter"), &permission_id); + env.storage() + .instance() + .set(&(permission_id, "permission"), &permission); + env.storage() + .instance() + .set(&(grantee, resource_id), &permission_id); + permission_id } - + pub fn revoke_access(env: &Env, permission_id: u64) { - let mut permission: AccessPermission = env.storage().instance() + let mut permission: AccessPermission = env + .storage() + .instance() .get(&(permission_id, "permission")) .unwrap_or_else(|| panic!("Permission not found")); - + permission.grantor.require_auth(); - + permission.is_active = false; - - env.storage().instance().set(&(permission_id, "permission"), &permission); + + env.storage() + .instance() + .set(&(permission_id, "permission"), &permission); } - + pub fn check_access(env: &Env, grantee: Address, resource_id: u64) -> bool { - let permission_id: u64 = env.storage().instance() + let permission_id: u64 = env + .storage() + .instance() .get(&(grantee, resource_id)) .unwrap_or_else(|| return false); - - let permission: AccessPermission = env.storage().instance() + + let permission: AccessPermission = env + .storage() + .instance() .get(&(permission_id, "permission")) .unwrap_or_else(|| return false); - + if !permission.is_active { return false; } - + if env.ledger().timestamp() > permission.access_expiry { return false; } - + true } - + pub fn get_permission(env: &Env, permission_id: u64) -> AccessPermission { - env.storage().instance() + env.storage() + .instance() .get(&(permission_id, "permission")) .unwrap_or_else(|| panic!("Permission not found")) } - + pub fn get_permissions_by_grantee(env: &Env, grantee: Address) -> Vec { let mut permissions = Vec::new(env); - + // In a real implementation, you'd iterate through storage // For now, return empty vector as placeholder permissions } - + pub fn get_permissions_by_grantor(env: &Env, grantor: Address) -> Vec { let mut permissions = Vec::new(env); - + // In a real implementation, you'd iterate through storage // For now, return empty vector as placeholder permissions } - + pub fn extend_access(env: &Env, permission_id: u64, additional_seconds: u64) { - let mut permission: AccessPermission = env.storage().instance() + let mut permission: AccessPermission = env + .storage() + .instance() .get(&(permission_id, "permission")) .unwrap_or_else(|| panic!("Permission not found")); - + permission.grantor.require_auth(); - + permission.access_expiry += additional_seconds; - - env.storage().instance().set(&(permission_id, "permission"), &permission); + + env.storage() + .instance() + .set(&(permission_id, "permission"), &permission); } } diff --git a/contracts/src/data_sharing.rs b/contracts/src/data_sharing.rs index cfc382ed..d7b58542 100644 --- a/contracts/src/data_sharing.rs +++ b/contracts/src/data_sharing.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, String, Vec, Map}; +use soroban_sdk::{contract, contractimpl, Address, Bytes, BytesN, Env, Map, String, Vec}; #[derive(Clone)] pub struct SharedData { @@ -25,9 +25,14 @@ impl DataSharing { duration_seconds: u64, ) -> u64 { owner.require_auth(); - - let share_id = env.storage().instance().get(&("share_counter")).unwrap_or(0u64) + 1; - + + let share_id = env + .storage() + .instance() + .get(&("share_counter")) + .unwrap_or(0u64) + + 1; + let shared_data = SharedData { owner: owner.clone(), recipient: recipient.clone(), @@ -37,82 +42,98 @@ impl DataSharing { is_active: true, shared_at: env.ledger().timestamp(), }; - + env.storage().instance().set(&("share_counter"), &share_id); - env.storage().instance().set(&(share_id, "shared_data"), &shared_data); - env.storage().instance().set(&(owner, recipient, document_hash), &share_id); - + env.storage() + .instance() + .set(&(share_id, "shared_data"), &shared_data); + env.storage() + .instance() + .set(&(owner, recipient, document_hash), &share_id); + share_id } - + pub fn revoke_shared_document(env: &Env, share_id: u64) { - let mut shared_data: SharedData = env.storage().instance() + let mut shared_data: SharedData = env + .storage() + .instance() .get(&(share_id, "shared_data")) .unwrap_or_else(|| panic!("Shared document not found")); - + shared_data.owner.require_auth(); - + shared_data.is_active = false; - - env.storage().instance().set(&(share_id, "shared_data"), &shared_data); + + env.storage() + .instance() + .set(&(share_id, "shared_data"), &shared_data); } - + pub fn get_shared_document(env: &Env, share_id: u64) -> SharedData { - env.storage().instance() + env.storage() + .instance() .get(&(share_id, "shared_data")) .unwrap_or_else(|| panic!("Shared document not found")) } - + pub fn get_shared_document_by_parties( env: &Env, owner: Address, recipient: Address, document_hash: BytesN<32>, ) -> u64 { - env.storage().instance() + env.storage() + .instance() .get(&(owner, recipient, document_hash)) .unwrap_or_else(|| panic!("Shared document not found")) } - + pub fn is_share_active(env: &Env, share_id: u64) -> bool { - let shared_data: SharedData = env.storage().instance() + let shared_data: SharedData = env + .storage() + .instance() .get(&(share_id, "shared_data")) .unwrap_or_else(|| panic!("Shared document not found")); - + if !shared_data.is_active { return false; } - + if env.ledger().timestamp() > shared_data.access_expiry { return false; } - + true } - + pub fn extend_share(env: &Env, share_id: u64, additional_seconds: u64) { - let mut shared_data: SharedData = env.storage().instance() + let mut shared_data: SharedData = env + .storage() + .instance() .get(&(share_id, "shared_data")) .unwrap_or_else(|| panic!("Shared document not found")); - + shared_data.owner.require_auth(); - + shared_data.access_expiry += additional_seconds; - - env.storage().instance().set(&(share_id, "shared_data"), &shared_data); + + env.storage() + .instance() + .set(&(share_id, "shared_data"), &shared_data); } - + pub fn get_shares_by_owner(env: &Env, owner: Address) -> Vec { let mut shares = Vec::new(env); - + // In a real implementation, you'd iterate through storage // For now, return empty vector as placeholder shares } - + pub fn get_shares_by_recipient(env: &Env, recipient: Address) -> Vec { let mut shares = Vec::new(env); - + // In a real implementation, you'd iterate through storage // For now, return empty vector as placeholder shares diff --git a/contracts/src/identity_registry.rs b/contracts/src/identity_registry.rs index 5af9e6ae..f6be8ecb 100644 --- a/contracts/src/identity_registry.rs +++ b/contracts/src/identity_registry.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Vec, Map}; +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Map, String, Vec}; #[derive(Clone)] pub struct Identity { @@ -22,9 +22,14 @@ impl IdentityRegistry { ipfs_cid: String, ) -> u64 { owner.require_auth(); - - let identity_id = env.storage().instance().get(&(&owner, &document_hash)).unwrap_or(0u64) + 1; - + + let identity_id = env + .storage() + .instance() + .get(&(&owner, &document_hash)) + .unwrap_or(0u64) + + 1; + let identity = Identity { owner: owner.clone(), document_hash: document_hash.clone(), @@ -33,65 +38,92 @@ impl IdentityRegistry { created_at: env.ledger().timestamp(), revoked: false, }; - - env.storage().instance().set(&(&owner, &document_hash), &identity_id); - env.storage().instance().set(&(identity_id, "identity"), &identity); - env.storage().instance().set(&(identity_id, "owner"), &owner); - + + env.storage() + .instance() + .set(&(&owner, &document_hash), &identity_id); + env.storage() + .instance() + .set(&(identity_id, "identity"), &identity); + env.storage() + .instance() + .set(&(identity_id, "owner"), &owner); + identity_id } - + pub fn update_identity( env: &Env, identity_id: u64, document_hash: BytesN<32>, ipfs_cid: String, ) { - let owner: Address = env.storage().instance().get(&(identity_id, "owner")) + let owner: Address = env + .storage() + .instance() + .get(&(identity_id, "owner")) .unwrap_or_else(|| panic!("Identity not found")); - + owner.require_auth(); - - let mut identity: Identity = env.storage().instance().get(&(identity_id, "identity")) + + let mut identity: Identity = env + .storage() + .instance() + .get(&(identity_id, "identity")) .unwrap_or_else(|| panic!("Identity not found")); - + identity.document_hash = document_hash; identity.ipfs_cid = ipfs_cid; - - env.storage().instance().set(&(identity_id, "identity"), &identity); + + env.storage() + .instance() + .set(&(identity_id, "identity"), &identity); } - + pub fn revoke_identity(env: &Env, identity_id: u64) { - let owner: Address = env.storage().instance().get(&(identity_id, "owner")) + let owner: Address = env + .storage() + .instance() + .get(&(identity_id, "owner")) .unwrap_or_else(|| panic!("Identity not found")); - + owner.require_auth(); - - let mut identity: Identity = env.storage().instance().get(&(identity_id, "identity")) + + let mut identity: Identity = env + .storage() + .instance() + .get(&(identity_id, "identity")) .unwrap_or_else(|| panic!("Identity not found")); - + identity.revoked = true; - - env.storage().instance().set(&(identity_id, "identity"), &identity); + + env.storage() + .instance() + .set(&(identity_id, "identity"), &identity); } - + pub fn get_identity(env: &Env, identity_id: u64) -> Identity { - env.storage().instance().get(&(identity_id, "identity")) + env.storage() + .instance() + .get(&(identity_id, "identity")) .unwrap_or_else(|| panic!("Identity not found")) } - + pub fn get_identity_by_owner(env: &Env, owner: Address) -> Vec { let mut identities = Vec::new(env); - + // In a real implementation, you'd iterate through storage // For now, return empty vector as placeholder identities } - + pub fn is_verified(env: &Env, identity_id: u64) -> bool { - let identity: Identity = env.storage().instance().get(&(identity_id, "identity")) + let identity: Identity = env + .storage() + .instance() + .get(&(identity_id, "identity")) .unwrap_or_else(|| panic!("Identity not found")); - + identity.verification_status && !identity.revoked } } diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index e8e911eb..266ede32 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -1,9 +1,9 @@ -pub mod identity_registry; -pub mod verification; pub mod access_control; pub mod data_sharing; +pub mod identity_registry; +pub mod verification; -pub use identity_registry::IdentityRegistry; -pub use verification::Verification; pub use access_control::AccessControl; pub use data_sharing::DataSharing; +pub use identity_registry::IdentityRegistry; +pub use verification::Verification; diff --git a/contracts/src/verification.rs b/contracts/src/verification.rs index 924e766b..381de657 100644 --- a/contracts/src/verification.rs +++ b/contracts/src/verification.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, String, Vec, Map}; +use soroban_sdk::{contract, contractimpl, Address, BytesN, Env, Map, String, Vec}; #[derive(Clone)] pub struct VerificationRecord { @@ -23,9 +23,14 @@ impl Verification { verification_commitment: BytesN<32>, ) -> u64 { verifier.require_auth(); - - let verification_id = env.storage().instance().get(&("verification_counter")).unwrap_or(0u64) + 1; - + + let verification_id = env + .storage() + .instance() + .get(&("verification_counter")) + .unwrap_or(0u64) + + 1; + let record = VerificationRecord { identity_id, verifier: verifier.clone(), @@ -34,72 +39,99 @@ impl Verification { status: String::from_str(env, "pending"), created_at: env.ledger().timestamp(), }; - - env.storage().instance().set(&("verification_counter"), &verification_id); - env.storage().instance().set(&(verification_id, "record"), &record); - env.storage().instance().set(&(identity_id, "verification"), &verification_id); - + + env.storage() + .instance() + .set(&("verification_counter"), &verification_id); + env.storage() + .instance() + .set(&(verification_id, "record"), &record); + env.storage() + .instance() + .set(&(identity_id, "verification"), &verification_id); + verification_id } - - pub fn verify_document( - env: &Env, - verification_id: u64, - approved: bool, - reason: String, - ) { - let mut record: VerificationRecord = env.storage().instance().get(&(verification_id, "record")) + + pub fn verify_document(env: &Env, verification_id: u64, approved: bool, reason: String) { + let mut record: VerificationRecord = env + .storage() + .instance() + .get(&(verification_id, "record")) .unwrap_or_else(|| panic!("Verification record not found")); - + record.verifier.require_auth(); - + if approved { record.status = String::from_str(env, "approved"); } else { record.status = String::from_str(env, "rejected"); } - - env.storage().instance().set(&(verification_id, "record"), &record); - env.storage().instance().set(&(verification_id, "reason"), &reason); + + env.storage() + .instance() + .set(&(verification_id, "record"), &record); + env.storage() + .instance() + .set(&(verification_id, "reason"), &reason); } - + pub fn approve_verification(env: &Env, verification_id: u64) { - let mut record: VerificationRecord = env.storage().instance().get(&(verification_id, "record")) + let mut record: VerificationRecord = env + .storage() + .instance() + .get(&(verification_id, "record")) .unwrap_or_else(|| panic!("Verification record not found")); - + record.verifier.require_auth(); - + record.status = String::from_str(env, "approved"); - - env.storage().instance().set(&(verification_id, "record"), &record); + + env.storage() + .instance() + .set(&(verification_id, "record"), &record); } - + pub fn reject_verification(env: &Env, verification_id: u64, reason: String) { - let mut record: VerificationRecord = env.storage().instance().get(&(verification_id, "record")) + let mut record: VerificationRecord = env + .storage() + .instance() + .get(&(verification_id, "record")) .unwrap_or_else(|| panic!("Verification record not found")); - + record.verifier.require_auth(); - + record.status = String::from_str(env, "rejected"); - - env.storage().instance().set(&(verification_id, "record"), &record); - env.storage().instance().set(&(verification_id, "reason"), &reason); + + env.storage() + .instance() + .set(&(verification_id, "record"), &record); + env.storage() + .instance() + .set(&(verification_id, "reason"), &reason); } - + pub fn get_verification(env: &Env, verification_id: u64) -> VerificationRecord { - env.storage().instance().get(&(verification_id, "record")) + env.storage() + .instance() + .get(&(verification_id, "record")) .unwrap_or_else(|| panic!("Verification record not found")) } - + pub fn get_verification_by_identity(env: &Env, identity_id: u64) -> u64 { - env.storage().instance().get(&(identity_id, "verification")) + env.storage() + .instance() + .get(&(identity_id, "verification")) .unwrap_or_else(|| panic!("No verification found for identity")) } - + pub fn get_verification_status(env: &Env, verification_id: u64) -> String { - let record: VerificationRecord = env.storage().instance().get(&(verification_id, "record")) + let record: VerificationRecord = env + .storage() + .instance() + .get(&(verification_id, "record")) .unwrap_or_else(|| panic!("Verification record not found")); - + record.status } } From 7dd11568668010afe6fc8afe825ad12c9139ebc7 Mon Sep 17 00:00:00 2001 From: Maverick Date: Sun, 14 Jun 2026 21:17:24 +0100 Subject: [PATCH 3/5] fix: resolve clippy error in identity_registry - Changed tuple key from &(&owner, &document_hash) to (owner.clone(), document_hash.clone()) - Soroban storage requires owned values in tuple keys, not references - Fixes E0277 trait bound error for TryFromVal - All clippy checks now pass --- contracts/src/identity_registry.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/src/identity_registry.rs b/contracts/src/identity_registry.rs index 3edc5efe..86747952 100644 --- a/contracts/src/identity_registry.rs +++ b/contracts/src/identity_registry.rs @@ -29,7 +29,7 @@ impl IdentityRegistry { let identity_id = env .storage() .instance() - .get::<_, u64>(&(&owner, &document_hash)) + .get::<_, u64>(&(owner.clone(), document_hash.clone())) .unwrap_or(0u64) + 1; @@ -44,7 +44,7 @@ impl IdentityRegistry { env.storage() .instance() - .set(&(&owner, &document_hash), &identity_id); + .set(&(owner.clone(), document_hash.clone()), &identity_id); env.storage() .instance() .set(&(identity_id, "identity"), &identity); From 6797600e2535e5d8cb275ade19d84a4d60fd0752 Mon Sep 17 00:00:00 2001 From: Maverick Date: Sun, 14 Jun 2026 21:28:35 +0100 Subject: [PATCH 4/5] feat: add testutils feature to Cargo.toml - Added [features] section with testutils feature that enables soroban-sdk/testutils - Fixes CI test failure where cargo test --features testutils was failing - Allows CI to run tests with testutils feature enabled --- contracts/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a3479a5d..9d9a08ad 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -15,5 +15,8 @@ overflow-checks = true lto = "fat" codegen-units = 1 +[features] +testutils = ["soroban-sdk/testutils"] + [lib] crate-type = ["cdylib", "rlib"] From d36691c53edeb96537dee37c3da94c0d732b2b27 Mon Sep 17 00:00:00 2001 From: Maverick Date: Sun, 14 Jun 2026 21:45:45 +0100 Subject: [PATCH 5/5] fix: add no_std and update soroban-sdk version for WASM build - Updated soroban-sdk from 21.0.0 to 21.7.7 for consistency - Added panic = "abort" to release profile for WASM compatibility - Fixes E0152 duplicate lang item error (panic_impl) - WASM build now compiles successfully for wasm32-unknown-unknown target --- contracts/Cargo.toml | 9 +++++++-- contracts/src/lib.rs | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 9d9a08ad..99601bdf 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -4,16 +4,21 @@ version = "0.1.0" edition = "2021" [dependencies] -soroban-sdk = "21.0.0" +soroban-sdk = "21.7.7" [dev-dependencies] -soroban-sdk = { version = "21.0.0", features = ["testutils"] } +soroban-sdk = { version = "21.7.7", features = ["testutils"] } [profile.release] opt-level = "z" overflow-checks = true lto = "fat" codegen-units = 1 +panic = "abort" + +[profile.release-with-logs] +inherits = "release" +debug-assertions = true [features] testutils = ["soroban-sdk/testutils"] diff --git a/contracts/src/lib.rs b/contracts/src/lib.rs index 72584dc5..44664bf4 100644 --- a/contracts/src/lib.rs +++ b/contracts/src/lib.rs @@ -1,3 +1,5 @@ +#![no_std] + pub mod access_control; pub mod data_sharing; pub mod errors;