diff --git a/package.json b/package.json index 35eebff0e..1942c1680 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snapshot-labs/snapshot.js", - "version": "0.7.3", + "version": "0.8.0-beta.0", "repository": "snapshot-labs/snapshot.js", "license": "MIT", "main": "dist/snapshot.cjs.js", @@ -19,9 +19,9 @@ "@ethersproject/wallet": "^5.6.2", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", - "cross-fetch": "^3.1.6", "json-to-graphql-query": "^2.2.4", - "lodash.set": "^4.3.2" + "lodash.set": "^4.3.2", + "ofetch": "^1.3.3" }, "devDependencies": { "@rollup/plugin-commonjs": "^18.1.0", diff --git a/src/sign/index.ts b/src/sign/index.ts index 007fccfb5..2090bbef9 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -1,4 +1,4 @@ -import fetch from 'cross-fetch'; +import { ofetch as fetch } from 'ofetch'; import { Web3Provider } from '@ethersproject/providers'; import { Wallet } from '@ethersproject/wallet'; import { getAddress } from '@ethersproject/address'; @@ -89,16 +89,18 @@ export default class Client { Accept: 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(envelop) + timeout: this.options.timeout || 20e3, + body: envelop }; - return new Promise((resolve, reject) => { - fetch(address, init) - .then((res) => { - if (res.ok) return resolve(res.json()); - throw res; - }) - .catch((e) => e.json().then((json) => reject(json))); - }); + + try { + return await fetch(address, init); + } catch (e) { + const isSequencerError = + e.data?.hasOwnProperty('error') && + e.data?.hasOwnProperty('error_description'); + return Promise.reject(isSequencerError ? e.data : e); + } } async space(web3: Web3Provider | Wallet, address: string, message: Space) { diff --git a/src/utils.ts b/src/utils.ts index 27b1f851e..39c7231f5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import fetch from 'cross-fetch'; +import { ofetch as fetch } from 'ofetch'; import { Interface } from '@ethersproject/abi'; import { Contract } from '@ethersproject/contracts'; import { isAddress } from '@ethersproject/address'; @@ -19,6 +19,8 @@ import voting from './voting'; interface Options { url?: string; + timeout?: number; + headers?: any; } interface Strategy { @@ -135,38 +137,56 @@ export async function multicall( } } -export async function subgraphRequest(url: string, query, options: any = {}) { - const res = await fetch(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...options?.headers - }, - body: JSON.stringify({ query: jsonToGraphQLQuery({ query }) }) - }); - let responseData: any = await res.text(); +export async function subgraphRequest(url: string, query, options?: Options) { try { - responseData = JSON.parse(responseData); + const init = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options?.headers + }, + timeout: options?.timeout || 20e3, + body: { query: jsonToGraphQLQuery({ query }) } + }; + + const body = await fetch(url, init); + + if (typeof body === 'string') { + return Promise.reject({ + errors: [ + { + message: 'Body is not a JSON object', + extensions: { code: 'INVALID_JSON' } + } + ] + }); + } + + if (body.errors) { + return Promise.reject(body); + } + + return body.data; } catch (e) { - throw new Error( - `Errors found in subgraphRequest: URL: ${url}, Status: ${ - res.status - }, Response: ${responseData.substring(0, 400)}` - ); - } - if (responseData.errors) { - throw new Error( - `Errors found in subgraphRequest: URL: ${url}, Status: ${ - res.status - }, Response: ${JSON.stringify(responseData.errors).substring(0, 400)}` + return Promise.reject( + e.data?.errors + ? e.data + : { + errors: [ + { + message: e.statusText || e.toString(), + extensions: { + code: e.status || 0 + } + } + ] + } ); } - const { data } = responseData; - return data || {}; } -export function getUrl(uri, gateway = gateways[0]) { +export function getUrl(uri: string, gateway = gateways[0]) { const ipfsGateway = `https://${gateway}`; if (!uri) return null; if ( @@ -184,18 +204,40 @@ export function getUrl(uri, gateway = gateways[0]) { return uri; } -export async function getJSON(uri, options: any = {}) { - const url = getUrl(uri, options.gateways); - return fetch(url).then((res) => res.json()); +export async function getJSON( + uri: string, + options: Options & { gateways?: string } = {} +) { + const url = getUrl(uri, options.gateways) || ''; + const body = await fetch(url, { + timeout: options.timeout || 30e3, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options?.headers + } + }); + + return typeof body === 'string' ? JSON.parse(body) : body; } export async function ipfsGet( gateway: string, ipfsHash: string, - protocolType = 'ipfs' + protocolType = 'ipfs', + options: Options = {} ) { const url = `https://${gateway}/${protocolType}/${ipfsHash}`; - return fetch(url).then((res) => res.json()); + const body = await fetch(url, { + timeout: options.timeout || 20e3, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options.headers + } + }); + + return typeof body === 'string' ? JSON.parse(body) : body; } export async function sendTransaction( @@ -220,10 +262,10 @@ export async function getScores( addresses: string[], snapshot: number | string = 'latest', scoreApiUrl = 'https://score.snapshot.org', - options: any = {} + options: Options & { returnValue?: string; pathname?: string } = {} ) { const url = new URL(scoreApiUrl); - url.pathname = '/api/scores'; + url.pathname = options.pathname || '/api/scores'; scoreApiUrl = url.toString(); try { @@ -234,25 +276,25 @@ export async function getScores( strategies, addresses }; - const res = await fetch(scoreApiUrl, { + + const body = await fetch(scoreApiUrl, { method: 'POST', headers: scoreApiHeaders, - body: JSON.stringify({ params }) + timeout: options.timeout || 60e3, + body: { params } }); - const obj = await res.json(); - - if (obj.error) { - return Promise.reject(obj.error); - } return options.returnValue === 'all' - ? obj.result - : obj.result[options.returnValue || 'scores']; + ? body.result + : body.result[options.returnValue || 'scores']; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } @@ -263,14 +305,14 @@ export async function getVp( snapshot: number | 'latest', space: string, delegation: boolean, - options?: Options + options: Options = {} ) { - if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const url = options.url || 'https://score.snapshot.org'; const init = { method: 'POST', headers: scoreApiHeaders, - body: JSON.stringify({ + timeout: options.timeout || 60e3, + body: { jsonrpc: '2.0', method: 'get_vp', params: { @@ -281,19 +323,20 @@ export async function getVp( space, delegation } - }) + } }; try { - const res = await fetch(options.url, init); - const json = await res.json(); - if (json.error) return Promise.reject(json.error); - if (json.result) return json.result; + const body = await fetch(url, init); + return body.result; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } @@ -304,14 +347,14 @@ export async function validate( network: string, snapshot: number | 'latest', params: any, - options: any + options: Options = {} ) { - if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const url = options.url || 'https://score.snapshot.org'; const init = { method: 'POST', headers: scoreApiHeaders, - body: JSON.stringify({ + timeout: options.timeout || 30e3, + body: { jsonrpc: '2.0', method: 'validate', params: { @@ -322,19 +365,20 @@ export async function validate( snapshot, params } - }) + } }; try { - const res = await fetch(options.url, init); - const json = await res.json(); - if (json.error) return Promise.reject(json.error); - return json.result; + const body = await fetch(url, init); + return body.result; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } diff --git a/test/e2e/sign/index.spec.js b/test/e2e/sign/index.spec.js new file mode 100644 index 000000000..89d907154 --- /dev/null +++ b/test/e2e/sign/index.spec.js @@ -0,0 +1,79 @@ +import { describe, test, expect } from 'vitest'; +import { Wallet } from '@ethersproject/wallet'; +import Client from '../../../src/sign/'; + +describe('Client', () => { + describe('send()', () => { + describe('on success', () => { + test('should return a JSON-RPC object', async () => { + expect.assertions(5); + const client = new Client(); + const pk = + 'f5c1c920babf354b83c9ab1634a10ff50a2835715482b36f181319a109c30921'; + const wallet = new Wallet(pk); + const address = wallet.address; + const types = { + Follow: [ + { name: 'from', type: 'address' }, + { name: 'space', type: 'string' } + ] + }; + const domain = { name: 'snapshot', version: '0.1.4' }; + const message = { + from: address, + space: 'fabien.eth', + timestamp: Math.floor(Date.now() / 1000) + }; + const sig = await wallet._signTypedData(domain, types, message); + + const result = await client.send({ + address, + sig, + data: { + domain, + types, + message + } + }); + + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('ipfs'); + expect(result).toHaveProperty('relayer'); + expect(result).not.toHaveProperty('error'); + expect(result).not.toHaveProperty('error_description'); + }); + }); + + describe('on error', () => { + const payload = { address: '', sig: '', data: '' }; + + test('should return the JSON-RPC error from sequencer', async () => { + expect.assertions(1); + const client = new Client(); + await expect(client.send(payload)).to.rejects.toEqual({ + error: 'client_error', + error_description: 'wrong envelope format' + }); + }); + + test.each([ + ['no response', 'https://unknown.snapshot.org'], + ['404 Not Found', 'https://httpstat.us/404'] + ])('should throw an error on network error (%s)', async (code, url) => { + expect.assertions(1); + const client = new Client(url); + await expect(() => client.send(payload)).rejects.toThrowError(code); + }); + + test('should throw an error on network error (timeout)', async () => { + expect.assertions(1); + const client = new Client('https://httpstat.us/200?sleep=5000', { + timeout: 500 + }); + await expect(() => client.send(payload)).rejects.toThrowError( + 'aborted' + ); + }); + }); + }); +}); diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js new file mode 100644 index 000000000..2ca97466c --- /dev/null +++ b/test/e2e/utils.spec.js @@ -0,0 +1,594 @@ +import { test, expect, describe } from 'vitest'; +import { + getScores, + getVp, + validate, + getJSON, + ipfsGet, + subgraphRequest +} from '../../src/utils'; + +const SCORE_API_URL = 'https://score.snapshot.org'; + +describe('getScores', () => { + const payload = [ + 'fabien.eth', + [ + { + name: 'eth-balance', + network: '1', + params: {} + } + ], + '1', + ['0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'], + 'latest' + ]; + + describe('on success', () => { + const scoresResponse = [ + { + '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7': 0.041582733391515345 + } + ]; + const stateResponse = 'pending'; + + test('should return only the scores property by default', async () => { + expect.assertions(1); + expect(await getScores(...payload)).toEqual(scoresResponse); + }); + + test( + 'should return the full scores object', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'all' + }) + ).toEqual({ + scores: scoresResponse, + state: stateResponse + }); + }, + 15e3, + 3 + ); + + test( + 'should return only the given field', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'state' + }) + ).toEqual(stateResponse); + }, + 15e3, + 3 + ); + + test( + 'should return undefined when the given field does not exist', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'test' + }) + ).toEqual(undefined); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + test( + 'should return the JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect(getScores('test.eth', [], '1', ['0x0'])).rejects.toEqual({ + code: 500, + message: 'unauthorized', + data: 'something wrong with the strategies' + }); + }, + 15e3, + 3 + ); + + test('should return a JSON-RPC-like error on network error (no response)', () => { + expect.assertions(1); + expect( + getScores(...payload, 'https://score-null.snapshot.org') + ).rejects.toEqual({ + code: 0, + message: expect.stringContaining('no response'), + data: '' + }); + }); + + test('should return a JSON-RPC-like error on network error (not found)', () => { + expect.assertions(1); + expect( + getScores(...payload, 'https://httpstat.us', { pathname: '404' }) + ).rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); + }); + + test('should return a JSON-RPC-like error on network error (timeout)', () => { + expect.assertions(1); + expect( + getScores(...payload, 'https://httpstat.us/?sleep=5000', { + timeout: 500, + pathname: '200' + }) + ).rejects.toEqual( + expect.objectContaining({ + code: 0, + message: expect.stringContaining('operation was aborted') + }) + ); + }); + }); +}); + +describe('getVp', () => { + const address = '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'; + const network = '1'; + const strategies = [ + { + name: 'eth-balance', + network: '1', + params: {} + }, + { + name: 'eth-balance', + network: '10', + params: {} + } + ]; + const s = 15109700; + const space = 'fabien.eth'; + const delegation = false; + const defaultOptions = [address, network, strategies, s, space, delegation]; + + describe('on success', () => { + test( + 'should return a voting power', + async () => { + expect.assertions(1); + expect(await getVp(...defaultOptions)).toEqual({ + vp: 10.49214268914954, + vp_by_strategy: [10.443718706159482, 0.04842398299005922], + vp_state: 'final' + }); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + test( + 'should return a JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect( + getVp('test', network, strategies, s, space, delegation) + ).rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }, + 15e3, + 3 + ); + + test('should return a JSON-RPC-like error on network error (no response)', () => { + expect.assertions(1); + expect( + getVp(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).rejects.toEqual({ + code: 0, + message: expect.stringContaining('no response'), + data: '' + }); + }); + + test('should return a JSON-RPC-like error on network error (not found)', () => { + expect.assertions(1); + expect( + getVp(...defaultOptions, { + url: 'https://httpstat.us/404' + }) + ).rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); + }); + + test('should return a JSON-RPC-like error on network error (timeout)', () => { + expect.assertions(1); + expect( + getVp(...defaultOptions, { + url: 'https://httpstat.us/200?sleep=5000', + timeout: 500 + }) + ).rejects.toEqual( + expect.objectContaining({ + code: 0, + message: expect.stringContaining('operation was aborted') + }) + ); + }); + }); +}); + +describe('validate', () => { + const validation = 'basic'; + const author = '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'; + const space = 'fabien.eth'; + const network = '1'; + const params = { + minScore: 0.9, + strategies: [ + { + name: 'eth-balance', + params: {} + } + ] + }; + const defaultOptions = [validation, author, space, network, 'latest', params]; + + describe('on success', () => { + test( + 'should return a boolean', + async () => { + expect.assertions(1); + expect(await validate(...defaultOptions)).toEqual(false); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + test( + 'should return the JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect( + validate(validation, 'test', space, network, 'latest', params) + ).rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }, + 15e3, + 3 + ); + + test('should return a JSON-RPC-like error on network error (no response)', () => { + expect.assertions(1); + expect( + validate(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).rejects.toEqual({ + code: 0, + message: expect.stringContaining('no response'), + data: '' + }); + }); + + test('should return a JSON-RPC-like error on network error (not found)', () => { + expect.assertions(1); + expect( + validate(...defaultOptions, { + url: 'https://httpstat.us/404' + }) + ).rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); + }); + + test('should return a JSON-RPC-like error on network error (timeout)', () => { + expect.assertions(1); + expect( + validate(...defaultOptions, { + url: 'https://httpstat.us/200?sleep=5000', + timeout: 500 + }) + ).rejects.toEqual( + expect.objectContaining({ + code: 0, + message: expect.stringContaining('operation was aborted') + }) + ); + }); + }); +}); + +describe('getJSON', () => { + describe('on success', () => { + test( + 'should return a JSON object from the specified URL', + async () => { + expect.assertions(1); + expect(await getJSON('https://hub.snapshot.org')).toEqual( + expect.objectContaining({ name: 'snapshot-hub' }) + ); + }, + 3e3, + 3 + ); + + test( + 'should return a JSON object from the specified CID', + async () => { + expect.assertions(1); + expect( + await getJSON( + 'bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi' + ) + ).toEqual(expect.objectContaining({ status: 'OK' })); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + test('should throw an error when the response is not a JSON file', () => { + expect.assertions(1); + expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( + /Unexpected.*JSON/ + ); + }); + + test('should throw an error when the url is an empty string', () => { + expect.assertions(1); + expect(() => getJSON('')).rejects.toThrowError( + /(Invalid|Failed to parse) URL/i + ); + }); + + test('should throw an error when the given argument is not valid CID', () => { + expect.assertions(1); + expect(() => getJSON('test-cid')).rejects.toThrowError( + '500 Internal Server Error' + ); + }); + + test('should throw an error when the url is not valid', () => { + expect.assertions(1); + expect(() => getJSON('https:// testurl.com')).rejects.toThrowError( + /(Invalid|Failed to parse) URL/i + ); + }); + + test('should throw an error on network error (no response)', () => { + expect.assertions(1); + expect(() => + getJSON('https://score-null.snapshot.org') + ).rejects.toThrowError('no response'); + }); + + test('should throw an error on network error (not found)', () => { + expect.assertions(1); + expect(() => getJSON('https://httpstat.us/404')).rejects.toThrowError( + '404 Not Found' + ); + }); + + test('should throw an error on network error (timeout)', () => { + expect.assertions(1); + expect(() => + getJSON('https://httpstat.us/200?sleep=5000', { timeout: 500 }) + ).rejects.toThrowError('operation was aborted'); + }); + }); +}); + +describe('ipfsGet', () => { + const cid = 'bafkreibatgmdqdxsair3j52zfhtntegshtypq2qbex3fgtorwx34kzippe'; + + describe('on success', () => { + test( + 'should return a JSON object', + async () => { + expect.assertions(1); + expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ + name: 'Vitalik' + }); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + test('should throw an error when the response is not a JSON file', () => { + expect.assertions(1); + expect(() => ipfsGet('snapshot.org', cid)).rejects.toThrowError( + /Unexpected.*JSON/ + ); + }); + + test('should throw an error on network error (no response)', () => { + expect.assertions(1); + expect(() => + ipfsGet('score-null.snapshot.org', cid) + ).rejects.toThrowError('no response'); + }); + + test('should throw an error on network error (not found)', () => { + expect.assertions(1); + expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( + '404 Not Found' + ); + }); + + test('should throw an error on network error (on invalid protocol argument)', () => { + expect.assertions(1); + expect(() => ipfsGet('pineapple.fyi', cid, 'test')).rejects.toThrowError( + '404 Not Found' + ); + }); + + test('should throw an error on network error (timeout)', () => { + expect.assertions(1); + expect(() => + ipfsGet( + `httpstat.us/200?sleep=5000&cachebuster=${Date.now()}`, + cid, + 'ipfs', + { timeout: 500 } + ) + ).rejects.toThrowError('operation was aborted'); + }); + }); +}); + +describe('subgraphRequest', () => { + const query = { + blocks: { + __args: { + where: { + ts: 1640000000, + network_in: ['1'] + } + }, + network: true, + number: true + } + }; + const HOST = 'https://blockfinder.snapshot.org'; + + describe('on success', () => { + test( + 'should return a JSON object', + async () => { + expect.assertions(1); + expect(await subgraphRequest(HOST, query)).toEqual({ + blocks: [{ network: '1', number: 13841761 }] + }); + }, + 15e3, + 3 + ); + }); + + describe('on error', () => { + const invalidQuery = { + blocks: { + __args: { + where: { + ts: 1640000000, + network_in: ['4'] + } + }, + network: true, + number: true + } + }; + + test( + 'should return the error response from subgraph', + () => { + expect.assertions(1); + expect(subgraphRequest(HOST, invalidQuery)).rejects.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'invalid network', + extensions: { code: 'INVALID_NETWORK' } + }) + ] + }) + ); + }, + 15e3, + 3 + ); + + test('should return an errors object on not JSON response', () => { + expect.assertions(1); + expect( + subgraphRequest('https://httpstat.us/200', query, { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + }) + ).rejects.toEqual({ + errors: [ + { + message: 'Body is not a JSON object', + extensions: { code: 'INVALID_JSON' } + } + ] + }); + }); + + test('should return an errors object on network error (no response)', () => { + expect.assertions(1); + expect( + subgraphRequest('https://test-null.snapshot.org', query) + ).rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: expect.stringContaining('no response') + } + ] + }); + }); + + test('should return an errors object on network error (not found)', () => { + expect.assertions(1); + expect(subgraphRequest('https://httpstat.us/404', query)).rejects.toEqual( + { + errors: [ + { + extensions: { code: 404 }, + message: 'Not Found' + } + ] + } + ); + }); + + test('should return an errors object on network error (timeout)', () => { + expect.assertions(1); + expect( + subgraphRequest('https://httpstat.us/200?sleep=5000', query, { + timeout: 500 + }) + ).rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: expect.stringContaining('operation was aborted') + } + ] + }); + }); + }); +}); diff --git a/test/e2e/utils/blockfinder.spec.js b/test/e2e/utils/blockfinder.spec.js new file mode 100644 index 000000000..0c6b633bd --- /dev/null +++ b/test/e2e/utils/blockfinder.spec.js @@ -0,0 +1,87 @@ +import { test, expect, describe } from 'vitest'; +import { getSnapshots } from '../../../src/utils/blockfinder'; +import getProvider from '../../../src/utils/provider'; + +describe('Blockfinder', () => { + const provider = getProvider('1'); + + describe('getSnapshot()', () => { + describe('on success', () => { + test('should return a list of blocks per network', async () => { + expect( + await getSnapshots('1', 17789783, provider, ['5', '137']) + ).toMatchObject({ + 1: 17789783, + 137: 45609596, + 5: 9421169 + }); + }); + + test('should return all latest if snapshot is latest', async () => { + expect( + await getSnapshots('1', 'latest', provider, ['5', '137']) + ).toMatchObject({ + 137: 'latest', + 5: 'latest' + }); + }); + }); + + describe('on error', () => { + test('should throw a GraphQL error from blockfinder', async () => { + await expect( + getSnapshots('1', 17780783, provider, ['5', '4', '137']) + ).to.rejects.toEqual({ + errors: [ + { + message: 'invalid network', + locations: [ + { + line: 1, + column: 9 + } + ], + path: ['blocks'], + extensions: { + code: 'INVALID_NETWORK' + } + } + ], + data: { + blocks: null + } + }); + }); + + test('should throw a graphql-like error on network error (invalid hostname)', async () => { + await expect( + getSnapshots('1', 17789785, provider, ['5', '137'], { + blockFinderUrl: 'http://localhost:12345' + }) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: expect.stringContaining('no response') + } + ] + }); + }); + + test('should throw a graphql-like error on network error (not found)', async () => { + await expect( + getSnapshots('1', 17789786, provider, ['5', '137'], { + blockFinderUrl: 'http://httpstat.us/404' + }) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 404 }, + message: 'Not Found' + } + ] + }); + }); + }); + }); +}); diff --git a/test/e2e/utils/blockfinder.spec.ts b/test/e2e/utils/blockfinder.spec.ts deleted file mode 100644 index 01396525c..000000000 --- a/test/e2e/utils/blockfinder.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { test, expect, describe } from 'vitest'; -import { getSnapshots } from '../../../src/utils/blockfinder'; -import getProvider from '../../../src/utils/provider'; - -describe('Test block finder', () => { - const provider = getProvider('1'); - test('getSnapshots should work without errors and return object', async () => { - expect( - await getSnapshots('1', 17789783, provider, ['5', '137']) - ).toMatchObject({ - '1': 17789783, - '137': 45609596, - '5': 9421169 - }); - }); - test('getSnapshots should return all latest if snapshot is latest', async () => { - expect( - await getSnapshots('1', 'latest', provider, ['5', '137']) - ).toMatchObject({ - '137': 'latest', - '5': 'latest' - }); - }); -}); diff --git a/test/e2e/utils/getScores.spec.ts b/test/e2e/utils/getScores.spec.ts deleted file mode 100644 index 183280536..000000000 --- a/test/e2e/utils/getScores.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test, expect, describe } from 'vitest'; -import { getScores } from '../../../src/utils'; - -describe('test getScores', () => { - test('getScores should return a promise rejection on error from score-api', async () => { - expect.assertions(1); - await expect( - getScores('test.eth', [], '1', ['0x0']) - ).to.rejects.toHaveProperty('code'); - }); - - test('getScores should return a promise rejection with JSON-RPC format on network error', async () => { - expect.assertions(1); - await expect( - getScores( - 'test.eth', - [], - '1', - [''], - 'latest', - 'https://score-null.snapshot.org' - ) - ).to.rejects.toEqual({ - code: 'ENOTFOUND', - message: - 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', - data: '' - }); - }); -}); diff --git a/test/schema.spec.ts b/test/schema.spec.js similarity index 100% rename from test/schema.spec.ts rename to test/schema.spec.js diff --git a/yarn.lock b/yarn.lock index b214a4e6f..72175819e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1789,13 +1789,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-fetch@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" - integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== - dependencies: - node-fetch "^2.6.11" - cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1899,6 +1892,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destr@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.1.tgz#2fc7bddc256fed1183e03f8d148391dde4023cb2" + integrity sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA== + diff-sequences@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" @@ -3439,12 +3437,10 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.6.11: - version "2.6.11" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" - integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== - dependencies: - whatwg-url "^5.0.0" +node-fetch-native@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.0.tgz#fbe8ac033cb6aa44bd106b5e4fd2b6277ba70fa1" + integrity sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA== node-gyp@^7.1.0: version "7.1.2" @@ -3641,6 +3637,15 @@ octal@^1.0.0: resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" integrity sha1-Y+cWKmjvvrniE1iNWOmJ0eXEUws= +ofetch@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" + integrity sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg== + dependencies: + destr "^2.0.1" + node-fetch-native "^1.4.0" + ufo "^1.3.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4779,11 +4784,6 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - tslib@1.11.1, tslib@^1.8.1, tslib@^1.9.0: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -4855,6 +4855,11 @@ ufo@^1.1.2: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.2.tgz#d0d9e0fa09dece0c31ffd57bd363f030a35cfe76" integrity sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ== +ufo@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.0.tgz#c92f8ac209daff607c57bbd75029e190930a0019" + integrity sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -4987,19 +4992,6 @@ vlq@^0.2.2: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"