From bae8ddc7d6b037dcd24ad411a68fac3944e31ac2 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Wed, 28 May 2025 17:59:22 +0400 Subject: [PATCH 01/14] feat: filter out addresses not supported by strategies and validation Co-Authored-By: Copilot <175728472+Copilot@users.noreply.github.com> --- package.json | 3 +- src/index.ts | 3 + src/strategies/index.ts | 2 + src/strategies/ocean-dao-brightid/index.ts | 16 ++-- .../subgraph-split-delegation/index.ts | 3 +- src/strategies/ticket/index.ts | 2 + src/strategies/whitelist-weighted/index.ts | 2 + src/strategies/whitelist/index.ts | 2 + src/types.ts | 9 ++ src/utils.ts | 54 +++++++++-- src/utils/delegation.ts | 15 +++- src/utils/vp.ts | 29 +++--- src/validations/arbitrum/index.ts | 3 + src/validations/basic/index.ts | 4 + src/validations/index.ts | 3 +- .../karma-eas-attestation/index.ts | 3 + src/validations/passport-gated/index.ts | 2 + src/validations/validation.ts | 27 +++++- test/strategy-with-params.test.ts | 3 +- test/strategy.test.ts | 2 +- test/unit/utils.test.ts | 89 +++++++++++++++++++ tsconfig.json | 2 +- 22 files changed, 242 insertions(+), 36 deletions(-) create mode 100644 src/types.ts create mode 100644 test/unit/utils.test.ts diff --git a/package.json b/package.json index 41dac5e0f..b443ea641 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,11 @@ "license": "MIT", "scripts": { "build": "tsc -p .", - "test": "jest -i strategy.test.ts", + "test": "yarn test:unit && jest -i strategy.test.ts", "test:vp": "jest -i vp.test.ts", "test:delegation": "jest -i delegation.test.ts", "test:validation": "jest -i validation.test.ts", + "test:unit": "jest -i test/unit/", "prepublishOnly": "npm run build", "postinstall": "npm run build", "postbuild": "copyfiles -u 1 \"src/**/*.md\" dist/ && copyfiles -u 1 \"src/**/*.json\" dist/", diff --git a/src/index.ts b/src/index.ts index e1e0f7e61..b8cbaff31 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,9 @@ import 'dotenv/config'; import strategies from './strategies'; import validations from './validations'; import utils from './utils'; +import { Protocol } from './types'; + +export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; export default { strategies, diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 3b712e356..63de77e59 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -482,6 +482,7 @@ import * as apecoinStaking from './apecoin-staking'; import * as shroomyVotingPower from './shroomy-voting-power'; import * as prlInSpRL2Balance from './prl-in-sprl2-balance'; import * as edenOnlineOverride from './eden-online-override'; +import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; const strategies = { 'shroomy-voting-power': shroomyVotingPower, @@ -1010,6 +1011,7 @@ Object.keys(strategies).forEach(function (strategyName) { strategies[strategyName].examples = examples; strategies[strategyName].schema = schema; strategies[strategyName].about = about; + strategies[strategyName].supportedProtocols ||= DEFAULT_SUPPORTED_PROTOCOLS; }); export default strategies; diff --git a/src/strategies/ocean-dao-brightid/index.ts b/src/strategies/ocean-dao-brightid/index.ts index b62449ead..abb485cdd 100644 --- a/src/strategies/ocean-dao-brightid/index.ts +++ b/src/strategies/ocean-dao-brightid/index.ts @@ -81,7 +81,7 @@ export async function strategy( .filter((address, index, self) => self.indexOf(address) === index); // Remove duplicates for (const chain of Object.keys(options.strategies)) { - let scores = await getScoresDirect( + const scores = await getScoresDirect( space, options.strategies[chain], chain, @@ -92,7 +92,7 @@ export async function strategy( // [{ address: '0x...', score: 0.5 },{ address: '0x...', score: 0.5 }] // sum scores for each address and return - scores = scores.reduce((finalScores: any, score: any) => { + const addressScores = scores.reduce((finalScores: any, score: any) => { for (const [address, value] of Object.entries(score)) { if (!finalScores[address]) { finalScores[address] = 0; @@ -105,19 +105,19 @@ export async function strategy( // sum delegations addresses.forEach((address) => { - if (!scores[address]) scores[address] = 0; + if (!addressScores[address]) addressScores[address] = 0; if (delegations[address]) { delegations[address].forEach((delegator: string) => { - scores[address] += scores[delegator] ?? 0; // add delegator score - scores[delegator] = 0; // set delegator score to 0 + addressScores[address] += addressScores[delegator] ?? 0; // add delegator score + addressScores[delegator] = 0; // set delegator score to 0 }); } }); - for (const key of Object.keys(scores)) { + for (const key of Object.keys(addressScores)) { totalScores[key] = totalScores[key] - ? totalScores[key] + scores[key] - : scores[key]; + ? totalScores[key] + addressScores[key] + : addressScores[key]; } } diff --git a/src/strategies/subgraph-split-delegation/index.ts b/src/strategies/subgraph-split-delegation/index.ts index 2895e16cf..39f583893 100644 --- a/src/strategies/subgraph-split-delegation/index.ts +++ b/src/strategies/subgraph-split-delegation/index.ts @@ -1,6 +1,7 @@ import { getAddress } from '@ethersproject/address'; import { subgraphRequest, getScoresDirect } from '../../utils'; import { Strategy } from '@snapshot-labs/snapshot.js/dist/src/voting/types'; +import { Snapshot } from '../../types'; export const author = 'aragon'; export const version = '0.1.0'; @@ -35,7 +36,7 @@ export async function strategy( subgraphUrl: DEFAULT_BACKEND_URL, strategies: [] }, - snapshot: string | number + snapshot: Snapshot ) { const blockTag = typeof snapshot === 'number' ? snapshot : 'latest'; const block = await provider.getBlock(blockTag); diff --git a/src/strategies/ticket/index.ts b/src/strategies/ticket/index.ts index 31c2dfcf8..614132bc6 100644 --- a/src/strategies/ticket/index.ts +++ b/src/strategies/ticket/index.ts @@ -1,6 +1,8 @@ export const author = 'bonustrack'; export const version = '0.1.0'; +export const supportedProtocols = ['evm', 'starknet']; + export async function strategy(space, network, provider, addresses, options) { return Object.fromEntries( addresses.map((address) => [address, options.value || 1]) diff --git a/src/strategies/whitelist-weighted/index.ts b/src/strategies/whitelist-weighted/index.ts index 3f41e16c3..f9a5d894c 100644 --- a/src/strategies/whitelist-weighted/index.ts +++ b/src/strategies/whitelist-weighted/index.ts @@ -1,6 +1,8 @@ export const author = 'vsergeev'; export const version = '0.1.0'; +export const supportedProtocols = ['evm', 'starknet']; + export async function strategy(space, network, provider, addresses, options) { const whitelist = Object.fromEntries( Object.entries(options?.addresses).map(([addr, weight]) => [ diff --git a/src/strategies/whitelist/index.ts b/src/strategies/whitelist/index.ts index 35967da2f..e89d21aba 100644 --- a/src/strategies/whitelist/index.ts +++ b/src/strategies/whitelist/index.ts @@ -1,6 +1,8 @@ export const author = 'bonustrack'; export const version = '0.1.0'; +export const supportedProtocols = ['evm', 'starknet']; + export async function strategy(space, network, provider, addresses, options) { const whitelist = options?.addresses.map((address) => address.toLowerCase()); return Object.fromEntries( diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 000000000..45732bb86 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,9 @@ +export type VpState = 'final' | 'pending'; +export type Score = Record; +export type VotingPower = { + vp: number; + vp_by_strategy: number[]; + vp_state: VpState; +}; +export type Snapshot = number | 'latest'; +export type Protocol = 'evm' | 'starknet'; diff --git a/src/utils.ts b/src/utils.ts index ca35b5cd6..04e417211 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -4,24 +4,32 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { getDelegations } from './utils/delegation'; import { getVp, getDelegations as getCoreDelegations } from './utils/vp'; import { createHash } from 'crypto'; +import { Protocol, Score, Snapshot } from './types'; export function sha256(str) { return createHash('sha256').update(str).digest('hex'); } -async function callStrategy(space, network, addresses, strategy, snapshot) { +async function callStrategy( + space: string, + network, + addresses: string[], + strategy, + snapshot: Snapshot +): Promise { if ( (snapshot !== 'latest' && strategy.params?.start > snapshot) || (strategy.params?.end && (snapshot === 'latest' || snapshot > strategy.params?.end)) - ) + ) { return {}; + } if (!_strategies.hasOwnProperty(strategy.name)) { throw new Error(`Invalid strategy: ${strategy.name}`); } - const score: any = await _strategies[strategy.name].strategy( + const score: Score = await _strategies[strategy.name].strategy( space, network, getProvider(network), @@ -32,8 +40,7 @@ async function callStrategy(space, network, addresses, strategy, snapshot) { const addressesLc = addresses.map((address) => address.toLowerCase()); return Object.fromEntries( Object.entries(score).filter( - ([address, vp]: any[]) => - vp > 0 && addressesLc.includes(address.toLowerCase()) + ([address, vp]) => vp > 0 && addressesLc.includes(address.toLowerCase()) ) ); } @@ -44,8 +51,8 @@ export async function getScoresDirect( network: string, provider, addresses: string[], - snapshot: number | string = 'latest' -) { + snapshot: Snapshot +): Promise { try { const networks = strategies.map((s) => s.network || network); const snapshots = await getSnapshots(network, snapshot, provider, networks); @@ -92,6 +99,39 @@ export function customFetch( ]); } +export function getFormattedAddressesByProtocol( + addresses: string[], + protocols: Protocol[] = ['evm'] +): string[] { + if (!protocols.length) { + throw new Error('At least one protocol must be specified'); + } + + return addresses + .map((address) => { + let evmAddress, starknetAddress; + + try { + evmAddress = snapshot.utils.getFormattedAddress(address, 'evm'); + } catch (e) {} + try { + starknetAddress = snapshot.utils.getFormattedAddress( + address, + 'starknet' + ); + } catch (e) {} + + if (evmAddress && protocols.includes('evm')) { + return evmAddress; + } + + if (!evmAddress && starknetAddress && protocols.includes('starknet')) { + return starknetAddress; + } + }) + .filter(Boolean); +} + export const { multicall, Multicaller, diff --git a/src/utils/delegation.ts b/src/utils/delegation.ts index b48a34665..99795fd7d 100644 --- a/src/utils/delegation.ts +++ b/src/utils/delegation.ts @@ -1,10 +1,16 @@ import { getAddress } from '@ethersproject/address'; import { getDelegatesBySpace } from '../utils'; +import { Snapshot } from '../types'; const DELEGATION_DATA_CACHE = {}; // delegations with overrides -export async function getDelegations(space, network, addresses, snapshot) { +export async function getDelegations( + space, + network, + addresses: string[], + snapshot: Snapshot +) { const addressesLc = addresses.map((address) => address.toLowerCase()); const delegatesBySpace = await getDelegatesBySpace(network, space, snapshot); @@ -46,7 +52,12 @@ function getDelegationReverseData(delegation) { }; } -export async function getDelegationsData(space, network, addresses, snapshot) { +export async function getDelegationsData( + space, + network, + addresses: string[], + snapshot: Snapshot +) { const cacheKey = `${space}-${network}-${snapshot}`; let delegationsReverse = DELEGATION_DATA_CACHE[cacheKey]; diff --git a/src/utils/vp.ts b/src/utils/vp.ts index 6bd74f2af..08f5ce6c0 100644 --- a/src/utils/vp.ts +++ b/src/utils/vp.ts @@ -2,12 +2,14 @@ import { formatBytes32String } from '@ethersproject/strings'; import { getAddress } from '@ethersproject/address'; import subgraphs from '@snapshot-labs/snapshot.js/src/delegationSubgraphs.json'; import { + getFormattedAddressesByProtocol, getProvider, getSnapshots, Multicaller, subgraphRequest } from '../utils'; import _strategies from '../strategies'; +import { Score, Snapshot, VotingPower } from '../types'; const DELEGATION_CONTRACT = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'; const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; @@ -23,10 +25,10 @@ export async function getVp( address: string, network: string, strategies: any[], - snapshot: number | 'latest', + snapshot: Snapshot, space: string, delegation?: boolean -) { +): Promise { const networks = [...new Set(strategies.map((s) => s.network || network))]; const snapshots = await getSnapshots( network, @@ -43,7 +45,7 @@ export async function getVp( ds.forEach((d, i) => (delegations[networks[i]] = d)); } - const p = strategies.map((strategy) => { + const p: Score[] = strategies.map((strategy) => { const n = strategy.network || network; let addresses = [address]; @@ -54,7 +56,10 @@ export async function getVp( if (addresses.length === 0) return {}; } - addresses = addresses.map(getAddress); + addresses = getFormattedAddressesByProtocol( + addresses, + strategy.supportedProtocols + ); return _strategies[strategy.name].strategy( space, n, @@ -76,12 +81,14 @@ export async function getVp( addresses = [...new Set(addresses)]; } - addresses = addresses.map(getAddress); + addresses = getFormattedAddressesByProtocol( + addresses, + strategies[i].supportedProtocols + ); return addresses.reduce((a, b) => a + (score[b] || 0), 0); }); const vp = vpByStrategy.reduce((a, b) => a + b, 0); - let vpState = 'final'; - if (snapshot === 'latest') vpState = 'pending'; + const vpState = snapshot === 'latest' ? 'pending' : 'final'; return { vp, @@ -93,7 +100,7 @@ export async function getVp( export async function getDelegationsOut( addresses: string[], network: string, - snapshot: number | 'latest', + snapshot: Snapshot, space: string ) { if (!subgraphs[network]) @@ -127,7 +134,7 @@ export async function getDelegationsOut( export async function getDelegationOut( address: string, network: string, - snapshot: number | 'latest', + snapshot: Snapshot, space: string ): Promise { const usersDelegationOut = await getDelegationsOut( @@ -142,7 +149,7 @@ export async function getDelegationOut( export async function getDelegationsIn( address: string, network: string, - snapshot: number | 'latest', + snapshot: Snapshot, space: string ): Promise { if (!subgraphs[network]) return []; @@ -206,7 +213,7 @@ export async function getDelegationsIn( export async function getDelegations( address: string, network: string, - snapshot: number | 'latest', + snapshot: Snapshot, space: string ): Promise { const [delegationOut, delegationsIn] = await Promise.all([ diff --git a/src/validations/arbitrum/index.ts b/src/validations/arbitrum/index.ts index 009b51a2f..699d8543a 100644 --- a/src/validations/arbitrum/index.ts +++ b/src/validations/arbitrum/index.ts @@ -20,6 +20,9 @@ export default class extends Validation { async validate(): Promise { if (this.params.strategies?.length > 8) throw new Error(`Max number of strategies exceeded`); + + this.validateAddressType(); + const minBps = this.params.minBps; const decimals = this.params.decimals; const excludeaddr = diff --git a/src/validations/basic/index.ts b/src/validations/basic/index.ts index 3dee0b85c..5d4fdd2b2 100644 --- a/src/validations/basic/index.ts +++ b/src/validations/basic/index.ts @@ -1,5 +1,6 @@ import Validation from '../validation'; import { getProvider, getScoresDirect } from '../../utils'; +import { Protocol } from '../../types'; export default class extends Validation { public id = 'basic'; @@ -7,13 +8,16 @@ export default class extends Validation { public version = '0.2.0'; public title = 'Basic'; public description = 'Use any strategy to determine if a user can vote.'; + public supportedProtocols: Protocol[] = ['evm', 'starknet']; async validate(): Promise { if (this.params.strategies?.length > 8) throw new Error(`Max number of strategies exceeded`); + const minScore = this.params.minScore; if (minScore) { + this.validateAddressType(); const scores = await getScoresDirect( this.space, this.params.strategies, diff --git a/src/validations/index.ts b/src/validations/index.ts index 3cdc99c23..f66f532e8 100644 --- a/src/validations/index.ts +++ b/src/validations/index.ts @@ -60,7 +60,8 @@ Object.keys(validationClasses).forEach(function (validationName) { title: validationInstance.title, description: validationInstance.description, proposalValidationOnly: validationInstance.proposalValidationOnly, - votingValidationOnly: validationInstance.votingValidationOnly + votingValidationOnly: validationInstance.votingValidationOnly, + supportedProtocols: validationInstance.supportedProtocols }; }); diff --git a/src/validations/karma-eas-attestation/index.ts b/src/validations/karma-eas-attestation/index.ts index f4d4d24dc..c6e897385 100644 --- a/src/validations/karma-eas-attestation/index.ts +++ b/src/validations/karma-eas-attestation/index.ts @@ -70,7 +70,10 @@ export default class extends Validation { public description = 'Use EAS attest.sh to determine if user can create a proposal.'; public proposalValidationOnly = true; + async validate(): Promise { + this.validateAddressType(); + const schemaId = this.params.schemaId; if (!schemaId) throw new Error(`Attestation schema not provided`); diff --git a/src/validations/passport-gated/index.ts b/src/validations/passport-gated/index.ts index a9aad2924..becf98ed1 100644 --- a/src/validations/passport-gated/index.ts +++ b/src/validations/passport-gated/index.ts @@ -171,6 +171,8 @@ export default class extends Validation { 'Protect your proposals from spam and vote manipulation by requiring users to have a valid Gitcoin Passport.'; async validate(currentAddress = this.author): Promise { + this.validateAddressType(); + const requiredStamps = this.params.stamps || []; const operator = this.params.operator; const scoreThreshold = this.params.scoreThreshold || 0; diff --git a/src/validations/validation.ts b/src/validations/validation.ts index 48913bcba..d5f6e6613 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -1,21 +1,25 @@ +import { Protocol, Snapshot } from '../types'; +import snapshot from '@snapshot-labs/snapshot.js'; + export default class Validation { public id = ''; public github = ''; public version = ''; public title = ''; public description = ''; + public supportedProtocols: Protocol[] = ['evm']; public author: string; public space: string; public network: string; - public snapshot: number | 'latest'; + public snapshot: Snapshot; public params: any; constructor( author: string, space: string, network: string, - snapshot: number | 'latest', + snapshot: Snapshot, params: any ) { this.author = author; @@ -28,4 +32,23 @@ export default class Validation { async validate(): Promise { return true; } + + validateAddressType(): boolean { + const formattedAddress = snapshot.utils.getFormattedAddress(this.author); + + if ( + (snapshot.utils.isEvmAddress(formattedAddress) && + this.supportedProtocols.includes('evm')) || + (snapshot.utils.isStarknetAddress(formattedAddress) && + this.supportedProtocols.includes('starknet')) + ) { + return true; + } + + throw new Error( + `Address "${this.author}" is not a valid ${this.supportedProtocols.join( + ' or ' + )} address` + ); + } } diff --git a/test/strategy-with-params.test.ts b/test/strategy-with-params.test.ts index 3ebdc1065..a63d4d629 100644 --- a/test/strategy-with-params.test.ts +++ b/test/strategy-with-params.test.ts @@ -1,5 +1,6 @@ // To test strategies by copy pasting score API body here import snapshot from '../src'; +import { Snapshot } from '../src/types'; const scoreAPIObj = { params: { @@ -1346,7 +1347,7 @@ snapshot.utils scoreAPIObj.params.network, provider, scoreAPIObj.params.addresses, - scoreAPIObj.params.snapshot + scoreAPIObj.params.snapshot as Snapshot ) .then(console.log) .then(() => { diff --git a/test/strategy.test.ts b/test/strategy.test.ts index 5e729838a..59192cc99 100644 --- a/test/strategy.test.ts +++ b/test/strategy.test.ts @@ -172,7 +172,7 @@ describe.each(examples)( expect(Array.isArray(scores)).toBe(true); // Check array contains a object expect(typeof scores[0]).toBe('object'); - // Check object contains atleast one address from example.json + // Check object contains at least one address from example.json expect(Object.keys(scores[0]).length).toBeGreaterThanOrEqual(1); expect( Object.keys(scores[0]).some((address) => diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts new file mode 100644 index 000000000..ec4cdbd96 --- /dev/null +++ b/test/unit/utils.test.ts @@ -0,0 +1,89 @@ +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()', () => { + const input = [ + VALID_EVM_ADDRESS, + 'invalidEVMAddress', + VALID_STARKNET_ADDRESS, + '' + ]; + + it('should return an empty array when no addresses are provided', () => { + const result = getFormattedAddressesByProtocol([]); + expect(result).toEqual([]); + }); + + it('should return an empty array when protocol is not valid', () => { + const result = getFormattedAddressesByProtocol(input, [ + // @ts-ignore + 'invalidProtocol' + ]); + expect(result).toEqual([]); + }); + + it('should throw an error when no protocol is provided', () => { + expect(() => { + getFormattedAddressesByProtocol([], []); + }).toThrow(); + }); + + it('should use evm as default protocol when no protocols provided', () => { + const result = getFormattedAddressesByProtocol(input); + expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); + }); + + it('should prioritize evm when address is valid for both protocols and both are specified', () => { + const result = getFormattedAddressesByProtocol( + [VALID_EVM_ADDRESS], + ['evm', 'starknet'] + ); + expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); + }); + + it('should return only formatted EVM addresses on evm protocol', () => { + const result = getFormattedAddressesByProtocol(input, ['evm']); + expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); + }); + + it('should return only formatted starknet addresses on starknet protocol', () => { + const result = getFormattedAddressesByProtocol(input, ['starknet']); + expect(result).toEqual([VALID_FORMATTED_STARKNET_ADDRESS]); + }); + + it('should return both formatted starknet and evm addresses on starknet and evm protocol', () => { + const result = getFormattedAddressesByProtocol(input, [ + 'starknet', + 'evm' + ]); + expect(result).toEqual([ + VALID_FORMATTED_EVM_ADDRESS, + VALID_FORMATTED_STARKNET_ADDRESS + ]); + }); + + it('should return empty array when all addresses are invalid for specified protocol', () => { + const result = getFormattedAddressesByProtocol( + ['invalidAddress'], + ['evm'] + ); + expect(result).toEqual([]); + }); + + it('should return empty array when all addresses are invalid for specified protocol', () => { + const result = getFormattedAddressesByProtocol( + ['invalidAddress'], + ['starknet'] + ); + expect(result).toEqual([]); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 709472bf3..cc30f96b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "allowSyntheticDefaultImports": true, "skipLibCheck": true }, - "include": ["src"], + "include": ["src", "test"], "files": ["./src/typings.d.ts"] } From 681215770b58bd9c2eb769d7c4fed75e26ff021f Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Thu, 12 Jun 2025 22:26:59 +0400 Subject: [PATCH 02/14] fix: add default supported protocols --- src/utils/vp.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/vp.ts b/src/utils/vp.ts index 08f5ce6c0..80c69b5dd 100644 --- a/src/utils/vp.ts +++ b/src/utils/vp.ts @@ -10,6 +10,7 @@ import { } from '../utils'; import _strategies from '../strategies'; import { Score, Snapshot, VotingPower } from '../types'; +import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; const DELEGATION_CONTRACT = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'; const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; @@ -58,7 +59,7 @@ export async function getVp( addresses = getFormattedAddressesByProtocol( addresses, - strategy.supportedProtocols + strategy.supportedProtocols ?? DEFAULT_SUPPORTED_PROTOCOLS ); return _strategies[strategy.name].strategy( space, From ae06559397b14bc034ce1b22e12533bd2172f727 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 13 Jun 2025 01:20:49 +0400 Subject: [PATCH 03/14] refactor: reduce code duplication with a template pattern --- src/validations/arbitrum/index.ts | 4 +--- src/validations/basic/index.ts | 3 +-- .../karma-eas-attestation/index.ts | 4 +--- src/validations/passport-gated/index.ts | 8 +++----- src/validations/validation.ts | 19 ++++++++++++++----- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/validations/arbitrum/index.ts b/src/validations/arbitrum/index.ts index 699d8543a..1ec1a9a39 100644 --- a/src/validations/arbitrum/index.ts +++ b/src/validations/arbitrum/index.ts @@ -17,12 +17,10 @@ export default class extends Validation { 'Use with erc20-votes to validate by percentage of votable supply.'; public proposalValidationOnly = true; - async validate(): Promise { + protected async doValidate(): Promise { if (this.params.strategies?.length > 8) throw new Error(`Max number of strategies exceeded`); - this.validateAddressType(); - const minBps = this.params.minBps; const decimals = this.params.decimals; const excludeaddr = diff --git a/src/validations/basic/index.ts b/src/validations/basic/index.ts index 5d4fdd2b2..ae851f218 100644 --- a/src/validations/basic/index.ts +++ b/src/validations/basic/index.ts @@ -10,14 +10,13 @@ export default class extends Validation { public description = 'Use any strategy to determine if a user can vote.'; public supportedProtocols: Protocol[] = ['evm', 'starknet']; - async validate(): Promise { + protected async doValidate(): Promise { if (this.params.strategies?.length > 8) throw new Error(`Max number of strategies exceeded`); const minScore = this.params.minScore; if (minScore) { - this.validateAddressType(); const scores = await getScoresDirect( this.space, this.params.strategies, diff --git a/src/validations/karma-eas-attestation/index.ts b/src/validations/karma-eas-attestation/index.ts index c6e897385..4cca6b2d7 100644 --- a/src/validations/karma-eas-attestation/index.ts +++ b/src/validations/karma-eas-attestation/index.ts @@ -71,9 +71,7 @@ export default class extends Validation { 'Use EAS attest.sh to determine if user can create a proposal.'; public proposalValidationOnly = true; - async validate(): Promise { - this.validateAddressType(); - + protected async doValidate(): Promise { const schemaId = this.params.schemaId; if (!schemaId) throw new Error(`Attestation schema not provided`); diff --git a/src/validations/passport-gated/index.ts b/src/validations/passport-gated/index.ts index becf98ed1..7b664751d 100644 --- a/src/validations/passport-gated/index.ts +++ b/src/validations/passport-gated/index.ts @@ -170,9 +170,7 @@ export default class extends Validation { public description = 'Protect your proposals from spam and vote manipulation by requiring users to have a valid Gitcoin Passport.'; - async validate(currentAddress = this.author): Promise { - this.validateAddressType(); - + protected async doValidate(customAuthor: string): Promise { const requiredStamps = this.params.stamps || []; const operator = this.params.operator; const scoreThreshold = this.params.scoreThreshold || 0; @@ -183,7 +181,7 @@ export default class extends Validation { const provider = snapshot.utils.getProvider(this.network); const proposalTs = (await provider.getBlock(this.snapshot)).timestamp; const validStamps = await validateStamps( - currentAddress, + customAuthor, operator, proposalTs, requiredStamps @@ -194,7 +192,7 @@ export default class extends Validation { } const validScore = await validatePassportScore( - currentAddress, + customAuthor, scoreThreshold ); diff --git a/src/validations/validation.ts b/src/validations/validation.ts index d5f6e6613..49bd96cca 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -1,3 +1,4 @@ +import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; import { Protocol, Snapshot } from '../types'; import snapshot from '@snapshot-labs/snapshot.js'; @@ -7,7 +8,7 @@ export default class Validation { public version = ''; public title = ''; public description = ''; - public supportedProtocols: Protocol[] = ['evm']; + public supportedProtocols: Protocol[] = DEFAULT_SUPPORTED_PROTOCOLS; public author: string; public space: string; @@ -29,12 +30,20 @@ export default class Validation { this.params = params; } - async validate(): Promise { + async validate(customAuthor = this.author): Promise { + this.validateAddressType(customAuthor); + + return this.doValidate(customAuthor); + } + + // Abstract method to be implemented by subclasses + // This contains the actual validation logic without global/commons validation + protected async doValidate(_customAuthor: string): Promise { return true; } - validateAddressType(): boolean { - const formattedAddress = snapshot.utils.getFormattedAddress(this.author); + validateAddressType(address: string): boolean { + const formattedAddress = snapshot.utils.getFormattedAddress(address); if ( (snapshot.utils.isEvmAddress(formattedAddress) && @@ -46,7 +55,7 @@ export default class Validation { } throw new Error( - `Address "${this.author}" is not a valid ${this.supportedProtocols.join( + `Address "${address}" is not a valid ${this.supportedProtocols.join( ' or ' )} address` ); From 8324004eadc679890d74226bc61e99300166f74b Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 13 Jun 2025 01:32:34 +0400 Subject: [PATCH 04/14] refactor: move the strategies length to the base class --- src/index.ts | 1 + src/validations/arbitrum/index.ts | 63 ++++++++++++++----------------- src/validations/basic/index.ts | 35 ++++++++--------- src/validations/validation.ts | 16 +++++++- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/src/index.ts b/src/index.ts index b8cbaff31..4d89c5818 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ import utils from './utils'; import { Protocol } from './types'; export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; +export const MAX_STRATEGIES_LENGTH = 8; export default { strategies, diff --git a/src/validations/arbitrum/index.ts b/src/validations/arbitrum/index.ts index 1ec1a9a39..5e9f4e2f9 100644 --- a/src/validations/arbitrum/index.ts +++ b/src/validations/arbitrum/index.ts @@ -16,47 +16,42 @@ export default class extends Validation { public description = 'Use with erc20-votes to validate by percentage of votable supply.'; public proposalValidationOnly = true; + public hasInnerStrategies = true; protected async doValidate(): Promise { - if (this.params.strategies?.length > 8) - throw new Error(`Max number of strategies exceeded`); - const minBps = this.params.minBps; const decimals = this.params.decimals; const excludeaddr = this.params.excludeaddr ?? '0x00000000000000000000000000000000000A4B86'; - if (minBps) { - const scores = await getScoresDirect( - this.space, - this.params.strategies, - this.network, - getProvider(this.network), - [this.author], - this.snapshot || 'latest' - ); - const totalScore: any = scores - .map((score: any) => - Object.values(score).reduce((a, b: any) => a + b, 0) - ) - .reduce((a, b: any) => a + b, 0); - const [[totalSupply], [excludedSupply]] = await multicall( - this.network, - getProvider(this.network), - abi, - [ - [this.params.address, 'totalSupply', []], - [this.params.address, 'getVotes', [excludeaddr]] - ], - { blockTag: this.snapshot || 'latest' } - ); - const votableSupply = parseFloat( - formatUnits(totalSupply.sub(excludedSupply).toString(), decimals) - ); - const bpsOfVotable = (totalScore * 10000) / votableSupply; - if (bpsOfVotable < minBps) return false; - } + if (!minBps) return true; + + const scores = await getScoresDirect( + this.space, + this.params.strategies, + this.network, + getProvider(this.network), + [this.author], + this.snapshot || 'latest' + ); + const totalScore: any = scores + .map((score: any) => Object.values(score).reduce((a, b: any) => a + b, 0)) + .reduce((a, b: any) => a + b, 0); + const [[totalSupply], [excludedSupply]] = await multicall( + this.network, + getProvider(this.network), + abi, + [ + [this.params.address, 'totalSupply', []], + [this.params.address, 'getVotes', [excludeaddr]] + ], + { blockTag: this.snapshot || 'latest' } + ); + const votableSupply = parseFloat( + formatUnits(totalSupply.sub(excludedSupply).toString(), decimals) + ); + const bpsOfVotable = (totalScore * 10000) / votableSupply; - return true; + return bpsOfVotable >= minBps; } } diff --git a/src/validations/basic/index.ts b/src/validations/basic/index.ts index ae851f218..551e8e383 100644 --- a/src/validations/basic/index.ts +++ b/src/validations/basic/index.ts @@ -9,30 +9,25 @@ export default class extends Validation { public title = 'Basic'; public description = 'Use any strategy to determine if a user can vote.'; public supportedProtocols: Protocol[] = ['evm', 'starknet']; + public hasInnerStrategies = true; protected async doValidate(): Promise { - if (this.params.strategies?.length > 8) - throw new Error(`Max number of strategies exceeded`); - const minScore = this.params.minScore; - if (minScore) { - const scores = await getScoresDirect( - this.space, - this.params.strategies, - this.network, - getProvider(this.network), - [this.author], - this.snapshot || 'latest' - ); - const totalScore: any = scores - .map((score: any) => - Object.values(score).reduce((a, b: any) => a + b, 0) - ) - .reduce((a, b: any) => a + b, 0); - if (totalScore < minScore) return false; - } + if (!minScore) return true; + + const scores = await getScoresDirect( + this.space, + this.params.strategies, + this.network, + getProvider(this.network), + [this.author], + this.snapshot || 'latest' + ); + const totalScore: any = scores + .map((score: any) => Object.values(score).reduce((a, b: any) => a + b, 0)) + .reduce((a, b: any) => a + b, 0); - return true; + return totalScore >= minScore; } } diff --git a/src/validations/validation.ts b/src/validations/validation.ts index 49bd96cca..35e3fa0b8 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -1,4 +1,4 @@ -import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; +import { DEFAULT_SUPPORTED_PROTOCOLS, MAX_STRATEGIES_LENGTH } from '..'; import { Protocol, Snapshot } from '../types'; import snapshot from '@snapshot-labs/snapshot.js'; @@ -9,6 +9,7 @@ export default class Validation { public title = ''; public description = ''; public supportedProtocols: Protocol[] = DEFAULT_SUPPORTED_PROTOCOLS; + public hasInnerStrategies = false; public author: string; public space: string; @@ -32,6 +33,7 @@ export default class Validation { async validate(customAuthor = this.author): Promise { this.validateAddressType(customAuthor); + this.validateStrategiesLength(); return this.doValidate(customAuthor); } @@ -42,7 +44,7 @@ export default class Validation { return true; } - validateAddressType(address: string): boolean { + private validateAddressType(address: string): boolean { const formattedAddress = snapshot.utils.getFormattedAddress(address); if ( @@ -60,4 +62,14 @@ export default class Validation { )} address` ); } + + private validateStrategiesLength(): boolean { + if ( + this.hasInnerStrategies && + this.params.strategies?.length > MAX_STRATEGIES_LENGTH + ) { + throw new Error(`Max number of strategies exceeded`); + } + return true; + } } From 016fad362469520940e8cbc3c2ea02550af12ccf Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 13 Jun 2025 02:20:06 +0400 Subject: [PATCH 05/14] test: add unit tests for the validation class --- src/constants.ts | 4 + src/index.ts | 4 - src/strategies/index.ts | 2 +- src/utils/vp.ts | 2 +- src/validations/validation.ts | 28 ++- test/unit/validation.test.ts | 333 ++++++++++++++++++++++++++++++++++ 6 files changed, 357 insertions(+), 16 deletions(-) create mode 100644 src/constants.ts create mode 100644 test/unit/validation.test.ts diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 000000000..ec71f0853 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +import { Protocol } from './types'; + +export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; +export const MAX_STRATEGIES_LENGTH = 8; diff --git a/src/index.ts b/src/index.ts index 4d89c5818..e1e0f7e61 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,6 @@ import 'dotenv/config'; import strategies from './strategies'; import validations from './validations'; import utils from './utils'; -import { Protocol } from './types'; - -export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; -export const MAX_STRATEGIES_LENGTH = 8; export default { strategies, diff --git a/src/strategies/index.ts b/src/strategies/index.ts index 1a23cc7f6..05e63acc5 100644 --- a/src/strategies/index.ts +++ b/src/strategies/index.ts @@ -485,7 +485,7 @@ import * as shroomyVotingPower from './shroomy-voting-power'; import * as pufferGetPastVotes from './puffer-getpastvotes'; import * as prlInSpRL2Balance from './prl-in-sprl2-balance'; import * as edenOnlineOverride from './eden-online-override'; -import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; +import { DEFAULT_SUPPORTED_PROTOCOLS } from '../constants'; const strategies = { 'shroomy-voting-power': shroomyVotingPower, diff --git a/src/utils/vp.ts b/src/utils/vp.ts index 80c69b5dd..9066f4270 100644 --- a/src/utils/vp.ts +++ b/src/utils/vp.ts @@ -10,7 +10,7 @@ import { } from '../utils'; import _strategies from '../strategies'; import { Score, Snapshot, VotingPower } from '../types'; -import { DEFAULT_SUPPORTED_PROTOCOLS } from '..'; +import { DEFAULT_SUPPORTED_PROTOCOLS } from '../constants'; const DELEGATION_CONTRACT = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446'; const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000'; diff --git a/src/validations/validation.ts b/src/validations/validation.ts index 35e3fa0b8..f17907852 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -1,6 +1,9 @@ -import { DEFAULT_SUPPORTED_PROTOCOLS, MAX_STRATEGIES_LENGTH } from '..'; -import { Protocol, Snapshot } from '../types'; import snapshot from '@snapshot-labs/snapshot.js'; +import { + DEFAULT_SUPPORTED_PROTOCOLS, + MAX_STRATEGIES_LENGTH +} from '../constants'; +import { Protocol, Snapshot } from '../types'; export default class Validation { public id = ''; @@ -40,20 +43,25 @@ export default class Validation { // Abstract method to be implemented by subclasses // This contains the actual validation logic without global/commons validation + // eslint-disable-next-line @typescript-eslint/no-unused-vars protected async doValidate(_customAuthor: string): Promise { return true; } private validateAddressType(address: string): boolean { - const formattedAddress = snapshot.utils.getFormattedAddress(address); + try { + const formattedAddress = snapshot.utils.getFormattedAddress(address); - if ( - (snapshot.utils.isEvmAddress(formattedAddress) && - this.supportedProtocols.includes('evm')) || - (snapshot.utils.isStarknetAddress(formattedAddress) && - this.supportedProtocols.includes('starknet')) - ) { - return true; + if ( + (snapshot.utils.isEvmAddress(formattedAddress) && + this.supportedProtocols.includes('evm')) || + (snapshot.utils.isStarknetAddress(formattedAddress) && + this.supportedProtocols.includes('starknet')) + ) { + return true; + } + } catch (error) { + // If isStarknetAddress throws an error, fall through to the standard error } throw new Error( diff --git a/test/unit/validation.test.ts b/test/unit/validation.test.ts new file mode 100644 index 000000000..bcc52a6c0 --- /dev/null +++ b/test/unit/validation.test.ts @@ -0,0 +1,333 @@ +import Validation from '../../src/validations/validation'; + +class TestValidation extends Validation {} + +describe('Validation', () => { + describe('constructor', () => { + it('should initialize all properties correctly', () => { + const author = '0x1234567890abcdef1234567890abcdef12345678'; + const space = 'test-space'; + const network = '1'; + const snapshot = 123456; + const params = { test: 'value' }; + + const validation = new TestValidation( + author, + space, + network, + snapshot, + params + ); + + expect(validation.author).toBe(author); + expect(validation.space).toBe(space); + expect(validation.network).toBe(network); + expect(validation.snapshot).toBe(snapshot); + expect(validation.params).toBe(params); + expect(validation.id).toBe(''); + expect(validation.github).toBe(''); + expect(validation.version).toBe(''); + expect(validation.title).toBe(''); + expect(validation.description).toBe(''); + expect(validation.supportedProtocols).toEqual(['evm']); + expect(validation.hasInnerStrategies).toBe(false); + }); + }); + + describe('validate() method', () => { + it('should call doValidate with default author when no custom author provided', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + const doValidateSpy = jest.spyOn(validation, 'doValidate' as any); + + await validation.validate(); + + expect(doValidateSpy).toHaveBeenCalledWith( + '0x1234567890abcdef1234567890abcdef12345678' + ); + }); + + it('should call doValidate with custom author when provided', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + const customAuthor = '0xabcdef1234567890abcdef1234567890abcdef12'; + const doValidateSpy = jest.spyOn(validation, 'doValidate' as any); + + await validation.validate(customAuthor); + + expect(doValidateSpy).toHaveBeenCalledWith(customAuthor); + }); + + it('should validate custom author address type', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm']; + const starknetAddress = + '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203A'; + + await expect(validation.validate(starknetAddress)).rejects.toThrow( + `Address "${starknetAddress}" is not a valid evm address` + ); + }); + + it('should return the result from doValidate', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + const mockResult = false; + jest.spyOn(validation, 'doValidate' as any).mockResolvedValue(mockResult); + + const result = await validation.validate(); + + expect(result).toBe(mockResult); + }); + }); + + describe('doValidate() method', () => { + it('should return true by default in base class', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + + const result = await validation.validate(); + expect(result).toBe(true); + }); + }); + + describe('validateStrategiesLength()', () => { + it('should not throw error when hasInnerStrategies is false', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + { strategies: new Array(10).fill({}) } + ); + validation.hasInnerStrategies = false; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error when hasInnerStrategies is true and strategies length is within limit', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + { strategies: new Array(5).fill({}) } + ); + validation.hasInnerStrategies = true; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error when hasInnerStrategies is true and strategies length equals limit', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + { strategies: new Array(8).fill({}) } + ); + validation.hasInnerStrategies = true; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should throw error when hasInnerStrategies is true and strategies length exceeds limit', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + { strategies: new Array(10).fill({}) } + ); + validation.hasInnerStrategies = true; + + await expect(validation.validate()).rejects.toThrow( + 'Max number of strategies exceeded' + ); + }); + + it('should not throw error when hasInnerStrategies is true and strategies is undefined', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + {} + ); + validation.hasInnerStrategies = true; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error when hasInnerStrategies is true and strategies is empty', async () => { + const validation = new TestValidation( + '0x1234567890abcdef1234567890abcdef12345678', + 'test-space', + '1', + 123456, + { strategies: [] } + ); + validation.hasInnerStrategies = true; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + }); + + describe('validateAddressType()', () => { + const VALID_EVM_ADDRESS = '0x1234567890abcdef1234567890abcdef12345678'; + const VALID_STARKNET_ADDRESS = + '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203A'; + const INVALID_ADDRESS = 'invalidAddress'; + + it('should not throw error for valid EVM address when evm protocol is supported', async () => { + const validation = new TestValidation( + VALID_EVM_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm']; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error for valid Starknet address when starknet protocol is supported', async () => { + const validation = new TestValidation( + VALID_STARKNET_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['starknet']; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error for valid EVM address when both protocols are supported', async () => { + const validation = new TestValidation( + VALID_EVM_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm', 'starknet']; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error for valid Starknet address when both protocols are supported', async () => { + const validation = new TestValidation( + VALID_STARKNET_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm', 'starknet']; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should not throw error for valid EVM address when only starknet protocol is supported', async () => { + const validation = new TestValidation( + VALID_EVM_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['starknet']; + + await expect(validation.validate()).resolves.not.toThrow(); + }); + + it('should throw error for valid Starknet address when only evm protocol is supported', async () => { + const validation = new TestValidation( + VALID_STARKNET_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm']; + + await expect(validation.validate()).rejects.toThrow( + `Address "${VALID_STARKNET_ADDRESS}" is not a valid evm address` + ); + }); + + it('should throw error for invalid address when evm protocol is supported', async () => { + const validation = new TestValidation( + INVALID_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm']; + + await expect(validation.validate()).rejects.toThrow( + `Address "${INVALID_ADDRESS}" is not a valid evm address` + ); + }); + + it('should throw error for invalid address when starknet protocol is supported', async () => { + const validation = new TestValidation( + INVALID_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['starknet']; + + await expect(validation.validate()).rejects.toThrow( + `Address "${INVALID_ADDRESS}" is not a valid starknet address` + ); + }); + + it('should throw error for invalid address when both protocols are supported', async () => { + const validation = new TestValidation( + INVALID_ADDRESS, + 'test-space', + '1', + 123456, + {} + ); + validation.supportedProtocols = ['evm', 'starknet']; + + await expect(validation.validate()).rejects.toThrow( + `Address "${INVALID_ADDRESS}" is not a valid evm or starknet address` + ); + }); + }); +}); From 40f4b2f31928f869c6ec12000335a8c7d9d0f27a Mon Sep 17 00:00:00 2001 From: Wan <495709+wa0x6e@users.noreply.github.com> Date: Sat, 21 Jun 2025 02:07:55 +0900 Subject: [PATCH 06/14] Update src/constants.ts Co-authored-by: Chaitanya --- src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index ec71f0853..6dde2ee9b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,4 @@ import { Protocol } from './types'; export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; -export const MAX_STRATEGIES_LENGTH = 8; +export const MAX_STRATEGIES_LENGTH = 10; From 600130f78e8a9b129ced325e761baf63b5222a1e Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:09:59 +0400 Subject: [PATCH 07/14] fix: remove strategies length validation, delegated to score-api --- src/constants.ts | 1 - src/validations/validation.ts | 16 +--------------- test/unit/validation.test.ts | 15 --------------- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 6dde2ee9b..9da04ac7f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,3 @@ import { Protocol } from './types'; export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; -export const MAX_STRATEGIES_LENGTH = 10; diff --git a/src/validations/validation.ts b/src/validations/validation.ts index f17907852..be41596c6 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -1,8 +1,5 @@ import snapshot from '@snapshot-labs/snapshot.js'; -import { - DEFAULT_SUPPORTED_PROTOCOLS, - MAX_STRATEGIES_LENGTH -} from '../constants'; +import { DEFAULT_SUPPORTED_PROTOCOLS } from '../constants'; import { Protocol, Snapshot } from '../types'; export default class Validation { @@ -36,7 +33,6 @@ export default class Validation { async validate(customAuthor = this.author): Promise { this.validateAddressType(customAuthor); - this.validateStrategiesLength(); return this.doValidate(customAuthor); } @@ -70,14 +66,4 @@ export default class Validation { )} address` ); } - - private validateStrategiesLength(): boolean { - if ( - this.hasInnerStrategies && - this.params.strategies?.length > MAX_STRATEGIES_LENGTH - ) { - throw new Error(`Max number of strategies exceeded`); - } - return true; - } } diff --git a/test/unit/validation.test.ts b/test/unit/validation.test.ts index bcc52a6c0..9e89cb4c0 100644 --- a/test/unit/validation.test.ts +++ b/test/unit/validation.test.ts @@ -157,21 +157,6 @@ describe('Validation', () => { await expect(validation.validate()).resolves.not.toThrow(); }); - it('should throw error when hasInnerStrategies is true and strategies length exceeds limit', async () => { - const validation = new TestValidation( - '0x1234567890abcdef1234567890abcdef12345678', - 'test-space', - '1', - 123456, - { strategies: new Array(10).fill({}) } - ); - validation.hasInnerStrategies = true; - - await expect(validation.validate()).rejects.toThrow( - 'Max number of strategies exceeded' - ); - }); - it('should not throw error when hasInnerStrategies is true and strategies is undefined', async () => { const validation = new TestValidation( '0x1234567890abcdef1234567890abcdef12345678', From 574839e78ba59b9b72b9ffd239e79bffbd5de96a Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:39:27 +0400 Subject: [PATCH 08/14] fix: return false instead of throwing error on unsupported address --- src/validations/validation.ts | 6 +++++- test/unit/validation.test.ts | 35 +++++++++++++++-------------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/validations/validation.ts b/src/validations/validation.ts index be41596c6..ad75c1708 100644 --- a/src/validations/validation.ts +++ b/src/validations/validation.ts @@ -32,7 +32,11 @@ export default class Validation { } async validate(customAuthor = this.author): Promise { - this.validateAddressType(customAuthor); + try { + this.validateAddressType(customAuthor); + } catch (e) { + return false; + } return this.doValidate(customAuthor); } diff --git a/test/unit/validation.test.ts b/test/unit/validation.test.ts index 9e89cb4c0..d4bc998b7 100644 --- a/test/unit/validation.test.ts +++ b/test/unit/validation.test.ts @@ -68,7 +68,7 @@ describe('Validation', () => { expect(doValidateSpy).toHaveBeenCalledWith(customAuthor); }); - it('should validate custom author address type', async () => { + it('should return false for invalid custom author address type', async () => { const validation = new TestValidation( '0x1234567890abcdef1234567890abcdef12345678', 'test-space', @@ -80,9 +80,8 @@ describe('Validation', () => { const starknetAddress = '0x07f71118e351c02f6EC7099C8CDf93AED66CEd8406E94631cC91637f7D7F203A'; - await expect(validation.validate(starknetAddress)).rejects.toThrow( - `Address "${starknetAddress}" is not a valid evm address` - ); + const result = await validation.validate(starknetAddress); + expect(result).toBe(false); }); it('should return the result from doValidate', async () => { @@ -255,7 +254,7 @@ describe('Validation', () => { await expect(validation.validate()).resolves.not.toThrow(); }); - it('should throw error for valid Starknet address when only evm protocol is supported', async () => { + it('should return false for valid Starknet address when only evm protocol is supported', async () => { const validation = new TestValidation( VALID_STARKNET_ADDRESS, 'test-space', @@ -265,12 +264,11 @@ describe('Validation', () => { ); validation.supportedProtocols = ['evm']; - await expect(validation.validate()).rejects.toThrow( - `Address "${VALID_STARKNET_ADDRESS}" is not a valid evm address` - ); + const result = await validation.validate(); + expect(result).toBe(false); }); - it('should throw error for invalid address when evm protocol is supported', async () => { + it('should return false for invalid address when evm protocol is supported', async () => { const validation = new TestValidation( INVALID_ADDRESS, 'test-space', @@ -280,12 +278,11 @@ describe('Validation', () => { ); validation.supportedProtocols = ['evm']; - await expect(validation.validate()).rejects.toThrow( - `Address "${INVALID_ADDRESS}" is not a valid evm address` - ); + const result = await validation.validate(); + expect(result).toBe(false); }); - it('should throw error for invalid address when starknet protocol is supported', async () => { + it('should return false for invalid address when starknet protocol is supported', async () => { const validation = new TestValidation( INVALID_ADDRESS, 'test-space', @@ -295,12 +292,11 @@ describe('Validation', () => { ); validation.supportedProtocols = ['starknet']; - await expect(validation.validate()).rejects.toThrow( - `Address "${INVALID_ADDRESS}" is not a valid starknet address` - ); + const result = await validation.validate(); + expect(result).toBe(false); }); - it('should throw error for invalid address when both protocols are supported', async () => { + it('should return false for invalid address when both protocols are supported', async () => { const validation = new TestValidation( INVALID_ADDRESS, 'test-space', @@ -310,9 +306,8 @@ describe('Validation', () => { ); validation.supportedProtocols = ['evm', 'starknet']; - await expect(validation.validate()).rejects.toThrow( - `Address "${INVALID_ADDRESS}" is not a valid evm or starknet address` - ); + const result = await validation.validate(); + expect(result).toBe(false); }); }); }); From 6d97b0ac2f34dc0bcaffb6355d9c69f9dc56e2ed Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Fri, 20 Jun 2025 21:47:21 +0400 Subject: [PATCH 09/14] perf: better code --- src/utils.ts | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 7d389e99b..555e989b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -109,27 +109,23 @@ export function getFormattedAddressesByProtocol( return addresses .map((address) => { - let evmAddress, starknetAddress; - - try { - evmAddress = snapshot.utils.getFormattedAddress(address, 'evm'); - } catch (e) {} - try { - starknetAddress = snapshot.utils.getFormattedAddress( - address, - 'starknet' - ); - } catch (e) {} - - if (evmAddress && protocols.includes('evm')) { - return evmAddress; + if (protocols.includes('evm')) { + try { + return snapshot.utils.getFormattedAddress(address, 'evm'); + } catch (e) { + // Continue to starknet if evm formatting fails and starknet is supported + } } - if (!evmAddress && starknetAddress && protocols.includes('starknet')) { - return starknetAddress; + if (protocols.includes('starknet')) { + try { + return snapshot.utils.getFormattedAddress(address, 'starknet'); + } catch (e) { + // Address format not supported by any protocol + } } }) - .filter(Boolean); + .filter(Boolean) as string[]; } export const { From c07d3339aa61c8a50cc41a2d3b4959d6ee9c411f Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:02:57 +0400 Subject: [PATCH 10/14] fix: throw error on invalid address when calling getVp --- src/constants.ts | 1 + src/utils.ts | 72 +++++++++++---- test/unit/utils.test.ts | 189 +++++++++++++++++++++++++++------------- 3 files changed, 182 insertions(+), 80 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 9da04ac7f..4c1bdb3ab 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ import { Protocol } from './types'; export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm']; +export const VALID_PROTOCOLS: Protocol[] = ['evm', 'starknet']; diff --git a/src/utils.ts b/src/utils.ts index 555e989b1..81364bf73 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,7 @@ import { getDelegations } from './utils/delegation'; import { getVp, getDelegations as getCoreDelegations } from './utils/vp'; import { createHash } from 'crypto'; import { Protocol, Score, Snapshot } from './types'; +import { VALID_PROTOCOLS } from './constants'; export function sha256(str) { return createHash('sha256').update(str).digest('hex'); @@ -99,33 +100,66 @@ 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(', ')}`); + } +} + +/** + * 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[] { - if (!protocols.length) { - throw new Error('At least one protocol must be specified'); - } + 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 - } + 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 } + } - if (protocols.includes('starknet')) { - try { - return snapshot.utils.getFormattedAddress(address, 'starknet'); - } catch (e) { - // Address format not supported by any protocol - } + if (protocols.includes('starknet')) { + try { + return snapshot.utils.getFormattedAddress(address, 'starknet'); + } catch (e) { + // Address format not supported by any protocol } - }) - .filter(Boolean) as string[]; + } + + throw new Error( + `Address "${address}" is not a valid ${protocols.join(' or ')} address` + ); + }); } export const { diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index ec4cdbd96..ba6759643 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -10,80 +10,147 @@ describe('utils', () => { '0x07f71118e351c02f6ec7099c8cdf93aed66ced8406e94631cc91637f7d7f203a'; describe('getFormattedAddressesByProtocol()', () => { - const input = [ - VALID_EVM_ADDRESS, - 'invalidEVMAddress', - VALID_STARKNET_ADDRESS, - '' - ]; - - it('should return an empty array when no addresses are provided', () => { - const result = getFormattedAddressesByProtocol([]); - expect(result).toEqual([]); - }); + // Test data constants + const INVALID_ADDRESS = 'invalidAddress'; + const EMPTY_ADDRESS = ''; + const STARKNET_ONLY_ADDRESS = VALID_STARKNET_ADDRESS; + const EVM_ONLY_ADDRESS = VALID_EVM_ADDRESS; - it('should return an empty array when protocol is not valid', () => { - const result = getFormattedAddressesByProtocol(input, [ - // @ts-ignore - 'invalidProtocol' - ]); - expect(result).toEqual([]); - }); + describe('Basic functionality', () => { + it('should return an empty array when no addresses are provided', () => { + const result = getFormattedAddressesByProtocol([]); + expect(result).toEqual([]); + }); - it('should throw an error when no protocol is provided', () => { - expect(() => { - getFormattedAddressesByProtocol([], []); - }).toThrow(); + it('should use evm as default protocol when no protocols provided', () => { + const result = getFormattedAddressesByProtocol([EVM_ONLY_ADDRESS]); + expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); + }); }); - it('should use evm as default protocol when no protocols provided', () => { - const result = getFormattedAddressesByProtocol(input); - 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 prioritize evm when address is valid for both protocols and both are specified', () => { - const result = getFormattedAddressesByProtocol( - [VALID_EVM_ADDRESS], - ['evm', 'starknet'] - ); - expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); - }); + it('should throw an error for single invalid protocol', () => { + expect(() => { + getFormattedAddressesByProtocol( + [EVM_ONLY_ADDRESS], + [ + // @ts-ignore + 'invalidProtocol' + ] + ); + }).toThrow('Invalid protocol(s): invalidProtocol'); + }); - it('should return only formatted EVM addresses on evm protocol', () => { - const result = getFormattedAddressesByProtocol(input, ['evm']); - expect(result).toEqual([VALID_FORMATTED_EVM_ADDRESS]); + 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'); + }); }); - it('should return only formatted starknet addresses on starknet protocol', () => { - const result = getFormattedAddressesByProtocol(input, ['starknet']); - expect(result).toEqual([VALID_FORMATTED_STARKNET_ADDRESS]); - }); + 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 return both formatted starknet and evm addresses on starknet and evm protocol', () => { - const result = getFormattedAddressesByProtocol(input, [ - 'starknet', - 'evm' - ]); - expect(result).toEqual([ - VALID_FORMATTED_EVM_ADDRESS, - VALID_FORMATTED_STARKNET_ADDRESS - ]); + it('should format Starknet addresses correctly', () => { + const result = getFormattedAddressesByProtocol( + [STARKNET_ONLY_ADDRESS], + ['starknet'] + ); + expect(result).toEqual([VALID_FORMATTED_STARKNET_ADDRESS]); + }); }); - it('should return empty array when all addresses are invalid for specified protocol', () => { - const result = getFormattedAddressesByProtocol( - ['invalidAddress'], - ['evm'] - ); - expect(result).toEqual([]); + 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); + }); }); - it('should return empty array when all addresses are invalid for specified protocol', () => { - const result = getFormattedAddressesByProtocol( - ['invalidAddress'], - ['starknet'] - ); - expect(result).toEqual([]); + 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 6d8f4de37556879bb41c64255f134f05b27fac94 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:09:59 +0400 Subject: [PATCH 11/14] ci: run test on CI --- .github/workflows/test.yml | 27 +++++++++++++++++++++++++++ .gitignore | 3 ++- 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..85cfb681e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +name: Tests CI + +on: [push] + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: ['16', '20'] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - name: yarn install, build and run tests + run: | + yarn install --frozen-lockfile + yarn build + yarn test --coverage + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 112e93edf..478a66171 100755 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ .DS_Store node_modules dist +coverage # Remove some common IDE working directories .idea .vscode .env -.history \ No newline at end of file +.history From cc68033ec653baf10b12dde711122a223cca8e68 Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:20:49 +0400 Subject: [PATCH 12/14] fix: add missing functions to default export in utils.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added sha256, customFetch, and getFormattedAddressesByProtocol to the default export object to ensure consistency between named exports and default export. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils.ts b/src/utils.ts index 81364bf73..f0ef69faf 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -177,7 +177,10 @@ export const { } = snapshot.utils; export default { + sha256, getScoresDirect, + customFetch, + getFormattedAddressesByProtocol, multicall, Multicaller, subgraphRequest, From 66669e9e4cab5f97cdf2166be1834ecf4670746d Mon Sep 17 00:00:00 2001 From: wa0x6e <495709+wa0x6e@users.noreply.github.com> Date: Sat, 21 Jun 2025 21:30:42 +0400 Subject: [PATCH 13/14] fix: add support for starknet addresses on math strategy --- src/strategies/math/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/strategies/math/index.ts b/src/strategies/math/index.ts index 6d6444832..1f10e616f 100644 --- a/src/strategies/math/index.ts +++ b/src/strategies/math/index.ts @@ -13,7 +13,8 @@ import { } from './options'; export const author = 'xJonathanLEI'; -export const version = '0.2.2'; +export const version = '0.2.3'; +export const supportedProtocols = ['evm', 'starknet']; export async function strategy( space, From b632fad4ed8946229651bd40221ff673985bdf57 Mon Sep 17 00:00:00 2001 From: ChaituVR Date: Mon, 23 Jun 2025 11:05:06 +0530 Subject: [PATCH 14/14] simplify secret header generation in rocketpool strategy --- .../rocketpool-node-operator-delegate-v8/index.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/strategies/rocketpool-node-operator-delegate-v8/index.ts b/src/strategies/rocketpool-node-operator-delegate-v8/index.ts index 65350c473..4f0a8a1fa 100644 --- a/src/strategies/rocketpool-node-operator-delegate-v8/index.ts +++ b/src/strategies/rocketpool-node-operator-delegate-v8/index.ts @@ -12,9 +12,11 @@ const signerRegistryAbi = [ 'function signerToNode(address) external view returns (address)' ]; -const snapshotSecretHeader = sha256( - `https://api.rocketpool.net/mainnet/delegates/block/${process.env.SNAPSHOT_API_STRATEGY_SALT}` -); +function getSnapshotSecretHeader() { + return sha256( + `https://api.rocketpool.net/mainnet/delegates/block/${process.env.SNAPSHOT_API_STRATEGY_SALT}` + ); +} export async function strategy( space, @@ -42,7 +44,7 @@ export async function strategy( 'https://api.rocketpool.net/mainnet/delegates/block/' + blockTag, { headers: { - 'X-Snapshot-API-Secret': snapshotSecretHeader + 'X-Snapshot-API-Secret': getSnapshotSecretHeader() } } );