@snapshot-labs/snapshot.js 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -0
- package/dist/schemas/index.d.ts +4 -0
- package/dist/snapshot.cjs.js +93 -7
- package/dist/snapshot.esm.js +93 -7
- package/dist/snapshot.min.js +3 -3
- package/dist/utils.d.ts +3 -1
- package/package.json +1 -1
- package/src/schemas/space.json +4 -1
- package/src/schemas/zodiac.json +2 -1
- package/src/utils.spec.js +475 -0
- package/src/utils.ts +117 -3
package/dist/utils.d.ts
CHANGED
|
@@ -31,7 +31,9 @@ export declare function sendTransaction(web3: any, contractAddress: string, abi:
|
|
|
31
31
|
export declare function getScores(space: string, strategies: Strategy[], network: string, addresses: string[], snapshot?: number | string, scoreApiUrl?: string, options?: any): Promise<any>;
|
|
32
32
|
export declare function getVp(address: string, network: string, strategies: Strategy[], snapshot: number | 'latest', space: string, delegation: boolean, options?: Options): Promise<any>;
|
|
33
33
|
export declare function validate(validation: string, author: string, space: string, network: string, snapshot: number | 'latest', params: any, options: any): Promise<any>;
|
|
34
|
-
export declare function validateSchema(schema: any, data: any
|
|
34
|
+
export declare function validateSchema(schema: any, data: any, options?: {
|
|
35
|
+
snapshotEnv: string;
|
|
36
|
+
}): true | import("ajv").ErrorObject<string, Record<string, any>, unknown>[] | null | undefined;
|
|
35
37
|
export declare function getEnsTextRecord(ens: string, record: string, network?: string, options?: any): Promise<any>;
|
|
36
38
|
export declare function getSpaceUri(id: string, network?: string, options?: any): Promise<string | null>;
|
|
37
39
|
export declare function getEnsOwner(ens: string, network?: string, options?: any): Promise<string | null>;
|
package/package.json
CHANGED
package/src/schemas/space.json
CHANGED
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"network": {
|
|
81
81
|
"type": "string",
|
|
82
|
+
"snapshotNetwork": true,
|
|
82
83
|
"title": "network",
|
|
83
84
|
"minLength": 1,
|
|
84
85
|
"maxLength": 32
|
|
@@ -114,7 +115,8 @@
|
|
|
114
115
|
"network": {
|
|
115
116
|
"type": "string",
|
|
116
117
|
"maxLength": 12,
|
|
117
|
-
"title": "network"
|
|
118
|
+
"title": "network",
|
|
119
|
+
"snapshotNetwork": true
|
|
118
120
|
},
|
|
119
121
|
"params": {
|
|
120
122
|
"type": "object",
|
|
@@ -346,6 +348,7 @@
|
|
|
346
348
|
},
|
|
347
349
|
"network": {
|
|
348
350
|
"type": "string",
|
|
351
|
+
"snapshotNetwork": true,
|
|
349
352
|
"title": "Network",
|
|
350
353
|
"maxLength": 12
|
|
351
354
|
}
|
package/src/schemas/zodiac.json
CHANGED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { describe, test, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import * as crossFetch from 'cross-fetch';
|
|
3
|
+
import { validate, getScores, getVp } from './utils';
|
|
4
|
+
|
|
5
|
+
vi.mock('cross-fetch', async () => {
|
|
6
|
+
const actual = await vi.importActual('cross-fetch');
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
default: vi.fn()
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
const fetch = vi.mocked(crossFetch.default);
|
|
14
|
+
|
|
15
|
+
describe('utils', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
vi.resetAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('validate', () => {
|
|
21
|
+
const payload = {
|
|
22
|
+
validation: 'basic',
|
|
23
|
+
author: '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7',
|
|
24
|
+
space: 'fabien.eth',
|
|
25
|
+
network: '1',
|
|
26
|
+
snapshot: 7929876,
|
|
27
|
+
params: {
|
|
28
|
+
minScore: 0.9,
|
|
29
|
+
strategies: [
|
|
30
|
+
{
|
|
31
|
+
name: 'eth-balance',
|
|
32
|
+
params: {}
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function _validate({
|
|
39
|
+
validation,
|
|
40
|
+
author,
|
|
41
|
+
space,
|
|
42
|
+
network,
|
|
43
|
+
snapshot,
|
|
44
|
+
params,
|
|
45
|
+
options
|
|
46
|
+
}) {
|
|
47
|
+
return validate(
|
|
48
|
+
validation ?? payload.validation,
|
|
49
|
+
author ?? payload.author,
|
|
50
|
+
space ?? payload.space,
|
|
51
|
+
network ?? payload.network,
|
|
52
|
+
snapshot ?? payload.snapshot,
|
|
53
|
+
params ?? payload.params,
|
|
54
|
+
options ?? {}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('when passing invalid args', () => {
|
|
59
|
+
const cases = [
|
|
60
|
+
[
|
|
61
|
+
'author is an invalid address',
|
|
62
|
+
{ author: 'test-address' },
|
|
63
|
+
/invalid author/i
|
|
64
|
+
],
|
|
65
|
+
['network is not valid', { network: 'mainnet' }, /invalid network/i],
|
|
66
|
+
['network is empty', { network: '' }, /invalid network/i],
|
|
67
|
+
[
|
|
68
|
+
'snapshot is smaller than start block',
|
|
69
|
+
{ snapshot: 1234 },
|
|
70
|
+
/snapshot \([0-9]+\) must be 'latest' or greater than network start block/i
|
|
71
|
+
]
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
test.each(cases)('throw an error when %s', async (title, args, err) => {
|
|
75
|
+
await expect(_validate(args)).rejects.toMatch(err);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('when passing valid args', () => {
|
|
80
|
+
test('send a JSON-RPC payload to score-api', async () => {
|
|
81
|
+
fetch.mockReturnValue({
|
|
82
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(_validate({})).resolves;
|
|
86
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
87
|
+
'https://score.snapshot.org',
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
jsonrpc: '2.0',
|
|
91
|
+
method: 'validate',
|
|
92
|
+
params: payload
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('send a POST request with JSON content-type', async () => {
|
|
99
|
+
fetch.mockReturnValue({
|
|
100
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(_validate({})).resolves;
|
|
104
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
105
|
+
'https://score.snapshot.org',
|
|
106
|
+
expect.objectContaining({
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
Accept: 'application/json',
|
|
110
|
+
'Content-Type': 'application/json'
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('can customize the score-api url', () => {
|
|
117
|
+
fetch.mockReturnValue({
|
|
118
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(
|
|
122
|
+
_validate({ options: { url: 'https://snapshot.org/?apiKey=xxx' } })
|
|
123
|
+
).resolves;
|
|
124
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
125
|
+
'https://snapshot.org/?apiKey=xxx',
|
|
126
|
+
expect.anything()
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('returns the JSON-RPC result property', () => {
|
|
131
|
+
const result = { result: 'OK' };
|
|
132
|
+
fetch.mockReturnValue({
|
|
133
|
+
json: () => new Promise((resolve) => resolve(result))
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(_validate({})).resolves.toEqual('OK');
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('when score-api is sending a JSON-RPC error', () => {
|
|
141
|
+
test('rejects with the JSON-RPC error object', () => {
|
|
142
|
+
const result = { error: { message: 'Oh no' } };
|
|
143
|
+
fetch.mockReturnValue({
|
|
144
|
+
json: () => new Promise((resolve) => resolve(result))
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(_validate({})).rejects.toEqual(result.error);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('when the fetch request is failing with not network error', () => {
|
|
152
|
+
test('rejects with the error', () => {
|
|
153
|
+
const result = new Error('Oh no');
|
|
154
|
+
fetch.mockReturnValue({
|
|
155
|
+
json: () => {
|
|
156
|
+
throw result;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(_validate({})).rejects.toEqual(result);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('getScores', () => {
|
|
165
|
+
const payload = {
|
|
166
|
+
space: 'test.eth',
|
|
167
|
+
network: '1',
|
|
168
|
+
snapshot: 7929876,
|
|
169
|
+
strategies: [
|
|
170
|
+
{
|
|
171
|
+
name: 'erc20-balance-of',
|
|
172
|
+
params: {
|
|
173
|
+
symbol: 'TEST',
|
|
174
|
+
address: '0xc23F41519D7DFaDf9eed53c00f08C06CD5cDde54',
|
|
175
|
+
network: '1',
|
|
176
|
+
decimals: 18
|
|
177
|
+
},
|
|
178
|
+
network: '1'
|
|
179
|
+
}
|
|
180
|
+
],
|
|
181
|
+
addresses: ['0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11']
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
function _getScores({
|
|
185
|
+
space,
|
|
186
|
+
strategies,
|
|
187
|
+
network,
|
|
188
|
+
addresses,
|
|
189
|
+
snapshot,
|
|
190
|
+
scoreApiUrl,
|
|
191
|
+
options
|
|
192
|
+
}) {
|
|
193
|
+
return getScores(
|
|
194
|
+
space ?? payload.space,
|
|
195
|
+
strategies ?? payload.strategies,
|
|
196
|
+
network ?? payload.network,
|
|
197
|
+
addresses ?? payload.addresses,
|
|
198
|
+
snapshot ?? payload.snapshot,
|
|
199
|
+
scoreApiUrl ?? 'https://score.snapshot.org',
|
|
200
|
+
options ?? {}
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
describe('when passing invalid args', () => {
|
|
205
|
+
const cases = [
|
|
206
|
+
[
|
|
207
|
+
'addresses contains invalid address',
|
|
208
|
+
{ addresses: ['test-address'] },
|
|
209
|
+
/invalid address/i
|
|
210
|
+
],
|
|
211
|
+
[
|
|
212
|
+
'addresses is not an array',
|
|
213
|
+
{ addresses: 'test-address' },
|
|
214
|
+
/addresses should be an array/i
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'addresses is an empty array',
|
|
218
|
+
{ addresses: [] },
|
|
219
|
+
/addresses can not be empty/i
|
|
220
|
+
],
|
|
221
|
+
['network is not valid', { network: 'mainnet' }, /invalid network/i],
|
|
222
|
+
['network is empty', { network: '' }, /invalid network/i],
|
|
223
|
+
[
|
|
224
|
+
'snapshot is smaller than start block',
|
|
225
|
+
{ snapshot: 1234 },
|
|
226
|
+
/snapshot \([0-9]+\) must be 'latest' or greater than network start block/i
|
|
227
|
+
],
|
|
228
|
+
[
|
|
229
|
+
'strategy contains invalid network',
|
|
230
|
+
{ strategies: [{ name: '', network: 'test' }] },
|
|
231
|
+
/invalid network \(.*\) in strategy/i
|
|
232
|
+
]
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
test.each(cases)('throw an error when %s', async (title, args, err) => {
|
|
236
|
+
await expect(_getScores(args)).rejects.toMatch(err);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('when passing valid args', () => {
|
|
241
|
+
test('send a JSON-RPC payload to score-api', async () => {
|
|
242
|
+
fetch.mockReturnValue({
|
|
243
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(_getScores({})).resolves;
|
|
247
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
248
|
+
'https://score.snapshot.org/api/scores',
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
body: JSON.stringify({ params: payload })
|
|
251
|
+
})
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('send a POST request with JSON content-type', async () => {
|
|
256
|
+
fetch.mockReturnValue({
|
|
257
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(_getScores({})).resolves;
|
|
261
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
262
|
+
'https://score.snapshot.org/api/scores',
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: {
|
|
266
|
+
Accept: 'application/json',
|
|
267
|
+
'Content-Type': 'application/json'
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('can customize the score-api url', () => {
|
|
274
|
+
fetch.mockReturnValue({
|
|
275
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(_getScores({ scoreApiUrl: 'https://snapshot.org/?apiKey=xxx' }))
|
|
279
|
+
.resolves;
|
|
280
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
281
|
+
'https://snapshot.org/api/scores?apiKey=xxx',
|
|
282
|
+
expect.anything()
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('returns the JSON-RPC result scores property', () => {
|
|
287
|
+
const result = { scores: 'SCORES', other: 'Other' };
|
|
288
|
+
fetch.mockReturnValue({
|
|
289
|
+
json: () => new Promise((resolve) => resolve({ result }))
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(_getScores({})).resolves.toEqual('SCORES');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('returns the JSON-RPC all properties', () => {
|
|
296
|
+
const result = { scores: 'SCORES', other: 'Other' };
|
|
297
|
+
fetch.mockReturnValue({
|
|
298
|
+
json: () => new Promise((resolve) => resolve({ result }))
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(
|
|
302
|
+
_getScores({ options: { returnValue: 'all' } })
|
|
303
|
+
).resolves.toEqual(result);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('when score-api is sending a JSON-RPC error', () => {
|
|
308
|
+
test('rejects with the JSON-RPC error object', () => {
|
|
309
|
+
const result = { error: { message: 'Oh no' } };
|
|
310
|
+
fetch.mockReturnValue({
|
|
311
|
+
json: () => new Promise((resolve) => resolve(result))
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(_getScores({})).rejects.toEqual(result.error);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('when the fetch request is failing with not network error', () => {
|
|
319
|
+
test('rejects with the error', () => {
|
|
320
|
+
const result = new Error('Oh no');
|
|
321
|
+
fetch.mockReturnValue({
|
|
322
|
+
json: () => {
|
|
323
|
+
throw result;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(_getScores({})).rejects.toEqual(result);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
describe('getVp', () => {
|
|
332
|
+
const payload = {
|
|
333
|
+
address: '0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11',
|
|
334
|
+
network: '1',
|
|
335
|
+
strategies: [
|
|
336
|
+
{
|
|
337
|
+
name: 'erc20-balance-of',
|
|
338
|
+
params: {
|
|
339
|
+
symbol: 'TEST',
|
|
340
|
+
address: '0xc23F41519D7DFaDf9eed53c00f08C06CD5cDde54',
|
|
341
|
+
network: '1',
|
|
342
|
+
decimals: 18
|
|
343
|
+
},
|
|
344
|
+
network: '1'
|
|
345
|
+
}
|
|
346
|
+
],
|
|
347
|
+
snapshot: 7929876,
|
|
348
|
+
space: 'test.eth',
|
|
349
|
+
delegation: false,
|
|
350
|
+
options: undefined
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
function _getVp({ voter, network, strategies, snapshot, options }) {
|
|
354
|
+
return getVp(
|
|
355
|
+
voter ?? payload.address,
|
|
356
|
+
network ?? payload.network,
|
|
357
|
+
strategies ?? payload.strategies,
|
|
358
|
+
snapshot ?? payload.snapshot,
|
|
359
|
+
'test.eth' ?? payload.space,
|
|
360
|
+
false ?? payload.delegation,
|
|
361
|
+
options ?? payload.options
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
describe('when passing invalid args', () => {
|
|
366
|
+
const cases = [
|
|
367
|
+
[
|
|
368
|
+
'voter is not a valid address',
|
|
369
|
+
{ voter: 'test-address' },
|
|
370
|
+
/invalid voter address/i
|
|
371
|
+
],
|
|
372
|
+
['voter is empty', { voter: '' }, /invalid voter address/i],
|
|
373
|
+
['network is not valid', { network: 'mainnet' }, /invalid network/i],
|
|
374
|
+
['network is empty', { network: '' }, /invalid network/i],
|
|
375
|
+
[
|
|
376
|
+
'snapshot is smaller than start block',
|
|
377
|
+
{ snapshot: 1234 },
|
|
378
|
+
/snapshot \([0-9]+\) must be 'latest' or greater than network start block/i
|
|
379
|
+
],
|
|
380
|
+
[
|
|
381
|
+
'strategy contains invalid network',
|
|
382
|
+
{ strategies: [{ name: '', network: 'test' }] },
|
|
383
|
+
/invalid network \(.*\) in strategy/i
|
|
384
|
+
]
|
|
385
|
+
];
|
|
386
|
+
|
|
387
|
+
test.each(cases)('throw an error when %s', async (title, args, err) => {
|
|
388
|
+
await expect(_getVp(args)).rejects.toMatch(err);
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('when passing valid args', () => {
|
|
393
|
+
test('send a JSON-RPC payload to score-api', async () => {
|
|
394
|
+
fetch.mockReturnValue({
|
|
395
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(_getVp({})).resolves;
|
|
399
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
400
|
+
'https://score.snapshot.org',
|
|
401
|
+
expect.objectContaining({
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
jsonrpc: '2.0',
|
|
404
|
+
method: 'get_vp',
|
|
405
|
+
params: payload
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test('send a POST request with JSON content-type', async () => {
|
|
412
|
+
fetch.mockReturnValue({
|
|
413
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(_getVp({})).resolves;
|
|
417
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
418
|
+
'https://score.snapshot.org',
|
|
419
|
+
expect.objectContaining({
|
|
420
|
+
method: 'POST',
|
|
421
|
+
headers: {
|
|
422
|
+
Accept: 'application/json',
|
|
423
|
+
'Content-Type': 'application/json'
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('can customize the score-api url', () => {
|
|
430
|
+
fetch.mockReturnValue({
|
|
431
|
+
json: () => new Promise((resolve) => resolve({ result: 'OK' }))
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(_getVp({ options: { url: 'https://snapshot.org' } })).resolves;
|
|
435
|
+
expect(fetch).toHaveBeenCalledWith(
|
|
436
|
+
'https://snapshot.org',
|
|
437
|
+
expect.anything()
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test('returns the JSON-RPC result property', () => {
|
|
442
|
+
const result = { data: 'OK' };
|
|
443
|
+
fetch.mockReturnValue({
|
|
444
|
+
json: () => new Promise((resolve) => resolve({ result }))
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
expect(_getVp({})).resolves.toEqual(result);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('when score-api is sending a JSON-RPC error', () => {
|
|
452
|
+
test('rejects with the JSON-RPC error object', () => {
|
|
453
|
+
const result = { error: { message: 'Oh no' } };
|
|
454
|
+
fetch.mockReturnValue({
|
|
455
|
+
json: () => new Promise((resolve) => resolve(result))
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
expect(_getVp({})).rejects.toEqual(result.error);
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
describe('when the fetch request is failing with not network error', () => {
|
|
463
|
+
test('rejects with the error', () => {
|
|
464
|
+
const result = new Error('Oh no');
|
|
465
|
+
fetch.mockReturnValue({
|
|
466
|
+
json: () => {
|
|
467
|
+
throw result;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(_getVp({})).rejects.toEqual(result);
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
});
|
package/src/utils.ts
CHANGED
|
@@ -31,13 +31,19 @@ export const SNAPSHOT_SUBGRAPH_URL = delegationSubgraphs;
|
|
|
31
31
|
const ENS_RESOLVER_ABI = [
|
|
32
32
|
'function text(bytes32 node, string calldata key) external view returns (string memory)'
|
|
33
33
|
];
|
|
34
|
+
const EMPTY_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
34
35
|
|
|
35
36
|
const scoreApiHeaders = {
|
|
36
37
|
Accept: 'application/json',
|
|
37
38
|
'Content-Type': 'application/json'
|
|
38
39
|
};
|
|
39
40
|
|
|
40
|
-
const ajv = new Ajv({
|
|
41
|
+
const ajv = new Ajv({
|
|
42
|
+
allErrors: true,
|
|
43
|
+
allowUnionTypes: true,
|
|
44
|
+
$data: true,
|
|
45
|
+
passContext: true
|
|
46
|
+
});
|
|
41
47
|
// @ts-ignore
|
|
42
48
|
addFormats(ajv);
|
|
43
49
|
|
|
@@ -68,6 +74,28 @@ ajv.addFormat('ethValue', {
|
|
|
68
74
|
}
|
|
69
75
|
});
|
|
70
76
|
|
|
77
|
+
const networksIds = Object.keys(networks);
|
|
78
|
+
const mainnetNetworkIds = Object.keys(networks).filter(
|
|
79
|
+
(id) => !networks[id].testnet
|
|
80
|
+
);
|
|
81
|
+
const testnetNetworkIds = Object.keys(networks).filter(
|
|
82
|
+
(id) => networks[id].testnet
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
ajv.addKeyword({
|
|
86
|
+
keyword: 'snapshotNetwork',
|
|
87
|
+
validate: function (schema, data) {
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
const snapshotEnv = this.snapshotEnv || 'default';
|
|
90
|
+
if (snapshotEnv === 'mainnet') return mainnetNetworkIds.includes(data);
|
|
91
|
+
if (snapshotEnv === 'testnet') return testnetNetworkIds.includes(data);
|
|
92
|
+
return networksIds.includes(data);
|
|
93
|
+
},
|
|
94
|
+
error: {
|
|
95
|
+
message: 'must be a valid network used by snapshot'
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
71
99
|
// Custom URL format to allow empty string values
|
|
72
100
|
// https://github.com/snapshot-labs/snapshot.js/pull/541/files
|
|
73
101
|
ajv.addFormat('customUrl', {
|
|
@@ -222,6 +250,33 @@ export async function getScores(
|
|
|
222
250
|
scoreApiUrl = 'https://score.snapshot.org',
|
|
223
251
|
options: any = {}
|
|
224
252
|
) {
|
|
253
|
+
if (!Array.isArray(addresses)) {
|
|
254
|
+
return inputError('addresses should be an array of addresses');
|
|
255
|
+
}
|
|
256
|
+
if (addresses.length === 0) {
|
|
257
|
+
return inputError('addresses can not be empty');
|
|
258
|
+
}
|
|
259
|
+
const invalidAddress = addresses.find((address) => !isValidAddress(address));
|
|
260
|
+
if (invalidAddress) {
|
|
261
|
+
return inputError(`Invalid address: ${invalidAddress}`);
|
|
262
|
+
}
|
|
263
|
+
if (!isValidNetwork(network)) {
|
|
264
|
+
return inputError(`Invalid network: ${network}`);
|
|
265
|
+
}
|
|
266
|
+
const invalidStrategy = strategies.find(
|
|
267
|
+
(strategy) => strategy.network && !isValidNetwork(strategy.network)
|
|
268
|
+
);
|
|
269
|
+
if (invalidStrategy) {
|
|
270
|
+
return inputError(
|
|
271
|
+
`Invalid network (${invalidStrategy.network}) in strategy ${invalidStrategy.name}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (!isValidSnapshot(snapshot, network)) {
|
|
275
|
+
return inputError(
|
|
276
|
+
`Snapshot (${snapshot}) must be 'latest' or greater than network start block (${networks[network].start})`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
225
280
|
const url = new URL(scoreApiUrl);
|
|
226
281
|
url.pathname = '/api/scores';
|
|
227
282
|
scoreApiUrl = url.toString();
|
|
@@ -267,6 +322,27 @@ export async function getVp(
|
|
|
267
322
|
) {
|
|
268
323
|
if (!options) options = {};
|
|
269
324
|
if (!options.url) options.url = 'https://score.snapshot.org';
|
|
325
|
+
if (!isValidAddress(address)) {
|
|
326
|
+
return inputError(`Invalid voter address: ${address}`);
|
|
327
|
+
}
|
|
328
|
+
if (!isValidNetwork(network)) {
|
|
329
|
+
return inputError(`Invalid network: ${network}`);
|
|
330
|
+
}
|
|
331
|
+
const invalidStrategy = strategies.find(
|
|
332
|
+
(strategy) => strategy.network && !isValidNetwork(strategy.network)
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (invalidStrategy) {
|
|
336
|
+
return inputError(
|
|
337
|
+
`Invalid network (${invalidStrategy.network}) in strategy ${invalidStrategy.name}`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
if (!isValidSnapshot(snapshot, network)) {
|
|
341
|
+
return inputError(
|
|
342
|
+
`Snapshot (${snapshot}) must be 'latest' or greater than network start block (${networks[network].start})`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
270
346
|
const init = {
|
|
271
347
|
method: 'POST',
|
|
272
348
|
headers: scoreApiHeaders,
|
|
@@ -306,6 +382,19 @@ export async function validate(
|
|
|
306
382
|
params: any,
|
|
307
383
|
options: any
|
|
308
384
|
) {
|
|
385
|
+
if (!isValidAddress(author)) {
|
|
386
|
+
return inputError(`Invalid author: ${author}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!isValidNetwork(network)) {
|
|
390
|
+
return inputError(`Invalid network: ${network}`);
|
|
391
|
+
}
|
|
392
|
+
if (!isValidSnapshot(snapshot, network)) {
|
|
393
|
+
return inputError(
|
|
394
|
+
`Snapshot (${snapshot}) must be 'latest' or greater than network start block (${networks[network].start})`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
309
398
|
if (!options) options = {};
|
|
310
399
|
if (!options.url) options.url = 'https://score.snapshot.org';
|
|
311
400
|
const init = {
|
|
@@ -338,9 +427,15 @@ export async function validate(
|
|
|
338
427
|
}
|
|
339
428
|
}
|
|
340
429
|
|
|
341
|
-
export function validateSchema(
|
|
430
|
+
export function validateSchema(
|
|
431
|
+
schema,
|
|
432
|
+
data,
|
|
433
|
+
options = {
|
|
434
|
+
snapshotEnv: 'default'
|
|
435
|
+
}
|
|
436
|
+
) {
|
|
342
437
|
const ajvValidate = ajv.compile(schema);
|
|
343
|
-
const valid = ajvValidate(data);
|
|
438
|
+
const valid = ajvValidate.call(options, data);
|
|
344
439
|
return valid ? valid : ajvValidate.errors;
|
|
345
440
|
}
|
|
346
441
|
|
|
@@ -497,6 +592,25 @@ export function getNumberWithOrdinal(n) {
|
|
|
497
592
|
return n + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
498
593
|
}
|
|
499
594
|
|
|
595
|
+
function isValidNetwork(network: string) {
|
|
596
|
+
return !!networks[network];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function isValidAddress(address: string) {
|
|
600
|
+
return isAddress(address) && address !== EMPTY_ADDRESS;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function isValidSnapshot(snapshot: number | string, network: string) {
|
|
604
|
+
return (
|
|
605
|
+
snapshot === 'latest' ||
|
|
606
|
+
(typeof snapshot === 'number' && snapshot >= networks[network].start)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function inputError(message: string) {
|
|
611
|
+
return Promise.reject(new Error(message));
|
|
612
|
+
}
|
|
613
|
+
|
|
500
614
|
export default {
|
|
501
615
|
call,
|
|
502
616
|
multicall,
|