Skip to content

Commit a1b0280

Browse files
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
1 parent cb489cb commit a1b0280

File tree

3 files changed

+184
-162
lines changed

3 files changed

+184
-162
lines changed
Lines changed: 26 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,59 @@
11
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
22
import { BaseEndpointTypes } from '../endpoint/crypto'
3-
import { makeLogger } from '@chainlink/external-adapter-framework/util'
4-
5-
const logger = makeLogger('NcfxCryptoEndpoint')
6-
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-
}
3+
import {
4+
WsMessage,
5+
WsPriceMessage,
6+
createSubscriptionBuilders,
7+
handleInfoMessage,
8+
parseProviderTime,
9+
wsOpenHandler,
10+
} from './utils'
11+
12+
// Crypto uses '/' separator (e.g., "ETH/USD") and 'offer' field for ask price
13+
const PAIR_SEPARATOR = '/'
2114

2215
type WsTransportTypes = BaseEndpointTypes & {
2316
Provider: {
2417
WsMessage: WsMessage
2518
}
2619
}
20+
2721
export const transport = new WebSocketTransport<WsTransportTypes>({
2822
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
2923
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-
},
24+
open: wsOpenHandler,
6725

6826
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-
}
27+
if (handleInfoMessage(message)) {
8028
return
8129
}
8230

83-
if (!message.currencyPair || !message.mid || !message.bid || !message.offer) {
84-
logger.debug('WS message does not contain valid data, skipping')
31+
const priceMessage = message as WsPriceMessage
32+
if (
33+
!priceMessage.currencyPair ||
34+
!priceMessage.mid ||
35+
!priceMessage.bid ||
36+
!priceMessage.offer
37+
) {
8538
return
8639
}
8740

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('/')
41+
const [base, quote] = priceMessage.currencyPair.split(PAIR_SEPARATOR)
9442
return [
9543
{
9644
params: { base, quote },
9745
response: {
98-
result: message.mid || 0, // Already validated in the filter above
46+
result: priceMessage.mid,
9947
data: {
100-
result: message.mid || 0, // Already validated in the filter above
48+
result: priceMessage.mid,
10149
},
10250
timestamps: {
103-
providerIndicatedTimeUnixMs: new Date(providerTime).getTime(),
51+
providerIndicatedTimeUnixMs: parseProviderTime(priceMessage.timestamp),
10452
},
10553
},
10654
},
10755
]
10856
},
10957
},
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-
},
58+
builders: createSubscriptionBuilders(PAIR_SEPARATOR),
12059
})
121-
122-
const isInfoMessage = (message: WsMessage): message is WsInfoMessage => {
123-
return (message as WsInfoMessage).Type !== undefined
124-
}
Lines changed: 28 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,62 @@
11
import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports'
2-
import { makeLogger } from '@chainlink/external-adapter-framework/util'
32
import { BaseEndpointTypesLwba } from '../endpoint/crypto-lwba'
4-
5-
const logger = makeLogger('NcfxLwbaEndpoint')
6-
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-
}
3+
import {
4+
WsMessage,
5+
WsPriceMessage,
6+
createSubscriptionBuilders,
7+
handleInfoMessage,
8+
parseProviderTime,
9+
wsOpenHandler,
10+
} from './utils'
11+
12+
// Crypto LWBA uses '/' separator (e.g., "ETH/USD") and 'offer' field for ask price
13+
const PAIR_SEPARATOR = '/'
2114

2215
type WsTransportTypes = BaseEndpointTypesLwba & {
2316
Provider: {
2417
WsMessage: WsMessage
2518
}
2619
}
20+
2721
export const transport = new WebSocketTransport<WsTransportTypes>({
2822
url: (context) => context.adapterSettings.WS_API_ENDPOINT,
2923
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-
},
24+
open: wsOpenHandler,
5525

5626
message(message: WsMessage) {
57-
if (isInfoMessage(message)) {
58-
logger.debug(`Received message ${message.Type}: ${message.Message}`)
27+
if (handleInfoMessage(message)) {
5928
return
6029
}
6130

62-
if (!message.currencyPair || !message.mid || !message.bid || !message.offer) {
63-
logger.debug('WS message does not contain valid data, skipping')
31+
const priceMessage = message as WsPriceMessage
32+
// Crypto feed uses 'offer' field instead of 'ask'
33+
if (
34+
!priceMessage.currencyPair ||
35+
!priceMessage.mid ||
36+
!priceMessage.bid ||
37+
!priceMessage.offer
38+
) {
6439
return
6540
}
6641

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('/')
42+
const [base, quote] = priceMessage.currencyPair.split(PAIR_SEPARATOR)
7343
return [
7444
{
7545
params: { base, quote },
7646
response: {
7747
result: null,
7848
data: {
79-
bid: message.bid || 0,
80-
mid: message.mid || 0,
81-
ask: message.offer || 0,
49+
bid: priceMessage.bid,
50+
mid: priceMessage.mid,
51+
ask: priceMessage.offer, // Map 'offer' to 'ask' in response
8252
},
8353
timestamps: {
84-
providerIndicatedTimeUnixMs: new Date(providerTime).getTime(),
54+
providerIndicatedTimeUnixMs: parseProviderTime(priceMessage.timestamp),
8555
},
8656
},
8757
},
8858
]
8959
},
9060
},
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-
},
61+
builders: createSubscriptionBuilders(PAIR_SEPARATOR),
10162
})
102-
103-
const isInfoMessage = (message: WsMessage): message is WsInfoMessage => {
104-
return (message as WsInfoMessage).Type !== undefined
105-
}

0 commit comments

Comments
 (0)