Skip to content

Commit

Permalink
New NCFX market status endpoint (#3387)
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-cll authored Aug 15, 2024
1 parent e01c723 commit ddda456
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 23 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-clouds-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@chainlink/ncfx-adapter': minor
---

New market-status endpoint
54 changes: 36 additions & 18 deletions packages/sources/ncfx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ This document was generated automatically. Please see [README Generator](../../s

## Environment Variables

| Required? | Name | Description | Type | Options | Default |
| :-------: | :-------------------: | :------------------------------------------------: | :----: | :-----: | :---------------------------------------------: |
| | API_USERNAME | Username for the NCFX Crypto API | string | | |
| | API_PASSWORD | Password for the NCFX Crypto API | string | | |
| | FOREX_WS_API_KEY | API key for Forex websocket endpoint | string | | |
| | WS_API_ENDPOINT | The WS API endpoint to use for the crypto endpoint | string | | `wss://cryptofeed.ws.newchangefx.com` |
| | FOREX_WS_API_ENDPOINT | The WS API endpoint to use for the forex endpoint | string | | `wss://fiat.ws.newchangefx.com/sub/fiat/ws/ref` |
| Required? | Name | Description | Type | Options | Default |
| :-------: | :---------------------------: | :-------------------------------------------------------: | :----: | :-----: | :--------------------------------------------------------------: |
| | API_USERNAME | Username for the NCFX Crypto API | string | | |
| | API_PASSWORD | Password for the NCFX Crypto API | string | | |
| | FOREX_WS_API_KEY | API key for Forex websocket endpoint | string | | |
| | WS_API_ENDPOINT | The WS API endpoint to use for the crypto endpoint | string | | `wss://cryptofeed.ws.newchangefx.com` |
| | FOREX_WS_API_ENDPOINT | The WS API endpoint to use for the forex endpoint | string | | `wss://fiat.ws.newchangefx.com/sub/fiat/ws/ref` |
| | MARKET_STATUS_WS_API_ENDPOINT | The WS API endpoint to use for the market status endpoint | string | | `wss://fiat.ws.newchangefx.com/general/reference/v1/markethours` |
| | MARKET_STATUS_WS_API_KEY | The WS API key to use for the market status endpoint | string | | |

---

Expand All @@ -24,9 +26,9 @@ There are no rate limits for this adapter.

## Input Parameters

| Required? | Name | Description | Type | Options | Default |
| :-------: | :------: | :-----------------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------: |
| | endpoint | The endpoint to use | string | [crypto-lwba](#crypto-lwba-endpoint), [crypto](#crypto-endpoint), [crypto_lwba](#crypto-lwba-endpoint), [cryptolwba](#crypto-lwba-endpoint), [forex](#forex-endpoint), [price](#crypto-endpoint) | `crypto` |
| Required? | Name | Description | Type | Options | Default |
| :-------: | :------: | :-----------------: | :----: | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------: |
| | endpoint | The endpoint to use | string | [crypto-lwba](#crypto-lwba-endpoint), [crypto](#crypto-endpoint), [crypto_lwba](#crypto-lwba-endpoint), [cryptolwba](#crypto-lwba-endpoint), [forex-market-status](#market-status-endpoint), [forex](#forex-endpoint), [market-status](#market-status-endpoint), [metals-market-status](#market-status-endpoint), [price](#crypto-endpoint) | `crypto` |

## Crypto Endpoint

Expand Down Expand Up @@ -55,9 +57,9 @@ Request:

---

## Crypto-lwba Endpoint
## Forex Endpoint

Supported names for this endpoint are: `crypto-lwba`, `crypto_lwba`, `cryptolwba`.
`forex` is the only supported name for this endpoint.

### Input Params

Expand All @@ -73,18 +75,18 @@ Request:
```json
{
"data": {
"endpoint": "crypto-lwba",
"base": "ETH",
"endpoint": "forex",
"base": "CAD",
"quote": "USD"
}
}
```

---

## Forex Endpoint
## Crypto-lwba Endpoint

`forex` is the only supported name for this endpoint.
Supported names for this endpoint are: `crypto-lwba`, `crypto_lwba`, `cryptolwba`.

### Input Params

Expand All @@ -100,13 +102,29 @@ Request:
```json
{
"data": {
"endpoint": "forex",
"base": "CAD",
"endpoint": "crypto-lwba",
"base": "ETH",
"quote": "USD"
}
}
```

---

## Market-status Endpoint

Supported names for this endpoint are: `forex-market-status`, `market-status`, `metals-market-status`.

### Input Params

| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With |
| :-------: | :----: | :-----: | :--------------------: | :----: | :---------------: | :-----: | :--------: | :------------: |
|| market | | The name of the market | string | `forex`, `metals` | | | |

### Example

There are no examples for this endpoint.

---

MIT License
10 changes: 10 additions & 0 deletions packages/sources/ncfx/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ export const config = new AdapterConfig({
description: 'The WS API endpoint to use for the forex endpoint',
default: 'wss://fiat.ws.newchangefx.com/sub/fiat/ws/ref',
},
MARKET_STATUS_WS_API_ENDPOINT: {
type: 'string',
description: 'The WS API endpoint to use for the market status endpoint',
default: 'wss://fiat.ws.newchangefx.com/general/reference/v1/markethours',
},
MARKET_STATUS_WS_API_KEY: {
type: 'string',
description: 'The WS API key to use for the market status endpoint',
sensitive: true,
},
})
1 change: 1 addition & 0 deletions packages/sources/ncfx/src/endpoint/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { cryptoEndpoint as crypto } from './crypto'
export { cryptoLwbaEndpoint as lwba } from './crypto-lwba'
export { forexEndpoint as forex } from './forex'
export { marketStatusEndpoint as marketStatus } from './market-status'
31 changes: 31 additions & 0 deletions packages/sources/ncfx/src/endpoint/market-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
MarketStatusEndpoint,
MarketStatusResultResponse,
} from '@chainlink/external-adapter-framework/adapter'
import { InputParameters } from '@chainlink/external-adapter-framework/validation'

import { config } from '../config'
import { markets, transport } from '../transport/market-status'

const inputParameters = new InputParameters({
market: {
aliases: [],
type: 'string',
description: 'The name of the market',
options: markets,
required: true,
},
})

export type BaseEndpointTypes = {
Parameters: typeof inputParameters.definition
Response: MarketStatusResultResponse
Settings: typeof config.settings
}

export const marketStatusEndpoint = new MarketStatusEndpoint({
name: 'market-status',
aliases: ['forex-market-status', 'metals-market-status'],
transport,
inputParameters,
})
4 changes: 2 additions & 2 deletions packages/sources/ncfx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { expose, ServerInstance } from '@chainlink/external-adapter-framework'
import { PriceAdapter } from '@chainlink/external-adapter-framework/adapter'
import { config } from './config'
import includes from './config/includes.json'
import { crypto, forex, lwba } from './endpoint'
import { crypto, forex, lwba, marketStatus } from './endpoint'

export const adapter = new PriceAdapter({
name: 'NCFX',
endpoints: [crypto, lwba, forex],
endpoints: [crypto, forex, lwba, marketStatus],
defaultEndpoint: crypto.name,
config,
includes,
Expand Down
70 changes: 70 additions & 0 deletions packages/sources/ncfx/src/transport/market-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { MarketStatus } from '@chainlink/external-adapter-framework/adapter'
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
import { makeLogger, ProviderResult } from '@chainlink/external-adapter-framework/util'
import type { BaseEndpointTypes } from '../endpoint/market-status'

export const markets = ['forex', 'metals'] as const

export type Market = (typeof markets)[number]

const marketToNcfxMarket: Record<Market, keyof WsMessage['marketStatus']> = {
forex: 'fx',
metals: 'metals',
}

type WsMessage = {
marketStatus: {
fx: string
metals: string
}
timestamp: string
}

type WsTransportTypes = BaseEndpointTypes & {
Provider: {
WsMessage: WsMessage
}
}

const logger = makeLogger('NcfxMarketStatusEndpoint')

export const transport = new WebSocketTransport<WsTransportTypes>({
url: (context) => context.adapterSettings.MARKET_STATUS_WS_API_ENDPOINT,
options: (context) => {
return { headers: { 'x-api-key': context.adapterSettings.MARKET_STATUS_WS_API_KEY } }
},
handlers: {
message(message: WsMessage): ProviderResult<WsTransportTypes>[] {
if (!('marketStatus' in message) || typeof message.marketStatus !== 'object') {
logger.warn('Invalid marketStatus field in response')
return []
}
return markets.map((market) => {
const marketStatus = parseMarketStatus(message.marketStatus[marketToNcfxMarket[market]])
return {
params: { market },
response: {
result: marketStatus,
data: {
result: marketStatus,
},
timestamps: {
providerIndicatedTimeUnixMs: new Date(message.timestamp).getTime(),
},
},
}
})
},
},
})

export function parseMarketStatus(marketStatus: string): MarketStatus {
if (marketStatus === 'open') {
return MarketStatus.OPEN
}
if (marketStatus === 'closed') {
return MarketStatus.CLOSED
}
logger.warn(`Unexpected market status value: ${marketStatus}`)
return MarketStatus.UNKNOWN
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,33 @@ exports[`websocket lwba endpoint should return success 1`] = `
},
}
`;

exports[`websocket market status endpoint should return success with closed 1`] = `
{
"data": {
"result": 1,
},
"result": 1,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1015,
"providerDataStreamEstablishedUnixMs": 1010,
"providerIndicatedTimeUnixMs": 1718916249594,
},
}
`;

exports[`websocket market status endpoint should return success with open 1`] = `
{
"data": {
"result": 2,
},
"result": 2,
"statusCode": 200,
"timestamps": {
"providerDataReceivedUnixMs": 1015,
"providerDataStreamEstablishedUnixMs": 1010,
"providerIndicatedTimeUnixMs": 1718916249594,
},
}
`;
52 changes: 49 additions & 3 deletions packages/sources/ncfx/test/integration/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
import { mockCryptoWebSocketServer, mockForexWebSocketServer } from './fixtures'
import { WebSocketClassProvider } from '@chainlink/external-adapter-framework/transports'
import {
TestAdapter,
setEnvVariables,
mockWebSocketProvider,
MockWebsocketServer,
} from '@chainlink/external-adapter-framework/util/testing-utils'
import FakeTimers from '@sinonjs/fake-timers'
import { Adapter } from '@chainlink/external-adapter-framework/adapter'
import FakeTimers from '@sinonjs/fake-timers'

import {
mockCryptoWebSocketServer,
mockForexResponse,
mockForexWebSocketServer,
mockMarketStatusWebSocketServer,
} from './fixtures'

describe('websocket', () => {
let mockWsServer: MockWebsocketServer | undefined
let mockWsServerForex: MockWebsocketServer | undefined
let mockWsServerMarketStatus: MockWebsocketServer | undefined
let testAdapter: TestAdapter
const wsEndpoint = 'ws://localhost:9090'
const wsEndpointForex = 'ws://localhost:9091'
const wsEndpointMarketStatus = 'ws://localhost:9092'
let oldEnv: NodeJS.ProcessEnv
const cryptoData = {
base: 'eth',
Expand All @@ -35,6 +43,14 @@ describe('websocket', () => {
quote: 'USD',
endpoint: 'forex',
}
const marketStatusOpenData = {
market: 'forex',
endpoint: 'market-status',
}
const marketStatusClosedData = {
market: 'metals',
endpoint: 'market-status',
}

beforeAll(async () => {
oldEnv = JSON.parse(JSON.stringify(process.env))
Expand All @@ -44,13 +60,16 @@ describe('websocket', () => {
process.env['METRICS_ENABLED'] = 'false'
process.env['WS_API_ENDPOINT'] = wsEndpoint
process.env['FOREX_WS_API_ENDPOINT'] = wsEndpointForex
process.env['MARKET_STATUS_WS_API_ENDPOINT'] = wsEndpointMarketStatus
process.env['API_USERNAME'] = 'test-api-username'
process.env['API_PASSWORD'] = 'test-api-password'
process.env['FOREX_WS_API_KEY'] = 'test-api-key'
process.env['MARKET_STATUS_WS_API_KEY'] = 'test-api-key'

mockWebSocketProvider(WebSocketClassProvider)
mockWsServer = mockCryptoWebSocketServer(wsEndpoint)
mockWsServerForex = mockForexWebSocketServer(wsEndpointForex)
mockWsServerMarketStatus = mockMarketStatusWebSocketServer(wsEndpointMarketStatus)

const adapter = (await import('./../../src')).adapter as unknown as Adapter
testAdapter = await TestAdapter.startWithMockedCache(adapter, {
Expand All @@ -63,13 +82,16 @@ describe('websocket', () => {
await testAdapter.request(cryptoDataLwba)
await testAdapter.request(cryptoDataLwbaInvariantViolation)
await testAdapter.request(forexData)
await testAdapter.waitForCache(7)
await testAdapter.request(marketStatusOpenData)
await testAdapter.request(marketStatusClosedData)
await testAdapter.waitForCache(6 + Object.keys(mockForexResponse).length)
})

afterAll(async () => {
setEnvVariables(oldEnv)
mockWsServer?.close()
mockWsServerForex?.close()
mockWsServerMarketStatus?.close()
testAdapter.clock?.uninstall()
await testAdapter.api.close()
})
Expand Down Expand Up @@ -129,4 +151,28 @@ describe('websocket', () => {
expect(response.statusCode).toEqual(400)
})
})

describe('market status endpoint', () => {
it('should return success with open', async () => {
const response = await testAdapter.request(marketStatusOpenData)
expect(response.statusCode).toEqual(200)
expect(response.json()).toMatchSnapshot()
})

it('should return success with closed', async () => {
const response = await testAdapter.request(marketStatusClosedData)
expect(response.statusCode).toEqual(200)
expect(response.json()).toMatchSnapshot()
})

it('should return error (empty market)', async () => {
const response = await testAdapter.request({ ...marketStatusOpenData, market: undefined })
expect(response.statusCode).toEqual(400)
})

it('should return error (unknown market)', async () => {
const response = await testAdapter.request({ ...marketStatusOpenData, market: 'unknown' })
expect(response.statusCode).toEqual(400)
})
})
})
Loading

0 comments on commit ddda456

Please sign in to comment.