From a59ff624cee702c491cac5661290f58d76dcae7c Mon Sep 17 00:00:00 2001 From: amossamuel851-tech Date: Wed, 26 Aug 2026 08:38:45 +0000 Subject: [PATCH] fix(security): replace simulated risk checks with honest unverified results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the fabricated address/transaction risk scores derived from hashes and placeholder known-scam lists. Address and transaction checks now report verified:false with an explicit "unable to verify" result whenever no real screening provider produced a score, and the UI surfaces an unverified state instead of presenting a guessed score as fact. Wallet similarity and known-scam-contract checks now operate on the real blocklist, and the placeholder isSuspiciousAddress recipient anomaly is removed. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- src/components/TransactionConfirmation.tsx | 18 +- src/hooks/useSecurity.ts | 12 ++ .../__tests__/blockchainSecurity.test.ts | 156 ++++++++++----- .../__tests__/transactionMonitor.test.ts | 57 ++++++ .../__tests__/walletValidator.test.ts | 36 +++- src/utils/security/blockchainSecurity.ts | 180 ++++++------------ src/utils/security/transactionMonitor.ts | 26 --- src/utils/security/walletValidator.ts | 27 +-- 8 files changed, 285 insertions(+), 227 deletions(-) create mode 100644 src/utils/security/__tests__/transactionMonitor.test.ts 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 f9c8dbab..91240c97 100644 --- a/src/utils/security/__tests__/blockchainSecurity.test.ts +++ b/src/utils/security/__tests__/blockchainSecurity.test.ts @@ -1,5 +1,16 @@ 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(), + info: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + // Mock fetch for API calls global.fetch = jest.fn(); @@ -48,7 +59,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low' as const, categories: ['low_risk'], labels: ['monitor'], - description: 'Address appears to have normal activity' + description: 'Address appears to have normal activity', + verified: true }; // Manually set cache @@ -69,7 +81,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low' as const, categories: ['low_risk'], labels: ['monitor'], - description: 'Address appears to have normal activity' + description: 'Address appears to have normal activity', + verified: true }; // Set expired cache (5 minutes + 1 second ago) @@ -81,17 +94,20 @@ describe('BlockchainSecurityService', () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, + /** Mock proxy JSON response with a verified numeric score. */ json: async () => ({ risk_score: 50, categories: ['medium_risk'] }) }); const result = await service.checkAddressRisk(testAddress); expect(fetch).toHaveBeenCalled(); expect(result.riskScore).toBeGreaterThan(0); + expect(result.verified).toBe(true); }); it('should call the local proxy endpoint', async () => { (global.fetch as jest.Mock).mockResolvedValueOnce({ ok: true, + /** Mock proxy JSON response with a verified numeric score. */ json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: [], description: 'Normal' }) }); @@ -102,16 +118,41 @@ describe('BlockchainSecurityService', () => { expect(fetchUrl).toContain(encodeURIComponent(testAddress)); }); - it('should fall back to simulation when proxy returns non-ok', async () => { + 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' }) }); const result = await service.checkAddressRisk(testAddress); - expect(result.riskScore).toBeGreaterThanOrEqual(0); - expect(result.riskScore).toBeLessThanOrEqual(100); + 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('should return default risk score on API failure', async () => { @@ -124,11 +165,12 @@ describe('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 }); }); - it('should handle different risk score ranges correctly', async () => { + it('should map proxy risk scores to risk levels and mark verified', async () => { // Mock different risk scores const testCases = [ { score: 10, expectedLevel: 'low' }, @@ -138,17 +180,17 @@ describe('BlockchainSecurityService', () => { ]; for (const { score, expectedLevel } of testCases) { - // Clear cache and mock the simulation to return specific score + // Clear cache and mock the proxy to return specific score service.clearCache(); - jest.spyOn(service as any, 'simulateAddressRiskCheck').mockResolvedValueOnce({ - score, - categories: [`${expectedLevel}_risk`], - labels: [], - description: 'Test' + (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); } }); }); @@ -165,7 +207,8 @@ describe('BlockchainSecurityService', () => { sanctions: false, mixer: false, gambling: false, - scam: false + scam: false, + verified: true }; service['cache'].set(`tx_${testHash}`, { @@ -177,9 +220,7 @@ describe('BlockchainSecurityService', () => { expect(result).toEqual(cachedResult); }); - it('should return default transaction risk on failure', async () => { - jest.spyOn(service as any, 'simulateTransactionRiskCheck').mockRejectedValueOnce(new Error('Simulation failed')); - + it('should return an unverified default (no fabricated score from the hash)', async () => { const result = await service.checkTransactionRisk(testHash); expect(result).toEqual({ hash: testHash, @@ -189,24 +230,9 @@ describe('BlockchainSecurityService', () => { sanctions: false, mixer: false, gambling: false, - scam: false - }); - }); - - it('should handle transaction with sanctions flag', async () => { - jest.spyOn(service as any, 'simulateTransactionRiskCheck').mockResolvedValueOnce({ - score: 95, - alerts: ['Transaction involves sanctioned address'], - sanctions: true, - mixer: false, - gambling: false, - scam: false + scam: false, + verified: false }); - - const result = await service.checkTransactionRisk(testHash); - expect(result.sanctions).toBe(true); - expect(result.alerts).toContain('Transaction involves sanctioned address'); - expect(result.riskLevel).toBe('critical'); }); }); @@ -218,7 +244,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'critical', categories: ['sanctions'], labels: [], - description: 'Sanctioned' + description: 'Sanctioned', + verified: true }); const result = await service.checkSanctions('0x123'); @@ -232,7 +259,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['low_risk'], labels: [], - description: 'Clean' + description: 'Clean', + verified: true }); const result = await service.checkSanctions('0x123'); @@ -255,7 +283,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'high', categories: ['mixer'], labels: [], - description: 'Mixer' + description: 'Mixer', + verified: true }); const result = await service.checkMixer('0x123'); @@ -269,7 +298,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['low_risk'], labels: [], - description: 'Clean' + description: 'Clean', + verified: true }); const result = await service.checkMixer('0x123'); @@ -285,7 +315,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'high', categories: ['scam', 'mixer'], labels: ['suspicious'], - description: 'High risk address' + description: 'High risk address', + verified: true }); const alerts = await service.getSecurityAlerts('0x123'); @@ -315,7 +346,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['low_risk'], labels: [], - description: 'Clean address' + description: 'Clean address', + verified: true })); const result = await service.validateTransaction(fromAddress, toAddress, value); @@ -323,6 +355,22 @@ describe('BlockchainSecurityService', () => { 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('should block transaction with critical risk sender', async () => { @@ -332,7 +380,8 @@ describe('BlockchainSecurityService', () => { riskLevel: address === fromAddress ? 'critical' : 'low', categories: address === fromAddress ? ['high_risk'] : ['low_risk'], labels: [], - description: address === fromAddress ? 'Critical risk' : 'Clean' + description: address === fromAddress ? 'Critical risk' : 'Clean', + verified: true })); const result = await service.validateTransaction(fromAddress, toAddress, value); @@ -347,7 +396,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['sanctions'], labels: [], - description: 'Sanctioned' + description: 'Sanctioned', + verified: true }); const result = await service.validateTransaction(fromAddress, toAddress, value); @@ -363,10 +413,13 @@ describe('BlockchainSecurityService', () => { riskLevel: address === toAddress ? 'high' : 'low', categories: address === toAddress ? ['high_risk'] : ['low_risk'], labels: [], - description: address === toAddress ? 'Risky recipient' : 'Clean sender' + description: address === toAddress ? 'Risky recipient' : 'Clean sender', + verified: true })); - const result = await service.validateTransaction(fromAddress, toAddress, value); + // 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); expect(result.warnings).toContain('High-value transaction to risky address'); }); @@ -378,7 +431,8 @@ describe('BlockchainSecurityService', () => { riskLevel: 'low', categories: ['mixer'], labels: [], - description: 'Mixer' + description: 'Mixer', + verified: true }); const result = await service.validateTransaction(fromAddress, toAddress, value); @@ -386,12 +440,13 @@ describe('BlockchainSecurityService', () => { expect(result.warnings).toContain('Transaction involves mixer-associated address'); }); - it('should handle validation errors gracefully', async () => { + it('should handle validation errors gracefully and report unverified', async () => { jest.spyOn(service, 'checkAddressRisk').mockRejectedValue(new Error('API Error')); 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); }); }); @@ -466,5 +521,12 @@ describe('BlockchainSecurityService', () => { }); }); }); + + describe('no simulated checks remain', () => { + it('does not expose simulateAddressRiskCheck or simulateTransactionRiskCheck', () => { + expect((service as any).simulateAddressRiskCheck).toBeUndefined(); + expect((service as any).simulateTransactionRiskCheck).toBeUndefined(); + }); + }); }); -}); \ No newline at end of file +}); 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()); } /**