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 7 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
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 MAX_STRATEGIES_LENGTH = 8;
2 changes: 2 additions & 0 deletions src/strategies/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +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 '../constants';

const strategies = {
'shroomy-voting-power': shroomyVotingPower,
Expand Down Expand Up @@ -1016,6 +1017,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;
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';
54 changes: 47 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<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 +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())
)
);
}
Expand All @@ -44,8 +51,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 +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,
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
30 changes: 19 additions & 11 deletions src/utils/vp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ 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';
import { DEFAULT_SUPPORTED_PROTOCOLS } from '../constants';

const DELEGATION_CONTRACT = '0x469788fE6E9E9681C6ebF3bF78e7Fd26Fc015446';
const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000';
Expand All @@ -23,10 +26,10 @@ export async function getVp(
address: string,
network: string,
strategies: any[],
snapshot: number | 'latest',
snapshot: Snapshot,
space: string,
delegation?: boolean
) {
): Promise<VotingPower> {
const networks = [...new Set(strategies.map((s) => s.network || network))];
const snapshots = await getSnapshots(
network,
Expand All @@ -43,7 +46,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];

Expand All @@ -54,7 +57,10 @@ export async function getVp(
if (addresses.length === 0) return {};
}

addresses = addresses.map(getAddress);
addresses = getFormattedAddressesByProtocol(
addresses,
strategy.supportedProtocols ?? DEFAULT_SUPPORTED_PROTOCOLS
);
return _strategies[strategy.name].strategy(
space,
n,
Expand All @@ -76,12 +82,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,
Expand All @@ -93,7 +101,7 @@ export async function getVp(
export async function getDelegationsOut(
addresses: string[],
network: string,
snapshot: number | 'latest',
snapshot: Snapshot,
space: string
) {
if (!subgraphs[network])
Expand Down Expand Up @@ -127,7 +135,7 @@ export async function getDelegationsOut(
export async function getDelegationOut(
address: string,
network: string,
snapshot: number | 'latest',
snapshot: Snapshot,
space: string
): Promise<string | null> {
const usersDelegationOut = await getDelegationsOut(
Expand All @@ -142,7 +150,7 @@ export async function getDelegationOut(
export async function getDelegationsIn(
address: string,
network: string,
snapshot: number | 'latest',
snapshot: Snapshot,
space: string
): Promise<string[]> {
if (!subgraphs[network]) return [];
Expand Down Expand Up @@ -206,7 +214,7 @@ export async function getDelegationsIn(
export async function getDelegations(
address: string,
network: string,
snapshot: number | 'latest',
snapshot: Snapshot,
space: string
): Promise<Delegation> {
const [delegationOut, delegationsIn] = await Promise.all([
Expand Down
Loading
Loading