|
1 | 1 | import { WebSocketTransport } from '@chainlink/external-adapter-framework/transports' |
2 | 2 | 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 = '/' |
21 | 14 |
|
22 | 15 | type WsTransportTypes = BaseEndpointTypes & { |
23 | 16 | Provider: { |
24 | 17 | WsMessage: WsMessage |
25 | 18 | } |
26 | 19 | } |
| 20 | + |
27 | 21 | export const transport = new WebSocketTransport<WsTransportTypes>({ |
28 | 22 | url: (context) => context.adapterSettings.WS_API_ENDPOINT, |
29 | 23 | 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, |
67 | 25 |
|
68 | 26 | 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)) { |
80 | 28 | return |
81 | 29 | } |
82 | 30 |
|
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 | + ) { |
85 | 38 | return |
86 | 39 | } |
87 | 40 |
|
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) |
94 | 42 | return [ |
95 | 43 | { |
96 | 44 | params: { base, quote }, |
97 | 45 | response: { |
98 | | - result: message.mid || 0, // Already validated in the filter above |
| 46 | + result: priceMessage.mid, |
99 | 47 | data: { |
100 | | - result: message.mid || 0, // Already validated in the filter above |
| 48 | + result: priceMessage.mid, |
101 | 49 | }, |
102 | 50 | timestamps: { |
103 | | - providerIndicatedTimeUnixMs: new Date(providerTime).getTime(), |
| 51 | + providerIndicatedTimeUnixMs: parseProviderTime(priceMessage.timestamp), |
104 | 52 | }, |
105 | 53 | }, |
106 | 54 | }, |
107 | 55 | ] |
108 | 56 | }, |
109 | 57 | }, |
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), |
120 | 59 | }) |
121 | | - |
122 | | -const isInfoMessage = (message: WsMessage): message is WsInfoMessage => { |
123 | | - return (message as WsInfoMessage).Type !== undefined |
124 | | -} |
0 commit comments