diff --git a/.changeset/rotten-humans-reply.md b/.changeset/rotten-humans-reply.md new file mode 100644 index 0000000000..86242c860e --- /dev/null +++ b/.changeset/rotten-humans-reply.md @@ -0,0 +1,5 @@ +--- +'@chainlink/gsr-adapter': minor +--- + +Added LWBA endpoint for GSR EA diff --git a/packages/sources/gsr/README.md b/packages/sources/gsr/README.md index 5cf96fd4f3..5e46df54d5 100644 --- a/packages/sources/gsr/README.md +++ b/packages/sources/gsr/README.md @@ -24,13 +24,13 @@ There are no rate limits for this adapter. ## Input Parameters -| Required? | Name | Description | Type | Options | Default | -| :-------: | :------: | :-----------------: | :----: | :------------------------------------------------------------------------------: | :-----: | -| | endpoint | The endpoint to use | string | [crypto](#price-endpoint), [price-ws](#price-endpoint), [price](#price-endpoint) | `price` | +| Required? | Name | Description | Type | Options | Default | +| :-------: | :------: | :-----------------: | :----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----: | +| | endpoint | The endpoint to use | string | [crypto-lwba](#price-endpoint), [crypto](#price-endpoint), [crypto_lwba](#price-endpoint), [cryptolwba](#price-endpoint), [price-ws](#price-endpoint), [price](#price-endpoint) | `price` | ## Price Endpoint -Supported names for this endpoint are: `crypto`, `price`, `price-ws`. +Supported names for this endpoint are: `crypto`, `crypto-lwba`, `crypto_lwba`, `cryptolwba`, `price`, `price-ws`. ### Input Params diff --git a/packages/sources/gsr/src/endpoint/price.ts b/packages/sources/gsr/src/endpoint/price.ts index d0032cb856..426431e18c 100644 --- a/packages/sources/gsr/src/endpoint/price.ts +++ b/packages/sources/gsr/src/endpoint/price.ts @@ -1,5 +1,7 @@ import { CryptoPriceEndpoint, + DEFAULT_LWBA_ALIASES, + LwbaResponseDataFields, priceEndpointInputParametersDefinition, } from '@chainlink/external-adapter-framework/adapter' import { SingleNumberResultResponse } from '@chainlink/external-adapter-framework/util' @@ -14,15 +16,17 @@ const inputParameters = new InputParameters(priceEndpointInputParametersDefiniti }, ]) +type OmitResultFromLwba = Omit + export type BaseEndpointTypes = { Parameters: typeof inputParameters.definition Settings: typeof config.settings - Response: SingleNumberResultResponse + Response: OmitResultFromLwba & SingleNumberResultResponse } export const endpoint = new CryptoPriceEndpoint({ name: 'price', - aliases: ['price-ws', 'crypto'], + aliases: ['price-ws', 'crypto', ...DEFAULT_LWBA_ALIASES], transport, inputParameters, }) diff --git a/packages/sources/gsr/src/index.ts b/packages/sources/gsr/src/index.ts index e7fb19c0d8..bce6ef7c6d 100644 --- a/packages/sources/gsr/src/index.ts +++ b/packages/sources/gsr/src/index.ts @@ -4,7 +4,7 @@ import { config } from './config' import { price } from './endpoint' export const adapter = new PriceAdapter({ - defaultEndpoint: 'price', + defaultEndpoint: price.name, name: 'GSR', endpoints: [price], config, diff --git a/packages/sources/gsr/src/transport/authutils.ts b/packages/sources/gsr/src/transport/authutils.ts new file mode 100644 index 0000000000..cffb78f7e5 --- /dev/null +++ b/packages/sources/gsr/src/transport/authutils.ts @@ -0,0 +1,54 @@ +import crypto from 'crypto' +import axios from 'axios' +import { makeLogger } from '@chainlink/external-adapter-framework/util' + +const logger = makeLogger('GSR Auth Token Utils') + +interface TokenError { + success: false + ts: number + error: string +} + +interface TokenSuccess { + success: true + ts: number + token: string + validUntil: string +} + +type AccessTokenResponse = TokenError | TokenSuccess + +const currentTimeNanoSeconds = (): number => new Date(Date.now()).getTime() * 1_000_000 + +const generateSignature = (userId: string, publicKey: string, privateKey: string, ts: number) => + crypto + .createHmac('sha256', privateKey) + .update(`userId=${userId}&apiKey=${publicKey}&ts=${ts}`) + .digest('hex') + +// restApiEndpoint is used for token auth +export const getToken = async ( + restApiEndpoint: string, + userId: string, + publicKey: string, + privateKey: string, +) => { + logger.debug('Fetching new access token') + + const ts = currentTimeNanoSeconds() + const signature = generateSignature(userId, publicKey, privateKey, ts) + const response = await axios.post(`${restApiEndpoint}/token`, { + apiKey: publicKey, + userId, + ts, + signature, + }) + + if (!response.data.success) { + logger.error(`Unable to get access token: ${response.data.error}`) + throw new Error(response.data.error) + } + + return response.data.token +} diff --git a/packages/sources/gsr/src/transport/price.ts b/packages/sources/gsr/src/transport/price.ts index b015d844c0..9c74d3b2b4 100644 --- a/packages/sources/gsr/src/transport/price.ts +++ b/packages/sources/gsr/src/transport/price.ts @@ -1,9 +1,7 @@ -import crypto from 'crypto' -import { config } from '../config' import { BaseEndpointTypes } from '../endpoint/price' import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports' -import axios from 'axios' import { makeLogger, ProviderResult } from '@chainlink/external-adapter-framework/util' +import { getToken } from './authutils' const logger = makeLogger('GSR WS price') @@ -12,6 +10,8 @@ type WsMessage = { data: { symbol: string price: number + bidPrice: number + askPrice: number ts: number } } @@ -22,57 +22,16 @@ export type WsTransportTypes = BaseEndpointTypes & { } } -export interface TokenError { - success: false - ts: number - error: string -} - -export interface TokenSuccess { - success: true - ts: number - token: string - validUntil: string -} - -export type AccessTokenResponse = TokenError | TokenSuccess - -const currentTimeNanoSeconds = (): number => new Date(Date.now()).getTime() * 1000000 - -const generateSignature = (userId: string, publicKey: string, privateKey: string, ts: number) => - crypto - .createHmac('sha256', privateKey) - .update(`userId=${userId}&apiKey=${publicKey}&ts=${ts}`) - .digest('hex') - -const getToken = async (settings: typeof config.settings) => { - logger.debug('Fetching new access token') - - const userId = settings.WS_USER_ID - const publicKey = settings.WS_PUBLIC_KEY - const privateKey = settings.WS_PRIVATE_KEY - const ts = currentTimeNanoSeconds() - const signature = generateSignature(userId, publicKey, privateKey, ts) - const response = await axios.post(`${settings.API_ENDPOINT}/token`, { - apiKey: publicKey, - userId, - ts, - signature, - }) - - if (!response.data.success) { - logger.error('Unable to get access token') - throw new Error(response.data.error) - } - - return response.data.token -} - export const transport = new WebSocketTransport({ url: (context) => context.adapterSettings.WS_API_ENDPOINT, options: async (context) => ({ headers: { - 'x-auth-token': await getToken(context.adapterSettings), + 'x-auth-token': await getToken( + context.adapterSettings.API_ENDPOINT, + context.adapterSettings.WS_USER_ID, + context.adapterSettings.WS_PUBLIC_KEY, + context.adapterSettings.WS_PRIVATE_KEY, + ), 'x-auth-userid': context.adapterSettings.WS_USER_ID, }, }), @@ -104,6 +63,9 @@ export const transport = new WebSocketTransport({ result: message.data.price, data: { result: message.data.price, + mid: message.data.price, + bid: message.data.bidPrice, + ask: message.data.askPrice, }, timestamps: { providerIndicatedTimeUnixMs: Math.round(message.data.ts / 1e6), // Value from provider is in nanoseconds @@ -118,11 +80,11 @@ export const transport = new WebSocketTransport({ // after you've already subscribed & unsubscribed to that pair on the same WS connection. subscribeMessage: (params) => ({ action: 'subscribe', - symbols: [`${params.base.toUpperCase()}.${params.quote.toUpperCase()}`], + symbols: [`${params.base}.${params.quote}`.toUpperCase()], }), unsubscribeMessage: (params) => ({ action: 'unsubscribe', - symbols: [`${params.base.toUpperCase()}.${params.quote.toUpperCase()}`], + symbols: [`${params.base}.${params.quote}`.toUpperCase()], }), }, }) diff --git a/packages/sources/gsr/test-payload.json b/packages/sources/gsr/test-payload.json index d28cef9c1a..955e3c28a7 100644 --- a/packages/sources/gsr/test-payload.json +++ b/packages/sources/gsr/test-payload.json @@ -1,6 +1,13 @@ { - "requests": [{ - "from": "ETH", - "to": "USD" - }] + "requests": [ + { + "from": "ETH", + "to": "USD" + }, + { + "from": "ETH", + "to": "USD", + "endpoint": "crypto-lwba" + } + ] } diff --git a/packages/sources/gsr/test/integration/__snapshots__/adapter.test.ts.snap b/packages/sources/gsr/test/integration/__snapshots__/adapter.test.ts.snap index 5254db509a..8fbaf9a4f3 100644 --- a/packages/sources/gsr/test/integration/__snapshots__/adapter.test.ts.snap +++ b/packages/sources/gsr/test/integration/__snapshots__/adapter.test.ts.snap @@ -1,8 +1,29 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`websocket websocket endpoint lwba endpoint should return success 1`] = ` +{ + "data": { + "ask": 1235, + "bid": 1233, + "mid": 1234, + "result": 1234, + }, + "result": 1234, + "statusCode": 200, + "timestamps": { + "providerDataReceivedUnixMs": Any, + "providerDataStreamEstablishedUnixMs": Any, + "providerIndicatedTimeUnixMs": 1669345393482, + }, +} +`; + exports[`websocket websocket endpoint should return success 1`] = ` { "data": { + "ask": 1235, + "bid": 1233, + "mid": 1234, "result": 1234, }, "result": 1234, diff --git a/packages/sources/gsr/test/integration/adapter.test.ts b/packages/sources/gsr/test/integration/adapter.test.ts index 532d12b651..8cd9d2f34a 100644 --- a/packages/sources/gsr/test/integration/adapter.test.ts +++ b/packages/sources/gsr/test/integration/adapter.test.ts @@ -18,6 +18,11 @@ describe('websocket', () => { base: 'ETH', quote: 'USD', } + const lwbaData = { + base: 'ETH', + quote: 'USD', + endpoint: 'crypto-lwba', + } beforeAll(async () => { oldEnv = JSON.parse(JSON.stringify(process.env)) @@ -63,6 +68,16 @@ describe('websocket', () => { }) }) + it('lwba endpoint should return success', async () => { + const response = await testAdapter.request(lwbaData) + expect(response.json()).toMatchSnapshot({ + timestamps: { + providerDataReceivedUnixMs: expect.any(Number), + providerDataStreamEstablishedUnixMs: expect.any(Number), + }, + }) + }) + it('should return error (empty data)', async () => { const response = await testAdapter.request({}) expect(response.statusCode).toEqual(400) diff --git a/packages/sources/gsr/test/integration/fixtures.ts b/packages/sources/gsr/test/integration/fixtures.ts index 976bad5601..417162b890 100644 --- a/packages/sources/gsr/test/integration/fixtures.ts +++ b/packages/sources/gsr/test/integration/fixtures.ts @@ -1,8 +1,8 @@ import nock from 'nock' import { MockWebsocketServer } from '@chainlink/external-adapter-framework/util/testing-utils' -export const mockTokenSuccess = (): nock.Scope => - nock('https://oracle.prod.gsr.io', { +const generateMockTokenSuccess = (basePath: string): nock.Scope => + nock(basePath, { encodedQueryParams: true, }) .post('/v1/token', { @@ -32,9 +32,13 @@ export const mockTokenSuccess = (): nock.Scope => ) .persist() +export const mockTokenSuccess = () => generateMockTokenSuccess('https://oracle.prod.gsr.io') + const base = 'ETH' const quote = 'USD' const price = 1234 +const bidPrice = 1233 +const askPrice = 1235 const time = 1669345393482 export const mockWebSocketServer = (URL: string) => { @@ -47,6 +51,8 @@ export const mockWebSocketServer = (URL: string) => { data: { symbol: `${base.toUpperCase()}.${quote.toUpperCase()}`, price, + bidPrice, + askPrice, ts: time * 1e6, }, }),