From 53de54b09cc9b1296f3b3c207ecdf8070956cdf4 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:07:41 +0400 Subject: [PATCH 1/3] fix: add address validation to getvp --- src/utils.ts | 90 ++++------ .../__snapshots__/utils.test.ts.snap | 13 +- test/integration/fixtures/vp-fixtures.ts | 84 ++++++++++ test/integration/utils.test.ts | 127 ++++++++++---- test/unit/utils.test.ts | 156 ------------------ 5 files changed, 219 insertions(+), 251 deletions(-) create mode 100644 test/integration/fixtures/vp-fixtures.ts delete mode 100644 test/unit/utils.test.ts diff --git a/src/utils.ts b/src/utils.ts index f3f443fee..debf32fe8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,7 +4,6 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { getDelegations } from './utils/delegation'; import { createHash } from 'crypto'; import { Protocol, Score, Snapshot, VotingPower } from './types'; -import { VALID_PROTOCOLS } from './constants'; export function sha256(str) { return createHash('sha256').update(str).digest('hex'); @@ -81,16 +80,23 @@ export async function getVp( snapshot: Snapshot, space: string ): Promise { - const scores: Score[] = await getScoresDirect( + const { formattedAddress } = validateAndFormatAddress(address); + + const scores = await getScoresDirect( space, strategies, network, getProvider(network), - [address], + [formattedAddress], snapshot ); - const vpByStrategy = scores.map((score) => score[address] || 0); + const vpByStrategy = scores.map((score) => { + const matchingKey = Object.keys(score).find( + (key) => key.toLowerCase() === formattedAddress.toLowerCase() + ); + return matchingKey ? score[matchingKey] : 0; + }); return { vp: vpByStrategy.reduce((a, b) => a + b, 0), @@ -124,66 +130,29 @@ export function customFetch( ]); } -/** - * Validates that protocols are non-empty and contain only valid protocol names. - * - * @param protocols - Array of protocol names to validate - */ -function validateProtocols(protocols: Protocol[]): void { - if (!protocols.length) { - throw new Error('At least one protocol must be specified'); - } - - const invalidProtocols = protocols.filter( - (p) => !VALID_PROTOCOLS.includes(p) - ); - if (invalidProtocols.length > 0) { - throw new Error(`Invalid protocol(s): ${invalidProtocols.join(', ')}`); - } +function getAddressType(address: string): Protocol | null { + if (/^0x[a-fA-F0-9]{40}$/.test(address)) return 'evm'; + if (/^0x[a-fA-F0-9]{64}$/.test(address)) return 'starknet'; + return null; } -/** - * Formats addresses according to the specified blockchain protocols. - * - * This function takes a list of addresses and formats them according to the provided - * protocols. It prioritizes EVM formatting when multiple protocols are specified and - * an address is valid for both. If EVM formatting fails but Starknet is supported, - * it falls back to Starknet formatting. Throws an error if any address cannot be - * formatted according to the specified protocols. - * - * @param addresses - Array of blockchain addresses to format - * @param protocols - Array of protocol names to validate against. Defaults to ['evm']. - * Valid protocols are 'evm' and 'starknet'. - * - * @returns Array of formatted addresses in the same order as input - */ -export function getFormattedAddressesByProtocol( - addresses: string[], - protocols: Protocol[] = ['evm'] -): string[] { - validateProtocols(protocols); - - return addresses.map((address) => { - if (protocols.includes('evm')) { - try { - return snapshot.utils.getFormattedAddress(address, 'evm'); - } catch (e) { - // Continue to starknet if evm formatting fails and starknet is supported - } - } +function validateAndFormatAddress(address: string): { + formattedAddress: string; + addressType: Protocol; +} { + const addressType = getAddressType(address); + if (!addressType) { + throw new Error('invalid address'); + } - if (protocols.includes('starknet')) { - try { - return snapshot.utils.getFormattedAddress(address, 'starknet'); - } catch (e) { - // Address format not supported by any protocol - } - } + let formattedAddress: string; + try { + formattedAddress = snapshot.utils.getFormattedAddress(address, addressType); + } catch { + throw new Error('invalid address'); + } - throw new Error( - `Address "${address}" is not a valid ${protocols.join(' or ')} address` - ); - }); + return { formattedAddress, addressType }; } export const { @@ -204,7 +173,6 @@ export default { sha256, getScoresDirect, customFetch, - getFormattedAddressesByProtocol, multicall, Multicaller, subgraphRequest, diff --git a/test/integration/__snapshots__/utils.test.ts.snap b/test/integration/__snapshots__/utils.test.ts.snap index 2479a881d..fa5540c22 100644 --- a/test/integration/__snapshots__/utils.test.ts.snap +++ b/test/integration/__snapshots__/utils.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`utils getVp 1`] = ` +exports[`utils getVp() should calculate VP for EVM address on evm protocol 1`] = ` { "vp": 21.55404462002206, "vp_by_strategy": [ @@ -12,3 +12,14 @@ exports[`utils getVp 1`] = ` "vp_state": "final", } `; + +exports[`utils getVp() should calculate VP for EVM address on mixed protocol 1`] = ` +{ + "vp": 10.998985610441185, + "vp_by_strategy": [ + 1, + 9.998985610441185, + ], + "vp_state": "final", +} +`; diff --git a/test/integration/fixtures/vp-fixtures.ts b/test/integration/fixtures/vp-fixtures.ts new file mode 100644 index 000000000..7ca419b99 --- /dev/null +++ b/test/integration/fixtures/vp-fixtures.ts @@ -0,0 +1,84 @@ +export const testConfig = { + network: '1', + snapshot: 15354134, + space: 'cvx.eth', + evmAddress: '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7', + starknetAddress: + '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203A' +}; + +export const strategies = { + withDelegation: [ + { + name: 'erc20-balance-of', + params: { + symbol: 'CVX', + address: '0x72a19342e8F1838460eBFCCEf09F6585e32db86E', + decimals: 18 + } + }, + { + name: 'eth-balance', + network: '100', + params: {} + }, + { + name: 'eth-balance', + network: '1', + params: {} + }, + { + name: 'eth-balance', + network: '10', + params: {} + } + ], + mixed: [ + { + name: 'whitelist', + params: { + addresses: [testConfig.evmAddress, testConfig.starknetAddress] + } + }, + { + name: 'eth-balance', + network: '100', + params: {} + } + ], + evmOnly: [ + { + name: 'eth-balance', + network: '100', + params: {} + } + ], + singleInvalid: [ + { + name: 'whitelist-invalid', + params: { + addresses: [testConfig.evmAddress, testConfig.starknetAddress] + } + }, + { + name: 'eth-balance', + network: '100', + params: {} + } + ], + multipleInvalid: [ + { + name: 'strategy-one-invalid', + params: {} + }, + { + name: 'strategy-two-invalid', + params: {} + }, + { + name: 'eth-balance', + network: '100', + params: {} + } + ] +}; diff --git a/test/integration/utils.test.ts b/test/integration/utils.test.ts index de1413ccf..e6899b7b6 100644 --- a/test/integration/utils.test.ts +++ b/test/integration/utils.test.ts @@ -1,38 +1,99 @@ import { getVp } from '../../src/utils'; +import { strategies, testConfig } from './fixtures/vp-fixtures'; -const address = '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'; -const space = 'cvx.eth'; -const network = '1'; -const snapshot = 15354134; -const strategies = [ - { - name: 'erc20-balance-of', - params: { - symbol: 'CVX', - address: '0x72a19342e8F1838460eBFCCEf09F6585e32db86E', - decimals: 18 - } - }, - { - name: 'eth-balance', - network: '100', - params: {} - }, - { - name: 'eth-balance', - network: '1', - params: {} - }, - { - name: 'eth-balance', - network: '10', - params: {} - } -]; +const { network, snapshot, space, evmAddress, starknetAddress } = testConfig; +const TEST_TIMEOUT = 20e3; describe('utils', () => { - it('getVp', async () => { - const scores = await getVp(address, network, strategies, snapshot, space); - expect(scores).toMatchSnapshot(); - }, 20e3); + describe('getVp()', () => { + it( + 'should calculate VP for EVM address on evm protocol', + async () => { + const scores = await getVp( + evmAddress, + network, + strategies.withDelegation, + snapshot, + space + ); + expect(scores).toMatchSnapshot(); + }, + TEST_TIMEOUT + ); + + it( + 'should calculate VP for EVM address on mixed protocol', + async () => { + const scores = await getVp( + evmAddress, + network, + strategies.mixed, + snapshot, + space + ); + expect(scores).toMatchSnapshot(); + }, + TEST_TIMEOUT + ); + + it( + 'should calculate VP for Starknet address on mixed protocol', + async () => { + const scores = await getVp( + starknetAddress, + network, + strategies.mixed, + snapshot, + space + ); + expect(scores).toMatchSnapshot(); + }, + TEST_TIMEOUT + ); + + it( + 'should calculate VP for Starknet address on evm protocol', + async () => { + const scores = await getVp( + starknetAddress, + network, + strategies.evmOnly, + snapshot, + space + ); + expect(scores).toMatchSnapshot(); + }, + TEST_TIMEOUT + ); + + it.each([ + ['too short address', '0xeF8305E140ac520225DAf050e2f71d5fBcC543'], + ['too long address', '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7123'], + ['non-hex characters', '0xeF8305E140ac520225DAf050e2f71d5fBcC543eG'], + ['missing 0x prefix', 'eF8305E140ac520225DAf050e2f71d5fBcC543e7'], + ['empty string', ''], + ['just 0x', '0x'], + [ + 'wrong length for starknet', + '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203' + ], + [ + 'invalid mixed case checksum', + '0xeF8305e140AC520225dAf050E2f71d5fbcC543e7' + ] + ])( + 'should throw an error with %s', + async (_description, invalidAddress) => { + await expect( + getVp( + invalidAddress, + network, + strategies.withDelegation, + snapshot, + space + ) + ).rejects.toThrow('invalid address'); + } + ); + }); }); diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts deleted file mode 100644 index ba6759643..000000000 --- a/test/unit/utils.test.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { getFormattedAddressesByProtocol } from '../../src/utils'; - -describe('utils', () => { - const VALID_EVM_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; - const VALID_FORMATTED_EVM_ADDRESS = - '0x1234567890AbcdEF1234567890aBcdef12345678'; - const VALID_STARKNET_ADDRESS = - '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203A'; - const VALID_FORMATTED_STARKNET_ADDRESS = - '0x07f71118e351c02f6ec7099c8cdf93aed66ced8406e94631cc91637f7d7f203a'; - - describe('getFormattedAddressesByProtocol()', () => { - // Test data constants - const INVALID_ADDRESS = 'invalidAddress'; - const EMPTY_ADDRESS = ''; - const STARKNET_ONLY_ADDRESS = VALID_STARKNET_ADDRESS; - const EVM_ONLY_ADDRESS = VALID_EVM_ADDRESS; - - describe('Basic functionality', () => { - it('should return an empty array when no addresses are provided', () => { - const result = getFormattedAddressesByProtocol([]); - expect(result).toEqual([]); - }); - - it('should use evm as default protocol when no protocols provided', () => { - const result = getFormattedAddressesByProtocol([EVM_ONLY_ADDRESS]); - expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); - }); - }); - - describe('Protocol validation', () => { - it('should throw an error when no protocols are provided', () => { - expect(() => { - getFormattedAddressesByProtocol([], []); - }).toThrow('At least one protocol must be specified'); - }); - - it('should throw an error for single invalid protocol', () => { - expect(() => { - getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - [ - // @ts-ignore - 'invalidProtocol' - ] - ); - }).toThrow('Invalid protocol(s): invalidProtocol'); - }); - - it('should throw an error for multiple invalid protocols', () => { - expect(() => { - getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - [ - // @ts-ignore - 'invalidProtocol1', - // @ts-ignore - 'invalidProtocol2' - ] - ); - }).toThrow('Invalid protocol(s): invalidProtocol1, invalidProtocol2'); - }); - }); - - describe('Single protocol formatting', () => { - it('should format EVM addresses correctly', () => { - const result = getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - ['evm'] - ); - expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); - }); - - it('should format Starknet addresses correctly', () => { - const result = getFormattedAddressesByProtocol( - [STARKNET_ONLY_ADDRESS], - ['starknet'] - ); - expect(result).toEqual([VALID_FORMATTED_STARKNET_ADDRESS]); - }); - }); - - describe('Multiple protocol formatting', () => { - it('should prioritize EVM when address is valid for both protocols', () => { - const result = getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - ['evm', 'starknet'] - ); - expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); - }); - - it('should fall back to Starknet when EVM formatting fails', () => { - const result = getFormattedAddressesByProtocol( - [STARKNET_ONLY_ADDRESS], - ['evm', 'starknet'] - ); - expect(result).toEqual([VALID_FORMATTED_STARKNET_ADDRESS]); - }); - - it('should format addresses from different protocols correctly', () => { - const result = getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS, STARKNET_ONLY_ADDRESS], - ['evm', 'starknet'] - ); - expect(result).toEqual([ - VALID_FORMATTED_EVM_ADDRESS, - VALID_FORMATTED_STARKNET_ADDRESS - ]); - }); - - it('should maintain protocol order independence for multiple valid protocols', () => { - const result1 = getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - ['evm', 'starknet'] - ); - const result2 = getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS], - ['starknet', 'evm'] - ); - expect(result1).toEqual(result2); - }); - }); - - describe('Error handling', () => { - it('should throw an error for completely invalid addresses', () => { - expect(() => { - getFormattedAddressesByProtocol([INVALID_ADDRESS], ['evm']); - }).toThrow('is not a valid evm address'); - }); - - it('should throw an error for empty string addresses', () => { - expect(() => { - getFormattedAddressesByProtocol([EMPTY_ADDRESS], ['evm']); - }).toThrow('is not a valid evm address'); - }); - - it('should throw an error when address is invalid for all specified protocols', () => { - expect(() => { - getFormattedAddressesByProtocol( - [INVALID_ADDRESS], - ['evm', 'starknet'] - ); - }).toThrow('is not a valid evm or starknet address'); - }); - - it('should throw an error on first invalid address in mixed array', () => { - expect(() => { - getFormattedAddressesByProtocol( - [EVM_ONLY_ADDRESS, INVALID_ADDRESS, STARKNET_ONLY_ADDRESS], - ['evm', 'starknet'] - ); - }).toThrow('is not a valid evm or starknet address'); - }); - }); - }); -}); From 68dc3e21d7573417e224f9dfac56c33bef0bc553 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 2 Jul 2025 22:37:07 +0400 Subject: [PATCH 2/3] fix: add addresses validations when getting scores --- src/utils.ts | 86 +++++++++++++------ .../__snapshots__/utils.test.ts.snap | 21 +++++ test/integration/utils.test.ts | 20 +++++ test/integration/validation/basic.test.ts | 2 +- 4 files changed, 104 insertions(+), 25 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index debf32fe8..3a4feba99 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,10 +24,6 @@ async function callStrategy( return {}; } - if (!_strategies.hasOwnProperty(strategy.name)) { - throw new Error(`Invalid strategy: ${strategy.name}`); - } - const score: Score = await _strategies[strategy.name].strategy( space, network, @@ -53,16 +49,23 @@ export async function getScoresDirect( snapshot: Snapshot ): Promise { try { - const networks = strategies.map((s) => s.network || network); - const snapshots = await getSnapshots(network, snapshot, provider, networks); - // @ts-ignore if (addresses.length === 0) return strategies.map(() => ({})); + + const addressesByProtocols = validateAndFormatAddresses(addresses); + validateStrategies(strategies); + + const networks = [...new Set(strategies.map((s) => s.network || network))]; + const snapshots = await getSnapshots(network, snapshot, provider, networks); + return await Promise.all( strategies.map((strategy) => callStrategy( space, strategy.network || network, - addresses, + getAddressesFromSupportedProtocols( + addressesByProtocols, + strategy.name + ), strategy, snapshots[strategy.network || network] ) @@ -80,20 +83,20 @@ export async function getVp( snapshot: Snapshot, space: string ): Promise { - const { formattedAddress } = validateAndFormatAddress(address); + validateStrategies(strategies, { allowEmpty: false }); const scores = await getScoresDirect( space, strategies, network, getProvider(network), - [formattedAddress], + [address], snapshot ); const vpByStrategy = scores.map((score) => { const matchingKey = Object.keys(score).find( - (key) => key.toLowerCase() === formattedAddress.toLowerCase() + (key) => key.toLowerCase() === address.toLowerCase() ); return matchingKey ? score[matchingKey] : 0; }); @@ -136,23 +139,58 @@ function getAddressType(address: string): Protocol | null { return null; } -function validateAndFormatAddress(address: string): { - formattedAddress: string; - addressType: Protocol; -} { - const addressType = getAddressType(address); - if (!addressType) { - throw new Error('invalid address'); +function validateAndFormatAddresses( + addresses: string[] +): Record { + const results: Record = { + evm: [], + starknet: [] + }; + + for (const address of addresses) { + const addressType = getAddressType(address); + if (!addressType) { + throw new Error('invalid address'); + } + + try { + results[addressType].push( + snapshot.utils.getFormattedAddress(address, addressType) + ); + } catch { + throw new Error('invalid address'); + } } - let formattedAddress: string; - try { - formattedAddress = snapshot.utils.getFormattedAddress(address, addressType); - } catch { - throw new Error('invalid address'); + return results; +} + +function getAddressesFromSupportedProtocols( + addressesByProtocols: Record, + strategyName: string +): string[] { + return Object.entries(addressesByProtocols) + .filter(([protocol]) => + _strategies[strategyName].supportedProtocols.includes(protocol) + ) + .flatMap(([, addresses]) => addresses); +} + +function validateStrategies( + strategies: any[], + { allowEmpty = true }: { allowEmpty?: boolean } = {} +): void { + if (!strategies.length && !allowEmpty) { + throw new Error('no strategies provided'); } - return { formattedAddress, addressType }; + const invalidStrategies = strategies + .filter((strategy) => !_strategies[strategy.name]) + .map((strategy) => strategy.name); + + if (invalidStrategies.length > 0) { + throw new Error(`Invalid strategies: ${invalidStrategies.join(', ')}`); + } } export const { diff --git a/test/integration/__snapshots__/utils.test.ts.snap b/test/integration/__snapshots__/utils.test.ts.snap index fa5540c22..7e7cefdfc 100644 --- a/test/integration/__snapshots__/utils.test.ts.snap +++ b/test/integration/__snapshots__/utils.test.ts.snap @@ -23,3 +23,24 @@ exports[`utils getVp() should calculate VP for EVM address on mixed protocol 1`] "vp_state": "final", } `; + +exports[`utils getVp() should calculate VP for Starknet address on evm protocol 1`] = ` +{ + "vp": 0, + "vp_by_strategy": [ + 0, + ], + "vp_state": "final", +} +`; + +exports[`utils getVp() should calculate VP for Starknet address on mixed protocol 1`] = ` +{ + "vp": 1, + "vp_by_strategy": [ + 1, + 0, + ], + "vp_state": "final", +} +`; diff --git a/test/integration/utils.test.ts b/test/integration/utils.test.ts index e6899b7b6..8d800d83c 100644 --- a/test/integration/utils.test.ts +++ b/test/integration/utils.test.ts @@ -95,5 +95,25 @@ describe('utils', () => { ).rejects.toThrow('invalid address'); } ); + + it('should throw an error with single invalid strategy', async () => { + await expect( + getVp(evmAddress, network, strategies.singleInvalid, snapshot, space) + ).rejects.toThrow('Invalid strategies: whitelist-invalid'); + }); + + it('should throw an error with multiple invalid strategies', async () => { + await expect( + getVp(evmAddress, network, strategies.multipleInvalid, snapshot, space) + ).rejects.toThrow( + 'Invalid strategies: strategy-one-invalid, strategy-two-invalid' + ); + }); + + it('should throw an error with empty strategies', async () => { + await expect( + getVp(evmAddress, network, [], snapshot, space) + ).rejects.toThrow('no strategies provided'); + }); }); }); diff --git a/test/integration/validation/basic.test.ts b/test/integration/validation/basic.test.ts index 2d4481f81..be1c7dee4 100644 --- a/test/integration/validation/basic.test.ts +++ b/test/integration/validation/basic.test.ts @@ -531,7 +531,7 @@ describe('Basic Validation Integration Tests', () => { ); await expect(validation.validate()).rejects.toThrow( - 'Invalid strategy: non-existent-strategy' + 'Invalid strategies: non-existent-strategy' ); }, 10000); }); From a9c81c82e2e5c86883de687f944e2700a16b79ad Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:35:43 +0400 Subject: [PATCH 3/3] perf: --- src/utils.ts | 71 +++++++++++++++++----------------- test/integration/utils.test.ts | 2 +- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 3a4feba99..62a8a4d5a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,6 @@ async function callStrategy( ) { return {}; } - const score: Score = await _strategies[strategy.name].strategy( space, network, @@ -32,12 +31,19 @@ async function callStrategy( strategy.params, snapshot ); - const addressesLc = addresses.map((address) => address.toLowerCase()); - return Object.fromEntries( - Object.entries(score).filter( - ([address, vp]) => vp > 0 && addressesLc.includes(address.toLowerCase()) - ) + + const normalizedAddresses = new Set( + addresses.map((address) => address.toLowerCase()) ); + const filteredScore: Score = {}; + + for (const [address, vp] of Object.entries(score)) { + if (vp > 0 && normalizedAddresses.has(address.toLowerCase())) { + filteredScore[address] = vp; + } + } + + return filteredScore; } export async function getScoresDirect( @@ -51,7 +57,7 @@ export async function getScoresDirect( try { if (addresses.length === 0) return strategies.map(() => ({})); - const addressesByProtocols = validateAndFormatAddresses(addresses); + const addressesByProtocol = categorizeAddressesByProtocol(addresses); validateStrategies(strategies); const networks = [...new Set(strategies.map((s) => s.network || network))]; @@ -62,10 +68,7 @@ export async function getScoresDirect( callStrategy( space, strategy.network || network, - getAddressesFromSupportedProtocols( - addressesByProtocols, - strategy.name - ), + filterAddressesForStrategy(addressesByProtocol, strategy.name), strategy, snapshots[strategy.network || network] ) @@ -83,7 +86,9 @@ export async function getVp( snapshot: Snapshot, space: string ): Promise { - validateStrategies(strategies, { allowEmpty: false }); + if (!strategies.length) { + throw new Error('no strategies provided'); + } const scores = await getScoresDirect( space, @@ -94,9 +99,10 @@ export async function getVp( snapshot ); + const normalizedAddress = address.toLowerCase(); const vpByStrategy = scores.map((score) => { const matchingKey = Object.keys(score).find( - (key) => key.toLowerCase() === address.toLowerCase() + (key) => key.toLowerCase() === normalizedAddress ); return matchingKey ? score[matchingKey] : 0; }); @@ -133,13 +139,13 @@ export function customFetch( ]); } -function getAddressType(address: string): Protocol | null { +function detectProtocol(address: string): Protocol | null { if (/^0x[a-fA-F0-9]{40}$/.test(address)) return 'evm'; if (/^0x[a-fA-F0-9]{64}$/.test(address)) return 'starknet'; return null; } -function validateAndFormatAddresses( +function categorizeAddressesByProtocol( addresses: string[] ): Record { const results: Record = { @@ -148,42 +154,37 @@ function validateAndFormatAddresses( }; for (const address of addresses) { - const addressType = getAddressType(address); + const addressType = detectProtocol(address); if (!addressType) { - throw new Error('invalid address'); + throw new Error(`Invalid address format: ${address}`); } try { - results[addressType].push( - snapshot.utils.getFormattedAddress(address, addressType) + const formattedAddress = snapshot.utils.getFormattedAddress( + address, + addressType ); + if (!results[addressType].includes(formattedAddress)) { + results[addressType].push(formattedAddress); + } } catch { - throw new Error('invalid address'); + throw new Error(`Invalid ${addressType} address: ${address}`); } } return results; } -function getAddressesFromSupportedProtocols( - addressesByProtocols: Record, +function filterAddressesForStrategy( + addressesByProtocol: Record, strategyName: string ): string[] { - return Object.entries(addressesByProtocols) - .filter(([protocol]) => - _strategies[strategyName].supportedProtocols.includes(protocol) - ) - .flatMap(([, addresses]) => addresses); + return _strategies[strategyName].supportedProtocols.flatMap( + (protocol: Protocol) => addressesByProtocol[protocol] || [] + ); } -function validateStrategies( - strategies: any[], - { allowEmpty = true }: { allowEmpty?: boolean } = {} -): void { - if (!strategies.length && !allowEmpty) { - throw new Error('no strategies provided'); - } - +function validateStrategies(strategies: any[]): void { const invalidStrategies = strategies .filter((strategy) => !_strategies[strategy.name]) .map((strategy) => strategy.name); diff --git a/test/integration/utils.test.ts b/test/integration/utils.test.ts index 8d800d83c..f5b29f17e 100644 --- a/test/integration/utils.test.ts +++ b/test/integration/utils.test.ts @@ -92,7 +92,7 @@ describe('utils', () => { snapshot, space ) - ).rejects.toThrow('invalid address'); + ).rejects.toThrow(/Invalid.*address/); } );