Skip to content
This repository was archived by the owner on Aug 22, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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 }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
.DS_Store
node_modules
dist
coverage

# Remove some common IDE working directories
.idea
.vscode
.env
.history
.history
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/",
Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Protocol } from './types';

export const DEFAULT_SUPPORTED_PROTOCOLS: Protocol[] = ['evm'];
export const VALID_PROTOCOLS: Protocol[] = ['evm', 'starknet'];
3 changes: 3 additions & 0 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,8 @@ import * as prlInSpRL2Balance from './prl-in-sprl2-balance';
import * as edenOnlineOverride from './eden-online-override';
import * as forteStaking from './forte-staking';

import { DEFAULT_SUPPORTED_PROTOCOLS } from '../constants';

const strategies = {
'shroomy-voting-power': shroomyVotingPower,
'apecoin-staking': apecoinStaking,
Expand Down Expand Up @@ -1018,6 +1020,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;
3 changes: 2 additions & 1 deletion src/strategies/math/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 8 additions & 8 deletions src/strategies/ocean-dao-brightid/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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];
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/strategies/subgraph-split-delegation/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/strategies/ticket/index.ts
Original file line number Diff line number Diff line change
@@ -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])
Expand Down
2 changes: 2 additions & 0 deletions src/strategies/whitelist-weighted/index.ts
Original file line number Diff line number Diff line change
@@ -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]) => [
Expand Down
2 changes: 2 additions & 0 deletions src/strategies/whitelist/index.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand Down
9 changes: 9 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type VpState = 'final' | 'pending';
export type Score = Record<string, number>;
export type VotingPower = {
vp: number;
vp_by_strategy: number[];
vp_state: VpState;
};
export type Snapshot = number | 'latest';
export type Protocol = 'evm' | 'starknet';
87 changes: 80 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,33 @@ 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';
import { VALID_PROTOCOLS } from './constants';

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<Score> {
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),
Expand All @@ -32,8 +41,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())
)
);
}
Expand All @@ -44,8 +52,8 @@ export async function getScoresDirect(
network: string,
provider,
addresses: string[],
snapshot: number | string = 'latest'
) {
snapshot: Snapshot
): Promise<Score[]> {
try {
const networks = strategies.map((s) => s.network || network);
const snapshots = await getSnapshots(network, snapshot, provider, networks);
Expand Down Expand Up @@ -92,6 +100,68 @@ 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[] {
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
}
}

if (protocols.includes('starknet')) {
try {
return snapshot.utils.getFormattedAddress(address, 'starknet');
} catch (e) {
// Address format not supported by any protocol
}
}

throw new Error(
`Address "${address}" is not a valid ${protocols.join(' or ')} address`
);
});
}

export const {
multicall,
Multicaller,
Expand All @@ -107,7 +177,10 @@ export const {
} = snapshot.utils;

export default {
sha256,
getScoresDirect,
customFetch,
getFormattedAddressesByProtocol,
multicall,
Multicaller,
subgraphRequest,
Expand Down
15 changes: 13 additions & 2 deletions src/utils/delegation.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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];

Expand Down
Loading
Loading