From a5a0e76e72d60856623d5ebd890a5bee02f7ae73 Mon Sep 17 00:00:00 2001 From: Mikhail Fedosov Date: Tue, 28 Oct 2025 18:49:54 +0400 Subject: [PATCH] refactor: Extract duplicate code into reusable utilities with TDD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 refactoring eliminates ~200 lines of duplicate code by extracting bridge filtering, data normalization, and event parsing logic into tested utility modules. Adds 32 tests following strict RED-GREEN-REFACTOR cycle. All 122 tests passing. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 44 +++++ src/utils/__tests__/bridge-filter.test.js | 168 ++++++++++++++++++ src/utils/__tests__/data-normalizer.test.js | 129 ++++++++++++++ src/utils/__tests__/event-parser.test.js | 164 +++++++++++++++++ .../__tests__/fetch-last-transfers.test.js | 10 ++ src/utils/aggregate-claims-transfers.js | 57 +----- src/utils/bridge-filter.js | 56 ++++++ src/utils/data-normalizer.js | 41 +++++ src/utils/event-parser.js | 33 ++++ src/utils/fetch-claims.js | 55 +----- src/utils/fetch-last-transfers.js | 104 ++++------- 11 files changed, 692 insertions(+), 169 deletions(-) create mode 100644 src/utils/__tests__/bridge-filter.test.js create mode 100644 src/utils/__tests__/data-normalizer.test.js create mode 100644 src/utils/__tests__/event-parser.test.js create mode 100644 src/utils/__tests__/fetch-last-transfers.test.js create mode 100644 src/utils/bridge-filter.js create mode 100644 src/utils/data-normalizer.js create mode 100644 src/utils/event-parser.js diff --git a/CLAUDE.md b/CLAUDE.md index cb5bd01..d99a961 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,9 @@ src/ ├── utils/ │ ├── bridge-detector.js # Auto-detect bridge type │ ├── bridge-contracts.js # Bridge interaction helpers +│ ├── bridge-filter.js # Extract bridges for specific network +│ ├── data-normalizer.js # Normalize BigNumber/amounts to strings +│ ├── event-parser.js # Parse event args into named fields │ ├── threedpass.js # 3DPass precompile utilities │ ├── token-detector.js # Auto-detect token type │ ├── assistant-detector.js # Assistant type detection @@ -114,6 +117,12 @@ src/ **Decimal Handling**: P3D has special decimal handling - native P3D uses 12 decimals on-chain but 18 decimals in EVM representation. Use `decimalsDisplayMultiplier: 1000000` in token configs to compensate. +**Bridge Filtering**: `bridge-filter.js` provides centralized logic for extracting bridges relevant to a specific network. It combines default bridges from config, import bridges defined at network level, and custom bridges from user settings, with automatic deduplication. + +**Data Normalization**: `data-normalizer.js` handles conversion of various amount formats (BigNumber objects, hex strings, numbers) into consistent string representations. Essential for working with event args from smart contracts. + +**Event Parsing**: `event-parser.js` converts raw event args arrays into named field objects, eliminating magic array indices and improving type safety. Automatically handles amount normalization. + ## Critical Implementation Details ### Bridge Instance Creation Flow @@ -156,10 +165,18 @@ When MetaMask changes networks, the context automatically updates provider, sign ## Testing Notes Tests exist in `src/utils/__tests__/`: +- `bridge-filter.test.js` - Network-specific bridge extraction +- `data-normalizer.test.js` - Amount/BigNumber normalization +- `event-parser.test.js` - Event args parsing +- `fetch-last-transfers.test.js` - Module import validation - `retry-with-fallback.test.js` - Provider fallback logic - `settings-consistency.test.js` - Settings validation +- `network-switcher.test.js` - Network switching logic +- `error-parser.test.js` - Error message parsing +- `decimal-converter.test.js` - Decimal conversion utilities Run tests with `pnpm test` for watch mode. +Run full test suite: `pnpm test -- --no-watch --passWithNoTests --watchAll=false` ## Test-Driven Development (TDD) @@ -265,3 +282,30 @@ import { getProvider } from './utils/provider-manager'; const provider = await getProvider('ETHEREUM', settings); // Automatically handles fallback if primary RPC fails ``` + +**Filtering bridges for a network**: +```javascript +import { getBridgesForNetwork } from './utils/bridge-filter'; +const bridges = getBridgesForNetwork(networkConfig, customBridges); +// Returns: Array of bridge instances for the network +// Combines default, import, and custom bridges with deduplication +``` + +**Normalizing event amounts**: +```javascript +import { normalizeAmount } from './utils/data-normalizer'; +const amount = normalizeAmount(event.args[1]); +// Handles BigNumber, string, number, objects with hex properties +// Returns: string representation or '0' if invalid +``` + +**Parsing event args**: +```javascript +import { parseExpatriationEvent, parseRepatriationEvent } from './utils/event-parser'; +const eventData = parseExpatriationEvent(event); +// Returns: { senderAddress, amount, reward, foreignAddress, data } + +const eventData = parseRepatriationEvent(event); +// Returns: { senderAddress, amount, reward, homeAddress, data } +// Eliminates magic array indices, uses normalizeAmount internally +``` diff --git a/src/utils/__tests__/bridge-filter.test.js b/src/utils/__tests__/bridge-filter.test.js new file mode 100644 index 0000000..0cad622 --- /dev/null +++ b/src/utils/__tests__/bridge-filter.test.js @@ -0,0 +1,168 @@ +import { getBridgesForNetwork } from '../bridge-filter'; + +describe('bridge-filter', () => { + describe('getBridgesForNetwork', () => { + it('should return empty array when networkConfig has no bridges', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {} + }; + const customBridges = {}; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toEqual([]); + }); + + it('should return bridges from networkConfig.bridges', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: { + bridge1: { address: '0x123', type: 'export' }, + bridge2: { address: '0x456', type: 'import' } + } + }; + const customBridges = {}; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(2); + expect(result).toContainEqual({ address: '0x123', type: 'export' }); + expect(result).toContainEqual({ address: '0x456', type: 'import' }); + }); + + it('should extract import bridges from network-level properties', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: {}, + P3D_IMPORT: { address: '0xabc', type: 'import' }, + USDT_WRAPPER: { address: '0xdef', type: 'import_wrapper' } + }; + const customBridges = {}; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(2); + expect(result).toContainEqual({ address: '0xabc', type: 'import' }); + expect(result).toContainEqual({ address: '0xdef', type: 'import_wrapper' }); + }); + + it('should not include non-bridge network properties', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: {}, + assistants: { assist1: { address: '0x111' } }, + tokens: { token1: { address: '0x222' } }, + P3D_IMPORT: { address: '0xabc', type: 'import' } + }; + const customBridges = {}; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(1); + expect(result[0].address).toBe('0xabc'); + }); + + it('should include custom export bridges when network is home network', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: {} + }; + const customBridges = { + custom1: { + address: '0xcustom1', + type: 'export', + homeNetwork: 'ETHEREUM', + foreignNetwork: 'BSC' + }, + custom2: { + address: '0xcustom2', + type: 'export', + homeNetwork: 'BSC', + foreignNetwork: 'ETHEREUM' + } + }; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(1); + expect(result[0].address).toBe('0xcustom1'); + }); + + it('should include custom import bridges when network is foreign network', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: {} + }; + const customBridges = { + custom1: { + address: '0xcustom1', + type: 'import', + homeNetwork: 'BSC', + foreignNetwork: 'ETHEREUM' + }, + custom2: { + address: '0xcustom2', + type: 'import', + homeNetwork: 'ETHEREUM', + foreignNetwork: 'BSC' + } + }; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(1); + expect(result[0].address).toBe('0xcustom1'); + }); + + it('should not include duplicate bridges', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: { + bridge1: { address: '0x123', type: 'export' } + } + }; + const customBridges = { + custom1: { + address: '0x123', + type: 'export', + homeNetwork: 'ETHEREUM' + } + }; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(1); + expect(result[0].address).toBe('0x123'); + }); + + it('should combine default bridges, import bridges, and custom bridges', () => { + const networkConfig = { + name: 'ETHEREUM', + contracts: {}, + bridges: { + bridge1: { address: '0x111', type: 'export' } + }, + P3D_IMPORT: { address: '0x222', type: 'import' } + }; + const customBridges = { + custom1: { + address: '0x333', + type: 'import_wrapper', + foreignNetwork: 'ETHEREUM' + } + }; + + const result = getBridgesForNetwork(networkConfig, customBridges); + + expect(result).toHaveLength(3); + expect(result.map(b => b.address).sort()).toEqual(['0x111', '0x222', '0x333']); + }); + }); +}); diff --git a/src/utils/__tests__/data-normalizer.test.js b/src/utils/__tests__/data-normalizer.test.js new file mode 100644 index 0000000..e010e27 --- /dev/null +++ b/src/utils/__tests__/data-normalizer.test.js @@ -0,0 +1,129 @@ +import { normalizeAmount } from '../data-normalizer'; + +describe('data-normalizer', () => { + describe('normalizeAmount', () => { + it('should handle BigNumber objects with toNumber function', () => { + const bigNumber = { + toNumber: () => 12345, + toString: () => '12345' + }; + + const result = normalizeAmount(bigNumber); + + expect(result).toBe('12345'); + }); + + it('should handle string values', () => { + const result = normalizeAmount('98765'); + + expect(result).toBe('98765'); + }); + + it('should handle number values', () => { + const result = normalizeAmount(42); + + expect(result).toBe('42'); + }); + + it('should handle objects with _hex property', () => { + const obj = { + _hex: '0x1234' + }; + + const result = normalizeAmount(obj); + + expect(result).toBe('0x1234'); + }); + + it('should handle objects with hex property', () => { + const obj = { + hex: '0xabcd' + }; + + const result = normalizeAmount(obj); + + expect(result).toBe('0xabcd'); + }); + + it('should handle objects with toString method', () => { + const obj = { + toString: () => '999' + }; + + const result = normalizeAmount(obj); + + expect(result).toBe('999'); + }); + + it('should return "0" for null', () => { + const result = normalizeAmount(null); + + expect(result).toBe('0'); + }); + + it('should return "0" for undefined', () => { + const result = normalizeAmount(undefined); + + expect(result).toBe('0'); + }); + + it('should return "0" for empty object', () => { + const result = normalizeAmount({}); + + expect(result).toBe('0'); + }); + + it('should prioritize _hex over hex if both exist', () => { + const obj = { + _hex: '0x1111', + hex: '0x2222' + }; + + const result = normalizeAmount(obj); + + expect(result).toBe('0x1111'); + }); + + it('should prioritize toNumber over string type', () => { + const bigNumber = { + toNumber: () => 100, + toString: () => '100', + valueOf: () => '100' + }; + + const result = normalizeAmount(bigNumber); + + expect(result).toBe('100'); + }); + + it('should handle zero as number', () => { + const result = normalizeAmount(0); + + expect(result).toBe('0'); + }); + + it('should handle zero as string', () => { + const result = normalizeAmount('0'); + + expect(result).toBe('0'); + }); + + it('should handle large numbers', () => { + const result = normalizeAmount(999999999999999); + + expect(result).toBe('999999999999999'); + }); + + it('should handle ethers BigNumber with hex', () => { + const bigNumber = { + _hex: '0x5f5e100', + _isBigNumber: true, + toString: () => '100000000' + }; + + const result = normalizeAmount(bigNumber); + + expect(result).toBe('0x5f5e100'); + }); + }); +}); diff --git a/src/utils/__tests__/event-parser.test.js b/src/utils/__tests__/event-parser.test.js new file mode 100644 index 0000000..95b568a --- /dev/null +++ b/src/utils/__tests__/event-parser.test.js @@ -0,0 +1,164 @@ +import { parseExpatriationEvent, parseRepatriationEvent } from '../event-parser'; + +describe('event-parser', () => { + describe('parseExpatriationEvent', () => { + it('should parse NewExpatriation event args', () => { + const event = { + args: [ + '0xSenderAddress', + { hex: '0x1234', toString: () => '4660' }, + { hex: '0x5678', toString: () => '22136' }, + '0xForeignAddress', + 'someData' + ] + }; + + const result = parseExpatriationEvent(event); + + expect(result).toEqual({ + senderAddress: '0xSenderAddress', + amount: '0x1234', + reward: '0x5678', + foreignAddress: '0xForeignAddress', + data: 'someData' + }); + }); + + it('should handle missing sender address', () => { + const event = { + args: [ + null, + { hex: '0x1234' }, + { hex: '0x5678' }, + '0xForeignAddress', + 'data' + ] + }; + + const result = parseExpatriationEvent(event); + + expect(result.senderAddress).toBe('Unknown'); + }); + + it('should handle missing foreign address', () => { + const event = { + args: [ + '0xSender', + { hex: '0x1234' }, + { hex: '0x5678' }, + null, + 'data' + ] + }; + + const result = parseExpatriationEvent(event); + + expect(result.foreignAddress).toBe('Unknown'); + }); + + it('should handle missing data', () => { + const event = { + args: [ + '0xSender', + { hex: '0x1234' }, + { hex: '0x5678' }, + '0xForeign', + null + ] + }; + + const result = parseExpatriationEvent(event); + + expect(result.data).toBe(''); + }); + }); + + describe('parseRepatriationEvent', () => { + it('should parse NewRepatriation event args', () => { + const event = { + args: [ + '0xSenderAddress', + { hex: '0xabcd', toString: () => '43981' }, + { hex: '0xef01', toString: () => '61185' }, + '0xHomeAddress', + 'repatriationData' + ] + }; + + const result = parseRepatriationEvent(event); + + expect(result).toEqual({ + senderAddress: '0xSenderAddress', + amount: '0xabcd', + reward: '0xef01', + homeAddress: '0xHomeAddress', + data: 'repatriationData' + }); + }); + + it('should handle missing sender address', () => { + const event = { + args: [ + null, + { hex: '0xabcd' }, + { hex: '0xef01' }, + '0xHome', + 'data' + ] + }; + + const result = parseRepatriationEvent(event); + + expect(result.senderAddress).toBe('Unknown'); + }); + + it('should handle missing home address', () => { + const event = { + args: [ + '0xSender', + { hex: '0xabcd' }, + { hex: '0xef01' }, + null, + 'data' + ] + }; + + const result = parseRepatriationEvent(event); + + expect(result.homeAddress).toBe('Unknown'); + }); + + it('should handle missing data', () => { + const event = { + args: [ + '0xSender', + { hex: '0xabcd' }, + { hex: '0xef01' }, + '0xHome', + null + ] + }; + + const result = parseRepatriationEvent(event); + + expect(result.data).toBe(''); + }); + + it('should normalize amount and reward using normalizeAmount', () => { + const event = { + args: [ + '0xSender', + 100, // Plain number + '200', // String + '0xHome', + 'data' + ] + }; + + const result = parseRepatriationEvent(event); + + expect(result.amount).toBe('100'); + expect(result.reward).toBe('200'); + }); + }); +}); diff --git a/src/utils/__tests__/fetch-last-transfers.test.js b/src/utils/__tests__/fetch-last-transfers.test.js new file mode 100644 index 0000000..6d08356 --- /dev/null +++ b/src/utils/__tests__/fetch-last-transfers.test.js @@ -0,0 +1,10 @@ +import { fetchLastTransfers } from '../fetch-last-transfers'; + +describe('fetch-last-transfers', () => { + describe('module', () => { + it('should import module without syntax errors', () => { + expect(fetchLastTransfers).toBeDefined(); + expect(typeof fetchLastTransfers).toBe('function'); + }); + }); +}); diff --git a/src/utils/aggregate-claims-transfers.js b/src/utils/aggregate-claims-transfers.js index 64461a7..ebd6a70 100644 --- a/src/utils/aggregate-claims-transfers.js +++ b/src/utils/aggregate-claims-transfers.js @@ -1,5 +1,6 @@ import { ethers } from 'ethers'; +import { normalizeAmount } from './data-normalizer'; /** * Aggregate claims and transfers with fraud detection @@ -44,34 +45,6 @@ export const aggregateClaimsAndTransfers = (claims, transfers) => { const amountsMatchBigNumber = (amount1, amount2) => { if (!amount1 || !amount2) return { match: false, reason: 'missing_amount' }; try { - // Helper function to normalize amounts (similar to formatAmount in ClaimList.js) - const normalizeAmount = (amount) => { - // Handle BigNumber objects (including deserialized ones from cache) - if (typeof amount?.toNumber === 'function') { - return amount.toString(); - } else if (typeof amount === 'string') { - return amount; - } else if (typeof amount === 'number') { - return amount.toString(); - } else if (typeof amount === 'object' && amount !== null) { - // Handle deserialized BigNumber objects from cache - // They might have properties like _hex, _isBigNumber, or be plain objects with hex values - if (amount._hex) { - return amount._hex; - } else if (amount.hex) { - return amount.hex; - } else if (amount.toString && typeof amount.toString === 'function') { - return amount.toString(); - } else { - return '0'; - } - } else if (!amount) { - return '0'; - } else { - return '0'; - } - }; - // Normalize both amounts const normalized1 = normalizeAmount(amount1); const normalized2 = normalizeAmount(amount2); @@ -162,34 +135,6 @@ export const aggregateClaimsAndTransfers = (claims, transfers) => { } try { - // Helper function to normalize amounts (same as in amountsMatchBigNumber) - const normalizeAmount = (amount) => { - // Handle BigNumber objects (including deserialized ones from cache) - if (typeof amount?.toNumber === 'function') { - return amount.toString(); - } else if (typeof amount === 'string') { - return amount; - } else if (typeof amount === 'number') { - return amount.toString(); - } else if (typeof amount === 'object' && amount !== null) { - // Handle deserialized BigNumber objects from cache - // They might have properties like _hex, _isBigNumber, or be plain objects with hex values - if (amount._hex) { - return amount._hex; - } else if (amount.hex) { - return amount.hex; - } else if (amount.toString && typeof amount.toString === 'function') { - return amount.toString(); - } else { - return '0'; - } - } else if (!amount) { - return '0'; - } else { - return '0'; - } - }; - // Normalize both rewards const normalizedClaimReward = normalizeAmount(claimReward); const normalizedTransferReward = normalizeAmount(transferReward); diff --git a/src/utils/bridge-filter.js b/src/utils/bridge-filter.js new file mode 100644 index 0000000..54efca2 --- /dev/null +++ b/src/utils/bridge-filter.js @@ -0,0 +1,56 @@ +/** + * Get bridges for a specific network + * + * Extracts bridges from three sources: + * 1. Default bridges from networkConfig.bridges + * 2. Import bridges defined at network level (not in bridges object) + * 3. Custom bridges that match the network (based on bridge type) + * + * @param {Object} networkConfig - Network configuration object + * @param {Object} customBridges - Custom bridges from settings + * @returns {Array} Array of bridge instances for the network + */ +export const getBridgesForNetwork = (networkConfig, customBridges = {}) => { + // Get default bridges from network config + const defaultBridges = networkConfig.bridges ? Object.values(networkConfig.bridges) : []; + + // Get import bridges defined at network level (not in bridges object) + const importBridges = Object.entries(networkConfig) + .filter(([key, value]) => + key !== 'bridges' && + key !== 'assistants' && + key !== 'tokens' && + key !== 'contracts' && + typeof value === 'object' && + value.address && + (value.type === 'import' || value.type === 'import_wrapper') + ) + .map(([key, value]) => value); + + const allDefaultBridges = [...defaultBridges, ...importBridges]; + + // Get custom bridges for this network + const customNetworkBridges = Object.values(customBridges).filter(bridge => { + // For export bridges: include when this network is the home network + if (bridge.type === 'export') { + return bridge.homeNetwork === networkConfig.name; + } + // For import bridges: include when this network is the foreign network + if (bridge.type === 'import' || bridge.type === 'import_wrapper') { + return bridge.foreignNetwork === networkConfig.name; + } + // For other types, use the old logic + return bridge.homeNetwork === networkConfig.name || bridge.foreignNetwork === networkConfig.name; + }); + + // Combine default bridges with custom bridges, avoiding duplicates + const networkBridgeInstances = [...allDefaultBridges]; + customNetworkBridges.forEach(customBridge => { + const exists = networkBridgeInstances.some(bridge => bridge.address === customBridge.address); + if (!exists) { + networkBridgeInstances.push(customBridge); + } + }); + + return networkBridgeInstances; +}; diff --git a/src/utils/data-normalizer.js b/src/utils/data-normalizer.js new file mode 100644 index 0000000..3b41bdb --- /dev/null +++ b/src/utils/data-normalizer.js @@ -0,0 +1,41 @@ +/** + * Normalize amount values from various formats to string + * + * Handles BigNumber objects, plain numbers, strings, and objects with hex properties. + * This is especially useful for handling event args from smart contracts which can be + * in various formats depending on how they were serialized/deserialized. + * + * @param {*} amount - Amount to normalize (BigNumber, string, number, or object) + * @returns {string} Normalized amount as string, or '0' if invalid + */ +export const normalizeAmount = (amount) => { + // Handle BigNumber objects (including deserialized ones from cache) + if (typeof amount?.toNumber === 'function') { + return amount.toString(); + } else if (typeof amount === 'string') { + return amount; + } else if (typeof amount === 'number') { + return amount.toString(); + } else if (typeof amount === 'object' && amount !== null) { + // Handle deserialized BigNumber objects from cache + // They might have properties like _hex, _isBigNumber, or be plain objects with hex values + if (amount._hex) { + return amount._hex; + } else if (amount.hex) { + return amount.hex; + } else if (amount.toString && typeof amount.toString === 'function') { + const stringValue = amount.toString(); + // Avoid default "[object Object]" from Object.prototype.toString + if (stringValue === '[object Object]') { + return '0'; + } + return stringValue; + } else { + return '0'; + } + } else if (!amount) { + return '0'; + } else { + return '0'; + } +}; diff --git a/src/utils/event-parser.js b/src/utils/event-parser.js new file mode 100644 index 0000000..23f5db2 --- /dev/null +++ b/src/utils/event-parser.js @@ -0,0 +1,33 @@ +import { normalizeAmount } from './data-normalizer'; + +/** + * Parse NewExpatriation event args into named fields + * + * @param {Object} event - Event object with args array + * @returns {Object} Parsed event data with named fields + */ +export const parseExpatriationEvent = (event) => { + return { + senderAddress: event.args[0] || 'Unknown', + amount: normalizeAmount(event.args[1]), + reward: normalizeAmount(event.args[2]), + foreignAddress: event.args[3] || 'Unknown', + data: event.args[4] || '' + }; +}; + +/** + * Parse NewRepatriation event args into named fields + * + * @param {Object} event - Event object with args array + * @returns {Object} Parsed event data with named fields + */ +export const parseRepatriationEvent = (event) => { + return { + senderAddress: event.args[0] || 'Unknown', + amount: normalizeAmount(event.args[1]), + reward: normalizeAmount(event.args[2]), + homeAddress: event.args[3] || 'Unknown', + data: event.args[4] || '' + }; +}; diff --git a/src/utils/fetch-claims.js b/src/utils/fetch-claims.js index fe42650..f0a8568 100644 --- a/src/utils/fetch-claims.js +++ b/src/utils/fetch-claims.js @@ -1,10 +1,11 @@ import { ethers } from 'ethers'; import { NETWORKS } from '../config/networks'; -import { - getAllClaims, - createCounterstakeContract +import { + getAllClaims, + createCounterstakeContract } from './bridge-contracts'; import { estimateClaimsFromTimeframe } from './claim-estimator'; +import { getBridgesForNetwork } from './bridge-filter'; /** * Fetch claims from all networks and bridges @@ -46,49 +47,11 @@ export const fetchClaimsFromAllNetworks = async ({ } console.log(`🔍 Processing network: ${networkKey} (${networkConfig.name})`); - - // Get bridges for this network from network config - const defaultBridges = networkConfig.bridges ? Object.values(networkConfig.bridges) : []; - - // Also get import bridges that are defined at the network level (not in bridges object) - const importBridges = Object.entries(networkConfig) - .filter(([key, value]) => - key !== 'bridges' && - key !== 'assistants' && - key !== 'tokens' && - key !== 'contracts' && - typeof value === 'object' && - value.address && - (value.type === 'import' || value.type === 'import_wrapper') - ) - .map(([key, value]) => value); - - const allDefaultBridges = [...defaultBridges, ...importBridges]; - - // Get custom bridges for this network - const customNetworkBridges = Object.values(customBridges).filter(bridge => { - // For export bridges: include when this network is the home network - if (bridge.type === 'export') { - return bridge.homeNetwork === networkConfig.name; - } - // For import bridges: include when this network is the foreign network - if (bridge.type === 'import' || bridge.type === 'import_wrapper') { - return bridge.foreignNetwork === networkConfig.name; - } - // For other types, use the old logic - return bridge.homeNetwork === networkConfig.name || bridge.foreignNetwork === networkConfig.name; - }); - - // Combine default bridges with custom bridges, avoiding duplicates - const networkBridgeInstances = [...allDefaultBridges]; - customNetworkBridges.forEach(customBridge => { - const exists = networkBridgeInstances.some(bridge => bridge.address === customBridge.address); - if (!exists) { - networkBridgeInstances.push(customBridge); - } - }); - - console.log(`🔍 Found ${networkBridgeInstances.length} bridges for ${networkKey}:`, + + // Get bridges for this network + const networkBridgeInstances = getBridgesForNetwork(networkConfig, customBridges); + + console.log(`🔍 Found ${networkBridgeInstances.length} bridges for ${networkKey}:`, networkBridgeInstances.map(b => ({ address: b.address, type: b.type })) ); diff --git a/src/utils/fetch-last-transfers.js b/src/utils/fetch-last-transfers.js index 700d7e9..411fc78 100644 --- a/src/utils/fetch-last-transfers.js +++ b/src/utils/fetch-last-transfers.js @@ -2,12 +2,14 @@ import { ethers } from 'ethers'; import { NETWORKS } from '../config/networks'; import { estimateBlocksFromTimeframe, getBlockTime } from './block-estimator'; import { getBlockTimestamp } from './bridge-contracts'; -import { - getCachedEvents, - setCachedEvents, - getMostRecentCachedBlock, - mergeEvents +import { + getCachedEvents, + setCachedEvents, + getMostRecentCachedBlock, + mergeEvents } from './event-cache'; +import { getBridgesForNetwork } from './bridge-filter'; +import { parseExpatriationEvent, parseRepatriationEvent } from './event-parser'; /** * Efficiently fetch events from most recent blocks first using chunked search @@ -126,54 +128,16 @@ export const fetchLastTransfers = async ({ } console.log(`🔍 Processing network: ${networkKey} (${networkConfig.name})`); - - // Get bridges for this network from network config - const defaultBridges = networkConfig.bridges ? Object.values(networkConfig.bridges) : []; - - // Also get import bridges that are defined at the network level (not in bridges object) - const importBridges = Object.entries(networkConfig) - .filter(([key, value]) => - key !== 'bridges' && - key !== 'assistants' && - key !== 'tokens' && - key !== 'contracts' && - typeof value === 'object' && - value.address && - (value.type === 'import' || value.type === 'import_wrapper') - ) - .map(([key, value]) => value); - - const allDefaultBridges = [...defaultBridges, ...importBridges]; - - // Get custom bridges for this network - const customNetworkBridges = Object.values(customBridges).filter(bridge => { - // For export bridges: include when this network is the home network - if (bridge.type === 'export') { - return bridge.homeNetwork === networkConfig.name; - } - // For import bridges: include when this network is the foreign network - if (bridge.type === 'import' || bridge.type === 'import_wrapper') { - return bridge.foreignNetwork === networkConfig.name; - } - // For other types, use the old logic - return bridge.homeNetwork === networkConfig.name || bridge.foreignNetwork === networkConfig.name; - }); - - // Combine default bridges with custom bridges, avoiding duplicates - const networkBridgeInstances = [...allDefaultBridges]; - customNetworkBridges.forEach(customBridge => { - const exists = networkBridgeInstances.some(bridge => bridge.address === customBridge.address); - if (!exists) { - networkBridgeInstances.push(customBridge); - } - }); - - console.log(`🔍 Found ${networkBridgeInstances.length} bridges for ${networkKey}:`, - networkBridgeInstances.map(b => ({ - address: b.address, - type: b.type, - homeNetwork: b.homeNetwork, - foreignNetwork: b.foreignNetwork + + // Get bridges for this network + const networkBridgeInstances = getBridgesForNetwork(networkConfig, customBridges); + + console.log(`🔍 Found ${networkBridgeInstances.length} bridges for ${networkKey}:`, + networkBridgeInstances.map(b => ({ + address: b.address, + type: b.type, + homeNetwork: b.homeNetwork, + foreignNetwork: b.foreignNetwork })) ); @@ -456,16 +420,19 @@ export const fetchLastTransfers = async ({ rawAmountEq: event.args[1]?.eq?.(0) }); + // Parse event args + const eventData = parseExpatriationEvent(event); + const transferWithInfo = { // Event data eventType: 'NewExpatriation', - senderAddress: event.args[0] || 'Unknown', // sender_address - amount: event.args[1] ? (event.args[1].hex || event.args[1].toString()) : '0', // amount - reward: event.args[2] ? (event.args[2].hex || event.args[2].toString()) : '0', // reward - foreignAddress: event.args[3] || 'Unknown', // foreign_address - recipientAddress: event.args[3] || 'Unknown', // foreign_address (for UI compatibility) - data: event.args[4] || '', // data - + senderAddress: eventData.senderAddress, + amount: eventData.amount, + reward: eventData.reward, + foreignAddress: eventData.foreignAddress, + recipientAddress: eventData.foreignAddress, // for UI compatibility + data: eventData.data, + // Event metadata blockNumber: event.blockNumber, transactionHash: event.transactionHash, @@ -571,16 +538,19 @@ export const fetchLastTransfers = async ({ data_obj: event.args?.data }); + // Parse event args + const eventData = parseRepatriationEvent(event); + const transferWithInfo = { // Event data eventType: 'NewRepatriation', - senderAddress: event.args[0] || 'Unknown', // sender_address - amount: event.args[1] ? (event.args[1].hex || event.args[1].toString()) : '0', // amount - reward: event.args[2] ? (event.args[2].hex || event.args[2].toString()) : '0', // reward - homeAddress: event.args[3] || 'Unknown', // home_address - recipientAddress: event.args[3] || 'Unknown', // home_address (for UI compatibility) - data: event.args[4] || '', // data - + senderAddress: eventData.senderAddress, + amount: eventData.amount, + reward: eventData.reward, + homeAddress: eventData.homeAddress, + recipientAddress: eventData.homeAddress, // for UI compatibility + data: eventData.data, + // Event metadata blockNumber: event.blockNumber, transactionHash: event.transactionHash,