Skip to content

Commit 9bd48e9

Browse files
wa0x6eChaituVR
andauthored
feat: fix fetch delegates error when skip value is greater than 5000 (#956)
* feat: fix fetch delegates error when `skip` value is greater than 5000 * refactor: improve code readability and testing * fix: handle case where there are more than 1000 duplicates * fix: raise error on 1000 duplicates instead * refactor: remove params * Update src/utils/delegation.ts Co-authored-by: Chaitanya <[email protected]> * Update src/utils/delegation.ts Co-authored-by: Chaitanya <[email protected]> * refactor: use map for unique delegation handling * chore: add more test * Update src/utils/delegation.ts * Update src/utils/delegation.ts * v0.11.0 --------- Co-authored-by: Chaitanya <[email protected]>
1 parent f77d09a commit 9bd48e9

File tree

5 files changed

+7142
-52
lines changed

5 files changed

+7142
-52
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@snapshot-labs/snapshot.js",
3-
"version": "0.10.1",
3+
"version": "0.11.0",
44
"repository": "snapshot-labs/snapshot.js",
55
"license": "MIT",
66
"main": "dist/snapshot.cjs.js",

src/utils.ts

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { signMessage, getBlockNumber } from './utils/web3';
1414
import { getHash, verify } from './sign/utils';
1515
import gateways from './gateways.json';
1616
import networks from './networks.json';
17-
import delegationSubgraphs from './delegationSubgraphs.json';
1817
import voting from './voting';
18+
import getDelegatesBySpace, { SNAPSHOT_SUBGRAPH_URL } from './utils/delegation';
1919

2020
interface Options {
2121
url?: string;
@@ -27,7 +27,6 @@ interface Strategy {
2727
params: any;
2828
}
2929

30-
export const SNAPSHOT_SUBGRAPH_URL = delegationSubgraphs;
3130
const ENS_RESOLVER_ABI = [
3231
'function text(bytes32 node, string calldata key) external view returns (string memory)'
3332
];
@@ -572,55 +571,6 @@ export async function getSpaceController(
572571
return await getEnsOwner(id, network, options);
573572
}
574573

575-
export async function getDelegatesBySpace(
576-
network: string,
577-
space: string,
578-
snapshot = 'latest',
579-
options: any = {}
580-
) {
581-
const subgraphUrl = options.subgraphUrl || SNAPSHOT_SUBGRAPH_URL[network];
582-
if (!subgraphUrl) {
583-
return Promise.reject(
584-
`Delegation subgraph not available for network ${network}`
585-
);
586-
}
587-
const spaceIn = ['', space];
588-
if (space.includes('.eth')) spaceIn.push(space.replace('.eth', ''));
589-
590-
const PAGE_SIZE = 1000;
591-
let result = [];
592-
let page = 0;
593-
const params: any = {
594-
delegations: {
595-
__args: {
596-
where: {
597-
space_in: spaceIn
598-
},
599-
first: PAGE_SIZE,
600-
skip: 0
601-
},
602-
delegator: true,
603-
space: true,
604-
delegate: true
605-
}
606-
};
607-
if (snapshot !== 'latest') {
608-
params.delegations.__args.block = { number: snapshot };
609-
}
610-
611-
while (true) {
612-
params.delegations.__args.skip = page * PAGE_SIZE;
613-
614-
const pageResult = await subgraphRequest(subgraphUrl, params);
615-
const pageDelegations = pageResult.delegations || [];
616-
result = result.concat(pageDelegations);
617-
page++;
618-
if (pageDelegations.length < PAGE_SIZE) break;
619-
}
620-
621-
return result;
622-
}
623-
624574
export function clone(item) {
625575
return JSON.parse(JSON.stringify(item));
626576
}
@@ -656,6 +606,8 @@ function inputError(message: string) {
656606
return Promise.reject(new Error(message));
657607
}
658608

609+
export { getDelegatesBySpace, SNAPSHOT_SUBGRAPH_URL };
610+
659611
export default {
660612
call,
661613
multicall,

src/utils/delegation.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { subgraphRequest } from '../utils';
2+
import delegationSubgraphs from '../delegationSubgraphs.json';
3+
4+
export const SNAPSHOT_SUBGRAPH_URL = delegationSubgraphs;
5+
const PAGE_SIZE = 1000;
6+
7+
type Delegation = {
8+
delegator: string;
9+
delegate: string;
10+
space: string;
11+
timestamp: number;
12+
};
13+
14+
export default async function getDelegatesBySpace(
15+
network: string,
16+
space: string,
17+
snapshot: string | number = 'latest',
18+
options: any = {}
19+
) {
20+
const subgraphUrl = options.subgraphUrl || SNAPSHOT_SUBGRAPH_URL[network];
21+
if (!subgraphUrl) {
22+
return Promise.reject(
23+
`Delegation subgraph not available for network ${network}`
24+
);
25+
}
26+
27+
let pivot = 0;
28+
const result = new Map<string, Delegation>();
29+
const spaceIn = buildSpaceIn(space);
30+
31+
while (true) {
32+
const newResults = await fetchData({
33+
url: subgraphUrl,
34+
spaces: spaceIn,
35+
pivot,
36+
snapshot
37+
});
38+
39+
if (checkAllDuplicates(newResults)) {
40+
throw new Error('Unable to paginate delegation');
41+
}
42+
43+
newResults.forEach((delegation) => {
44+
concatUniqueDelegation(result, delegation);
45+
pivot = delegation.timestamp;
46+
});
47+
48+
if (newResults.length < PAGE_SIZE) break;
49+
}
50+
51+
return [...result.values()];
52+
}
53+
54+
function checkAllDuplicates(delegations: Delegation[]) {
55+
return (
56+
delegations.length === PAGE_SIZE &&
57+
delegations[0].timestamp === delegations[delegations.length - 1].timestamp
58+
);
59+
}
60+
61+
function delegationKey(delegation: Delegation) {
62+
return `${delegation.delegator}-${delegation.delegate}-${delegation.space}`;
63+
}
64+
65+
function concatUniqueDelegation(
66+
result: Map<string, Delegation>,
67+
delegation: Delegation
68+
): void {
69+
const key = delegationKey(delegation);
70+
if (!result.has(key)) {
71+
result.set(key, delegation);
72+
}
73+
}
74+
75+
function buildSpaceIn(space: string) {
76+
const spaces = ['', space];
77+
if (space.includes('.eth')) spaces.push(space.replace('.eth', ''));
78+
79+
return spaces;
80+
}
81+
82+
async function fetchData({
83+
url,
84+
spaces,
85+
pivot,
86+
snapshot
87+
}: {
88+
url: string;
89+
spaces: string[];
90+
pivot: number;
91+
snapshot: string | number;
92+
}): Promise<Delegation[]> {
93+
const params: any = {
94+
delegations: {
95+
__args: {
96+
where: {
97+
space_in: spaces,
98+
timestamp_gte: pivot
99+
},
100+
first: PAGE_SIZE,
101+
skip: 0,
102+
orderBy: 'timestamp',
103+
orderDirection: 'asc'
104+
},
105+
delegator: true,
106+
space: true,
107+
delegate: true,
108+
timestamp: true
109+
}
110+
};
111+
112+
if (snapshot !== 'latest') {
113+
params.delegations.__args.block = { number: snapshot };
114+
}
115+
116+
return (await subgraphRequest(url, params)).delegations || [];
117+
}

test/e2e/utils/delegation.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { test, expect, describe } from 'vitest';
2+
import { getDelegatesBySpace } from '../../../src/utils';
3+
import fixtures from '../../examples/delegates.json';
4+
5+
const SPACE = 'stgdao.eth';
6+
const NETWORK = '42161';
7+
const BLOCK_NUMBER = 73900000;
8+
9+
describe('test delegation', () => {
10+
describe('getDelegatesBySpace()', () => {
11+
test('should return a list of delegations at specified block', async () => {
12+
expect.assertions(2);
13+
const results: any = await getDelegatesBySpace(
14+
NETWORK,
15+
SPACE,
16+
BLOCK_NUMBER
17+
);
18+
19+
expect(results.length).toEqual(fixtures.length);
20+
expect(
21+
results.every((a: any) => {
22+
return fixtures.some((b) => {
23+
return (
24+
a.delegator === b.delegator &&
25+
a.space === b.space &&
26+
a.delegate === b.delegate
27+
);
28+
});
29+
})
30+
).toEqual(true);
31+
});
32+
33+
test('should return an empty array when no results', async () => {
34+
expect.assertions(1);
35+
const results: any = await getDelegatesBySpace(NETWORK, SPACE, 22531439);
36+
37+
expect(results.length).toEqual(0);
38+
});
39+
40+
test('should reject with a message on unsupported network', async () => {
41+
expect.assertions(1);
42+
await expect(
43+
getDelegatesBySpace('99999', SPACE, BLOCK_NUMBER)
44+
).to.rejects.toMatch(/not available/);
45+
});
46+
47+
test('should raise error on subgraph graphql errors', async () => {
48+
expect.assertions(1);
49+
await expect(getDelegatesBySpace('1', SPACE, 0)).to.rejects.toMatch(
50+
/startBlock/
51+
);
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)