@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/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): true | import("ajv").ErrorObject<string, Record<string, any>, unknown>[] | null | undefined;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapshot-labs/snapshot.js",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "repository": "snapshot-labs/snapshot.js",
5
5
  "license": "MIT",
6
6
  "main": "dist/snapshot.cjs.js",
@@ -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
  }
@@ -15,7 +15,8 @@
15
15
  "properties": {
16
16
  "network": {
17
17
  "title": "Network",
18
- "type": "string"
18
+ "type": "string",
19
+ "snapshotNetwork": true
19
20
  },
20
21
  "multisend": {
21
22
  "title": "Multisend contract address",
@@ -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({ allErrors: true, allowUnionTypes: true, $data: true });
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(schema, data) {
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,