@shapeshiftoss/unchained-client 9.1.0 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcoin/parser/index.d.ts +1 -0
- package/dist/bitcoin/parser/index.js +2 -1
- package/dist/dogecoin/index.d.ts +3 -0
- package/dist/dogecoin/index.js +19 -0
- package/dist/evm/parser/index.js +2 -2
- package/dist/generated/avalanche/api.d.ts +1 -1
- package/dist/generated/avalanche/api.js +1 -1
- package/dist/generated/avalanche/base.d.ts +1 -1
- package/dist/generated/avalanche/base.js +1 -1
- package/dist/generated/avalanche/common.d.ts +1 -1
- package/dist/generated/avalanche/common.js +1 -1
- package/dist/generated/avalanche/configuration.d.ts +1 -1
- package/dist/generated/avalanche/configuration.js +1 -1
- package/dist/generated/avalanche/index.d.ts +1 -1
- package/dist/generated/avalanche/index.js +1 -1
- package/dist/generated/bitcoin/api.d.ts +1 -1
- package/dist/generated/bitcoin/api.js +1 -1
- package/dist/generated/bitcoin/base.d.ts +1 -1
- package/dist/generated/bitcoin/base.js +1 -1
- package/dist/generated/bitcoin/common.d.ts +1 -1
- package/dist/generated/bitcoin/common.js +1 -1
- package/dist/generated/bitcoin/configuration.d.ts +1 -1
- package/dist/generated/bitcoin/configuration.js +1 -1
- package/dist/generated/bitcoin/index.d.ts +1 -1
- package/dist/generated/bitcoin/index.js +1 -1
- package/dist/generated/dogecoin/api.d.ts +1041 -0
- package/dist/generated/dogecoin/api.js +521 -0
- package/dist/generated/dogecoin/base.d.ts +55 -0
- package/dist/generated/dogecoin/base.js +64 -0
- package/dist/generated/dogecoin/common.d.ts +65 -0
- package/dist/generated/dogecoin/common.js +137 -0
- package/dist/generated/dogecoin/configuration.d.ts +83 -0
- package/dist/generated/dogecoin/configuration.js +42 -0
- package/dist/generated/dogecoin/index.d.ts +13 -0
- package/dist/generated/dogecoin/index.js +31 -0
- package/dist/generated/ethereum/api.d.ts +1 -1
- package/dist/generated/ethereum/api.js +1 -1
- package/dist/generated/ethereum/base.d.ts +1 -1
- package/dist/generated/ethereum/base.js +1 -1
- package/dist/generated/ethereum/common.d.ts +1 -1
- package/dist/generated/ethereum/common.js +1 -1
- package/dist/generated/ethereum/configuration.d.ts +1 -1
- package/dist/generated/ethereum/configuration.js +1 -1
- package/dist/generated/ethereum/index.d.ts +1 -1
- package/dist/generated/ethereum/index.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/websocket.d.ts +11 -3
- package/dist/websocket.js +124 -97
- package/package.json +1 -1
package/dist/websocket.js
CHANGED
|
@@ -10,25 +10,119 @@ const logger = new logger_1.Logger({
|
|
|
10
10
|
namespace: ['unchained-client', 'websocket'],
|
|
11
11
|
level: process.env.LOG_LEVEL
|
|
12
12
|
});
|
|
13
|
+
const NORMAL_CLOSURE_CODE = 1000;
|
|
13
14
|
class Client {
|
|
14
15
|
constructor(url) {
|
|
15
16
|
this.pingInterval = 10000;
|
|
17
|
+
this.retryAttempts = 5;
|
|
18
|
+
this.retryCount = 0;
|
|
16
19
|
this.txs = {};
|
|
17
20
|
this.url = url;
|
|
18
21
|
this.connections = {
|
|
19
22
|
txs: undefined
|
|
20
23
|
};
|
|
21
24
|
}
|
|
25
|
+
async initialize() {
|
|
26
|
+
const ws = new isomorphic_ws_1.default(this.url);
|
|
27
|
+
this.connections.txs = { ws };
|
|
28
|
+
// wait for connection to open with handshake timeout
|
|
29
|
+
await new Promise((resolve, reject) => {
|
|
30
|
+
setTimeout(() => reject(new Error('timeout while trying to connect')), 5000);
|
|
31
|
+
ws.onopen = ({ target }) => this.onOpen(target, resolve, reject);
|
|
32
|
+
ws.onclose = (event) => this.onClose(event, resolve, reject);
|
|
33
|
+
ws.onerror = (event) => this.onError(event);
|
|
34
|
+
ws.onmessage = (event) => this.onMessage(event);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
onOpen(ws, resolve, reject) {
|
|
38
|
+
this.retryCount = 0;
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
// browsers do not support ping/pong frame, send ping payload instead
|
|
41
|
+
const payload = { subscriptionId: '', method: 'ping' };
|
|
42
|
+
ws.send(JSON.stringify(payload));
|
|
43
|
+
}, this.pingInterval);
|
|
44
|
+
this.heartbeat('txs');
|
|
45
|
+
if (this.connections.txs) {
|
|
46
|
+
this.connections.txs.interval = interval;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.connections.txs = { ws, interval };
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// subscribe to all subscriptions
|
|
53
|
+
Object.entries(this.txs).forEach(([id, { data }]) => {
|
|
54
|
+
const payload = { subscriptionId: id, method: 'subscribe', data };
|
|
55
|
+
ws.send(JSON.stringify(payload));
|
|
56
|
+
});
|
|
57
|
+
resolve(true);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
reject(err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
onClose(event, resolve, reject) {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
logger.warn({ fn: 'onClose', code: event.code, reason: event.reason, type: event.type }, 'websocket closed');
|
|
66
|
+
((_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.pingTimeout) && clearTimeout(this.connections.txs.pingTimeout);
|
|
67
|
+
((_b = this.connections.txs) === null || _b === void 0 ? void 0 : _b.interval) && clearInterval(this.connections.txs.interval);
|
|
68
|
+
// attempt reconnect logic on non standard exit codes
|
|
69
|
+
if (event.code !== NORMAL_CLOSURE_CODE) {
|
|
70
|
+
if (++this.retryCount >= this.retryAttempts) {
|
|
71
|
+
reject(new Error('failed to reconnect, connection closed'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
setTimeout(() => this.initialize(), 500 * this.retryCount ** 2);
|
|
75
|
+
}
|
|
76
|
+
resolve(false);
|
|
77
|
+
}
|
|
78
|
+
onError(event) {
|
|
79
|
+
if (!event.message)
|
|
80
|
+
return;
|
|
81
|
+
logger.error({ fn: 'onError', err: event.error, message: event.message, type: event.type }, 'websocket error');
|
|
82
|
+
// send connection errors to all subscription onError handlers
|
|
83
|
+
Object.entries(this.txs).forEach(([id, sub]) => {
|
|
84
|
+
var _a;
|
|
85
|
+
(_a = sub.onError) === null || _a === void 0 ? void 0 : _a.call(sub, { subscriptionId: id, type: 'error', message: event.message });
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
onMessage(event) {
|
|
89
|
+
var _a, _b, _c, _d;
|
|
90
|
+
if (event.type !== 'message')
|
|
91
|
+
return;
|
|
92
|
+
// trigger heartbeat keep alive on pong response
|
|
93
|
+
if (event.data === 'pong') {
|
|
94
|
+
this.heartbeat('txs');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const message = JSON.parse(event.data.toString());
|
|
99
|
+
const subscriptionId = this.getDefaultSubscriptionId();
|
|
100
|
+
// narrow type to ErrorResponse if key `type` exists and forward to correct onError handler
|
|
101
|
+
if ('type' in message) {
|
|
102
|
+
return (_b = (_a = this.txs[message.subscriptionId || subscriptionId]) === null || _a === void 0 ? void 0 : _a.onError) === null || _b === void 0 ? void 0 : _b.call(_a, message);
|
|
103
|
+
}
|
|
104
|
+
// forward the transaction message to the correct onMessage handler
|
|
105
|
+
(_d = (_c = this.txs[message.subscriptionId || subscriptionId]) === null || _c === void 0 ? void 0 : _c.onMessage) === null || _d === void 0 ? void 0 : _d.call(_c, message);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
logger.warn(`failed to handle onmessage event: ${JSON.stringify(event)}: ${err}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
22
111
|
heartbeat(topic) {
|
|
23
112
|
const connection = this.connections[topic];
|
|
24
113
|
if (!connection)
|
|
25
114
|
return;
|
|
26
115
|
connection.pingTimeout && clearTimeout(connection.pingTimeout);
|
|
27
116
|
connection.pingTimeout = setTimeout(() => {
|
|
117
|
+
var _a;
|
|
28
118
|
logger.warn({ fn: 'pingTimeout' }, `${topic} heartbeat failed`);
|
|
29
|
-
connection === null ||
|
|
119
|
+
(_a = connection.ws) === null || _a === void 0 ? void 0 : _a.close();
|
|
30
120
|
}, this.pingInterval + 5000);
|
|
31
121
|
}
|
|
122
|
+
// for websocket connections that do not return the subscriptionId on the message, use the initial subscriptionId as the default
|
|
123
|
+
getDefaultSubscriptionId() {
|
|
124
|
+
return Object.keys(this.txs)[0];
|
|
125
|
+
}
|
|
32
126
|
/**
|
|
33
127
|
* Subscribe to transaction history and updates for newly confirmed and pending transactions.
|
|
34
128
|
*
|
|
@@ -42,95 +136,20 @@ class Client {
|
|
|
42
136
|
* @param [onError] optional handler for any error messages associated with `subscriptionId`
|
|
43
137
|
*/
|
|
44
138
|
async subscribeTxs(subscriptionId, data, onMessage, onError) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
return;
|
|
139
|
+
var _a, _b, _c, _d;
|
|
140
|
+
const addresses = [
|
|
141
|
+
...new Set([...((_c = (_b = (_a = this.txs[subscriptionId]) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.addresses) !== null && _c !== void 0 ? _c : []), ...data.addresses])
|
|
142
|
+
];
|
|
143
|
+
// keep track of all subscribed addresses and onMessage/onError handlers associated with each subscriptionId
|
|
144
|
+
this.txs[subscriptionId] = { onMessage, onError, data: { topic: 'txs', addresses } };
|
|
145
|
+
// initialize websocket connection if one does not already exist
|
|
146
|
+
if (!((_d = this.connections.txs) === null || _d === void 0 ? void 0 : _d.ws)) {
|
|
147
|
+
return this.initialize();
|
|
148
|
+
}
|
|
149
|
+
// subscribe if connection exists and is ready
|
|
150
|
+
if (this.connections.txs.ws.readyState === isomorphic_ws_1.default.OPEN) {
|
|
151
|
+
this.connections.txs.ws.send(JSON.stringify({ subscriptionId, method: 'subscribe', data }));
|
|
60
152
|
}
|
|
61
|
-
const ws = new isomorphic_ws_1.default(this.url);
|
|
62
|
-
const interval = setInterval(() => {
|
|
63
|
-
// browsers do not support ping/pong frame, send ping payload instead
|
|
64
|
-
const payload = { subscriptionId: '', method: 'ping' };
|
|
65
|
-
ws.send(JSON.stringify(payload));
|
|
66
|
-
}, this.pingInterval);
|
|
67
|
-
this.connections.txs = { ws, interval };
|
|
68
|
-
// send connection errors to all subscription onError handlers
|
|
69
|
-
ws.onerror = (event) => {
|
|
70
|
-
Object.entries(this.txs).forEach(([id, sub]) => {
|
|
71
|
-
sub.onError && sub.onError({ subscriptionId: id, type: 'error', message: event.message });
|
|
72
|
-
});
|
|
73
|
-
};
|
|
74
|
-
// clear heartbeat timeout on close
|
|
75
|
-
ws.onclose = () => {
|
|
76
|
-
var _a, _b;
|
|
77
|
-
((_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.pingTimeout) && clearTimeout(this.connections.txs.pingTimeout);
|
|
78
|
-
((_b = this.connections.txs) === null || _b === void 0 ? void 0 : _b.interval) && clearInterval(this.connections.txs.interval);
|
|
79
|
-
};
|
|
80
|
-
ws.onmessage = (event) => {
|
|
81
|
-
var _a, _b;
|
|
82
|
-
if (!event)
|
|
83
|
-
return;
|
|
84
|
-
if (event.type !== 'message')
|
|
85
|
-
return;
|
|
86
|
-
if (event.data === 'pong') {
|
|
87
|
-
// trigger heartbeat keep alive on pong response
|
|
88
|
-
this.heartbeat('txs');
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
const message = JSON.parse(event.data.toString());
|
|
93
|
-
// narrow type to ErrorResponse if key `type` exists and forward to correct onError handler
|
|
94
|
-
if ('type' in message) {
|
|
95
|
-
const onErrorHandler = (_a = this.txs[message.subscriptionId || subscriptionId]) === null || _a === void 0 ? void 0 : _a.onError;
|
|
96
|
-
onErrorHandler && onErrorHandler(message);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// forward the transaction message to the correct onMessage handler
|
|
100
|
-
const onMessageHandler = (_b = this.txs[message.subscriptionId || subscriptionId]) === null || _b === void 0 ? void 0 : _b.onMessage;
|
|
101
|
-
onMessageHandler && onMessageHandler(message);
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
logger.warn(`failed to handle onmessage event: ${JSON.stringify(event)}: ${err}`);
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
// wait for the connection to open
|
|
108
|
-
const openConnection = new Promise((resolve, reject) => {
|
|
109
|
-
ws.onopen = () => {
|
|
110
|
-
this.heartbeat('txs');
|
|
111
|
-
try {
|
|
112
|
-
// subscribe to all queued subscriptions
|
|
113
|
-
Object.values(this.txs).forEach((tx) => {
|
|
114
|
-
if (!tx.data)
|
|
115
|
-
return;
|
|
116
|
-
const payload = { subscriptionId, method: 'subscribe', data: tx.data };
|
|
117
|
-
ws.send(JSON.stringify(payload));
|
|
118
|
-
delete this.txs[subscriptionId].data;
|
|
119
|
-
});
|
|
120
|
-
// subscribe to initial subscription
|
|
121
|
-
const payload = { subscriptionId, method: 'subscribe', data };
|
|
122
|
-
ws.send(JSON.stringify(payload));
|
|
123
|
-
resolve(true);
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
reject(err);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
});
|
|
130
|
-
const connectionTimeout = new Promise((_, reject) => {
|
|
131
|
-
setTimeout(() => reject(new Error('timeout while trying to connect')), 5000);
|
|
132
|
-
});
|
|
133
|
-
await Promise.race([openConnection, connectionTimeout]);
|
|
134
153
|
}
|
|
135
154
|
/**
|
|
136
155
|
* Unsubscribe from transaction history and updates for newly confirmed and pending transactions.
|
|
@@ -146,19 +165,27 @@ class Client {
|
|
|
146
165
|
* @param data.addresses list of addresses to unsubscribe from
|
|
147
166
|
*/
|
|
148
167
|
unsubscribeTxs(subscriptionId, data) {
|
|
149
|
-
var _a, _b, _c;
|
|
150
|
-
if (((_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.ws.readyState) !== isomorphic_ws_1.default.OPEN)
|
|
168
|
+
var _a, _b, _c, _d, _e;
|
|
169
|
+
if (((_b = (_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.ws) === null || _b === void 0 ? void 0 : _b.readyState) !== isomorphic_ws_1.default.OPEN)
|
|
151
170
|
return;
|
|
152
171
|
if (!subscriptionId)
|
|
153
172
|
this.txs = {};
|
|
154
|
-
if (subscriptionId &&
|
|
155
|
-
|
|
173
|
+
if (subscriptionId && this.txs[subscriptionId]) {
|
|
174
|
+
// unsubscribe addresses from the current subscribed address set if addresses provided
|
|
175
|
+
if ((_c = data === null || data === void 0 ? void 0 : data.addresses) === null || _c === void 0 ? void 0 : _c.length) {
|
|
176
|
+
this.txs[subscriptionId].data.addresses = this.txs[subscriptionId].data.addresses.filter((address) => !data.addresses.includes(address));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// delete subscription if no addresses provided
|
|
180
|
+
delete this.txs[subscriptionId];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
156
183
|
const payload = {
|
|
157
184
|
subscriptionId: subscriptionId !== null && subscriptionId !== void 0 ? subscriptionId : '',
|
|
158
185
|
method: 'unsubscribe',
|
|
159
|
-
data: { topic: 'txs', addresses: (
|
|
186
|
+
data: { topic: 'txs', addresses: (_d = data === null || data === void 0 ? void 0 : data.addresses) !== null && _d !== void 0 ? _d : [] }
|
|
160
187
|
};
|
|
161
|
-
(
|
|
188
|
+
(_e = this.connections.txs) === null || _e === void 0 ? void 0 : _e.ws.send(JSON.stringify(payload));
|
|
162
189
|
}
|
|
163
190
|
/**
|
|
164
191
|
* Close and unsubscribe from any subscriptions
|
|
@@ -169,9 +196,9 @@ class Client {
|
|
|
169
196
|
*/
|
|
170
197
|
close(topic) {
|
|
171
198
|
const closeTxs = () => {
|
|
172
|
-
var _a;
|
|
199
|
+
var _a, _b;
|
|
173
200
|
this.unsubscribeTxs();
|
|
174
|
-
(_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.ws.close();
|
|
201
|
+
(_b = (_a = this.connections.txs) === null || _a === void 0 ? void 0 : _a.ws) === null || _b === void 0 ? void 0 : _b.close(NORMAL_CLOSURE_CODE);
|
|
175
202
|
};
|
|
176
203
|
// close all connections if no topic is provided
|
|
177
204
|
if (!topic) {
|