From bdad2314ba983d3d4fc603b7c7e15ea98cb84370 Mon Sep 17 00:00:00 2001 From: ChaituVR Date: Fri, 17 May 2024 23:26:49 +0530 Subject: [PATCH 1/6] Fix: Pass api key in headers and detect html error --- src/utils.spec.js | 80 ++++++++++++++++++++------------ src/utils.ts | 75 ++++++++++++++++++++---------- test/e2e/utils/getScores.spec.ts | 22 ++++++++- test/e2e/utils/getVp.spec.ts | 4 +- 4 files changed, 122 insertions(+), 59 deletions(-) diff --git a/src/utils.spec.js b/src/utils.spec.js index 25c3bc9ef..b33648b27 100644 --- a/src/utils.spec.js +++ b/src/utils.spec.js @@ -78,13 +78,14 @@ describe('utils', () => { describe('when passing valid args', () => { test('send a JSON-RPC payload to score-api', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_validate({})).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://score.snapshot.org', + 'https://score.snapshot.org/', expect.objectContaining({ body: JSON.stringify({ jsonrpc: '2.0', @@ -96,13 +97,14 @@ describe('utils', () => { }); test('send a POST request with JSON content-type', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_validate({})).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://score.snapshot.org', + 'https://score.snapshot.org/', expect.objectContaining({ method: 'POST', headers: { @@ -114,23 +116,28 @@ describe('utils', () => { }); test('can customize the score-api url', () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect( _validate({ options: { url: 'https://snapshot.org/?apiKey=xxx' } }) ).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://snapshot.org/?apiKey=xxx', - expect.anything() + 'https://snapshot.org/', + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-API-KEY': 'xxx' + }) + }) ); }); test('returns the JSON-RPC result property', () => { const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve(result)) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_validate({})).resolves.toEqual('OK'); @@ -141,7 +148,7 @@ describe('utils', () => { test('rejects with the JSON-RPC error object', () => { const result = { error: { message: 'Oh no' } }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve(result)) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_validate({})).rejects.toEqual(result.error); @@ -152,7 +159,7 @@ describe('utils', () => { test('rejects with the error', () => { const result = new Error('Oh no'); fetch.mockReturnValue({ - json: () => { + text: () => { throw result; } }); @@ -196,7 +203,7 @@ describe('utils', () => { network ?? payload.network, addresses ?? payload.addresses, snapshot ?? payload.snapshot, - scoreApiUrl ?? 'https://score.snapshot.org', + scoreApiUrl ?? 'https://score.snapshot.org/', options ?? {} ); } @@ -239,8 +246,9 @@ describe('utils', () => { describe('when passing valid args', () => { test('send a JSON-RPC payload to score-api', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getScores({})).resolves; @@ -253,8 +261,9 @@ describe('utils', () => { }); test('send a POST request with JSON content-type', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getScores({})).resolves; @@ -270,23 +279,29 @@ describe('utils', () => { ); }); - test('can customize the score-api url', () => { + test('can customize the score-api url and if apiKey should be passed in headers', () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getScores({ scoreApiUrl: 'https://snapshot.org/?apiKey=xxx' })) .resolves; expect(fetch).toHaveBeenCalledWith( - 'https://snapshot.org/api/scores?apiKey=xxx', - expect.anything() + 'https://snapshot.org/api/scores', + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-API-KEY': 'xxx' + }) + }) ); }); test('returns the JSON-RPC result scores property', () => { const result = { scores: 'SCORES', other: 'Other' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result })) + text: () => + new Promise((resolve) => resolve(JSON.stringify({ result }))) }); expect(_getScores({})).resolves.toEqual('SCORES'); @@ -295,7 +310,8 @@ describe('utils', () => { test('returns the JSON-RPC all properties', () => { const result = { scores: 'SCORES', other: 'Other' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result })) + text: () => + new Promise((resolve) => resolve(JSON.stringify({ result }))) }); expect( @@ -308,7 +324,7 @@ describe('utils', () => { test('rejects with the JSON-RPC error object', () => { const result = { error: { message: 'Oh no' } }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve(result)) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getScores({})).rejects.toEqual(result.error); @@ -319,7 +335,7 @@ describe('utils', () => { test('rejects with the error', () => { const result = new Error('Oh no'); fetch.mockReturnValue({ - json: () => { + text: () => { throw result; } }); @@ -391,13 +407,14 @@ describe('utils', () => { describe('when passing valid args', () => { test('send a JSON-RPC payload to score-api', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getVp({})).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://score.snapshot.org', + 'https://score.snapshot.org/', expect.objectContaining({ body: JSON.stringify({ jsonrpc: '2.0', @@ -409,13 +426,14 @@ describe('utils', () => { }); test('send a POST request with JSON content-type', async () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getVp({})).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://score.snapshot.org', + 'https://score.snapshot.org/', expect.objectContaining({ method: 'POST', headers: { @@ -427,13 +445,14 @@ describe('utils', () => { }); test('can customize the score-api url', () => { + const result = { result: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result: 'OK' })) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getVp({ options: { url: 'https://snapshot.org' } })).resolves; expect(fetch).toHaveBeenCalledWith( - 'https://snapshot.org', + 'https://snapshot.org/', expect.anything() ); }); @@ -441,7 +460,8 @@ describe('utils', () => { test('returns the JSON-RPC result property', () => { const result = { data: 'OK' }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve({ result })) + text: () => + new Promise((resolve) => resolve(JSON.stringify({ result }))) }); expect(_getVp({})).resolves.toEqual(result); @@ -452,7 +472,7 @@ describe('utils', () => { test('rejects with the JSON-RPC error object', () => { const result = { error: { message: 'Oh no' } }; fetch.mockReturnValue({ - json: () => new Promise((resolve) => resolve(result)) + text: () => new Promise((resolve) => resolve(JSON.stringify(result))) }); expect(_getVp({})).rejects.toEqual(result.error); @@ -463,7 +483,7 @@ describe('utils', () => { test('rejects with the error', () => { const result = new Error('Oh no'); fetch.mockReturnValue({ - json: () => { + text: () => { throw result; } }); diff --git a/src/utils.ts b/src/utils.ts index 98adddfee..10c430054 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,6 +20,7 @@ import getDelegatesBySpace, { SNAPSHOT_SUBGRAPH_URL } from './utils/delegation'; interface Options { url?: string; + headers?: any; } interface Strategy { @@ -38,6 +39,37 @@ const scoreApiHeaders = { 'Content-Type': 'application/json' }; +const DEFAULT_SCORE_API_URL = 'https://score.snapshot.org'; + +const formatScoreAPIUrl = (url = DEFAULT_SCORE_API_URL) => { + const scoreURL = new URL(url); + const apiKey = scoreURL.searchParams.get('apiKey'); + let headers: any = { ...scoreApiHeaders }; + if (apiKey) { + scoreURL.searchParams.delete('apiKey'); + headers = { ...scoreApiHeaders, 'X-API-KEY': apiKey }; + } + return { + url: scoreURL.toString(), + headers + }; +}; + +const parseScoreAPIResponse = async (res: any) => { + let json: any = await res.text(); + try { + json = JSON.parse(json); + } catch (e) { + return Promise.reject({ + code: 500, + message: 'Failed to parse response from score API', + data: json + }); + } + if (json.error) return Promise.reject(json.error); + return json; +}; + const ajv = new Ajv({ allErrors: true, allowUnionTypes: true, @@ -135,7 +167,6 @@ ajv.addKeyword({ errors: true }); - // Custom URL format to allow empty string values // https://github.com/snapshot-labs/snapshot.js/pull/541/files ajv.addFormat('customUrl', { @@ -287,7 +318,7 @@ export async function getScores( network: string, addresses: string[], snapshot: number | string = 'latest', - scoreApiUrl = 'https://score.snapshot.org', + scoreApiUrl = DEFAULT_SCORE_API_URL, options: any = {} ) { if (!Array.isArray(addresses)) { @@ -317,9 +348,9 @@ export async function getScores( ); } - const url = new URL(scoreApiUrl); - url.pathname = '/api/scores'; - scoreApiUrl = url.toString(); + const urlObject = new URL(scoreApiUrl); + urlObject.pathname = '/api/scores'; + const { url, headers } = formatScoreAPIUrl(urlObject.toString()); try { const params = { @@ -329,16 +360,12 @@ export async function getScores( strategies, addresses }; - const res = await fetch(scoreApiUrl, { + const res = await fetch(url, { method: 'POST', - headers: scoreApiHeaders, + headers, body: JSON.stringify({ params }) }); - const obj = await res.json(); - - if (obj.error) { - return Promise.reject(obj.error); - } + const obj = await parseScoreAPIResponse(res); return options.returnValue === 'all' ? obj.result @@ -360,8 +387,7 @@ export async function getVp( delegation: boolean, options?: Options ) { - if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const { url, headers } = formatScoreAPIUrl(options?.url); if (!isValidAddress(address)) { return inputError(`Invalid voter address: ${address}`); } @@ -385,7 +411,7 @@ export async function getVp( const init = { method: 'POST', - headers: scoreApiHeaders, + headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'get_vp', @@ -401,10 +427,9 @@ export async function getVp( }; 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 res = await fetch(url, init); + const response = await parseScoreAPIResponse(res); + return response.result; } catch (e) { if (e.errno) { return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); @@ -420,7 +445,7 @@ export async function validate( network: string, snapshot: number | 'latest', params: any, - options: any + options?: Options ) { if (!isValidAddress(author)) { return inputError(`Invalid author: ${author}`); @@ -436,10 +461,11 @@ export async function validate( } if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const { url, headers } = formatScoreAPIUrl(options.url); + const init = { method: 'POST', - headers: scoreApiHeaders, + headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'validate', @@ -455,9 +481,8 @@ export async function validate( }; try { - const res = await fetch(options.url, init); - const json = await res.json(); - if (json.error) return Promise.reject(json.error); + const res = await fetch(url, init); + const json = await parseScoreAPIResponse(res); return json.result; } catch (e) { if (e.errno) { diff --git a/test/e2e/utils/getScores.spec.ts b/test/e2e/utils/getScores.spec.ts index b806edbf6..fb90b0ae7 100644 --- a/test/e2e/utils/getScores.spec.ts +++ b/test/e2e/utils/getScores.spec.ts @@ -23,9 +23,27 @@ describe('test getScores', () => { 'https://score-null.snapshot.org' ) ).to.rejects.toEqual({ - code: 'ENOTFOUND', + code: 'ECONNRESET', message: - 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: read ECONNRESET', + data: '' + }); + }); + test('getScores should not pass API Key if it is passed in URL', async () => { + expect.assertions(1); + await expect( + getScores( + 'test.eth', + [], + '1', + ['0x9e8f6CF284Db7a80646D9d322A37b3dAF461F8DD'], + 'latest', + 'https://score-null.snapshot.org?apiKey=123' + ) + ).to.rejects.toEqual({ + code: 'ECONNRESET', + message: + 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: read ECONNRESET', data: '' }); }); diff --git a/test/e2e/utils/getVp.spec.ts b/test/e2e/utils/getVp.spec.ts index b5a44f7b2..8abf00ee1 100644 --- a/test/e2e/utils/getVp.spec.ts +++ b/test/e2e/utils/getVp.spec.ts @@ -51,9 +51,9 @@ describe('test getVp', () => { { url: 'https://score-null.snapshot.org' } ) ).to.rejects.toEqual({ - code: 'ENOTFOUND', + code: 'ECONNRESET', message: - 'FetchError: request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + 'FetchError: request to https://score-null.snapshot.org/ failed, reason: read ECONNRESET', data: '' }); }); From 0bd7e15c5421e8668439335134c19d58262b63f1 Mon Sep 17 00:00:00 2001 From: ChaituVR Date: Fri, 17 May 2024 23:35:19 +0530 Subject: [PATCH 2/6] fix tests --- test/e2e/utils/getScores.spec.ts | 8 ++++---- test/e2e/utils/getVp.spec.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/e2e/utils/getScores.spec.ts b/test/e2e/utils/getScores.spec.ts index fb90b0ae7..ecf7c7142 100644 --- a/test/e2e/utils/getScores.spec.ts +++ b/test/e2e/utils/getScores.spec.ts @@ -23,9 +23,9 @@ describe('test getScores', () => { 'https://score-null.snapshot.org' ) ).to.rejects.toEqual({ - code: 'ECONNRESET', + code: 'ENOTFOUND', message: - 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: read ECONNRESET', + 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', data: '' }); }); @@ -41,9 +41,9 @@ describe('test getScores', () => { 'https://score-null.snapshot.org?apiKey=123' ) ).to.rejects.toEqual({ - code: 'ECONNRESET', + code: 'ENOTFOUND', message: - 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: read ECONNRESET', + 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', data: '' }); }); diff --git a/test/e2e/utils/getVp.spec.ts b/test/e2e/utils/getVp.spec.ts index 8abf00ee1..b5a44f7b2 100644 --- a/test/e2e/utils/getVp.spec.ts +++ b/test/e2e/utils/getVp.spec.ts @@ -51,9 +51,9 @@ describe('test getVp', () => { { url: 'https://score-null.snapshot.org' } ) ).to.rejects.toEqual({ - code: 'ECONNRESET', + code: 'ENOTFOUND', message: - 'FetchError: request to https://score-null.snapshot.org/ failed, reason: read ECONNRESET', + 'FetchError: request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', data: '' }); }); From 32ad18fd9928902104f5ae7e2bb5f94481aae877 Mon Sep 17 00:00:00 2001 From: ChaituVR Date: Sat, 18 May 2024 08:33:08 +0530 Subject: [PATCH 3/6] Change to normal function --- src/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 10c430054..fb458a0f2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,7 +41,7 @@ const scoreApiHeaders = { const DEFAULT_SCORE_API_URL = 'https://score.snapshot.org'; -const formatScoreAPIUrl = (url = DEFAULT_SCORE_API_URL) => { +function formatScoreAPIUrl(url = DEFAULT_SCORE_API_URL) { const scoreURL = new URL(url); const apiKey = scoreURL.searchParams.get('apiKey'); let headers: any = { ...scoreApiHeaders }; @@ -53,9 +53,9 @@ const formatScoreAPIUrl = (url = DEFAULT_SCORE_API_URL) => { url: scoreURL.toString(), headers }; -}; +} -const parseScoreAPIResponse = async (res: any) => { +async function parseScoreAPIResponse(res: any) { let json: any = await res.text(); try { json = JSON.parse(json); @@ -68,7 +68,7 @@ const parseScoreAPIResponse = async (res: any) => { } if (json.error) return Promise.reject(json.error); return json; -}; +} const ajv = new Ajv({ allErrors: true, From cc6c4b27a7b6b4e1e957f3c3795022d6217e3fe0 Mon Sep 17 00:00:00 2001 From: Chaitanya Date: Sat, 18 May 2024 08:39:36 +0530 Subject: [PATCH 4/6] Update test/e2e/utils/getScores.spec.ts Co-authored-by: Wan <495709+wa0x6e@users.noreply.github.com> --- test/e2e/utils/getScores.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/e2e/utils/getScores.spec.ts b/test/e2e/utils/getScores.spec.ts index ecf7c7142..e7a03beba 100644 --- a/test/e2e/utils/getScores.spec.ts +++ b/test/e2e/utils/getScores.spec.ts @@ -29,6 +29,7 @@ describe('test getScores', () => { data: '' }); }); + test('getScores should not pass API Key if it is passed in URL', async () => { expect.assertions(1); await expect( From bbbf94180862a5f10dd2f9deeabad2cf6ad08945 Mon Sep 17 00:00:00 2001 From: ChaituVR Date: Sat, 18 May 2024 08:41:00 +0530 Subject: [PATCH 5/6] Update formatScoreAPIUrl to include options.path --- src/utils.ts | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index fb458a0f2..111220845 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,8 +41,14 @@ const scoreApiHeaders = { const DEFAULT_SCORE_API_URL = 'https://score.snapshot.org'; -function formatScoreAPIUrl(url = DEFAULT_SCORE_API_URL) { +function formatScoreAPIUrl( + url = DEFAULT_SCORE_API_URL, + options = { + path: '' + } +) { const scoreURL = new URL(url); + if (options.path) scoreURL.pathname = options.path; const apiKey = scoreURL.searchParams.get('apiKey'); let headers: any = { ...scoreApiHeaders }; if (apiKey) { @@ -56,18 +62,18 @@ function formatScoreAPIUrl(url = DEFAULT_SCORE_API_URL) { } async function parseScoreAPIResponse(res: any) { - let json: any = await res.text(); + let data: any = await res.text(); try { - json = JSON.parse(json); + data = JSON.parse(data); } catch (e) { return Promise.reject({ - code: 500, + code: res.status || 500, message: 'Failed to parse response from score API', - data: json + data }); } - if (json.error) return Promise.reject(json.error); - return json; + if (data.error) return Promise.reject(data.error); + return data; } const ajv = new Ajv({ @@ -350,7 +356,9 @@ export async function getScores( const urlObject = new URL(scoreApiUrl); urlObject.pathname = '/api/scores'; - const { url, headers } = formatScoreAPIUrl(urlObject.toString()); + const { url, headers } = formatScoreAPIUrl(scoreApiUrl, { + path: '/api/scores' + }); try { const params = { @@ -365,11 +373,11 @@ export async function getScores( headers, body: JSON.stringify({ params }) }); - const obj = await parseScoreAPIResponse(res); + const response = await parseScoreAPIResponse(res); return options.returnValue === 'all' - ? obj.result - : obj.result[options.returnValue || 'scores']; + ? response.result + : response.result[options.returnValue || 'scores']; } catch (e) { if (e.errno) { return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); @@ -482,8 +490,8 @@ export async function validate( try { const res = await fetch(url, init); - const json = await parseScoreAPIResponse(res); - return json.result; + const response = await parseScoreAPIResponse(res); + return response.result; } catch (e) { if (e.errno) { return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); From ab55ee77c2dd3c2819138ffbf1dce6a30256d808 Mon Sep 17 00:00:00 2001 From: Chaitanya Date: Sat, 18 May 2024 22:57:18 +0530 Subject: [PATCH 6/6] v0.11.23 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fd420c583..9b9f29364 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snapshot-labs/snapshot.js", - "version": "0.11.22", + "version": "0.11.23", "repository": "snapshot-labs/snapshot.js", "license": "MIT", "main": "dist/snapshot.cjs.js",