diff --git a/src/components/TransactionConfirmation.tsx b/src/components/TransactionConfirmation.tsx index f65628ad..2532d84a 100644 --- a/src/components/TransactionConfirmation.tsx +++ b/src/components/TransactionConfirmation.tsx @@ -457,10 +457,12 @@ export const TransactionConfirmation: React.FC< )}
- {validation.riskScore >= 50 ? ( + {!validation.riskVerified ? ( + + ) : validation.riskScore >= 50 ? ( @@ -472,18 +474,22 @@ export const TransactionConfirmation: React.FC<

Security Assessment

- {getRiskLevelText(validation.riskScore)} + {validation.riskVerified + ? getRiskLevelText(validation.riskScore) + : "Not verified"}

- Risk Score: {validation.riskScore}/100 + {validation.riskVerified + ? `Risk Score: ${validation.riskScore}/100` + : "Address risk screening is unavailable, so no risk score is available. Verify the recipient address manually."}

diff --git a/src/hooks/useSecurity.ts b/src/hooks/useSecurity.ts index 0e9a5de3..8fc1c3c7 100644 --- a/src/hooks/useSecurity.ts +++ b/src/hooks/useSecurity.ts @@ -29,6 +29,11 @@ export interface TransactionValidation { warnings: string[]; blocks: string[]; requiresConfirmation: boolean; + /** + * False when address-risk screening could not run. The UI must then show an + * explicit "not verified" state rather than presenting riskScore as fact. + */ + riskVerified: boolean; details: any; } @@ -93,6 +98,9 @@ export const useSecurity = () => { // Blockchain security check const addressRisk = await blockchainSecurity.checkAddressRisk(walletAddress); + if (!addressRisk.verified) { + warnings.push('Address risk screening unavailable - address could not be verified'); + } if (addressRisk.riskLevel === 'critical') { blocks.push('Address has critical security risk'); } else if (addressRisk.riskLevel === 'high') { @@ -142,6 +150,7 @@ export const useSecurity = () => { warnings: ['Wallet not connected'], blocks: ['Wallet must be connected'], requiresConfirmation: false, + riskVerified: false, details: null }; } @@ -162,6 +171,7 @@ export const useSecurity = () => { warnings, blocks, requiresConfirmation: false, + riskVerified: false, details: null }; } @@ -208,6 +218,7 @@ export const useSecurity = () => { warnings, blocks, requiresConfirmation, + riskVerified: securityValidation.verified, details: { transactionValidation, securityValidation, @@ -224,6 +235,7 @@ export const useSecurity = () => { warnings, blocks, requiresConfirmation: false, + riskVerified: false, details: null }; } diff --git a/src/utils/security/__tests__/blockchainSecurity.test.ts b/src/utils/security/__tests__/blockchainSecurity.test.ts index 4c3c5d48..ba3b2f86 100644 --- a/src/utils/security/__tests__/blockchainSecurity.test.ts +++ b/src/utils/security/__tests__/blockchainSecurity.test.ts @@ -1,9 +1,17 @@ import { BlockchainSecurityService, SecurityServiceConfig } from '../blockchainSecurity'; +// Mock the canonical logger so importing blockchainSecurity does not pull in the +// logger → csrfClient → walletStore → chains module graph (which needs browser globals). jest.mock('@/utils/logger', () => ({ - logger: { error: jest.fn(), warn: jest.fn(), info: jest.fn(), debug: jest.fn() }, + logger: { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, })); +// Mock fetch for API calls global.fetch = jest.fn(); const mockConfig: SecurityServiceConfig = { @@ -31,65 +39,204 @@ describe('BlockchainSecurityService', () => { }); describe('checkAddressRisk', () => { - const address = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + const testAddress = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + + it('should return cached result when available and not expired', async () => { + const cachedResult = { + address: testAddress, + riskScore: 25, + riskLevel: 'low' as const, + categories: ['low_risk'], + labels: ['monitor'], + description: 'Address appears to have normal activity', + verified: true + }; + + // Manually set cache + service['cache'].set(`address_${testAddress}`, { + data: cachedResult, + timestamp: Date.now() + }); + + const result = await service.checkAddressRisk(testAddress); + expect(result).toEqual(cachedResult); + expect(fetch).not.toHaveBeenCalled(); + }); + + it('should fetch new data when cache is expired', async () => { + const cachedResult = { + address: testAddress, + riskScore: 25, + riskLevel: 'low' as const, + categories: ['low_risk'], + labels: ['monitor'], + description: 'Address appears to have normal activity', + verified: true + }; + + // Set expired cache (5 minutes + 1 second ago) + const expiredTime = Date.now() - (5 * 60 * 1000) - 1000; + service['cache'].set(`address_${testAddress}`, { + data: cachedResult, + timestamp: expiredTime + }); it('returns risk data with level and score', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: ['monitor'], description: 'Clean' }), + /** Mock proxy JSON response with a verified numeric score. */ + json: async () => ({ risk_score: 50, categories: ['medium_risk'] }) }); - const result = await service.checkAddressRisk(address); - expect(result).toHaveProperty('address', address); - expect(result).toHaveProperty('riskScore'); - expect(result).toHaveProperty('riskLevel'); - expect(['low', 'medium', 'high', 'critical']).toContain(result.riskLevel); - expect(result.riskScore).toBeGreaterThanOrEqual(0); - expect(result.riskScore).toBeLessThanOrEqual(100); + const result = await service.checkAddressRisk(testAddress); + expect(fetch).toHaveBeenCalled(); + expect(result.riskScore).toBeGreaterThan(0); + expect(result.verified).toBe(true); }); it('caches results for 5 minutes', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, - json: async () => ({ risk_score: 20, categories: [], labels: [], description: '' }), + /** Mock proxy JSON response with a verified numeric score. */ + json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: [], description: 'Normal' }) }); await service.checkAddressRisk(address); expect(global.fetch).toHaveBeenCalledTimes(1); - const result2 = await service.checkAddressRisk(address); - expect(global.fetch).toHaveBeenCalledTimes(1); - expect(result2.riskScore).toBe(20); + it('should return an unverified result when proxy returns non-ok', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 502, + /** Mock proxy JSON error response. */ + json: async () => ({ error: 'Bad gateway' }) + }); - jest.advanceTimersByTime(5 * 60 * 1000 - 1); - await service.checkAddressRisk(address); - expect(global.fetch).toHaveBeenCalledTimes(1); + const result = await service.checkAddressRisk(testAddress); + expect(result.verified).toBe(false); + expect(result.categories).toEqual(['unknown']); + expect(result.labels).toEqual(['unable_to_verify']); + }); + + it('should return an unverified result when proxy response has no numeric score', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + /** Mock proxy JSON response missing a numeric score. */ + json: async () => ({ categories: ['low_risk'] }) + }); + + const result = await service.checkAddressRisk(testAddress); + expect(result.verified).toBe(false); + expect(result.categories).toEqual(['unknown']); + }); + + it('should return an unverified result when proxy explicitly reports verified:false', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + /** Mock proxy JSON response explicitly reporting unverified. */ + json: async () => ({ risk_score: 10, verified: false }) + }); + + const result = await service.checkAddressRisk(testAddress); + expect(result.verified).toBe(false); }); it('uses simulated data when API fails', async () => { (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); - const result = await service.checkAddressRisk(address); - expect(result).toHaveProperty('riskScore'); - expect(result.riskScore).toBeGreaterThanOrEqual(0); - expect(result.riskScore).toBeLessThanOrEqual(100); - expect(result.categories).toBeDefined(); - expect(result.labels).toBeDefined(); + const result = await service.checkAddressRisk(testAddress); + expect(result).toEqual({ + address: testAddress, + riskScore: 50, + riskLevel: 'medium', + categories: ['unknown'], + labels: ['unable_to_verify'], + description: 'Unable to verify address risk due to service unavailability', + verified: false + }); }); - it('uses simulated data when API returns non-ok', async () => { - (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: false, status: 500 }); + it('should map proxy risk scores to risk levels and mark verified', async () => { + // Mock different risk scores + const testCases = [ + { score: 10, expectedLevel: 'low' }, + { score: 40, expectedLevel: 'medium' }, + { score: 60, expectedLevel: 'high' }, + { score: 85, expectedLevel: 'critical' } + ]; + + for (const { score, expectedLevel } of testCases) { + // Clear cache and mock the proxy to return specific score + service.clearCache(); + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + /** Mock proxy JSON response with a verified score for the test case. */ + json: async () => ({ risk_score: score, categories: [`${expectedLevel}_risk`] }) + }); + + const result = await service.checkAddressRisk(testAddress); + expect(result.riskLevel).toBe(expectedLevel); + expect(result.verified).toBe(true); + } + }); + }); - const result = await service.checkAddressRisk(address); - expect(result).toHaveProperty('riskScore'); - expect(result.riskScore).toBeGreaterThanOrEqual(0); + describe('checkTransactionRisk', () => { + const testHash = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + + it('should return cached transaction risk when available', async () => { + const cachedResult = { + hash: testHash, + riskScore: 30, + riskLevel: 'medium' as const, + alerts: ['Test alert'], + sanctions: false, + mixer: false, + gambling: false, + scam: false, + verified: true + }; + + service['cache'].set(`tx_${testHash}`, { + data: cachedResult, + timestamp: Date.now() + }); + + const result = await service.checkTransactionRisk(testHash); + expect(result).toEqual(cachedResult); + }); + + it('should return an unverified default (no fabricated score from the hash)', async () => { + const result = await service.checkTransactionRisk(testHash); + expect(result).toEqual({ + hash: testHash, + riskScore: 50, + riskLevel: 'medium', + alerts: ['Unable to verify transaction risk'], + sanctions: false, + mixer: false, + gambling: false, + scam: false, + verified: false + }); }); }); - describe('validateTransaction', () => { - const from = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; - const to = '0x1234567890123456789012345678901234567890'; - const value = '1000000000000000000'; + describe('checkSanctions', () => { + it('should return true when address is sanctioned', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ + address: '0x123', + riskScore: 90, + riskLevel: 'critical', + categories: ['sanctions'], + labels: [], + description: 'Sanctioned', + verified: true + }); + + const result = await service.checkSanctions('0x123'); + expect(result).toBe(true); + }); it('returns valid for clean transaction', async () => { jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ @@ -99,11 +246,35 @@ describe('BlockchainSecurityService', () => { categories: ['low_risk'], labels: [], description: 'Clean', + verified: true }); - const result = await service.validateTransaction(from, to, value); - expect(result.isValid).toBe(true); - expect(result.blocks).toHaveLength(0); + const result = await service.checkSanctions('0x123'); + expect(result).toBe(false); + }); + + it('should return false on error', async () => { + jest.spyOn(service, 'checkAddressRisk').mockRejectedValueOnce(new Error('API Error')); + + const result = await service.checkSanctions('0x123'); + expect(result).toBe(false); + }); + }); + + describe('checkMixer', () => { + it('should return true when address is associated with mixer', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ + address: '0x123', + riskScore: 70, + riskLevel: 'high', + categories: ['mixer'], + labels: [], + description: 'Mixer', + verified: true + }); + + const result = await service.checkMixer('0x123'); + expect(result).toBe(true); }); it('blocks sanctioned addresses', async () => { @@ -113,13 +284,39 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['sanctions'], labels: [], - description: 'Sanctioned', + description: 'Clean', + verified: true }); - const result = await service.validateTransaction(from, to, value); - expect(result.isValid).toBe(false); - expect(result.blocks).toContain('Sender address is on sanctions list'); - expect(result.blocks).toContain('Recipient address is on sanctions list'); + const result = await service.checkMixer('0x123'); + expect(result).toBe(false); + }); + }); + + describe('getSecurityAlerts', () => { + it('should return security alerts for address', async () => { + jest.spyOn(service, 'checkAddressRisk').mockResolvedValueOnce({ + address: '0x123', + riskScore: 80, + riskLevel: 'high', + categories: ['scam', 'mixer'], + labels: ['suspicious'], + description: 'High risk address', + verified: true + }); + + const alerts = await service.getSecurityAlerts('0x123'); + expect(alerts).toHaveLength(2); + expect(alerts[0].type).toBe('scam'); + expect(alerts[0].severity).toBe('high'); + expect(alerts[1].type).toBe('mixer'); + }); + + it('should return empty array on error', async () => { + jest.spyOn(service, 'checkAddressRisk').mockRejectedValueOnce(new Error('API Error')); + + const alerts = await service.getSecurityAlerts('0x123'); + expect(alerts).toEqual([]); }); it('warns about mixer interactions', async () => { @@ -129,12 +326,31 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['mixer'], labels: [], - description: 'Mixer associated', - }); + description: 'Clean address', + verified: true + })); const result = await service.validateTransaction(from, to, value); expect(result.isValid).toBe(true); - expect(result.warnings).toContain('Transaction involves mixer-associated address'); + expect(result.riskScore).toBe(10); + expect(result.warnings).toHaveLength(0); + expect(result.blocks).toHaveLength(0); + expect(result.verified).toBe(true); + }); + + it('should report verified:false when screening did not run', async () => { + jest.spyOn(service, 'checkAddressRisk').mockImplementation(async (address) => ({ + address, + riskScore: 50, + riskLevel: 'medium', + categories: ['unknown'], + labels: ['unable_to_verify'], + description: 'Unable to verify address risk due to service unavailability', + verified: false + })); + + const result = await service.validateTransaction(fromAddress, toAddress, value); + expect(result.verified).toBe(false); }); it('blocks critical risk addresses', async () => { @@ -144,7 +360,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'critical' as const, categories: ['high_risk'], labels: [], - description: 'Critical', + description: address === fromAddress ? 'Critical risk' : 'Clean', + verified: true })); const result = await service.validateTransaction(from, to, value); @@ -160,7 +377,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: [], labels: [], - description: '', + description: 'Sanctioned', + verified: true }); const result = await service.validateTransaction(from, to, '0x1'); @@ -174,10 +392,13 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: [], labels: [], - description: '', - }); + description: address === toAddress ? 'Risky recipient' : 'Clean sender', + verified: true + })); - const result = await service.validateTransaction(from, to, '1000000000000000000'); + // Value must exceed the 1 ETH threshold for the high-value warning to fire. + const highValue = '2000000000000000000'; // 2 ETH + const result = await service.validateTransaction(fromAddress, toAddress, highValue); expect(result.isValid).toBe(true); }); @@ -188,25 +409,21 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: [], labels: [], - description: '', + description: 'Mixer', + verified: true }); const result = await service.validateTransaction(from, to, '1.5'); expect(result.isValid).toBe(true); }); - it('handles scientific notation value format', async () => { - jest.spyOn(service, 'checkAddressRisk').mockResolvedValue({ - address: 'test', - riskScore: 5, - riskLevel: 'low', - categories: [], - labels: [], - description: '', - }); + it('should handle validation errors gracefully and report unverified', async () => { + jest.spyOn(service, 'checkAddressRisk').mockRejectedValue(new Error('API Error')); - const result = await service.validateTransaction(from, to, '1e18'); - expect(result.isValid).toBe(true); + const result = await service.validateTransaction(fromAddress, toAddress, value); + expect(result.isValid).toBe(true); // Should not block on errors + expect(result.warnings).toContain('Unable to complete security validation'); + expect(result.verified).toBe(false); }); }); @@ -232,5 +449,12 @@ describe('BlockchainSecurityService', () => { await service.checkAddressRisk(address); expect(global.fetch).toHaveBeenCalledTimes(2); }); + + describe('no simulated checks remain', () => { + it('does not expose simulateAddressRiskCheck or simulateTransactionRiskCheck', () => { + expect((service as any).simulateAddressRiskCheck).toBeUndefined(); + expect((service as any).simulateTransactionRiskCheck).toBeUndefined(); + }); + }); }); }); diff --git a/src/utils/security/__tests__/transactionMonitor.test.ts b/src/utils/security/__tests__/transactionMonitor.test.ts new file mode 100644 index 00000000..c99586ff --- /dev/null +++ b/src/utils/security/__tests__/transactionMonitor.test.ts @@ -0,0 +1,57 @@ +import { TransactionMonitor } from '../transactionMonitor'; + +// Mock the canonical logger to avoid pulling in the logger → walletStore → chains graph. +jest.mock('@/utils/logger', () => ({ + logger: { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + +describe('TransactionMonitor (honest recipient checks)', () => { + beforeEach(() => { + jest.clearAllMocks(); + (TransactionMonitor as any).instance = null; + }); + + it('no longer exposes the placeholder isSuspiciousAddress check', () => { + expect((TransactionMonitor.prototype as any).isSuspiciousAddress).toBeUndefined(); + }); + + it('does not fabricate a "flagged in security database" recipient anomaly', () => { + const monitor = TransactionMonitor.getInstance(); + const wallet = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + const now = Date.now(); + + for (let i = 0; i < 8; i++) { + monitor.addTransaction(wallet, { + hash: `0x${i.toString(16)}`, + to: `0x000000000000000000000000000000000000000${i}`, + value: '1', + timestamp: now - 1000 * (8 - i), + }); + } + + const anomalies = monitor.getWalletAnomalies(wallet); + const fabricated = anomalies.filter( + (a) => a.details && a.details.reason === 'Address flagged in security database' + ); + expect(fabricated).toHaveLength(0); + }); + + it('still records transactions and produces metrics', () => { + const monitor = TransactionMonitor.getInstance(); + const wallet = '0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'; + + monitor.addTransaction(wallet, { + hash: '0x1', + to: '0xabc', + value: '0', + timestamp: Date.now(), + }); + + expect(monitor.getWalletMetrics(wallet)).not.toBeNull(); + }); +}); diff --git a/src/utils/security/__tests__/walletValidator.test.ts b/src/utils/security/__tests__/walletValidator.test.ts index 62b1d294..7a85173d 100644 --- a/src/utils/security/__tests__/walletValidator.test.ts +++ b/src/utils/security/__tests__/walletValidator.test.ts @@ -1,5 +1,16 @@ import { WalletValidator, AddressValidationResult } from '../walletValidator'; +// Mock the canonical logger so importing walletValidator does not pull in the +// logger → csrfClient → walletStore → chains module graph (which needs browser globals). +jest.mock('@/utils/logger', () => ({ + logger: { + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + // Mock viem functions jest.mock('viem', () => ({ isAddress: jest.fn(), @@ -24,9 +35,13 @@ jest.mock('@/lib/viem-client', () => ({ })); describe('WalletValidator', () => { + // Snapshot the static blocklist so tests that mutate it do not leak state. + const originalRiskyWallets = [...(WalletValidator as any).RISKY_WALLETS]; + beforeEach(() => { jest.clearAllMocks(); - + (WalletValidator as any).RISKY_WALLETS = [...originalRiskyWallets]; + // Mock window.location Object.defineProperty(window, 'location', { value: { @@ -37,6 +52,10 @@ describe('WalletValidator', () => { }); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + describe('validateWalletAddressInput', () => { const mockPublicClient = require('@/lib/viem-client').publicClient; const { isAddress, getAddress } = require('viem'); @@ -738,17 +757,28 @@ describe('WalletValidator', () => { }); describe('hasSimilarityToKnownAddresses', () => { - it('should return false (placeholder implementation)', () => { + it('should return false for addresses not similar to known risky addresses', () => { const result = (WalletValidator as any).hasSimilarityToKnownAddresses('0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'); expect(result).toBe(false); }); + + it('should return true for addresses similar to a known risky address', () => { + // 0x0000000000000000000000000000000000000001 has >80 % similarity with the null address + const result = (WalletValidator as any).hasSimilarityToKnownAddresses('0x0000000000000000000000000000000000000001'); + expect(result).toBe(true); + }); }); describe('isKnownScamContract', () => { - it('should return false (placeholder implementation)', () => { + it('should return false for addresses not in the blocklist', () => { const result = (WalletValidator as any).isKnownScamContract('0x742d35Cc6634C0532925a3b8D4C9db96C4b4Db45'); expect(result).toBe(false); }); + + it('should return true for null address (in blocklist)', () => { + const result = (WalletValidator as any).isKnownScamContract('0x0000000000000000000000000000000000000000'); + expect(result).toBe(true); + }); }); describe('isSuspiciousMethod', () => { diff --git a/src/utils/security/blockchainSecurity.ts b/src/utils/security/blockchainSecurity.ts index e4ed04e7..d4de4098 100644 --- a/src/utils/security/blockchainSecurity.ts +++ b/src/utils/security/blockchainSecurity.ts @@ -1,4 +1,5 @@ import { parseEther } from 'viem'; +import { logger } from '@/utils/logger'; export interface SecurityServiceConfig { apiKey?: string; @@ -13,6 +14,12 @@ export interface AddressRiskScore { categories: string[]; labels: string[]; description: string; + /** + * True only when the score came from a real screening provider. False when + * the provider is unconfigured/unavailable — callers must not present the + * score as a real risk signal in that case. + */ + verified: boolean; } export interface TransactionRisk { @@ -24,6 +31,10 @@ export interface TransactionRisk { mixer: boolean; gambling: boolean; scam: boolean; + /** + * True only when a real transaction-risk provider produced this result. + */ + verified: boolean; } export interface SecurityAlert { @@ -147,7 +158,10 @@ export class BlockchainSecurityService { } /** - * Checks address risk score using Chainalysis-like service + * Checks address risk score using a Chainalysis-like service. + * + * Returns an "unable to verify" result (`verified: false`) whenever the + * service is not configured or unavailable, instead of fabricating a score. */ async checkAddressRisk(address: string): Promise { const cacheKey = `address_${address}`; @@ -176,7 +190,16 @@ export class BlockchainSecurityService { if (response && response.ok) { const body = await response.json(); - const score = typeof body.risk_score === 'number' ? body.risk_score : 50; + const score = typeof body.risk_score === 'number' ? body.risk_score : null; + const verified = score !== null && body.verified !== false; + + // No trustworthy score: report honestly rather than guessing. + if (!verified || score === null) { + const result = this.getDefaultRiskScore(address); + this.setCache(cacheKey, result); + return result; + } + const categories = Array.isArray(body.categories) ? body.categories : []; const result: AddressRiskScore = { address, @@ -184,24 +207,15 @@ export class BlockchainSecurityService { riskLevel: this.getRiskLevel(score), categories, labels: Array.isArray(body.labels) ? body.labels : [], - description: body.description || '' + description: body.description || '', + verified: true }; this.setCache(cacheKey, result); return result; } - // If the proxy returned an error or is unavailable, fall back to simulated check - const riskScore = await this.simulateAddressRiskCheck(address); - - const result: AddressRiskScore = { - address, - riskScore: riskScore.score, - riskLevel: this.getRiskLevel(riskScore.score), - categories: riskScore.categories, - labels: riskScore.labels, - description: riskScore.description - }; - + // Proxy unavailable: no verified risk data exists, so report honestly. + const result = this.getDefaultRiskScore(address); this.setCache(cacheKey, result); return result; @@ -212,35 +226,19 @@ export class BlockchainSecurityService { } /** - * Checks transaction risk + * Checks transaction risk. + * + * No real transaction-risk data source is integrated yet, so this returns the + * honest "unable to verify" result instead of deriving a score from the hash. */ async checkTransactionRisk(hash: string): Promise { const cacheKey = `tx_${hash}`; const cached = this.getFromCache(cacheKey); if (cached) return cached; - try { - // Simulate transaction risk check - const riskData = await this.simulateTransactionRiskCheck(hash); - - const result: TransactionRisk = { - hash, - riskScore: riskData.score, - riskLevel: this.getRiskLevel(riskData.score), - alerts: riskData.alerts, - sanctions: riskData.sanctions, - mixer: riskData.mixer, - gambling: riskData.gambling, - scam: riskData.scam - }; - - this.setCache(cacheKey, result); - return result; - - } catch (error) { - logger.error('Failed to check transaction risk:', error); - return this.getDefaultTransactionRisk(hash); - } + const result = this.getDefaultTransactionRisk(hash); + this.setCache(cacheKey, result); + return result; } /** @@ -306,14 +304,17 @@ export class BlockchainSecurityService { riskScore: number; warnings: string[]; blocks: string[]; + verified: boolean; }> { const warnings: string[] = []; const blocks: string[] = []; let totalRiskScore = 0; + let allVerified = true; try { // Check sender risk — use the highest risk score seen across all checks const senderRisk = await this.checkAddressRisk(from); + allVerified = allVerified && senderRisk.verified; totalRiskScore = Math.max(totalRiskScore, senderRisk.riskScore); // Critical risk blocks the transaction; high risk only warns @@ -325,6 +326,7 @@ export class BlockchainSecurityService { // Check recipient risk independently — both parties must be evaluated const recipientRisk = await this.checkAddressRisk(to); + allVerified = allVerified && recipientRisk.verified; totalRiskScore = Math.max(totalRiskScore, recipientRisk.riskScore); if (recipientRisk.riskLevel === 'critical') { @@ -366,6 +368,7 @@ export class BlockchainSecurityService { } catch (error) { logger.error('Failed to validate transaction:', error); // Degrade gracefully — warn rather than block on service failure + allVerified = false; warnings.push('Unable to complete security validation'); } @@ -373,95 +376,13 @@ export class BlockchainSecurityService { isValid: blocks.length === 0, riskScore: totalRiskScore, warnings, - blocks + blocks, + verified: allVerified }; } /** - * Simulates address risk check (placeholder for real API) - */ - private async simulateAddressRiskCheck(address: string): Promise<{ - score: number; - categories: string[]; - labels: string[]; - description: string; - }> { - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 100)); - - // Derive a deterministic score from the address hex so the same address - // always returns the same risk score (useful for testing/demo purposes) - const addressHash = address.toLowerCase().replace('0x', ''); - // Parse first 8 hex chars as a 32-bit integer, then mod 100 for a 0–99 score - const score = parseInt(addressHash.slice(0, 8), 16) % 100; - - const categories: string[] = []; - const labels: string[] = []; - - // Bucket the score into risk tiers and assign matching categories/labels - if (score > 80) { - categories.push('high_risk'); - labels.push('suspicious_activity'); - } else if (score > 60) { - categories.push('medium_risk'); - labels.push('requires_review'); - } else if (score > 40) { - categories.push('low_risk'); - labels.push('monitor'); - } - - // Heuristic: addresses starting with 0x000 are likely contract addresses - if (address.startsWith('0x000')) { - categories.push('contract'); - labels.push('smart_contract'); - } - - // Map score quartile to a human-readable description - const descriptions = [ - 'Address appears to have normal activity', - 'Address shows some unusual patterns', - 'Address has elevated risk factors', - 'Address requires immediate investigation' - ]; - - const description = descriptions[Math.floor(score / 25)]; - - return { score, categories, labels, description }; - } - - /** - * Simulates transaction risk check (placeholder for real API) - */ - private async simulateTransactionRiskCheck(hash: string): Promise<{ - score: number; - alerts: string[]; - sanctions: boolean; - mixer: boolean; - gambling: boolean; - scam: boolean; - }> { - // Simulate API delay - await new Promise(resolve => setTimeout(resolve, 100)); - - const hashShort = hash.slice(2, 10); - const score = parseInt(hashShort, 16) % 100; - - const alerts: string[] = []; - const sanctions = score > 90; - const mixer = score > 70 && score < 80; - const gambling = score > 60 && score < 70; - const scam = score > 85; - - if (sanctions) alerts.push('Transaction involves sanctioned address'); - if (mixer) alerts.push('Transaction involves mixer service'); - if (gambling) alerts.push('Transaction involves gambling service'); - if (scam) alerts.push('Transaction involves known scam address'); - - return { score, alerts, sanctions, mixer, gambling, scam }; - } - - /** - * Gets default risk score for failed checks + * Gets the honest default for checks that could not be performed. */ private getDefaultRiskScore(address: string): AddressRiskScore { return { @@ -470,12 +391,13 @@ export class BlockchainSecurityService { riskLevel: 'medium', categories: ['unknown'], labels: ['unable_to_verify'], - description: 'Unable to verify address risk due to service unavailability' + description: 'Unable to verify address risk due to service unavailability', + verified: false }; } /** - * Gets default transaction risk for failed checks + * Gets the honest default for transaction checks that could not be performed. */ private getDefaultTransactionRisk(hash: string): TransactionRisk { return { @@ -486,7 +408,8 @@ export class BlockchainSecurityService { sanctions: false, mixer: false, gambling: false, - scam: false + scam: false, + verified: false }; } @@ -600,16 +523,19 @@ export async function checkAddressRiskViaProxy(address: string): Promise
tx.to).filter(Boolean)); if (!previousRecipients.has(recipient) && history.length > 5) { @@ -476,15 +459,6 @@ export class TransactionMonitor { }; } - /** - * Checks if an address is suspicious - */ - private isSuspiciousAddress(address: string): boolean { - // This would integrate with external security databases - // For now, return false as placeholder - return false; - } - /** * Formats ether values */ diff --git a/src/utils/security/walletValidator.ts b/src/utils/security/walletValidator.ts index 6ed60527..4d08894c 100644 --- a/src/utils/security/walletValidator.ts +++ b/src/utils/security/walletValidator.ts @@ -54,12 +54,6 @@ export class WalletValidator { // Add more known scam addresses as needed ]; - private static readonly KNOWN_SCAM_ADDRESSES: string[] = [ - // Known scam addresses (to be updated regularly) - '0x1234567890123456789012345678901234567890', // Example scam address - // Add more known scam addresses - ]; - // Ethereum address regex pattern private static readonly ETHEREUM_ADDRESS_REGEX = /^0x[0-9a-fA-F]{40}$/; @@ -192,8 +186,7 @@ export class WalletValidator { // Blacklist check if (checkBlacklist) { const normalizedAddress = address.toLowerCase(); - if (this.RISKY_WALLETS.includes(normalizedAddress) || - this.KNOWN_SCAM_ADDRESSES.includes(normalizedAddress)) { + if (this.RISKY_WALLETS.includes(normalizedAddress)) { isBlacklisted = true; errors.push('Address is flagged as known scam or compromised'); riskScore += 50; @@ -456,23 +449,21 @@ export class WalletValidator { } /** - * Checks if address has similarity to known addresses (placeholder implementation) + * Detects addresses that are close (edit-distance) to known risky addresses, + * catching typosquatting/impersonation attempts against the blocklist. */ private static hasSimilarityToKnownAddresses(address: string): boolean { - // This would need a more sophisticated implementation - // Could compare against known exchange addresses, project addresses, etc. - return false; + const normalizedAddress = address.toLowerCase(); + return this.RISKY_WALLETS.some(known => + this.calculateSimilarity(normalizedAddress, known.toLowerCase()) > 0.8 + ); } /** - * Checks if address is a known scam contract (placeholder implementation) + * Checks if address is a known scam/compromised contract against the blocklist. */ private static isKnownScamContract(address: string): boolean { - // This would need to be maintained and updated regularly - const knownScamContracts: string[] = [ - // Add known scam contract addresses - ]; - return knownScamContracts.includes(address.toLowerCase()); + return this.RISKY_WALLETS.includes(address.toLowerCase()); } /**