Skip to content

Commit

Permalink
Feature/df 18971 gsr lwba endpoint (#2938)
Browse files Browse the repository at this point in the history
* GSR LWBA endpoint

* add changeset

* update integration test

* update tests

* updates to separate price and lwba endpoints following discussion with GSR & associated changes

* back to price endpoint alias due to GSR DP decision

* review fix

* update to use framework LWBA types

* update readme endpoints

* number readability

---------

Co-authored-by: cl-ea <[email protected]>
  • Loading branch information
mmcallister-cll and cl-ea authored Jan 11, 2024
1 parent 0f0d9be commit 816452b
Show file tree
Hide file tree
Showing 10 changed files with 139 additions and 65 deletions.
5 changes: 5 additions & 0 deletions .changeset/rotten-humans-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/gsr-adapter': minor
---

Added LWBA endpoint for GSR EA
8 changes: 4 additions & 4 deletions packages/sources/gsr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions packages/sources/gsr/src/endpoint/price.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -14,15 +16,17 @@ const inputParameters = new InputParameters(priceEndpointInputParametersDefiniti
},
])

type OmitResultFromLwba = Omit<LwbaResponseDataFields, 'Result'>

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,
})
2 changes: 1 addition & 1 deletion packages/sources/gsr/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
54 changes: 54 additions & 0 deletions packages/sources/gsr/src/transport/authutils.ts
Original file line number Diff line number Diff line change
@@ -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<AccessTokenResponse>(`${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
}
66 changes: 14 additions & 52 deletions packages/sources/gsr/src/transport/price.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand All @@ -12,6 +10,8 @@ type WsMessage = {
data: {
symbol: string
price: number
bidPrice: number
askPrice: number
ts: number
}
}
Expand All @@ -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<AccessTokenResponse>(`${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<WsTransportTypes>({
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,
},
}),
Expand Down Expand Up @@ -104,6 +63,9 @@ export const transport = new WebSocketTransport<WsTransportTypes>({
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
Expand All @@ -118,11 +80,11 @@ export const transport = new WebSocketTransport<WsTransportTypes>({
// 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()],
}),
},
})
15 changes: 11 additions & 4 deletions packages/sources/gsr/test-payload.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
{
"requests": [{
"from": "ETH",
"to": "USD"
}]
"requests": [
{
"from": "ETH",
"to": "USD"
},
{
"from": "ETH",
"to": "USD",
"endpoint": "crypto-lwba"
}
]
}
Original file line number Diff line number Diff line change
@@ -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<Number>,
"providerDataStreamEstablishedUnixMs": Any<Number>,
"providerIndicatedTimeUnixMs": 1669345393482,
},
}
`;
exports[`websocket websocket endpoint should return success 1`] = `
{
"data": {
"ask": 1235,
"bid": 1233,
"mid": 1234,
"result": 1234,
},
"result": 1234,
Expand Down
15 changes: 15 additions & 0 deletions packages/sources/gsr/test/integration/adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 8 additions & 2 deletions packages/sources/gsr/test/integration/fixtures.ts
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down Expand Up @@ -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) => {
Expand All @@ -47,6 +51,8 @@ export const mockWebSocketServer = (URL: string) => {
data: {
symbol: `${base.toUpperCase()}.${quote.toUpperCase()}`,
price,
bidPrice,
askPrice,
ts: time * 1e6,
},
}),
Expand Down

0 comments on commit 816452b

Please sign in to comment.