capitalisk-dex 17.0.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.
@@ -0,0 +1,400 @@
1
+ const BigOrderBook = require('big-order-book');
2
+ const crypto = require('crypto');
3
+ const { mapListFields } = require('./utils');
4
+
5
+ const EMPTY_ORDER_BOOK_HASH = '0000000000000000000000000000000000000000';
6
+ const emptyGenerator = function * () {};
7
+
8
+ class TradeEngine {
9
+ constructor(options) {
10
+ this.baseCurrency = options.baseCurrency;
11
+ this.quoteCurrency = options.quoteCurrency;
12
+ this.baseOrderHeightExpiry = options.baseOrderHeightExpiry;
13
+ this.quoteOrderHeightExpiry = options.quoteOrderHeightExpiry;
14
+ this.baseMinPartialTake = options.baseMinPartialTake;
15
+ this.quoteMinPartialTake = options.quoteMinPartialTake;
16
+ this.market = `${this.quoteCurrency}/${this.baseCurrency}`;
17
+ this.orderBook = new BigOrderBook({
18
+ minPartialTakeValue: this.baseMinPartialTake,
19
+ minPartialTakeSize: this.quoteMinPartialTake,
20
+ priceDecimalPrecision: options.priceDecimalPrecision
21
+ });
22
+
23
+ this._askMap = new Map();
24
+ this._bidMap = new Map();
25
+ this._orderMap = new Map();
26
+ this._sourceWalletOrderMap = new Map();
27
+
28
+ this.orderBookHash = EMPTY_ORDER_BOOK_HASH;
29
+
30
+ this._resetProcessedHeightsInfo();
31
+ }
32
+
33
+ _sha1(string) {
34
+ return crypto.createHash('sha1').update(string).digest('hex');
35
+ }
36
+
37
+ _resetProcessedHeightsInfo() {
38
+ this.lastProcessedHeightsInfo = {
39
+ [this.baseCurrency]: {
40
+ height: 0,
41
+ orderIds: new Set()
42
+ },
43
+ [this.quoteCurrency]: {
44
+ height: 0,
45
+ orderIds: new Set()
46
+ }
47
+ };
48
+ }
49
+
50
+ expireBidOrders(heightThreshold) {
51
+ let expiredOrders = [];
52
+ for (let [orderId, order] of this._bidMap) {
53
+ if (order.expiryHeight > heightThreshold) {
54
+ break;
55
+ }
56
+ expiredOrders.push(order);
57
+ this._removeFromOrderBook(orderId);
58
+ this._bidMap.delete(orderId);
59
+ this._orderMap.delete(orderId);
60
+ this._removeFromWalletOrderMap(order.sourceWalletAddress, orderId);
61
+ }
62
+ return expiredOrders;
63
+ }
64
+
65
+ expireAskOrders(heightThreshold) {
66
+ let expiredOrders = [];
67
+ for (let [orderId, order] of this._askMap) {
68
+ if (order.expiryHeight > heightThreshold) {
69
+ break;
70
+ }
71
+ expiredOrders.push(order);
72
+ this._removeFromOrderBook(orderId);
73
+ this._askMap.delete(orderId);
74
+ this._orderMap.delete(orderId);
75
+ this._removeFromWalletOrderMap(order.sourceWalletAddress, orderId);
76
+ }
77
+ return expiredOrders;
78
+ }
79
+
80
+ wasOrderProcessed(orderId, orderSourceChain, orderHeight) {
81
+ let lastChainProcessedHeightInfo = this.lastProcessedHeightsInfo[orderSourceChain];
82
+ let topChainHeight = lastChainProcessedHeightInfo.height;
83
+
84
+ return orderHeight < topChainHeight || lastChainProcessedHeightInfo.orderIds.has(orderId);
85
+ }
86
+
87
+ trackProcessedOrder(order) {
88
+ let lastChainProcessedHeightInfo = this.lastProcessedHeightsInfo[order.sourceChain];
89
+ let topChainHeight = lastChainProcessedHeightInfo.height;
90
+ if (order.height > topChainHeight) {
91
+ lastChainProcessedHeightInfo.height = order.height;
92
+ lastChainProcessedHeightInfo.orderIds = new Set([order.id]);
93
+ } else if (order.height < topChainHeight) {
94
+ let error = new Error(
95
+ `Could not process order with ID ${
96
+ order.id
97
+ } because it was below the last processed height of ${
98
+ topChainHeight
99
+ } for the chain ${order.sourceChain}`
100
+ );
101
+ error.name = 'HeightAlreadyProcessedError';
102
+ throw error;
103
+ } else {
104
+ if (lastChainProcessedHeightInfo.orderIds.has(order.id)) {
105
+ let error = new Error(
106
+ `Could not process order with ID ${
107
+ order.id
108
+ } because it was already processed`
109
+ );
110
+ error.name = 'OrderAlreadyProcessedError';
111
+ throw error;
112
+ }
113
+ lastChainProcessedHeightInfo.orderIds.add(order.id);
114
+ }
115
+ }
116
+
117
+ addOrder(order) {
118
+ this.trackProcessedOrder(order);
119
+
120
+ let existingOrder = this.orderBook.has(order.id);
121
+
122
+ if (existingOrder) {
123
+ let error = new Error(`An order with ID ${order.id} already exists`);
124
+ error.name = 'DuplicateOrderError';
125
+ throw error;
126
+ }
127
+
128
+ let orderHeightExpiry;
129
+ if (order.sourceChain === this.quoteCurrency) {
130
+ orderHeightExpiry = this.quoteOrderHeightExpiry;
131
+ } else {
132
+ orderHeightExpiry = this.baseOrderHeightExpiry;
133
+ }
134
+
135
+ let newOrder = {};
136
+ newOrder.id = order.id;
137
+ newOrder.side = order.side;
138
+ if (order.size != null) {
139
+ newOrder.size = order.size;
140
+ }
141
+ if (order.value != null) {
142
+ newOrder.value = order.value;
143
+ }
144
+ if (order.price != null) {
145
+ newOrder.price = order.price;
146
+ }
147
+ newOrder.type = order.type;
148
+ newOrder.targetChain = order.targetChain;
149
+ newOrder.targetWalletAddress = order.targetWalletAddress;
150
+ newOrder.senderAddress = order.senderAddress;
151
+ newOrder.sourceChain = order.sourceChain;
152
+ newOrder.sourceChainAmount = order.sourceChainAmount;
153
+ newOrder.sourceWalletAddress = order.sourceWalletAddress;
154
+ newOrder.height = order.height;
155
+ newOrder.expiryHeight = order.height + orderHeightExpiry;
156
+ newOrder.timestamp = order.timestamp;
157
+
158
+ let result = this._addToOrderBook(newOrder);
159
+
160
+ result.makers.forEach((makerOrder) => {
161
+ if (makerOrder.side === 'ask') {
162
+ if (makerOrder.sizeRemaining <= 0n) {
163
+ this._askMap.delete(makerOrder.id);
164
+ this._orderMap.delete(makerOrder.id);
165
+ this._removeFromWalletOrderMap(makerOrder.sourceWalletAddress, makerOrder.id);
166
+ }
167
+ } else {
168
+ if (makerOrder.valueRemaining <= 0n) {
169
+ this._bidMap.delete(makerOrder.id);
170
+ this._orderMap.delete(makerOrder.id);
171
+ this._removeFromWalletOrderMap(makerOrder.sourceWalletAddress, makerOrder.id);
172
+ }
173
+ }
174
+ });
175
+
176
+ if (newOrder.type !== 'market') {
177
+ if (newOrder.side === 'ask') {
178
+ if (result.taker.sizeRemaining > 0n) {
179
+ this._askMap.set(newOrder.id, newOrder);
180
+ this._orderMap.set(newOrder.id, newOrder);
181
+ this._addToWalletOrderMap(newOrder);
182
+ }
183
+ } else if (result.taker.valueRemaining > 0n) {
184
+ this._bidMap.set(newOrder.id, newOrder);
185
+ this._orderMap.set(newOrder.id, newOrder);
186
+ this._addToWalletOrderMap(newOrder);
187
+ }
188
+ }
189
+
190
+ return result;
191
+ }
192
+
193
+ _addToWalletOrderMap(order) {
194
+ if (!this._sourceWalletOrderMap.has(order.sourceWalletAddress)) {
195
+ this._sourceWalletOrderMap.set(order.sourceWalletAddress, new Map());
196
+ }
197
+ let orderMap = this._sourceWalletOrderMap.get(order.sourceWalletAddress);
198
+ orderMap.set(order.id, order);
199
+ }
200
+
201
+ _removeFromWalletOrderMap(sourceWalletAddress, orderId) {
202
+ let orderMap = this._sourceWalletOrderMap.get(sourceWalletAddress);
203
+ if (orderMap) {
204
+ let result = orderMap.delete(orderId);
205
+ if (!orderMap.size) {
206
+ this._sourceWalletOrderMap.delete(sourceWalletAddress);
207
+ }
208
+ return result;
209
+ }
210
+ return false;
211
+ }
212
+
213
+ getSourceWalletOrderIterator(sourceWalletAddress) {
214
+ let orderMap = this._sourceWalletOrderMap.get(sourceWalletAddress);
215
+ return orderMap ? orderMap.values() : emptyGenerator();
216
+ }
217
+
218
+ *getSourceWalletBidIterator(sourceWalletAddress) {
219
+ let orderIterator = this.getSourceWalletOrderIterator(sourceWalletAddress);
220
+ for (let order of orderIterator) {
221
+ if (order.side === 'bid') {
222
+ yield order;
223
+ }
224
+ }
225
+ }
226
+
227
+ *getSourceWalletAskIterator(sourceWalletAddress) {
228
+ let orderIterator = this.getSourceWalletOrderIterator(sourceWalletAddress);
229
+ for (let order of orderIterator) {
230
+ if (order.side === 'ask') {
231
+ yield order;
232
+ }
233
+ }
234
+ }
235
+
236
+ getOrder(orderId) {
237
+ return this._orderMap.get(orderId);
238
+ }
239
+
240
+ addCloseOrder(order) {
241
+ this.trackProcessedOrder(order);
242
+
243
+ let targetOrderId = order.orderIdToClose;
244
+ let targetOrder = this.getOrder(targetOrderId);
245
+ if (!targetOrder) {
246
+ throw new Error(
247
+ `An order with ID ${targetOrderId} could not be found`
248
+ );
249
+ }
250
+
251
+ let result = this._removeFromOrderBook(targetOrderId);
252
+ if (targetOrder.side === 'ask') {
253
+ this._askMap.delete(targetOrderId);
254
+ } else {
255
+ this._bidMap.delete(targetOrderId);
256
+ }
257
+ this._orderMap.delete(targetOrderId);
258
+ this._removeFromWalletOrderMap(targetOrder.sourceWalletAddress, targetOrderId);
259
+ return result;
260
+ }
261
+
262
+ peekBids() {
263
+ return this.orderBook.getMaxBid();
264
+ }
265
+
266
+ peekAsks() {
267
+ return this.orderBook.getMinAsk();
268
+ }
269
+
270
+ getBidIteratorFromMin() {
271
+ return this.orderBook.getBidIteratorFromMin();
272
+ }
273
+
274
+ getBidIteratorFromMax() {
275
+ return this.orderBook.getBidIteratorFromMax();
276
+ }
277
+
278
+ getAskIteratorFromMin() {
279
+ return this.orderBook.getAskIteratorFromMin();
280
+ }
281
+
282
+ getAskIteratorFromMax() {
283
+ return this.orderBook.getAskIteratorFromMax();
284
+ }
285
+
286
+ getBidLevelIteratorFromMin() {
287
+ return this.orderBook.getBidLevelIteratorFromMin();
288
+ }
289
+
290
+ getBidLevelIteratorFromMax() {
291
+ return this.orderBook.getBidLevelIteratorFromMax();
292
+ }
293
+
294
+ getAskLevelIteratorFromMin() {
295
+ return this.orderBook.getAskLevelIteratorFromMin();
296
+ }
297
+
298
+ getAskLevelIteratorFromMax() {
299
+ return this.orderBook.getAskLevelIteratorFromMax();
300
+ }
301
+
302
+ getOrderIterator() {
303
+ return this._orderMap.values();
304
+ }
305
+
306
+ getBids() {
307
+ return [...this.getBidIteratorFromMax()];
308
+ }
309
+
310
+ getAsks() {
311
+ return [...this.getAskIteratorFromMin()];
312
+ }
313
+
314
+ getOrders() {
315
+ return [...this.getOrderIterator()];
316
+ }
317
+
318
+ getSnapshot() {
319
+ let askLimitOrders = mapListFields(this.getAsks(), {
320
+ size: String,
321
+ sizeRemaining: String,
322
+ lastSizeTaken: String,
323
+ lastValueTaken: String,
324
+ sourceChainAmount: String
325
+ });
326
+ let bidLimitOrders = mapListFields(this.getBids(), {
327
+ value: String,
328
+ valueRemaining: String,
329
+ lastSizeTaken: String,
330
+ lastValueTaken: String,
331
+ sourceChainAmount: String
332
+ });
333
+ return {
334
+ orderBookHash: this.orderBookHash,
335
+ askLimitOrders,
336
+ bidLimitOrders
337
+ };
338
+ }
339
+
340
+ _addToOrderBook(order) {
341
+ this.orderBookHash = this._sha1(`${this.orderBookHash}+${order.id}`);
342
+ return this.orderBook.add(order);
343
+ }
344
+
345
+ _removeFromOrderBook(orderId) {
346
+ this.orderBookHash = this._sha1(`${this.orderBookHash}-${orderId}`);
347
+ return this.orderBook.remove(orderId);
348
+ }
349
+
350
+ setSnapshot(snapshot) {
351
+ this.clear();
352
+ snapshot.askLimitOrders.sort((a, b) => {
353
+ if (a.expiryHeight > b.expiryHeight) {
354
+ return 1;
355
+ }
356
+ if (a.expiryHeight < b.expiryHeight) {
357
+ return -1;
358
+ }
359
+ return 0;
360
+ });
361
+ snapshot.bidLimitOrders.sort((a, b) => {
362
+ if (a.expiryHeight > b.expiryHeight) {
363
+ return 1;
364
+ }
365
+ if (a.expiryHeight < b.expiryHeight) {
366
+ return -1;
367
+ }
368
+ return 0;
369
+ });
370
+ snapshot.askLimitOrders.forEach((order) => {
371
+ let newOrder = {...order};
372
+ this._addToOrderBook(newOrder);
373
+ this._askMap.set(newOrder.id, newOrder);
374
+ this._orderMap.set(newOrder.id, newOrder);
375
+ this._addToWalletOrderMap(newOrder);
376
+ });
377
+ snapshot.bidLimitOrders.forEach((order) => {
378
+ let newOrder = {...order};
379
+ this._addToOrderBook(newOrder);
380
+ this._bidMap.set(newOrder.id, newOrder);
381
+ this._orderMap.set(newOrder.id, newOrder);
382
+ this._addToWalletOrderMap(newOrder);
383
+ });
384
+ if (snapshot.orderBookHash) {
385
+ this.orderBookHash = snapshot.orderBookHash
386
+ }
387
+ }
388
+
389
+ clear() {
390
+ this.orderBookHash = EMPTY_ORDER_BOOK_HASH;
391
+ this._resetProcessedHeightsInfo();
392
+ this._askMap.clear();
393
+ this._bidMap.clear();
394
+ this._orderMap.clear();
395
+ this._sourceWalletOrderMap.clear();
396
+ this.orderBook.clear();
397
+ }
398
+ }
399
+
400
+ module.exports = TradeEngine;
package/utils.js ADDED
@@ -0,0 +1,16 @@
1
+ function mapListFields(list, fieldMapper) {
2
+ let fieldList = Object.keys(fieldMapper);
3
+ return list.map((item) => {
4
+ let itemClone = {...item};
5
+ for (let field of fieldList) {
6
+ if (field in itemClone) {
7
+ itemClone[field] = fieldMapper[field](itemClone[field]);
8
+ }
9
+ }
10
+ return itemClone;
11
+ });
12
+ }
13
+
14
+ module.exports = {
15
+ mapListFields
16
+ };