Skip to content

Commit f80dea0

Browse files
NCFX EA refactor (#4634)
* refactor(ncfx): extract shared WebSocket utilities to utils.ts - Create utils.ts with shared types (WsMessage, WsInfoMessage, WsPriceMessage) - Add shared functions: isInfoMessage, parseProviderTime, handleInfoMessage - Add wsOpenHandler for WebSocket authentication - Add createSubscriptionBuilders for subscribe/unsubscribe messages - Refactor crypto.ts and lwba.ts to use shared utilities - Reduces code duplication across WebSocket transports * Changeset
1 parent 407d1c3 commit f80dea0

4 files changed

Lines changed: 187 additions & 153 deletions

File tree

.changeset/old-mangos-lie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@chainlink/ncfx-adapter': patch
3+
---
4+
5+
Refactor shared WebSocket utilities to utils
Lines changed: 26 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,63 @@
11
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
2-
import { BaseEndpointTypes } from '../endpoint/crypto'
32
import { makeLogger } from '@chainlink/external-adapter-framework/util'
3+
import { BaseEndpointTypes } from '../endpoint/crypto'
4+
import {
5+
WsMessage,
6+
WsPriceMessage,
7+
createSubscriptionBuilders,
8+
handleInfoMessage,
9+
parseProviderTime,
10+
wsOpenHandler,
11+
} from './utils'
412

513
const logger = makeLogger('NcfxCryptoEndpoint')
614

7-
type WsMessage = WsInfoMessage | WsPriceMessage
8-
9-
type WsInfoMessage = {
10-
Type: string
11-
Message: string
12-
}
13-
14-
type WsPriceMessage = {
15-
timestamp: string // e.g. 2023-01-31T20:10:41
16-
currencyPair: string // e.g. ETH/USD
17-
bid?: number // e.g. 1595.4999
18-
offer?: number // e.g. 1595.5694
19-
mid?: number // e.g. 1595.5346
20-
}
15+
// Crypto uses '/' separator (e.g., "ETH/USD") and 'offer' field for ask price
16+
const PAIR_SEPARATOR = '/'
2117

2218
type WsTransportTypes = BaseEndpointTypes & {
2319
Provider: {
2420
WsMessage: WsMessage
2521
}
2622
}
23+
2724
export const transport = new WebSocketTransport<WsTransportTypes>({
2825
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
2926
handlers: {
30-
open(connection, context) {
31-
return new Promise<void>((resolve, reject) => {
32-
// Set up listener
33-
connection.addEventListener('message', (event: MessageEvent) => {
34-
const parsed = JSON.parse(event.data.toString())
35-
if (parsed.Message === 'Successfully Authenticated') {
36-
logger.debug('Got logged in response, connection is ready')
37-
resolve()
38-
} else {
39-
reject(
40-
new Error(`Unexpected message after WS connection open: ${event.data.toString()}`),
41-
)
42-
}
43-
})
44-
// Send login payload
45-
logger.debug('Logging in WS connection')
46-
connection.send(
47-
JSON.stringify({
48-
request: 'login',
49-
username: context.adapterSettings.API_USERNAME,
50-
password: context.adapterSettings.API_PASSWORD,
51-
}),
52-
)
53-
}).catch((error) => {
54-
if (
55-
error.message ===
56-
'Unexpected message after WS connection open: {"Type":"Error","Message":"Login failed, Invalid login"}'
57-
) {
58-
logger.error(`Login failed, Invalid login`)
59-
logger.error(`Possible Solutions:
60-
1. Doublecheck your supplied credentials.
61-
2. Contact Data Provider to ensure your subscription is active
62-
3. If credentials are supplied under the node licensing agreement with Chainlink Labs, please make contact with us and we will look into it.`)
63-
}
64-
throw error
65-
})
66-
},
27+
open: wsOpenHandler,
6728

6829
message(message: WsMessage) {
69-
if (isInfoMessage(message)) {
70-
logger.debug(`Received message ${message.Type}: ${message.Message}`)
71-
if (
72-
message.Message ===
73-
"Request contains pairs you don't have access to, please check the request"
74-
) {
75-
logger.error(`Request contains pairs you don't have access to`)
76-
logger.error(`Possible Solutions:
77-
1. Confirm you are using the same symbol found in the job spec with the correct case.
78-
2. There maybe an issue with the job spec or the Data Provider may have delisted the asset. Reach out to Chainlink Labs.`)
79-
}
30+
if (handleInfoMessage(message)) {
8031
return
8132
}
8233

83-
if (!message.currencyPair || !message.mid || !message.bid || !message.offer) {
34+
const priceMessage = message as WsPriceMessage
35+
if (
36+
!priceMessage.currencyPair ||
37+
!priceMessage.mid ||
38+
!priceMessage.bid ||
39+
!priceMessage.offer
40+
) {
8441
logger.debug('WS message does not contain valid data, skipping')
8542
return
8643
}
8744

88-
// Expected timestamp in datetime format from NCFX API is missing timezone
89-
// Documented as UTC eg: "2023-06-06 16:03:47.750"
90-
const providerTime = message.timestamp.includes('Z')
91-
? message.timestamp
92-
: `${message.timestamp}Z`
93-
const [base, quote] = message.currencyPair.split('/')
45+
const [base, quote] = priceMessage.currencyPair.split(PAIR_SEPARATOR)
9446
return [
9547
{
9648
params: { base, quote },
9749
response: {
98-
result: message.mid || 0, // Already validated in the filter above
50+
result: priceMessage.mid || 0,
9951
data: {
100-
result: message.mid || 0, // Already validated in the filter above
52+
result: priceMessage.mid || 0,
10153
},
10254
timestamps: {
103-
providerIndicatedTimeUnixMs: new Date(providerTime).getTime(),
55+
providerIndicatedTimeUnixMs: parseProviderTime(priceMessage.timestamp),
10456
},
10557
},
10658
},
10759
]
10860
},
10961
},
110-
builders: {
111-
subscribeMessage: (params) => ({
112-
request: 'subscribe',
113-
ccy: `${params.base}/${params.quote}`,
114-
}),
115-
unsubscribeMessage: (params) => ({
116-
request: 'unsubscribe',
117-
ccy: `${params.base}/${params.quote}`,
118-
}),
119-
},
62+
builders: createSubscriptionBuilders(PAIR_SEPARATOR),
12063
})
121-
122-
const isInfoMessage = (message: WsMessage): message is WsInfoMessage => {
123-
return (message as WsInfoMessage).Type !== undefined
124-
}
Lines changed: 27 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,66 @@
11
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
22
import { makeLogger } from '@chainlink/external-adapter-framework/util'
33
import { BaseEndpointTypesLwba } from '../endpoint/crypto-lwba'
4+
import {
5+
WsMessage,
6+
WsPriceMessage,
7+
createSubscriptionBuilders,
8+
handleInfoMessage,
9+
parseProviderTime,
10+
wsOpenHandler,
11+
} from './utils'
412

513
const logger = makeLogger('NcfxLwbaEndpoint')
614

7-
type WsMessage = WsInfoMessage | WsPriceMessage
8-
9-
type WsInfoMessage = {
10-
Type: string
11-
Message: string
12-
}
13-
14-
type WsPriceMessage = {
15-
timestamp: string // e.g. 2023-01-31T20:10:41
16-
currencyPair: string // e.g. ETH/USD
17-
bid?: number // e.g. 1595.4999
18-
offer?: number // e.g. 1595.5694
19-
mid?: number // e.g. 1595.5346
20-
}
15+
// Crypto LWBA uses '/' separator (e.g., "ETH/USD") and 'offer' field for ask price
16+
const PAIR_SEPARATOR = '/'
2117

2218
type WsTransportTypes = BaseEndpointTypesLwba & {
2319
Provider: {
2420
WsMessage: WsMessage
2521
}
2622
}
23+
2724
export const transport = new WebSocketTransport<WsTransportTypes>({
2825
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
2926
handlers: {
30-
open(connection, context) {
31-
return new Promise((resolve, reject) => {
32-
// Set up listener
33-
connection.addEventListener('message', (event: MessageEvent) => {
34-
const parsed = JSON.parse(event.data.toString())
35-
if (parsed.Message === 'Successfully Authenticated') {
36-
logger.debug('Got logged in response, connection is ready')
37-
resolve()
38-
} else {
39-
reject(
40-
new Error(`Unexpected message after WS connection open: ${event.data.toString()}`),
41-
)
42-
}
43-
})
44-
// Send login payload
45-
logger.debug('Logging in WS connection')
46-
connection.send(
47-
JSON.stringify({
48-
request: 'login',
49-
username: context.adapterSettings.API_USERNAME,
50-
password: context.adapterSettings.API_PASSWORD,
51-
}),
52-
)
53-
})
54-
},
27+
open: wsOpenHandler,
5528

5629
message(message: WsMessage) {
57-
if (isInfoMessage(message)) {
58-
logger.debug(`Received message ${message.Type}: ${message.Message}`)
30+
if (handleInfoMessage(message)) {
5931
return
6032
}
6133

62-
if (!message.currencyPair || !message.mid || !message.bid || !message.offer) {
34+
const priceMessage = message as WsPriceMessage
35+
// Crypto feed uses 'offer' field instead of 'ask'
36+
if (
37+
!priceMessage.currencyPair ||
38+
!priceMessage.mid ||
39+
!priceMessage.bid ||
40+
!priceMessage.offer
41+
) {
6342
logger.debug('WS message does not contain valid data, skipping')
6443
return
6544
}
6645

67-
// Expected timestamp in datetime format from NCFX API is missing timezone
68-
// Documented as UTC eg: "2023-06-06 16:03:47.750"
69-
const providerTime = message.timestamp.includes('Z')
70-
? message.timestamp
71-
: `${message.timestamp}Z`
72-
const [base, quote] = message.currencyPair.split('/')
46+
const [base, quote] = priceMessage.currencyPair.split(PAIR_SEPARATOR)
7347
return [
7448
{
7549
params: { base, quote },
7650
response: {
7751
result: null,
7852
data: {
79-
bid: message.bid || 0,
80-
mid: message.mid || 0,
81-
ask: message.offer || 0,
53+
bid: priceMessage.bid || 0,
54+
mid: priceMessage.mid || 0,
55+
ask: priceMessage.offer || 0, // Map 'offer' to 'ask' in response
8256
},
8357
timestamps: {
84-
providerIndicatedTimeUnixMs: new Date(providerTime).getTime(),
58+
providerIndicatedTimeUnixMs: parseProviderTime(priceMessage.timestamp),
8559
},
8660
},
8761
},
8862
]
8963
},
9064
},
91-
builders: {
92-
subscribeMessage: (params) => ({
93-
request: 'subscribe',
94-
ccy: `${params.base}/${params.quote}`,
95-
}),
96-
unsubscribeMessage: (params) => ({
97-
request: 'unsubscribe',
98-
ccy: `${params.base}/${params.quote}`,
99-
}),
100-
},
65+
builders: createSubscriptionBuilders(PAIR_SEPARATOR),
10166
})
102-
103-
const isInfoMessage = (message: WsMessage): message is WsInfoMessage => {
104-
return (message as WsInfoMessage).Type !== undefined
105-
}

0 commit comments

Comments
 (0)