diff --git a/__mocks__/viem.js b/__mocks__/viem.js index b6089ff6..ef52a311 100644 --- a/__mocks__/viem.js +++ b/__mocks__/viem.js @@ -1,45 +1,24 @@ -const asBigInt = (value) => - typeof value === 'bigint' ? value : BigInt(value); - -const formatUnitsValue = (value, decimals = 18) => { - const unitDecimals = Number(decimals); - const raw = asBigInt(value); - const sign = raw < 0n ? '-' : ''; - const abs = raw < 0n ? -raw : raw; - const base = 10n ** BigInt(unitDecimals); - const whole = abs / base; - const fraction = abs % base; - - if (unitDecimals === 0 || fraction === 0n) { - return `${sign}${whole}`; - } - - const fractionText = fraction - .toString() - .padStart(unitDecimals, '0') - .replace(/0+$/, ''); - - return `${sign}${whole}.${fractionText}`; +const mockReceipt = { + status: 'success', + blockNumber: BigInt(18000000), + transactionHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + blockHash: '0x0000000000000000000000000000000000000000000000000000000000000000', + contractAddress: null, + cumulativeGasUsed: BigInt(100000), + gasUsed: BigInt(50000), + logs: [], + logsBloom: '0x0000000000000000000000000000000000000000000000000000000000000000', + from: '0x0000000000000000000000000000000000000000', + to: '0x0000000000000000000000000000000000000000', + effectiveGasPrice: BigInt(20000000000), + type: 'eip1559', }; -const parseUnitsValue = (value, decimals = 18) => { - const unitDecimals = Number(decimals); - const text = String(value).trim(); - const sign = text.startsWith('-') ? -1n : 1n; - const unsigned = text.replace(/^[+-]/, ''); - const [whole = '0', fraction = ''] = unsigned.split('.'); - const paddedFraction = fraction - .padEnd(unitDecimals, '0') - .slice(0, unitDecimals); - const normalized = `${whole || '0'}${paddedFraction || ''}`; - const parsed = BigInt(normalized || '0'); - - return sign * parsed; +const mockClient = { + getTransactionReceipt: jest.fn().mockRejectedValue(new Error('receipt not found')), + waitForTransactionReceipt: jest.fn().mockRejectedValue(new Error('timeout')), }; -const isAddressValue = (value) => - typeof value === 'string' && /^0x[a-fA-F0-9]{40}$/.test(value); - module.exports = { createPublicClient: jest.fn((config = {}) => ({ ...config, @@ -66,4 +45,13 @@ module.exports = { parseEther: jest.fn((value) => parseUnitsValue(value, 18)), parseUnits: jest.fn(parseUnitsValue), recoverMessageAddress: jest.fn(() => Promise.resolve('0x123')), + createPublicClient: jest.fn(() => mockClient), + http: jest.fn(() => 'http://mock-transport'), + fallback: jest.fn((transports) => transports[0]), + isAddress: jest.fn((addr) => /^0x[a-fA-F0-9]{40}$/.test(addr)), + getAddress: jest.fn((addr) => addr), + isHex: jest.fn(() => true), + formatEther: jest.fn((wei) => Number(wei) / 1e18), + parseEther: jest.fn((eth) => BigInt(Math.floor(Number(eth) * 1e18))), + parseUnits: jest.fn((val, decimals) => BigInt(Number(val) * Math.pow(10, decimals))), }; diff --git a/__mocks__/viem/chains.js b/__mocks__/viem/chains.js index 4c39de3e..8d36f560 100644 --- a/__mocks__/viem/chains.js +++ b/__mocks__/viem/chains.js @@ -1,5 +1,8 @@ module.exports = { mainnet: { id: 1, name: 'Ethereum' }, + sepolia: { id: 11155111, name: 'Sepolia' }, polygon: { id: 137, name: 'Polygon' }, + polygonMumbai: { id: 80001, name: 'Polygon Mumbai' }, bsc: { id: 56, name: 'BSC' }, + bscTestnet: { id: 97, name: 'BSC Testnet' }, }; \ No newline at end of file diff --git a/package.json b/package.json index 27182833..4f539b64 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "lint": "eslint . --max-warnings=0 && node scripts/sort-package-json.mjs", "perf:budgets": "node scripts/check-performance-budgets.mjs", "perf:ci": "npm run build && npm run perf:budgets", - "sort-package-json": "node scripts/sort-package-json.mjs", - "start": "next start", + "security:check-globals": "node scripts/check-exposed-globals.mjs", + "validate:env": "node scripts/validate-env.js", "storybook": "storybook dev -p 6006", "test": "jest", "test:ci": "jest --coverage --watchAll=false --ci", diff --git a/scripts/check-exposed-globals.mjs b/scripts/check-exposed-globals.mjs new file mode 100644 index 00000000..aef1dfe0 --- /dev/null +++ b/scripts/check-exposed-globals.mjs @@ -0,0 +1,37 @@ +import { readFileSync, existsSync } from 'fs'; +import { glob } from 'glob'; + +const SENSITIVE_PATTERNS = [ + /__[A-Z][A-Z_]+__/g, +]; + +async function main() { + const files = await glob('src/**/*.{ts,tsx,js,jsx}', { + ignore: ['src/**/*.test.*', 'src/**/__tests__/**', 'node_modules/**'], + }); + + let hasError = false; + + for (const file of files) { + if (!existsSync(file)) continue; + const content = readFileSync(file, 'utf-8'); + const matches = content.match(SENSITIVE_PATTERNS[0]); + if (matches) { + for (const match of matches) { + console.error(`[FAIL] Found exposed global '${match}' in ${file}`); + hasError = true; + } + } + } + + if (hasError) { + process.exit(1); + } + + console.log('[PASS] No exposed globals found.'); +} + +main().catch((err) => { + console.error('Script failed:', err); + process.exit(1); +}); diff --git a/src/app/api/security/address-check/route.ts b/src/app/api/security/address-check/route.ts index cf9eea9f..e934afe4 100644 --- a/src/app/api/security/address-check/route.ts +++ b/src/app/api/security/address-check/route.ts @@ -1,8 +1,5 @@ import { NextRequest, NextResponse } from 'next/server'; -const CHAINALYSIS_API_KEY = process.env.CHAINALYSIS_API_KEY || ''; -const CHAINALYSIS_API_URL = process.env.CHAINALYSIS_API_URL || 'https://api.chainalysis.com/api/v2'; - export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const address = searchParams.get('address')?.trim(); @@ -11,37 +8,46 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Address parameter required' }, { status: 400 }); } - if (address.length < 10) { - return NextResponse.json({ error: 'Invalid address' }, { status: 400 }); + if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { + return NextResponse.json({ error: 'Invalid Ethereum address' }, { status: 400 }); } - if (!CHAINALYSIS_API_KEY) { + const apiKey = process.env.CHAINALYSIS_API_KEY; + + if (!apiKey) { return NextResponse.json({ + address, risk_score: 50, - risk_level: 'medium', - categories: ['unavailable'], - description: 'Chainalysis API key not configured on server', + categories: ['unknown'], + description: 'Risk check unavailable (service not configured)', }); } try { - const response = await fetch(`${CHAINALYSIS_API_URL}/address/${address}`, { - headers: { - Authorization: `Bearer ${CHAINALYSIS_API_KEY}`, - 'Content-Type': 'application/json', - }, - signal: AbortSignal.timeout(10000), - }); + const response = await fetch( + `https://api.chainalysis.com/api/v2/address/${address}`, + { + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(10000), + } + ); if (!response.ok) { - throw new Error(`Chainalysis API returned ${response.status}`); + const errorText = await response.text(); + return NextResponse.json( + { error: `Upstream service error: ${response.status}`, detail: errorText }, + { status: response.status } + ); } const data = await response.json(); return NextResponse.json(data); } catch (error) { return NextResponse.json( - { error: 'Failed to check address risk', risk_score: 50, risk_level: 'medium' }, + { error: error instanceof Error ? error.message : 'Unknown error' }, { status: 502 } ); } diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index da444062..39b2215f 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -77,27 +77,27 @@ function buildChartCSS(id: string, config: ChartConfig): string { if (!colorConfig.length) return '' - // Build CSS rules safely as a string (only uses known-safe values: - // theme keys, color strings from config, and CSS variable names) - const cssText = Object.entries(THEMES) - .map( - ([theme, prefix]) => ` -${prefix} [data-chart=${id}] { -${colorConfig - .map(([key, itemConfig]) => { - const color = - itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || - itemConfig.color - return color ? ` --color-${encodeURIComponent(key)}: ${color};` : null - }) - .filter(Boolean) - .join("\n")} -} -` - ) + const cssContent = Object.entries(THEMES) + .map(([theme, prefix]) => { + const rules = colorConfig + .map(([key, itemConfig]) => { + const color = + itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || + itemConfig.color + return color ? ` --color-${key}: ${color};` : null + }) + .filter(Boolean) + .join("\n") + return rules ? `${prefix} [data-chart=${id}] {\n${rules}\n}` : "" + }) + .filter(Boolean) .join("\n") - return + if (!cssContent) { + return null + } + + return } const ChartTooltip = Tooltip diff --git a/src/lib/__tests__/batchTransaction.test.ts b/src/lib/__tests__/batchTransaction.test.ts new file mode 100644 index 00000000..fcf7b2eb --- /dev/null +++ b/src/lib/__tests__/batchTransaction.test.ts @@ -0,0 +1,121 @@ +import type { CartItem } from '@/types/cart'; + +const mockProperty = (overrides = {}) => ({ + id: 'prop-1', + title: 'Test Property', + tokenInfo: { available: 100, price: 0.1 }, + status: 'active', + ...overrides, +}); + +const validItem: CartItem = { + id: 'item-1', + property: mockProperty(), + quantity: 1, + addedAt: new Date().toISOString(), +}; + +describe('BatchTransactionService', () => { + const walletAddress = '0x1234567890123456789012345678901234567890'; + + beforeEach(() => { + delete process.env.NEXT_PUBLIC_DEMO_TX; + jest.resetModules(); + }); + + describe('executeBatchPurchase', () => { + it('returns validation error when item quantity exceeds available', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const overPurchased: CartItem = { + ...validItem, + property: mockProperty({ tokenInfo: { available: 1, price: 0.1 } }), + quantity: 5, + }; + + const result = await BatchTransactionService.executeBatchPurchase( + [overPurchased], + walletAddress + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('Validation failed'); + expect(result.results[0].error).toContain('Insufficient tokens'); + }); + + it('returns validation error when property is inactive', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const inactiveItem: CartItem = { + ...validItem, + property: mockProperty({ status: 'inactive' }), + }; + + const result = await BatchTransactionService.executeBatchPurchase( + [inactiveItem], + walletAddress + ); + + expect(result.success).toBe(false); + }); + + it('throws when no items provided and catches error', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const result = await BatchTransactionService.executeBatchPurchase([], walletAddress); + + expect(result.success).toBe(false); + expect(result.results).toHaveLength(0); + }); + + it('uses demo mode when NEXT_PUBLIC_DEMO_TX is true', async () => { + process.env.NEXT_PUBLIC_DEMO_TX = 'true'; + jest.resetModules(); + + const { BatchTransactionService } = await import('../batchTransaction'); + const result = await BatchTransactionService.executeBatchPurchase( + [validItem], + walletAddress + ); + + expect(result.success).toBe(true); + expect(result.transactionHash).toMatch(/^0x[a-f0-9]{64}$/); + expect(result.totalGasUsed).toBeGreaterThan(0); + }); + }); + + describe('estimateGas', () => { + it('returns base gas for empty items', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const gas = BatchTransactionService.estimateGas([]); + expect(gas).toBe(0.005); + }); + + it('calculates gas proportionally to item count', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const gas1 = BatchTransactionService.estimateGas([validItem]); + const gas3 = BatchTransactionService.estimateGas([validItem, validItem, validItem]); + expect(gas3).toBeGreaterThan(gas1); + }); + }); + + describe('getTransactionStatus', () => { + it('returns pending when receipt is not available', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const result = await BatchTransactionService.getTransactionStatus( + '0x0000000000000000000000000000000000000000000000000000000000000000' + ); + + expect(result.status).toBe('pending'); + }); + }); + + describe('waitForConfirmation', () => { + it('returns timeout when transaction is not found', async () => { + const { BatchTransactionService } = await import('../batchTransaction'); + const result = await BatchTransactionService.waitForConfirmation( + '0x0000000000000000000000000000000000000000000000000000000000000000', + 100 + ); + + expect(result.status).toBe('timeout'); + }); + }); +}); diff --git a/src/lib/batchTransaction.ts b/src/lib/batchTransaction.ts index 2c7e278b..bae5211d 100644 --- a/src/lib/batchTransaction.ts +++ b/src/lib/batchTransaction.ts @@ -1,7 +1,9 @@ import type { CartItem } from '@/types/cart'; import type { BatchTransactionResult } from '@/types/cart'; import { logger } from '@/utils/logger'; -import { generateMockTxHash } from '@/utils/secureId'; +import { publicClient } from '@/lib/viem-client'; + +const DEMO_MODE = process.env.NEXT_PUBLIC_DEMO_TX === 'true'; export class BatchTransactionService { @@ -27,62 +29,14 @@ export class BatchTransactionService { }; } - if (IS_DEMO_MODE) { - await new Promise(resolve => setTimeout(resolve, 2000)); - - // Generate mock transaction hash using secure random values - const transactionHash = generateMockTxHash(); - - const results = items.map(item => ({ - propertyId: item.property.id, - success: rand1 > 0.1, // 90% success rate for demo - transactionHash: rand2 > 0.1 ? transactionHash : undefined, - error: rand3 > 0.1 ? undefined : 'Transaction failed: Insufficient gas', - }; - }); - - const allSuccessful = results.every(result => result.success); - const totalGasUsed = items.length * 0.0025 + 0.005; - - return { - success: allSuccessful, - transactionHash: allSuccessful ? transactionHash : undefined, - results, - totalGasUsed, - error: allSuccessful ? undefined : 'Some transactions failed' - }; + if (DEMO_MODE) { + return this.executeDemoBatchPurchase(items); } - const txPromises = items.map(async (item) => { - try { - const hash = `0x${Array.from({length: 64}, () => - Math.floor(Math.random() * 16).toString(16)).join('')}`; - - const receipt = await publicClient.waitForTransactionReceipt({ hash }); - - return { - propertyId: item.property.id, - success: receipt.status === 'success', - transactionHash: hash, - error: receipt.status !== 'success' ? 'Transaction reverted' : undefined, - }; - } catch (err) { - return { - propertyId: item.property.id, - success: false, - error: err instanceof Error ? err.message : 'Transaction failed', - }; - } - }); - - const results = await Promise.all(txPromises); - const allSuccessful = results.every(result => result.success); - - return { - success: allSuccessful, - results, - error: allSuccessful ? undefined : 'Some transactions failed', - }; + // In production, this would submit a multicall transaction to the contract + // and wait for the receipt using viem's waitForTransactionReceipt. + // The actual contract interaction is chain-specific and requires a wallet client. + throw new Error('Production batch execution requires a configured wallet client'); } catch (error) { logger.error('Batch transaction failed:', error); return { @@ -97,6 +51,41 @@ export class BatchTransactionService { } } + private static async executeDemoBatchPurchase( + items: CartItem[] + ): Promise { + // Simulate blockchain transaction delay + await new Promise(resolve => setTimeout(resolve, 2000)); + + const transactionHash = `0x${Array.from({length: 64}, () => + Math.floor(Math.random() * 16).toString(16)).join('')}`; + + const results = items.map(item => ({ + propertyId: item.property.id, + success: true, + transactionHash, + })); + + const totalGasUsed = items.length * 0.0025 + 0.005; + + logger.info('Demo batch transaction completed', { + success: true, + transactionHash, + totalGasUsed, + itemsProcessed: items.length + }); + + return { + success: true, + transactionHash, + results, + totalGasUsed, + }; + } + + /** + * Estimate gas for batch transaction + */ static estimateGas(items: CartItem[]): number { const BASE_GAS = 0.005; const GAS_PER_TRANSACTION = 0.0025; @@ -109,36 +98,32 @@ export class BatchTransactionService { item.property.status === 'active'; } + /** + * Get transaction status using viem publicClient + */ static async getTransactionStatus(transactionHash: string): Promise<{ status: 'pending' | 'confirmed' | 'failed'; blockNumber?: number; confirmations?: number; }> { - // Mock transaction status check - await new Promise(resolve => setTimeout(resolve, 1000)); - - // Simulate different statuses - const random = crypto.getRandomValues(new Uint8Array(1))[0] / 256; - if (random < 0.7) { - const blockBytes = crypto.getRandomValues(new Uint8Array(4)); - const blockNumber = ((blockBytes[0] << 24) | (blockBytes[1] << 16) | (blockBytes[2] << 8) | blockBytes[3]) >>> 0; - const confirmBytes = crypto.getRandomValues(new Uint8Array(1))[0]; - return { - status: 'confirmed', - blockNumber: (blockNumber % 1000000) + 18000000, - confirmations: (confirmBytes % 50) + 1 - }; - } else if (random < 0.9) { - return { - status: 'pending' - }; - } else { + try { + const receipt = await publicClient.getTransactionReceipt({ + hash: transactionHash as `0x${string}`, + }); + return { - status: 'failed' + status: receipt.status === 'success' ? 'confirmed' : 'failed', + blockNumber: Number(receipt.blockNumber), + confirmations: 1, }; + } catch { + return { status: 'pending' }; } } + /** + * Wait for transaction confirmation using viem's waitForTransactionReceipt + */ static async waitForConfirmation( transactionHash: string, maxWaitTime: number = 300000 @@ -147,18 +132,18 @@ export class BatchTransactionService { blockNumber?: number; confirmations?: number; }> { - const startTime = Date.now(); - - while (Date.now() - startTime < maxWaitTime) { - const status = await this.getTransactionStatus(transactionHash); - - if (status.status === 'confirmed' || status.status === 'failed') { - return status; - } + try { + const receipt = await publicClient.waitForTransactionReceipt({ + hash: transactionHash as `0x${string}`, + timeout: maxWaitTime, + }); - await new Promise(resolve => setTimeout(resolve, 5000)); + return { + status: receipt.status === 'success' ? 'confirmed' : 'failed', + blockNumber: Number(receipt.blockNumber), + }; + } catch { + return { status: 'timeout' }; } - - return { status: 'timeout' }; } } diff --git a/src/utils/security/__tests__/blockchainSecurity.test.ts b/src/utils/security/__tests__/blockchainSecurity.test.ts index 54b19492..f9c8dbab 100644 --- a/src/utils/security/__tests__/blockchainSecurity.test.ts +++ b/src/utils/security/__tests__/blockchainSecurity.test.ts @@ -6,9 +6,9 @@ global.fetch = jest.fn(); describe('BlockchainSecurityService', () => { let service: BlockchainSecurityService; const mockConfig: SecurityServiceConfig = { - baseUrl: 'https://api.test.com', + baseUrl: 'http://localhost:3000', timeout: 5000, - apiKey: 'test-api-key' + apiKey: undefined }; beforeEach(() => { @@ -89,8 +89,33 @@ describe('BlockchainSecurityService', () => { expect(result.riskScore).toBeGreaterThan(0); }); + it('should call the local proxy endpoint', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: true, + json: async () => ({ risk_score: 30, categories: ['low_risk'], labels: [], description: 'Normal' }) + }); + + await service.checkAddressRisk(testAddress); + + const fetchUrl = (global.fetch as jest.Mock).mock.calls[0][0]; + expect(fetchUrl).toContain('/api/security/address-check'); + expect(fetchUrl).toContain(encodeURIComponent(testAddress)); + }); + + it('should fall back to simulation when proxy returns non-ok', async () => { + (global.fetch as jest.Mock).mockResolvedValueOnce({ + ok: false, + status: 502, + json: async () => ({ error: 'Bad gateway' }) + }); + + const result = await service.checkAddressRisk(testAddress); + expect(result.riskScore).toBeGreaterThanOrEqual(0); + expect(result.riskScore).toBeLessThanOrEqual(100); + }); + it('should return default risk score on API failure', async () => { - (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('API Error')); + (global.fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); const result = await service.checkAddressRisk(testAddress); expect(result).toEqual({ diff --git a/src/utils/security/blockchainSecurity.ts b/src/utils/security/blockchainSecurity.ts index 7ab5297d..12d9bfe2 100644 --- a/src/utils/security/blockchainSecurity.ts +++ b/src/utils/security/blockchainSecurity.ts @@ -155,35 +155,42 @@ export class BlockchainSecurityService { if (cached) return cached; try { - // Try calling a remote API if available. If `fetch` returns a Promise - // (for example when tests mock it), await it and use the response. - // Otherwise, fall back to the local simulation to preserve test behavior. - const fetchResult = typeof fetch === 'function' ? fetch(`${this.config.baseUrl}/address/${address}`, { - headers: this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {} - }) : null; - - if (fetchResult && typeof fetchResult.then === 'function') { - const response = await fetchResult; - if (response && response.ok) { - const body = await response.json(); - const score = typeof body.risk_score === 'number' ? body.risk_score : 50; - const categories = Array.isArray(body.categories) ? body.categories : []; - const result: AddressRiskScore = { - address, - riskScore: score, - riskLevel: this.getRiskLevel(score), - categories, - labels: Array.isArray(body.labels) ? body.labels : [], - description: body.description || '' - }; - this.setCache(cacheKey, result); - return result; - } - // If response not ok, throw to be caught below and return default - throw new Error('Remote service returned non-OK response'); + // Call the local API proxy route which securely holds the API key server-side. + // This avoids exposing the key in the client bundle. + const baseUrl = typeof window !== 'undefined' + ? window.location.origin + : this.config.baseUrl; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + let response; + try { + response = await fetch( + `${baseUrl}/api/security/address-check?address=${encodeURIComponent(address)}`, + { signal: controller.signal } + ); + } finally { + clearTimeout(timeoutId); } - // No remote fetch available — use the internal simulation + if (response && response.ok) { + const body = await response.json(); + const score = typeof body.risk_score === 'number' ? body.risk_score : 50; + const categories = Array.isArray(body.categories) ? body.categories : []; + const result: AddressRiskScore = { + address, + riskScore: score, + riskLevel: this.getRiskLevel(score), + categories, + labels: Array.isArray(body.labels) ? body.labels : [], + description: body.description || '' + }; + 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 = { @@ -573,15 +580,14 @@ export class BlockchainSecurityService { } } -// Server-side singleton (only use this on the server; the API key stays server-side) -export function createServerSecurityService(config?: SecurityServiceConfig): BlockchainSecurityService { - const effectiveConfig = config ?? { - baseUrl: process.env.CHAINALYSIS_API_URL || 'https://api.chainalysis.com/api/v2', - timeout: 10000, - apiKey: process.env.CHAINALYSIS_API_KEY || undefined, - }; - return BlockchainSecurityService.getInstance(effectiveConfig); -} +// Default configuration for development +const defaultConfig: SecurityServiceConfig = { + baseUrl: 'http://localhost:3000', + timeout: 10000, + // API key is now configured only on the server side via CHAINALYSIS_API_KEY env var. + // The browser never has access to this key. + apiKey: undefined +}; // Client-side proxy: calls our own API endpoint so the API key never reaches the browser. export async function checkAddressRiskViaProxy(address: string): Promise { diff --git a/src/utils/security/transactionSecurity.ts b/src/utils/security/transactionSecurity.ts index bb14a4b0..b3f5c7f1 100644 --- a/src/utils/security/transactionSecurity.ts +++ b/src/utils/security/transactionSecurity.ts @@ -95,7 +95,6 @@ function getSessionSalt(): string { } return salt; } -const sessionDeviceId = typeof crypto !== 'undefined' ? crypto.randomUUID() : 'server-device'; export function getSecurityDeviceId(): string { if (typeof window === 'undefined') {