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.
package/index.js ADDED
@@ -0,0 +1,2806 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const util = require('util');
6
+ const path = require('path');
7
+ const writeFile = util.promisify(fs.writeFile);
8
+ const readFile = util.promisify(fs.readFile);
9
+ const readdir = util.promisify(fs.readdir);
10
+ const unlink = util.promisify(fs.unlink);
11
+ const mkdir = util.promisify(fs.mkdir);
12
+ const ProperSkipList = require('proper-skip-list');
13
+ const defaultConfig = require('./defaults/config');
14
+ const TradeEngine = require('./trade-engine');
15
+ const BigIntCalculator = require('./big-int-calculator');
16
+ const { mapListFields } = require('./utils');
17
+ const packageJSON = require('./package.json');
18
+
19
+ const DEFAULT_MODULE_ALIAS = 'capitalisk_dex';
20
+ const { CAPITALISK_DEX_PASSWORD } = process.env;
21
+ const CIPHER_ALGORITHM = 'aes-192-cbc';
22
+ const CIPHER_KEY = CAPITALISK_DEX_PASSWORD ? crypto.scryptSync(CAPITALISK_DEX_PASSWORD, 'salt', 24) : undefined;
23
+ const CIPHER_IV = Buffer.alloc(16, 0);
24
+ const DEFAULT_MULTISIG_READY_DELAY = 5000;
25
+ const DEFAULT_PROTOCOL_EXCLUDE_REASON = false;
26
+ const DEFAULT_PROTOCOL_MAX_ARGUMENT_LENGTH = 64;
27
+ const DEFAULT_PRICE_DECIMAL_PRECISION = 4;
28
+ const DEFAULT_OUTBOUND_TRANSACTION_BLOCK_CACHE_SIZE = 62000;
29
+
30
+ /**
31
+ * Capitalisk DEX module specification
32
+ */
33
+ module.exports = class CapitaliskDEXModule {
34
+ constructor({alias, config, updates, appConfig, logger, updater}) {
35
+ this.options = {...defaultConfig, ...config};
36
+ this.appConfig = appConfig;
37
+ this.alias = alias || DEFAULT_MODULE_ALIAS;
38
+ this.updater = updater;
39
+ if (!updates) {
40
+ updates = [];
41
+ }
42
+ for (let update of updates) {
43
+ if (!update.criteria) {
44
+ update.criteria = {};
45
+ }
46
+ if (!update.criteria.baseChainHeight) {
47
+ update.criteria.baseChainHeight = 0;
48
+ }
49
+ }
50
+ updates.sort((a, b) => {
51
+ let critA = a.criteria;
52
+ let critB = b.criteria;
53
+ if (critA.baseChainHeight < critB.baseChainHeight) {
54
+ return -1;
55
+ }
56
+ if (critA.baseChainHeight > critB.baseChainHeight) {
57
+ return 1;
58
+ }
59
+ return 0;
60
+ });
61
+ let updateCount = updates.length;
62
+ for (let i = 1; i < updateCount; i++) {
63
+ let currentUpdate = updates[i];
64
+ let previousUpdate = updates[i - 1];
65
+ if (currentUpdate.criteria.baseChainHeight - previousUpdate.criteria.baseChainHeight <= this.options.orderBookSnapshotFinality) {
66
+ throw new Error(
67
+ `DEX updates ${
68
+ previousUpdate.id
69
+ } and ${
70
+ currentUpdate.id
71
+ } were scheduled too close to each other. There must be at least ${
72
+ this.options.orderBookSnapshotFinality
73
+ } height difference between them`
74
+ );
75
+ }
76
+ }
77
+ this.updates = updates;
78
+ if (this.updater.activeUpdate) {
79
+ this.pendingUpdates = this.updates.filter(update => update.id !== this.updater.activeUpdate.id);
80
+ } else {
81
+ this.pendingUpdates = [...this.updates];
82
+ }
83
+ this.chainSymbols = Object.keys(this.options.chains);
84
+ if (this.chainSymbols.length !== 2) {
85
+ throw new Error('DEX module can only handle on 2 chains');
86
+ }
87
+ this.logger = logger;
88
+ this.multisigWalletInfo = {};
89
+ this.isForked = false;
90
+ this.lastSnapshot = null;
91
+ this.finalizedSnapshot = null;
92
+ this.pendingTransfers = new Map();
93
+ this.chainSymbols.forEach((chainSymbol) => {
94
+ this.multisigWalletInfo[chainSymbol] = {
95
+ members: new Set(),
96
+ memberCount: 0,
97
+ requiredSignatureCount: null
98
+ };
99
+ });
100
+
101
+ this.passiveMode = this.options.passiveMode;
102
+ this.baseChainSymbol = this.options.baseChain;
103
+ this.quoteChainSymbol = this.chainSymbols.find(chain => chain !== this.baseChainSymbol);
104
+ let baseChainOptions = this.options.chains[this.baseChainSymbol];
105
+ let quoteChainOptions = this.options.chains[this.quoteChainSymbol];
106
+ this.baseAddress = baseChainOptions.multisigAddress;
107
+ this.quoteAddress = quoteChainOptions.multisigAddress;
108
+ this.multisigReadyDelay = this.options.multisigReadyDelay || DEFAULT_MULTISIG_READY_DELAY;
109
+
110
+ this.priceDecimalPrecision = this.options.priceDecimalPrecision == null ?
111
+ DEFAULT_PRICE_DECIMAL_PRECISION : this.options.priceDecimalPrecision;
112
+
113
+ this.outboundTransactionBlockCaches = {};
114
+ this.outboundTransactionBlockCaches[this.baseChainSymbol] = new Map();
115
+ this.outboundTransactionBlockCaches[this.quoteChainSymbol] = new Map();
116
+
117
+ if (this.priceDecimalPrecision <= 0) {
118
+ throw new Error('DEX module priceDecimalPrecision config must be greater than 0');
119
+ }
120
+
121
+ this.validPriceRegex = new RegExp(`^([0-9]+[.]?|[0-9]*[.][0-9]{1,${this.priceDecimalPrecision}})$`);
122
+
123
+ this.defaultMaxOrderAmount = BigInt(Number.MAX_SAFE_INTEGER);
124
+
125
+ this.tradeEngine = new TradeEngine({
126
+ baseCurrency: this.baseChainSymbol,
127
+ quoteCurrency: this.quoteChainSymbol,
128
+ baseOrderHeightExpiry: baseChainOptions.orderHeightExpiry,
129
+ quoteOrderHeightExpiry: quoteChainOptions.orderHeightExpiry,
130
+ baseMinPartialTake: BigInt(baseChainOptions.minPartialTake || 0),
131
+ quoteMinPartialTake: BigInt(quoteChainOptions.minPartialTake || 0),
132
+ priceDecimalPrecision: this.priceDecimalPrecision
133
+ });
134
+ this.processedHeights = {
135
+ [this.baseChainSymbol]: 0,
136
+ [this.quoteChainSymbol]: 0
137
+ };
138
+
139
+ this.chainCrypto = {};
140
+ this.recentPricesSkipList = new ProperSkipList();
141
+ this.recentTransfersSkipList = new ProperSkipList();
142
+
143
+ this.timestampTransforms = {};
144
+ this.lastSkippedBlocks = {};
145
+
146
+ this.bigIntPriceCalculator = new BigIntCalculator({
147
+ decimalPrecision: this.priceDecimalPrecision
148
+ });
149
+
150
+ this.bigIntFeeCalculators = {};
151
+ this.chainExchangeFeeBases = {};
152
+
153
+ this.chainSymbols.forEach((chainSymbol) => {
154
+ let chainOptions = this.options.chains[chainSymbol];
155
+
156
+ this.timestampTransforms[chainSymbol] = {
157
+ multiplier: chainOptions.timestampMultiplier || 1,
158
+ offset: chainOptions.timestampOffset || 0
159
+ };
160
+
161
+ this.bigIntFeeCalculators[chainSymbol] = new BigIntCalculator({
162
+ decimalPrecision: this._getDecimalCount(chainOptions.exchangeFeeRate)
163
+ });
164
+
165
+ this.chainExchangeFeeBases[chainSymbol] = BigInt(chainOptions.exchangeFeeBase);
166
+
167
+ if (chainOptions.encryptedPassphrase) {
168
+ if (!CAPITALISK_DEX_PASSWORD) {
169
+ throw new Error(
170
+ `Cannot decrypt the encryptedPassphrase from the ${
171
+ this.alias
172
+ } module config for the ${
173
+ chainSymbol
174
+ } chain without a valid CAPITALISK_DEX_PASSWORD environment variable`
175
+ );
176
+ }
177
+ if (chainOptions.passphrase) {
178
+ throw new Error(
179
+ `The ${
180
+ this.alias
181
+ } module config for the ${
182
+ chainSymbol
183
+ } chain should have either a passphrase or encryptedPassphrase but not both`
184
+ );
185
+ }
186
+ try {
187
+ let decipher = crypto.createDecipheriv(CIPHER_ALGORITHM, CIPHER_KEY, CIPHER_IV);
188
+ let decrypted = decipher.update(chainOptions.encryptedPassphrase, 'hex', 'utf8');
189
+ decrypted += decipher.final('utf8');
190
+ chainOptions.passphrase = decrypted;
191
+ } catch (error) {
192
+ throw new Error(
193
+ `Failed to decrypt encryptedPassphrase in ${
194
+ this.alias
195
+ } config for chain ${
196
+ chainSymbol
197
+ } - Check that the CAPITALISK_DEX_PASSWORD environment variable is correct`
198
+ );
199
+ }
200
+ }
201
+ if (chainOptions.encryptedSharedPassphrase) {
202
+ if (!CAPITALISK_DEX_PASSWORD) {
203
+ throw new Error(
204
+ `Cannot decrypt the encryptedSharedPassphrase from the ${
205
+ this.alias
206
+ } config for the ${
207
+ chainSymbol
208
+ } chain without a valid CAPITALISK_DEX_PASSWORD environment variable`
209
+ );
210
+ }
211
+ if (chainOptions.sharedPassphrase) {
212
+ throw new Error(
213
+ `The ${
214
+ this.alias
215
+ } config for the ${
216
+ chainSymbol
217
+ } chain should have either a sharedPassphrase or encryptedSharedPassphrase but not both`
218
+ );
219
+ }
220
+ try {
221
+ let decipher = crypto.createDecipheriv(CIPHER_ALGORITHM, CIPHER_KEY, CIPHER_IV);
222
+ let decrypted = decipher.update(chainOptions.encryptedSharedPassphrase, 'hex', 'utf8');
223
+ decrypted += decipher.final('utf8');
224
+ chainOptions.sharedPassphrase = decrypted;
225
+ } catch (error) {
226
+ throw new Error(
227
+ `Failed to decrypt encryptedSharedPassphrase in ${
228
+ this.alias
229
+ } config for chain ${
230
+ chainSymbol
231
+ } - Check that the CAPITALISK_DEX_PASSWORD environment variable is correct`
232
+ );
233
+ }
234
+ }
235
+
236
+ if (chainOptions.chainCryptoLibPath == null) {
237
+ throw new Error(
238
+ `The ${
239
+ this.alias
240
+ } config for chain ${
241
+ chainSymbol
242
+ } should specify a chainCryptoLibPath`
243
+ );
244
+ }
245
+
246
+ let ChainCryptoClass = require(path.resolve(chainOptions.chainCryptoLibPath));
247
+
248
+ this.chainCrypto[chainSymbol] = new ChainCryptoClass({
249
+ chainSymbol,
250
+ chainOptions,
251
+ logger: this.logger
252
+ });
253
+ });
254
+
255
+ if (this.options.dividendLibPath) {
256
+ this.computeDividends = require(path.resolve(this.options.dividendLibPath));
257
+ } else {
258
+ this.computeDividends = async ({chainSymbol, contributionData, chainOptions, memberCount}) => {
259
+ return Object.keys(contributionData).map((walletAddress) => {
260
+ let payableContribution = contributionData[walletAddress] * BigInt(Math.floor(chainOptions.dividendRate * 10000)) / 10000n;
261
+ let totalPayableAmount = payableContribution * BigInt(Math.floor(chainOptions.exchangeFeeRate * 10000)) / 10000n;
262
+ return {
263
+ walletAddress,
264
+ amount: totalPayableAmount / BigInt(memberCount)
265
+ };
266
+ });
267
+ };
268
+ }
269
+ this.orderBookSnapshotBackupDirPath = path.resolve(this.options.orderBookSnapshotBackupDirPath);
270
+ this.orderBookUpdateSnapshotDirPath = path.resolve(this.options.orderBookUpdateSnapshotDirPath);
271
+ this.orderBookSnapshotFilePath = path.resolve(this.options.orderBookSnapshotFilePath);
272
+ this.latestBasePriceTimestamp = null;
273
+ this.latestQuotePriceTimestamp = null;
274
+ this.unprocessedBaseTransactions = [];
275
+ this.unprocessedQuoteTransactions = [];
276
+ }
277
+
278
+ get dependencies() {
279
+ let chainConfigList = Object.values(this.options.chains);
280
+ return ['app', 'network'].concat(chainConfigList.map(chainConfig => chainConfig.moduleAlias));
281
+ }
282
+
283
+ static get alias() {
284
+ return DEFAULT_MODULE_ALIAS;
285
+ }
286
+
287
+ static get info() {
288
+ return {
289
+ author: 'Jonathan Gros-Dubois',
290
+ version: packageJSON.version,
291
+ name: DEFAULT_MODULE_ALIAS
292
+ };
293
+ }
294
+
295
+ static get migrations() {
296
+ return [];
297
+ }
298
+
299
+ static get defaults() {
300
+ return defaultConfig;
301
+ }
302
+
303
+ _getDecimalCount(decimalNumber) {
304
+ return (String(decimalNumber).split('.')[1] || '').length;
305
+ }
306
+
307
+ _execQueryAgainstIterator(query, sourceIterator, idExtractorFn, allowFiltering, allowSorting) {
308
+ query = query || {};
309
+ let { before, after, limit, sort, ...filterMap } = query;
310
+ if (sort && !this.options.apiEnableAdvancedSorting && !allowSorting) {
311
+ let error = new Error('Advanced sorting is disabled');
312
+ error.name = 'InvalidQueryError';
313
+ throw error;
314
+ }
315
+ let filterFields = Object.keys(filterMap);
316
+ let useFiltering = before || after || filterFields.length;
317
+ if (useFiltering && !this.options.apiEnableAdvancedFiltering && !allowFiltering) {
318
+ let error = new Error(
319
+ 'Advanced filtering is disabled'
320
+ );
321
+ error.name = 'InvalidQueryError';
322
+ throw error;
323
+ }
324
+ if (filterFields.length > this.options.apiMaxFilterFields) {
325
+ let error = new Error(
326
+ `Too many custom filter fields were specified in the query - The maximum allowed is ${
327
+ this.options.apiMaxFilterFields
328
+ }`
329
+ );
330
+ error.name = 'InvalidQueryError';
331
+ throw error;
332
+ }
333
+ if (limit == null) {
334
+ limit = this.options.apiDefaultPageLimit;
335
+ }
336
+ if (typeof limit !== 'number') {
337
+ let error = new Error(
338
+ 'If specified, the limit parameter of the query must be a number'
339
+ );
340
+ error.name = 'InvalidQueryError';
341
+ throw error;
342
+ }
343
+ if (limit > this.options.apiMaxPageLimit) {
344
+ let error = new Error(
345
+ `The limit parameter of the query cannot be greater than ${
346
+ this.options.apiMaxPageLimit
347
+ }`
348
+ );
349
+ error.name = 'InvalidQueryError';
350
+ throw error;
351
+ }
352
+ let [sortField, sortOrderString] = (sort || '').split(':');
353
+ if (sortOrderString != null && sortOrderString !== 'asc' && sortOrderString !== 'desc') {
354
+ let error = new Error(
355
+ 'If specified, the sort order must be either asc or desc'
356
+ );
357
+ error.name = 'InvalidQueryError';
358
+ throw error;
359
+ }
360
+ let sortOrder = sortOrderString === 'desc' ? -1 : 1;
361
+ let iterator;
362
+ if (sortField) {
363
+ let list = [...sourceIterator];
364
+ list.sort((a, b) => {
365
+ let valueA = a[sortField];
366
+ let valueB = b[sortField];
367
+ if (valueA > valueB) {
368
+ return sortOrder;
369
+ }
370
+ if (valueA < valueB) {
371
+ return -sortOrder;
372
+ }
373
+ return 0;
374
+ });
375
+ iterator = list;
376
+ } else {
377
+ iterator = sourceIterator;
378
+ }
379
+
380
+ let result = [];
381
+ if (after) {
382
+ let beforeString = before == null ? null : String(before);
383
+ let afterString = String(after);
384
+ let isCapturing = false;
385
+ for (let item of iterator) {
386
+ let itemIdString = String(idExtractorFn(item));
387
+ if (before && itemIdString === beforeString) {
388
+ break;
389
+ }
390
+ if (isCapturing) {
391
+ let itemMatchesFilter = filterFields.every(
392
+ field => String(item[field]) === String(filterMap[field])
393
+ );
394
+ if (itemMatchesFilter) {
395
+ result.push(item);
396
+ }
397
+ } else if (itemIdString === afterString) {
398
+ isCapturing = true;
399
+ }
400
+ if (result.length >= limit) {
401
+ break;
402
+ }
403
+ }
404
+ return result;
405
+ }
406
+ if (before) {
407
+ let previousItems = [];
408
+ let beforeString = String(before);
409
+ for (let item of iterator) {
410
+ if (String(idExtractorFn(item)) === beforeString) {
411
+ let length = previousItems.length;
412
+ let firstIndex = length - limit;
413
+ if (firstIndex < 0) {
414
+ firstIndex = 0;
415
+ }
416
+ result = previousItems.slice(firstIndex, length);
417
+ break;
418
+ }
419
+ let itemMatchesFilter = filterFields.every(
420
+ field => String(item[field]) === String(filterMap[field])
421
+ );
422
+ if (itemMatchesFilter) {
423
+ previousItems.push(item);
424
+ }
425
+ }
426
+ return result;
427
+ }
428
+ for (let item of iterator) {
429
+ let itemMatchesFilter = filterFields.every(
430
+ field => String(item[field]) === String(filterMap[field])
431
+ );
432
+ if (itemMatchesFilter) {
433
+ result.push(item);
434
+ }
435
+ if (result.length >= limit) {
436
+ break;
437
+ }
438
+ }
439
+ return result;
440
+ }
441
+
442
+ get events() {
443
+ return [
444
+ 'bootstrap'
445
+ ];
446
+ }
447
+
448
+ get actions() {
449
+ return {
450
+ getStatus: {
451
+ handler: () => {
452
+ return {
453
+ version: CapitaliskDEXModule.info.version,
454
+ orderBookHash: this.tradeEngine.orderBookHash,
455
+ processedHeights: this.processedHeights,
456
+ baseChain: this.options.baseChain,
457
+ priceDecimalPrecision: this.priceDecimalPrecision,
458
+ chains: {
459
+ [this.baseChainSymbol]: this._getChainInfo(this.baseChainSymbol),
460
+ [this.quoteChainSymbol]: this._getChainInfo(this.quoteChainSymbol)
461
+ },
462
+ pendingUpdates: this.pendingUpdates
463
+ };
464
+ }
465
+ },
466
+ getMarket: {
467
+ handler: () => {
468
+ return {
469
+ baseSymbol: this.baseChainSymbol,
470
+ quoteSymbol: this.quoteChainSymbol
471
+ };
472
+ }
473
+ },
474
+ getBids: {
475
+ handler: (action) => {
476
+ let query = {...action.params};
477
+ // Optimization.
478
+ if (query.sort === 'price:desc') {
479
+ delete query.sort;
480
+ }
481
+ let orderIterator;
482
+ if (query.sourceWalletAddress) {
483
+ // Optimization.
484
+ orderIterator = this.tradeEngine.getSourceWalletBidIterator(query.sourceWalletAddress);
485
+ delete query.sourceWalletAddress;
486
+ } else {
487
+ orderIterator = this.tradeEngine.getBidIteratorFromMax();
488
+ }
489
+ let orderList = this._execQueryAgainstIterator(query, orderIterator, item => item.id);
490
+ return mapListFields(orderList, {
491
+ value: String,
492
+ sourceChainAmount: String,
493
+ valueRemaining: String,
494
+ lastSizeTaken: String,
495
+ lastValueTaken: String
496
+ });
497
+ }
498
+ },
499
+ getAsks: {
500
+ handler: (action) => {
501
+ let query = {...action.params};
502
+ // Optimization.
503
+ if (query.sort === 'price:asc') {
504
+ delete query.sort;
505
+ }
506
+ let orderIterator;
507
+ if (query.sourceWalletAddress) {
508
+ // Optimization.
509
+ orderIterator = this.tradeEngine.getSourceWalletAskIterator(query.sourceWalletAddress);
510
+ delete query.sourceWalletAddress;
511
+ } else {
512
+ orderIterator = this.tradeEngine.getAskIteratorFromMin();
513
+ }
514
+ let orderList = this._execQueryAgainstIterator(query, orderIterator, item => item.id);
515
+ return mapListFields(orderList, {
516
+ size: String,
517
+ sourceChainAmount: String,
518
+ sizeRemaining: String,
519
+ lastSizeTaken: String,
520
+ lastValueTaken: String
521
+ });
522
+ }
523
+ },
524
+ getOrders: {
525
+ handler: (action) => {
526
+ let query = {...action.params};
527
+ let orderIterator;
528
+ if (query.sourceWalletAddress) {
529
+ // Optimization.
530
+ orderIterator = this.tradeEngine.getSourceWalletOrderIterator(query.sourceWalletAddress);
531
+ delete query.sourceWalletAddress;
532
+ } else {
533
+ orderIterator = this.tradeEngine.getOrderIterator();
534
+ }
535
+ let orderList = this._execQueryAgainstIterator(query, orderIterator, item => item.id);
536
+ return mapListFields(orderList, {
537
+ value: String,
538
+ size: String,
539
+ sourceChainAmount: String,
540
+ valueRemaining: String,
541
+ sizeRemaining: String,
542
+ lastSizeTaken: String,
543
+ lastValueTaken: String
544
+ });
545
+ }
546
+ },
547
+ getOrderBook: {
548
+ handler: (action) => {
549
+ let query = {...action.params};
550
+ let { depth } = query;
551
+ if (depth == null) {
552
+ depth = Math.floor(this.options.apiDefaultPageLimit / 2);
553
+ } else {
554
+ delete query.depth;
555
+ }
556
+ if (typeof depth != 'number') {
557
+ let error = new Error(
558
+ 'If specified, the depth parameter of the query must be a number'
559
+ );
560
+ error.name = 'InvalidQueryError';
561
+ throw error;
562
+ }
563
+ let doubleDepth = depth * 2;
564
+ let askLevelIterator = this.tradeEngine.getAskLevelIteratorFromMin();
565
+ let bidLevelIterator = this.tradeEngine.getBidLevelIteratorFromMax();
566
+
567
+ let orderBook = [];
568
+ let lastEntry = {};
569
+
570
+ for (let askLevel of askLevelIterator) {
571
+ if (askLevel.price === lastEntry.price) {
572
+ lastEntry.sizeRemaining += askLevel.sizeRemaining;
573
+ } else {
574
+ if (orderBook.length >= depth) break;
575
+ lastEntry = {
576
+ side: 'ask',
577
+ price: askLevel.price,
578
+ sizeRemaining: askLevel.sizeRemaining
579
+ };
580
+ orderBook.push(lastEntry);
581
+ }
582
+ }
583
+
584
+ orderBook.reverse();
585
+
586
+ for (let bidLevel of bidLevelIterator) {
587
+ if (bidLevel.price === lastEntry.price) {
588
+ lastEntry.valueRemaining += bidLevel.valueRemaining;
589
+ } else {
590
+ if (orderBook.length >= doubleDepth) break;
591
+ lastEntry = {
592
+ side: 'bid',
593
+ price: bidLevel.price,
594
+ valueRemaining: bidLevel.valueRemaining
595
+ };
596
+ orderBook.push(lastEntry);
597
+ }
598
+ }
599
+
600
+ let orderLevelList = this._execQueryAgainstIterator(query, orderBook, item => item.price);
601
+ return mapListFields(orderLevelList, {
602
+ valueRemaining: String,
603
+ sizeRemaining: String
604
+ });
605
+ }
606
+ },
607
+ getRecentPrices: {
608
+ handler: (action) => {
609
+ let priceEntryIterator = this.recentPricesSkipList.findEntriesFromMax();
610
+ let priceGenerator = this._getValuesGenerator(priceEntryIterator);
611
+ let recentPricesList = this._execQueryAgainstIterator(action.params, priceGenerator, item => item.baseTimestamp);
612
+ return mapListFields(recentPricesList, {
613
+ volume: String
614
+ });
615
+ }
616
+ },
617
+ getPendingTransfers: {
618
+ handler: (action) => {
619
+ let transferList = this._execQueryAgainstIterator(
620
+ action.params,
621
+ this.pendingTransfers.values(),
622
+ item => item.id,
623
+ true,
624
+ true
625
+ );
626
+ return transferList.map((transfer) => {
627
+ let { signatures, ...transactionWithoutSignatures } = transfer.transaction;
628
+ return {
629
+ id: transfer.id,
630
+ transaction: transactionWithoutSignatures,
631
+ recipientAddress: transfer.recipientAddress,
632
+ targetChain: transfer.targetChain,
633
+ collectedSignatureCount: transfer.processedSignerAddressSet.size,
634
+ contributors: [...transfer.processedSignerAddressSet],
635
+ timestamp: transfer.timestamp,
636
+ type: transfer.type,
637
+ originOrderId: transfer.originOrderId,
638
+ closerOrderId: transfer.closerOrderId,
639
+ takerOrderId: transfer.takerOrderId,
640
+ makerOrderId: transfer.makerOrderId,
641
+ makerCount: transfer.makerCount
642
+ };
643
+ });
644
+ }
645
+ },
646
+ getRecentTransfers: {
647
+ handler: (action) => {
648
+ let recentTransfersIterator = this.recentTransfersSkipList.findEntriesFromMax();
649
+ let transferGenerator = this._getNestedObjectValuesGenerator(recentTransfersIterator);
650
+ let transferList = this._execQueryAgainstIterator(action.params, transferGenerator, item => item.id, true);
651
+ return transferList.map((transfer) => {
652
+ let { signatures, ...transactionWithoutSignatures } = transfer.transaction;
653
+ return {
654
+ id: transfer.id,
655
+ transaction: transactionWithoutSignatures,
656
+ recipientAddress: transfer.recipientAddress,
657
+ targetChain: transfer.targetChain,
658
+ collectedSignatureCount: transfer.processedSignerAddressSet.size,
659
+ contributors: [...transfer.processedSignerAddressSet],
660
+ timestamp: transfer.timestamp,
661
+ type: transfer.type,
662
+ originOrderId: transfer.originOrderId,
663
+ closerOrderId: transfer.closerOrderId,
664
+ takerOrderId: transfer.takerOrderId,
665
+ makerOrderId: transfer.makerOrderId,
666
+ makerCount: transfer.makerCount
667
+ };
668
+ });
669
+ }
670
+ }
671
+ };
672
+ }
673
+
674
+ *_getValuesGenerator(entriesIterator) {
675
+ for (let [key, value] of entriesIterator) {
676
+ yield value;
677
+ }
678
+ }
679
+
680
+ *_getNestedObjectValuesGenerator(iterator) {
681
+ for (let [key, nestedObject] of iterator) {
682
+ let values = Object.values(nestedObject);
683
+ for (let value of values) {
684
+ yield value;
685
+ }
686
+ }
687
+ }
688
+
689
+ _getChainInfo(chainSymbol) {
690
+ let chainOptions = this.options.chains[chainSymbol];
691
+ let multisigWalletInfo = this.multisigWalletInfo[chainSymbol];
692
+ return {
693
+ multisigAddressSystem: chainOptions.multisigAddressSystem,
694
+ multisigAddress: chainOptions.multisigAddress,
695
+ multisigMembers: [...multisigWalletInfo.members],
696
+ multisigRequiredSignatureCount: multisigWalletInfo.requiredSignatureCount,
697
+ minOrderAmount: String(chainOptions.minOrderAmount || 0n),
698
+ maxOrderAmount: String(
699
+ chainOptions.maxOrderAmount == null ? this.defaultMaxOrderAmount : chainOptions.maxOrderAmount
700
+ ),
701
+ minPartialTake: String(chainOptions.minPartialTake || 0n),
702
+ exchangeFeeBase: String(chainOptions.exchangeFeeBase),
703
+ exchangeFeeRate: chainOptions.exchangeFeeRate,
704
+ requiredConfirmations: chainOptions.requiredConfirmations,
705
+ orderHeightExpiry: chainOptions.orderHeightExpiry
706
+ };
707
+ }
708
+
709
+ _getSignatureQuota(targetChain, transaction) {
710
+ let walletInfo = this.multisigWalletInfo[targetChain];
711
+ if (!walletInfo) {
712
+ return NaN;
713
+ }
714
+ return transaction.signatures.length - walletInfo.requiredSignatureCount;
715
+ }
716
+
717
+ async _verifySignature(targetChain, transaction, signaturePacket) {
718
+ let hasMemberAddress = this.multisigWalletInfo[targetChain].members.has(signaturePacket.signerAddress);
719
+ if (!hasMemberAddress) {
720
+ return false;
721
+ }
722
+ try {
723
+ return await this.chainCrypto[targetChain].verifyTransactionSignature(transaction, signaturePacket);
724
+ } catch (error) {
725
+ this.logger.warn(
726
+ `Error encountered while attempting to verify the signature from member ${
727
+ signaturePacket.signerAddress
728
+ } for the transaction ${
729
+ transaction.id
730
+ } for the ${targetChain} network - ${error.message}`
731
+ );
732
+ }
733
+ return false;
734
+ }
735
+
736
+ async _processSignature(signatureData) {
737
+ let { signaturePacket, transactionId } = signatureData;
738
+ if (!signaturePacket) {
739
+ signaturePacket = {};
740
+ }
741
+ let transfer = this.pendingTransfers.get(transactionId);
742
+ let { signerAddress } = signaturePacket;
743
+ if (!transfer) {
744
+ throw new Error(
745
+ `Could not find a pending transfer ${
746
+ transactionId
747
+ } to match the signature from the signer ${
748
+ signerAddress
749
+ }`
750
+ );
751
+ }
752
+ let { transaction, processedSignerAddressSet, targetChain } = transfer;
753
+ if (processedSignerAddressSet.has(signerAddress)) {
754
+ throw new Error(
755
+ `A signature from the signer ${
756
+ signerAddress
757
+ } has already been received for the transaction ${
758
+ transactionId
759
+ }`
760
+ );
761
+ }
762
+
763
+ let isValidSignature = await this._verifySignature(targetChain, transaction, signaturePacket);
764
+ if (!isValidSignature) {
765
+ throw new Error(
766
+ `The signature from the signer ${
767
+ signerAddress
768
+ } for the transaction ${
769
+ transactionId
770
+ } was invalid or did not correspond to the account in its current state`
771
+ );
772
+ }
773
+
774
+ processedSignerAddressSet.add(signerAddress);
775
+ transaction.signatures.push(signaturePacket);
776
+
777
+ let signatureQuota = this._getSignatureQuota(targetChain, transaction);
778
+ if (signatureQuota >= 0 && transfer.readyTimestamp == null) {
779
+ transfer.readyTimestamp = Date.now();
780
+ }
781
+ }
782
+
783
+ expireMultisigTransactions() {
784
+ let now = Date.now();
785
+ for (let [txnId, transfer] of this.pendingTransfers) {
786
+ if (now - transfer.timestamp < this.options.multisigExpiry) {
787
+ break;
788
+ }
789
+ this.pendingTransfers.delete(txnId);
790
+ }
791
+ }
792
+
793
+ flushPendingMultisigTransactions() {
794
+ let transactionsToBroadcastPerChain = {};
795
+ let now = Date.now();
796
+
797
+ for (let transfer of this.pendingTransfers.values()) {
798
+ if (transfer.readyTimestamp != null && transfer.readyTimestamp + this.multisigReadyDelay <= now) {
799
+ if (!transactionsToBroadcastPerChain[transfer.targetChain]) {
800
+ transactionsToBroadcastPerChain[transfer.targetChain] = [];
801
+ }
802
+ transactionsToBroadcastPerChain[transfer.targetChain].push(transfer.transaction);
803
+ }
804
+ }
805
+
806
+ let chainSymbolList = Object.keys(transactionsToBroadcastPerChain);
807
+ for (let chainSymbol of chainSymbolList) {
808
+ let maxBatchSize = this.options.multisigMaxBatchSize;
809
+ let transactionsToBroadcast = transactionsToBroadcastPerChain[chainSymbol].slice(0, maxBatchSize);
810
+ this._broadcastTransactionsToChain(chainSymbol, transactionsToBroadcast);
811
+ }
812
+ }
813
+
814
+ flushPendingSignatures() {
815
+ let signaturesToBroadcast = [];
816
+
817
+ for (let transfer of this.pendingTransfers.values()) {
818
+ for (let signaturePacket of transfer.transaction.signatures) {
819
+ signaturesToBroadcast.push({
820
+ signaturePacket,
821
+ transactionId: transfer.transaction.id
822
+ });
823
+ }
824
+ }
825
+
826
+ if (signaturesToBroadcast.length) {
827
+ let maxBatchSize = this.options.signatureMaxBatchSize;
828
+ this._broadcastSignaturesToSubnet(
829
+ signaturesToBroadcast.slice(0, maxBatchSize)
830
+ );
831
+ }
832
+ }
833
+
834
+ async _broadcastTransactionsToChain(targetChain, transactions) {
835
+ let chainOptions = this.options.chains[targetChain];
836
+ if (chainOptions && chainOptions.moduleAlias) {
837
+ for (let transaction of transactions) {
838
+ try {
839
+ await this.channel.invoke(`${chainOptions.moduleAlias}:postTransaction`, {
840
+ transaction
841
+ });
842
+ } catch (error) {
843
+ this.logger.warn(
844
+ `Error encountered while attempting to post transaction ${
845
+ transaction.id
846
+ } to the ${targetChain} network - ${error.message}`
847
+ );
848
+ }
849
+ }
850
+ }
851
+ }
852
+
853
+ _getExpectedCounterpartyTransactionCount(transaction) {
854
+ let transactionData = transaction.message || '';
855
+ let header = transactionData.split(':')[0];
856
+ let parts = header.split(',');
857
+ let txnType = parts[0];
858
+ if (txnType === 't1') {
859
+ return parts[3] || 1;
860
+ }
861
+ if (txnType === 't2') {
862
+ return 1;
863
+ }
864
+ return 0;
865
+ }
866
+
867
+ _getTakerOrderIdFromTransaction(transaction, maxIdLength) {
868
+ let transactionData = transaction.message || '';
869
+ let header = transactionData.split(':')[0];
870
+ let parts = header.split(',');
871
+ let txnType = parts[0];
872
+ if (txnType === 't1') {
873
+ return parts[2].slice(0, maxIdLength);
874
+ }
875
+ if (txnType === 't2') {
876
+ return parts[3].slice(0, maxIdLength);
877
+ }
878
+ return null;
879
+ }
880
+
881
+ _isTakerTransaction(transaction) {
882
+ let transactionData = transaction.message || '';
883
+ let header = transactionData.split(':')[0];
884
+ return header.split(',')[0] === 't1';
885
+ }
886
+
887
+ _isMakerTransaction(transaction) {
888
+ let transactionData = transaction.message || '';
889
+ let header = transactionData.split(':')[0];
890
+ return header.split(',')[0] === 't2';
891
+ }
892
+
893
+ async _getRecentPrices() {
894
+ let tradeHistorySize = this.options.tradeHistorySize;
895
+ if (!tradeHistorySize) {
896
+ return [];
897
+ }
898
+
899
+ let baseChainOptions = this.options.chains[this.baseChainSymbol];
900
+ let quoteChainOptions = this.options.chains[this.quoteChainSymbol];
901
+
902
+ let baseChainReadMaxTransactions = baseChainOptions.readMaxTransactions == null ? tradeHistorySize : baseChainOptions.readMaxTransactions;
903
+ let quoteChainReadMaxTransactions = quoteChainOptions.readMaxTransactions == null ? tradeHistorySize : quoteChainOptions.readMaxTransactions;
904
+
905
+ let historyStartTimestamp = this.options.tradeHistoryStartTimestamp == null ? 0 : this.options.tradeHistoryStartTimestamp;
906
+ if (this.latestBasePriceTimestamp == null) {
907
+ this.latestBasePriceTimestamp = this.options.tradeHistoryStartTimestamp == null ? 0 :
908
+ this._denormalizeTimestamp(this.baseChainSymbol, this.options.tradeHistoryStartTimestamp);
909
+ }
910
+ if (this.latestQuotePriceTimestamp == null) {
911
+ this.latestQuotePriceTimestamp = this.options.tradeHistoryStartTimestamp == null ? 0 :
912
+ this._denormalizeTimestamp(this.quoteChainSymbol, this.options.tradeHistoryStartTimestamp);
913
+ }
914
+
915
+ let [baseChainTxnList, quoteChainTxnlist] = await Promise.all([
916
+ this._getOutboundTransactions(this.baseChainSymbol, this.baseAddress, this.latestBasePriceTimestamp, baseChainReadMaxTransactions),
917
+ this._getOutboundTransactions(this.quoteChainSymbol, this.quoteAddress, this.latestQuotePriceTimestamp, quoteChainReadMaxTransactions)
918
+ ]);
919
+
920
+ let baseChainTxns = [...this.unprocessedBaseTransactions];
921
+ let quoteChainTxns = [...this.unprocessedQuoteTransactions];
922
+
923
+ let unprocessedBaseTransactionIdSet = new Set(baseChainTxns.map(txn => txn.id));
924
+ let unprocessedQuoteTransactionIdSet = new Set(quoteChainTxns.map(txn => txn.id));
925
+
926
+ for (let baseTxn of baseChainTxnList) {
927
+ if (!unprocessedBaseTransactionIdSet.has(baseTxn.id)) {
928
+ baseChainTxns.push(baseTxn);
929
+ }
930
+ }
931
+
932
+ for (let quoteTxn of quoteChainTxnlist) {
933
+ if (!unprocessedQuoteTransactionIdSet.has(quoteTxn.id)) {
934
+ quoteChainTxns.push(quoteTxn);
935
+ }
936
+ }
937
+
938
+ let quoteChainMakers = {};
939
+ let quoteChainTakers = {};
940
+
941
+ let baseChainMaxIdLength = baseChainOptions.protocolMaxArgumentLength || DEFAULT_PROTOCOL_MAX_ARGUMENT_LENGTH;
942
+ let quoteChainMaxIdLength = quoteChainOptions.protocolMaxArgumentLength || DEFAULT_PROTOCOL_MAX_ARGUMENT_LENGTH;
943
+ let maxTakerIdLength = Math.min(baseChainMaxIdLength, quoteChainMaxIdLength);
944
+
945
+ for (let txn of quoteChainTxns) {
946
+ let isMaker = this._isMakerTransaction(txn);
947
+ let isTaker = this._isTakerTransaction(txn);
948
+ let takerOrderId = this._getTakerOrderIdFromTransaction(txn, maxTakerIdLength);
949
+ if (isMaker) {
950
+ if (!quoteChainMakers[takerOrderId]) {
951
+ quoteChainMakers[takerOrderId] = [];
952
+ }
953
+ quoteChainMakers[takerOrderId].push(txn);
954
+ } else if (isTaker) {
955
+ quoteChainTakers[takerOrderId] = [txn];
956
+ }
957
+ }
958
+
959
+ let txnPairsMap = {};
960
+
961
+ for (let txn of baseChainTxns) {
962
+ let isMaker = this._isMakerTransaction(txn);
963
+ let isTaker = this._isTakerTransaction(txn);
964
+
965
+ if (!isMaker && !isTaker) {
966
+ continue;
967
+ }
968
+
969
+ let counterpartyTakerId = this._getTakerOrderIdFromTransaction(txn, maxTakerIdLength);
970
+ let counterpartyTxns = quoteChainMakers[counterpartyTakerId] || quoteChainTakers[counterpartyTakerId] || [];
971
+
972
+ if (!counterpartyTxns.length) {
973
+ continue;
974
+ }
975
+
976
+ // Group base chain orders which were matched with the same counterparty order together.
977
+ if (!txnPairsMap[counterpartyTakerId]) {
978
+ txnPairsMap[counterpartyTakerId] = {
979
+ base: [],
980
+ quote: counterpartyTxns
981
+ };
982
+ }
983
+ let txnPair = txnPairsMap[counterpartyTakerId];
984
+ txnPair.base.push(txn);
985
+ }
986
+
987
+ let priceHistory = [];
988
+
989
+ // Filter out all entries which are incompete.
990
+ let txnPairsList = Object.values(txnPairsMap).filter((txnPair) => {
991
+ let firstBaseTxn = txnPair.base[0];
992
+ let firstQuoteTxn = txnPair.quote[0];
993
+ if (!firstBaseTxn || !firstQuoteTxn) {
994
+ return false;
995
+ }
996
+ let expectedBaseCount = this._getExpectedCounterpartyTransactionCount(firstQuoteTxn);
997
+ let expectedQuoteCount = this._getExpectedCounterpartyTransactionCount(firstBaseTxn);
998
+
999
+ return txnPair.base.length >= expectedBaseCount && txnPair.quote.length >= expectedQuoteCount;
1000
+ });
1001
+
1002
+ let processedBaseTxnIdSet = new Set();
1003
+ let processedQuoteTxnIdSet = new Set();
1004
+
1005
+ let lastBaseEntryTimestamp = 0;
1006
+ let lastQuoteEntryTimestamp = 0;
1007
+
1008
+ for (let txnPair of txnPairsList) {
1009
+ let baseChainFeeBase = this.chainExchangeFeeBases[this.baseChainSymbol];
1010
+ let baseChainFeeRate = baseChainOptions.exchangeFeeRate;
1011
+ let baseTotalFee = baseChainFeeBase * BigInt(txnPair.base.length);
1012
+ let baseCalc = this.bigIntFeeCalculators[this.baseChainSymbol];
1013
+ let fullBaseAmount = txnPair.base.reduce(
1014
+ (accumulator, txn) => {
1015
+ return accumulator + baseCalc.divideBigIntByDecimal(BigInt(txn.amount), 1 - baseChainFeeRate);
1016
+ },
1017
+ 0n
1018
+ ) + baseTotalFee;
1019
+
1020
+ let quoteChainFeeBase = this.chainExchangeFeeBases[this.quoteChainSymbol];
1021
+ let quoteChainFeeRate = quoteChainOptions.exchangeFeeRate;
1022
+ let quoteTotalFee = quoteChainFeeBase * BigInt(txnPair.quote.length);
1023
+ let quoteCalc = this.bigIntFeeCalculators[this.quoteChainSymbol];
1024
+ let fullQuoteAmount = txnPair.quote.reduce(
1025
+ (accumulator, txn) => {
1026
+ return accumulator + quoteCalc.divideBigIntByDecimal(BigInt(txn.amount), 1 - quoteChainFeeRate);
1027
+ },
1028
+ 0n
1029
+ ) + quoteTotalFee;
1030
+
1031
+ let price = this.bigIntPriceCalculator.divideBigIntByBigInt(fullBaseAmount, fullQuoteAmount);
1032
+
1033
+ for (let txn of txnPair.base) {
1034
+ processedBaseTxnIdSet.add(txn.id);
1035
+ }
1036
+ for (let txn of txnPair.quote) {
1037
+ processedQuoteTxnIdSet.add(txn.id);
1038
+ }
1039
+
1040
+ lastBaseEntryTimestamp = txnPair.base[txnPair.base.length - 1].timestamp;
1041
+ lastQuoteEntryTimestamp = txnPair.quote[txnPair.quote.length - 1].timestamp;
1042
+
1043
+ priceHistory.push({
1044
+ baseTimestamp: lastBaseEntryTimestamp,
1045
+ quoteTimestamp: lastQuoteEntryTimestamp,
1046
+ price,
1047
+ volume: fullBaseAmount
1048
+ });
1049
+ }
1050
+
1051
+ if (baseChainTxns.length) {
1052
+ let lastBaseChainTxn = baseChainTxns[baseChainTxns.length - 1];
1053
+ this.latestBasePriceTimestamp = lastBaseChainTxn.timestamp;
1054
+ }
1055
+ if (quoteChainTxns.length) {
1056
+ let lastQuoteChainTxn = quoteChainTxns[quoteChainTxns.length - 1];
1057
+ this.latestQuotePriceTimestamp = lastQuoteChainTxn.timestamp;
1058
+ }
1059
+
1060
+ let unprocessedBaseExpiry = this._normalizeTimestamp(this.baseChainSymbol, lastBaseEntryTimestamp) - this.options.tradeHistoryUnprocessedTransactionExpiry;
1061
+ let unprocessedQuoteExpiry = this._normalizeTimestamp(this.quoteChainSymbol, lastQuoteEntryTimestamp) - this.options.tradeHistoryUnprocessedTransactionExpiry;
1062
+
1063
+ this.unprocessedBaseTransactions = baseChainTxns.filter((txn) => {
1064
+ let isTrade = this._isMakerTransaction(txn) || this._isTakerTransaction(txn);
1065
+ return isTrade && !processedBaseTxnIdSet.has(txn.id) && this._normalizeTimestamp(this.baseChainSymbol, txn.timestamp) > unprocessedBaseExpiry;
1066
+ });
1067
+
1068
+ this.unprocessedQuoteTransactions = quoteChainTxns.filter((txn) => {
1069
+ let isTrade = this._isMakerTransaction(txn) || this._isTakerTransaction(txn);
1070
+ return isTrade && !processedQuoteTxnIdSet.has(txn.id) && this._normalizeTimestamp(this.quoteChainSymbol, txn.timestamp) > unprocessedQuoteExpiry;
1071
+ });
1072
+
1073
+ return priceHistory;
1074
+ }
1075
+
1076
+ async updateTradeHistory() {
1077
+ let recentPriceList;
1078
+ try {
1079
+ recentPriceList = await this._getRecentPrices();
1080
+ } catch (error) {
1081
+ this.logger.error(
1082
+ `Failed to fetch recent trade history because of error: ${
1083
+ error.message
1084
+ }`
1085
+ );
1086
+ return;
1087
+ }
1088
+
1089
+ let mergedPriceMap = new Map();
1090
+
1091
+ for (let priceEntry of recentPriceList) {
1092
+ let existingPriceEntry = mergedPriceMap.get(priceEntry.baseTimestamp);
1093
+ if (existingPriceEntry) {
1094
+ let existingEntryWeightedPrice = this.bigIntPriceCalculator.multiplyBigIntByDecimal(
1095
+ existingPriceEntry.volume,
1096
+ existingPriceEntry.price
1097
+ );
1098
+ let newEntryWeightedPrice = this.bigIntPriceCalculator.multiplyBigIntByDecimal(
1099
+ priceEntry.volume,
1100
+ priceEntry.price
1101
+ );
1102
+ let totalVolume = existingPriceEntry.volume + priceEntry.volume;
1103
+ existingPriceEntry.volume = totalVolume;
1104
+ existingPriceEntry.price = this.bigIntPriceCalculator.divideBigIntByBigInt(
1105
+ existingEntryWeightedPrice + newEntryWeightedPrice,
1106
+ totalVolume
1107
+ );
1108
+ } else {
1109
+ mergedPriceMap.set(priceEntry.baseTimestamp, priceEntry);
1110
+ }
1111
+ }
1112
+ for (let priceItem of mergedPriceMap.values()) {
1113
+ this.recentPricesSkipList.upsert(priceItem.baseTimestamp, priceItem);
1114
+ }
1115
+ while (this.recentPricesSkipList.length > this.options.tradeHistorySize) {
1116
+ this.recentPricesSkipList.delete(this.recentPricesSkipList.minKey());
1117
+ }
1118
+ }
1119
+
1120
+ async load(channel) {
1121
+ this.channel = channel;
1122
+
1123
+ try {
1124
+ await mkdir(this.orderBookSnapshotBackupDirPath, {recursive: true});
1125
+ } catch (error) {
1126
+ this.logger.error(
1127
+ `Failed to create snapshot directory ${
1128
+ this.orderBookSnapshotBackupDirPath
1129
+ } because of error: ${
1130
+ error.message
1131
+ }`
1132
+ );
1133
+ }
1134
+
1135
+ let lastProcessedTimestamp = null;
1136
+ try {
1137
+ lastProcessedTimestamp = await this.loadSnapshot();
1138
+ } catch (error) {
1139
+ this.logger.error(
1140
+ `Failed to load initial snapshot because of error: ${error.message} - DEX node will start with an empty order book`
1141
+ );
1142
+ }
1143
+
1144
+ await Promise.all(
1145
+ this.chainSymbols.map(async (chainSymbol) => {
1146
+ return this.chainCrypto[chainSymbol].load(channel, lastProcessedTimestamp);
1147
+ })
1148
+ );
1149
+
1150
+ this._multisigExpiryInterval = setInterval(() => {
1151
+ this.expireMultisigTransactions();
1152
+ }, this.options.multisigExpiryCheckInterval);
1153
+
1154
+ this._multisigFlushInterval = setInterval(() => {
1155
+ this.flushPendingMultisigTransactions();
1156
+ }, this.options.multisigFlushInterval);
1157
+
1158
+ this._signatureFlushInterval = setInterval(() => {
1159
+ this.flushPendingSignatures();
1160
+ }, this.options.signatureFlushInterval);
1161
+
1162
+ await this.updateTradeHistory();
1163
+
1164
+ this._tradeHistoryUpdateInterval = setInterval(() => {
1165
+ this.updateTradeHistory();
1166
+ }, this.options.tradeHistoryUpdateInterval);
1167
+
1168
+ await this.channel.invoke('app:updateModuleState', {
1169
+ [this.alias]: {
1170
+ baseAddress: this.baseAddress,
1171
+ quoteAddress: this.quoteAddress
1172
+ }
1173
+ });
1174
+
1175
+ let hasMultisigWalletsInfo = false;
1176
+
1177
+ this.channel.subscribe(`network:event:${this.alias}:signatures`, async ({data}) => {
1178
+ if (!hasMultisigWalletsInfo) {
1179
+ return;
1180
+ }
1181
+ let signatureDataList = Array.isArray(data) ? data.slice(0, this.options.signatureMaxBatchSize) : [];
1182
+ await Promise.all(
1183
+ signatureDataList.map(async (signatureData) => {
1184
+ try {
1185
+ await this._processSignature(signatureData || {});
1186
+ } catch (error) {
1187
+ this.logger.debug(
1188
+ `Failed to process signature because of error: ${error.message}`
1189
+ );
1190
+ }
1191
+ })
1192
+ );
1193
+ });
1194
+
1195
+ let loadMultisigWalletInfo = async () => {
1196
+ return Promise.all(
1197
+ this.chainSymbols.map(async (chainSymbol) => {
1198
+ let chainOptions = this.options.chains[chainSymbol];
1199
+ let multisigMembers = await this._getMultisigWalletMembers(chainSymbol, chainOptions.multisigAddress);
1200
+ let multisigMemberSet = new Set(multisigMembers);
1201
+ this.multisigWalletInfo[chainSymbol].members = multisigMemberSet;
1202
+ this.multisigWalletInfo[chainSymbol].memberCount = multisigMemberSet.size;
1203
+ this.multisigWalletInfo[chainSymbol].requiredSignatureCount = await this._getMinMultisigRequiredSignatures(chainSymbol, chainOptions.multisigAddress);
1204
+ })
1205
+ );
1206
+ };
1207
+
1208
+ let isTargetAddressValid = (targetChainSymbol, targetWalletAddress) => {
1209
+ if (!targetWalletAddress) {
1210
+ return false;
1211
+ }
1212
+ let targetChainOptions = this.options.chains[targetChainSymbol];
1213
+ if (targetWalletAddress === targetChainOptions.multisigAddress) {
1214
+ return false;
1215
+ }
1216
+ return true;
1217
+ };
1218
+
1219
+ let processBlock = async ({chainSymbol, chainHeight, latestChainHeights, blockData}) => {
1220
+ this.logger.info(
1221
+ `Chain ${chainSymbol}: Processing block at height ${chainHeight}`
1222
+ );
1223
+
1224
+ let baseChainHeight = latestChainHeights[this.baseChainSymbol];
1225
+ if (baseChainHeight < this.options.dexEnabledFromHeight) {
1226
+ this.logger.info(
1227
+ `Base chain height ${baseChainHeight} is below the DEX enabled height of ${this.options.dexEnabledFromHeight}`
1228
+ );
1229
+ return;
1230
+ }
1231
+ if (!hasMultisigWalletsInfo) {
1232
+ await loadMultisigWalletInfo();
1233
+ hasMultisigWalletsInfo = true;
1234
+ this.logger.info(
1235
+ `Loaded DEX wallets info`
1236
+ );
1237
+ }
1238
+
1239
+ let chainOptions = this.options.chains[chainSymbol];
1240
+ let minOrderAmount = BigInt(chainOptions.minOrderAmount || 0);
1241
+ let maxOrderAmount = chainOptions.maxOrderAmount == null ? this.defaultMaxOrderAmount : BigInt(chainOptions.maxOrderAmount);
1242
+
1243
+ let latestBlockTimestamp = blockData.timestamp;
1244
+
1245
+ if (chainSymbol === this.baseChainSymbol) {
1246
+ // Process pending updates.
1247
+ let updatesToActivate = [];
1248
+
1249
+ for (let update of this.pendingUpdates) {
1250
+ let targetHeight = update.criteria.baseChainHeight;
1251
+ if (targetHeight > baseChainHeight) {
1252
+ break;
1253
+ }
1254
+ if (targetHeight === baseChainHeight) {
1255
+ updatesToActivate.push(update);
1256
+ }
1257
+ }
1258
+
1259
+ if (
1260
+ this.updater.activeUpdate &&
1261
+ this.updater.activeUpdate.criteria.baseChainHeight <= baseChainHeight - this.options.orderBookSnapshotFinality
1262
+ ) {
1263
+ let updateSnapshotFilePath = this._getUpdateSnapshotFilePath(this.updater.activeUpdate.id);
1264
+ this.updater.mergeActiveUpdate();
1265
+ try {
1266
+ await unlink(updateSnapshotFilePath);
1267
+ } catch (error) {
1268
+ this.logger.error(
1269
+ `Failed to delete update snapshot file at path ${
1270
+ updateSnapshotFilePath
1271
+ } because of error: ${error.message}`
1272
+ );
1273
+ }
1274
+ }
1275
+
1276
+ if (updatesToActivate.length) {
1277
+ let update = updatesToActivate[0];
1278
+ if (updatesToActivate.length > 1) {
1279
+ this.logger.error('Cannot activate multiple updates on the same base chain height');
1280
+ } else if (this.updater.activeUpdate) {
1281
+ this.logger.error(
1282
+ `Cannot activate update with id ${
1283
+ update.id
1284
+ } because an existing update with id ${
1285
+ this.updater.activeUpdate.id
1286
+ } is already active and has not been merged`
1287
+ );
1288
+ } else {
1289
+ let currentOrderBook = this.tradeEngine.getSnapshot();
1290
+ let processedChainHeights = {...latestChainHeights};
1291
+ processedChainHeights[this.baseChainSymbol]--;
1292
+ let snapshot = {
1293
+ orderBook: currentOrderBook,
1294
+ chainHeights: processedChainHeights
1295
+ };
1296
+ let updateSnapshotFilePath = this._getUpdateSnapshotFilePath(update.id);
1297
+ let error;
1298
+ try {
1299
+ await mkdir(this.orderBookUpdateSnapshotDirPath, {recursive: true});
1300
+ await this.saveSnapshot(snapshot, updateSnapshotFilePath);
1301
+ } catch (err) {
1302
+ error = err;
1303
+ this.logger.fatal(`Failed to save snapshot before update because of error: ${error.message}`);
1304
+ }
1305
+ if (!error) {
1306
+ this.updater.activateUpdate(update);
1307
+ }
1308
+ process.exit();
1309
+ }
1310
+ }
1311
+
1312
+ // Make a new snapshot every orderBookSnapshotFinality blocks.
1313
+ if (chainHeight % this.options.orderBookSnapshotFinality === 0) {
1314
+ let currentOrderBook = this.tradeEngine.getSnapshot();
1315
+ if (this.lastSnapshot) {
1316
+ try {
1317
+ await this.saveSnapshot(this.lastSnapshot);
1318
+ } catch (error) {
1319
+ this.logger.error(`Failed to save snapshot because of error: ${error.message}`);
1320
+ }
1321
+ }
1322
+ let processedChainHeights = {...latestChainHeights};
1323
+ processedChainHeights[this.baseChainSymbol]--;
1324
+ this.lastSnapshot = {
1325
+ orderBook: currentOrderBook,
1326
+ chainHeights: processedChainHeights
1327
+ };
1328
+ }
1329
+ if (
1330
+ !this.passiveMode &&
1331
+ this.options.dexDisabledFromHeight != null &&
1332
+ chainHeight % this.options.dexDisabledFromHeight === 0
1333
+ ) {
1334
+ let currentOrderBook = this.tradeEngine.getSnapshot();
1335
+ this.tradeEngine.clear();
1336
+ try {
1337
+ await this.refundOrderBook(
1338
+ {
1339
+ orderBook: currentOrderBook,
1340
+ chainHeights: {...latestChainHeights}
1341
+ },
1342
+ latestBlockTimestamp,
1343
+ {
1344
+ [this.baseChainSymbol]: this.options.chains[this.baseChainSymbol].dexMovedToAddress,
1345
+ [this.quoteChainSymbol]: this.options.chains[this.quoteChainSymbol].dexMovedToAddress
1346
+ }
1347
+ );
1348
+ } catch (error) {
1349
+ this.logger.error(
1350
+ `Failed to refund the order book according to config because of error: ${error.message}`
1351
+ );
1352
+ }
1353
+ }
1354
+ }
1355
+
1356
+ if (blockData.numberOfTransactions === 0) {
1357
+ this.logger.info(
1358
+ `Chain ${chainSymbol}: No transactions in block ${blockData.id} at height ${chainHeight}`
1359
+ );
1360
+ }
1361
+
1362
+ // The height pointer for dividends needs to be delayed so that DEX member dividends are only distributed
1363
+ // over a range where there is no risk of fork in the underlying blockchain.
1364
+ let dividendTargetHeight = chainHeight - chainOptions.dividendHeightOffset;
1365
+ if (
1366
+ dividendTargetHeight > chainOptions.dividendStartHeight &&
1367
+ dividendTargetHeight % chainOptions.dividendHeightInterval === 0
1368
+ ) {
1369
+ try {
1370
+ await processDividends({
1371
+ chainSymbol,
1372
+ chainHeight,
1373
+ toHeight: dividendTargetHeight,
1374
+ latestBlockTimestamp
1375
+ });
1376
+ } catch (error) {
1377
+ throw new Error(
1378
+ `Failed to process dividends at target height ${dividendTargetHeight} because of error: ${error.message}`
1379
+ );
1380
+ }
1381
+ }
1382
+
1383
+ let blockTransactions = await Promise.all([
1384
+ this._getInboundTransactionsFromBlock(chainSymbol, chainOptions.multisigAddress, blockData.id),
1385
+ this._getOutboundTransactionsFromBlock(chainSymbol, chainOptions.multisigAddress, blockData.id)
1386
+ ]);
1387
+
1388
+ let [inboundTxns, outboundTxns] = blockTransactions;
1389
+
1390
+ outboundTxns.forEach((txn) => {
1391
+ let pendingTransfer = this.pendingTransfers.get(txn.id);
1392
+ if (pendingTransfer) {
1393
+ let recentTransfer = {...pendingTransfer, transaction: txn};
1394
+ let existingRecentTransfers = this.recentTransfersSkipList.find(recentTransfer.timestamp);
1395
+ if (existingRecentTransfers) {
1396
+ existingRecentTransfers[txn.id] = recentTransfer;
1397
+ } else {
1398
+ this.recentTransfersSkipList.upsert(recentTransfer.timestamp, {[txn.id]: recentTransfer});
1399
+ }
1400
+ this.pendingTransfers.delete(txn.id);
1401
+ }
1402
+ });
1403
+
1404
+ let expiryTimestamp = Date.now() - this.options.recentTransfersExpiry;
1405
+ this.recentTransfersSkipList.deleteRange(0, expiryTimestamp, true);
1406
+
1407
+ let orders = inboundTxns.map((txn) => {
1408
+ let orderTxn = {...txn};
1409
+ orderTxn.sourceChain = chainSymbol;
1410
+ orderTxn.sourceWalletAddress = orderTxn.senderAddress;
1411
+ let amount = BigInt(orderTxn.amount);
1412
+
1413
+ let transferMessageString = orderTxn.message == null ? '' : orderTxn.message;
1414
+
1415
+ if (transferMessageString === 'credit') {
1416
+ // The credit operation does nothing - The DEX wallet will simply accept the tokens.
1417
+ orderTxn.type = 'credit';
1418
+ return orderTxn;
1419
+ }
1420
+
1421
+ if (amount > maxOrderAmount) {
1422
+ orderTxn.type = 'oversized';
1423
+ orderTxn.sourceChainAmount = amount;
1424
+ this.logger.debug(
1425
+ `Chain ${chainSymbol}: Incoming order ${orderTxn.id} amount ${orderTxn.sourceChainAmount.toString()} was too large - Maximum order amount is ${maxOrderAmount}`
1426
+ );
1427
+ return orderTxn;
1428
+ }
1429
+
1430
+ orderTxn.sourceChainAmount = amount;
1431
+
1432
+ if (
1433
+ this.options.dexDisabledFromHeight != null &&
1434
+ chainHeight >= this.options.dexDisabledFromHeight
1435
+ ) {
1436
+ if (chainOptions.dexMovedToAddress) {
1437
+ orderTxn.type = 'moved';
1438
+ orderTxn.movedToAddress = chainOptions.dexMovedToAddress;
1439
+ this.logger.debug(
1440
+ `Chain ${chainSymbol}: Cannot process order ${orderTxn.id} because the DEX has moved to the address ${chainOptions.dexMovedToAddress}`
1441
+ );
1442
+ return orderTxn;
1443
+ }
1444
+ orderTxn.type = 'disabled';
1445
+ this.logger.debug(
1446
+ `Chain ${chainSymbol}: Cannot process order ${orderTxn.id} because the DEX has been disabled`
1447
+ );
1448
+ return orderTxn;
1449
+ }
1450
+
1451
+ let dataParts = transferMessageString.split(',');
1452
+ let targetChain = dataParts[0];
1453
+
1454
+ orderTxn.targetChain = targetChain;
1455
+ let isSupportedChain = this.options.chains[targetChain] && targetChain !== chainSymbol;
1456
+ if (!isSupportedChain) {
1457
+ orderTxn.type = 'invalid';
1458
+ orderTxn.reason = 'Invalid target chain';
1459
+ this.logger.debug(
1460
+ `Chain ${chainSymbol}: Incoming order ${orderTxn.id} has an invalid target chain ${targetChain}`
1461
+ );
1462
+ return orderTxn;
1463
+ }
1464
+
1465
+ if (
1466
+ (dataParts[1] === 'limit' || dataParts[1] === 'market') &&
1467
+ amount < minOrderAmount
1468
+ ) {
1469
+ orderTxn.type = 'undersized';
1470
+ this.logger.debug(
1471
+ `Chain ${chainSymbol}: Incoming order ${orderTxn.id} amount ${orderTxn.sourceChainAmount.toString()} was too small - Minimum order amount is ${minOrderAmount}`
1472
+ );
1473
+ return orderTxn;
1474
+ }
1475
+
1476
+ if (this.tradeEngine.wasOrderProcessed(orderTxn.id, chainSymbol, chainHeight)) {
1477
+ orderTxn.type = 'redundant';
1478
+ orderTxn.reason = 'Already processed';
1479
+ this.logger.debug(
1480
+ `Chain ${chainSymbol}: Failed to process order ${orderTxn.id} because it was already processed`
1481
+ );
1482
+ return orderTxn;
1483
+ }
1484
+
1485
+ if (dataParts[1] === 'limit') {
1486
+ // E.g. clsk,limit,.5,9205805648791671841L
1487
+ let priceString = dataParts[2];
1488
+ let price = Number(priceString);
1489
+ let targetWalletAddress = dataParts[3];
1490
+ if (!this.validPriceRegex.test(priceString) || isNaN(price) || price === 0) {
1491
+ orderTxn.type = 'invalid';
1492
+ orderTxn.reason = 'Invalid price';
1493
+ this.logger.debug(
1494
+ `Chain ${chainSymbol}: Incoming limit order ${orderTxn.id} has an invalid price`
1495
+ );
1496
+ return orderTxn;
1497
+ }
1498
+ if (!isTargetAddressValid(orderTxn.targetChain, targetWalletAddress)) {
1499
+ orderTxn.type = 'invalid';
1500
+ orderTxn.reason = 'Invalid wallet address';
1501
+ this.logger.debug(
1502
+ `Chain ${chainSymbol}: Incoming limit order ${orderTxn.id} has an invalid target wallet address`
1503
+ );
1504
+ return orderTxn;
1505
+ }
1506
+ if (this._isLimitOrderTooSmallToConvert(chainSymbol, amount, price)) {
1507
+ orderTxn.type = 'invalid';
1508
+ orderTxn.reason = 'Too small to convert';
1509
+ this.logger.debug(
1510
+ `Chain ${chainSymbol}: Incoming limit order ${orderTxn.id} was too small to cover fees`
1511
+ );
1512
+ return orderTxn;
1513
+ }
1514
+
1515
+ orderTxn.type = 'limit';
1516
+ orderTxn.height = chainHeight;
1517
+ orderTxn.price = price;
1518
+ orderTxn.targetWalletAddress = targetWalletAddress;
1519
+ if (chainSymbol === this.baseChainSymbol) {
1520
+ orderTxn.side = 'bid';
1521
+ orderTxn.value = amount;
1522
+ } else {
1523
+ orderTxn.side = 'ask';
1524
+ orderTxn.size = amount;
1525
+ }
1526
+ } else if (dataParts[1] === 'market') {
1527
+ // E.g. clsk,market,9205805648791671841L
1528
+ let targetWalletAddress = dataParts[2];
1529
+ if (!isTargetAddressValid(orderTxn.targetChain, targetWalletAddress)) {
1530
+ orderTxn.type = 'invalid';
1531
+ orderTxn.reason = 'Invalid wallet address';
1532
+ this.logger.debug(
1533
+ `Chain ${chainSymbol}: Incoming market order ${orderTxn.id} has an invalid target wallet address`
1534
+ );
1535
+ return orderTxn;
1536
+ }
1537
+ if (this._isMarketOrderTooSmallToConvert(chainSymbol, amount)) {
1538
+ orderTxn.type = 'invalid';
1539
+ orderTxn.reason = 'Too small to convert';
1540
+ this.logger.debug(
1541
+ `Chain ${chainSymbol}: Incoming market order ${orderTxn.id} was too small to cover fees`
1542
+ );
1543
+ return orderTxn;
1544
+ }
1545
+ orderTxn.type = 'market';
1546
+ orderTxn.height = chainHeight;
1547
+ orderTxn.targetWalletAddress = targetWalletAddress;
1548
+ if (chainSymbol === this.baseChainSymbol) {
1549
+ orderTxn.side = 'bid';
1550
+ orderTxn.value = amount;
1551
+ } else {
1552
+ orderTxn.side = 'ask';
1553
+ orderTxn.size = amount;
1554
+ }
1555
+ } else if (dataParts[1] === 'close') {
1556
+ // E.g. clsk,close,1787318409505302601
1557
+ let targetOrderId = dataParts[2];
1558
+ if (!targetOrderId) {
1559
+ orderTxn.type = 'invalid';
1560
+ orderTxn.reason = 'Missing order ID';
1561
+ this.logger.debug(
1562
+ `Chain ${chainSymbol}: Incoming close order ${orderTxn.id} is missing an order ID`
1563
+ );
1564
+ return orderTxn;
1565
+ }
1566
+
1567
+ let targetOrder = this.tradeEngine.getOrder(targetOrderId);
1568
+ if (!targetOrder) {
1569
+ orderTxn.type = 'invalid';
1570
+ orderTxn.reason = 'Invalid order ID';
1571
+ this.logger.debug(
1572
+ `Chain ${chainSymbol}: Failed to close order with ID ${targetOrderId} because it could not be found`
1573
+ );
1574
+ return orderTxn;
1575
+ }
1576
+ if (targetOrder.sourceChain !== orderTxn.sourceChain) {
1577
+ orderTxn.type = 'invalid';
1578
+ orderTxn.reason = 'Wrong chain';
1579
+ this.logger.debug(
1580
+ `Chain ${chainSymbol}: Could not close order ID ${targetOrderId} because it is on a different chain`
1581
+ );
1582
+ return orderTxn;
1583
+ }
1584
+ if (targetOrder.sourceWalletAddress !== orderTxn.sourceWalletAddress) {
1585
+ orderTxn.type = 'invalid';
1586
+ orderTxn.reason = 'Not authorized';
1587
+ this.logger.debug(
1588
+ `Chain ${chainSymbol}: Could not close order ID ${targetOrderId} because it belongs to a different account`
1589
+ );
1590
+ return orderTxn;
1591
+ }
1592
+ orderTxn.type = 'close';
1593
+ orderTxn.height = chainHeight;
1594
+ orderTxn.orderIdToClose = targetOrderId;
1595
+ } else {
1596
+ orderTxn.type = 'invalid';
1597
+ orderTxn.reason = 'Invalid operation';
1598
+ this.logger.debug(
1599
+ `Chain ${chainSymbol}: Incoming transaction ${orderTxn.id} is not a supported DEX order`
1600
+ );
1601
+ }
1602
+ return orderTxn;
1603
+ });
1604
+
1605
+ let closeOrders = orders.filter(orderTxn => orderTxn.type === 'close');
1606
+
1607
+ let limitAndMarketOrders = orders.filter(orderTxn => orderTxn.type === 'limit' || orderTxn.type === 'market');
1608
+
1609
+ let invalidOrders = orders.filter(orderTxn => orderTxn.type === 'invalid');
1610
+
1611
+ let oversizedOrders = orders.filter(orderTxn => orderTxn.type === 'oversized');
1612
+
1613
+ let undersizedOrders = orders.filter(orderTxn => orderTxn.type === 'undersized');
1614
+
1615
+ let movedOrders = orders.filter(orderTxn => orderTxn.type === 'moved');
1616
+
1617
+ let disabledOrders = orders.filter(orderTxn => orderTxn.type === 'disabled');
1618
+
1619
+ if (!this.passiveMode) {
1620
+ movedOrders.forEach(async (orderTxn) => {
1621
+ let protocolMessage = this._computeProtocolMessage(orderTxn.sourceChain, 'r5', [orderTxn.id, orderTxn.movedToAddress], 'DEX has moved');
1622
+ try {
1623
+ await this.execRefundTransaction(orderTxn, latestBlockTimestamp, protocolMessage, {type: 'r5', originOrderId: orderTxn.id});
1624
+ } catch (error) {
1625
+ this.logger.error(
1626
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for moved DEX order ID ${
1627
+ orderTxn.id
1628
+ } to ${
1629
+ orderTxn.sourceWalletAddress
1630
+ } on chain ${
1631
+ orderTxn.sourceChain
1632
+ } because of error: ${
1633
+ error.message
1634
+ }`
1635
+ );
1636
+ }
1637
+ });
1638
+
1639
+ disabledOrders.forEach(async (orderTxn) => {
1640
+ let protocolMessage = this._computeProtocolMessage(orderTxn.sourceChain, 'r6', [orderTxn.id], 'DEX has been disabled');
1641
+ try {
1642
+ await this.execRefundTransaction(orderTxn, latestBlockTimestamp, protocolMessage, {type: 'r6', originOrderId: orderTxn.id});
1643
+ } catch (error) {
1644
+ this.logger.error(
1645
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for disabled DEX order ID ${
1646
+ orderTxn.id
1647
+ } to ${
1648
+ orderTxn.sourceWalletAddress
1649
+ } on chain ${
1650
+ orderTxn.sourceChain
1651
+ } because of error: ${
1652
+ error.message
1653
+ }`
1654
+ );
1655
+ }
1656
+ });
1657
+
1658
+ invalidOrders.forEach(async (orderTxn) => {
1659
+ let reasonMessage = 'Invalid order';
1660
+ if (orderTxn.reason) {
1661
+ reasonMessage += ` - ${orderTxn.reason}`;
1662
+ }
1663
+ let protocolMessage = this._computeProtocolMessage(orderTxn.sourceChain, 'r1', [orderTxn.id], reasonMessage);
1664
+ try {
1665
+ await this.execRefundTransaction(orderTxn, latestBlockTimestamp, protocolMessage, {type: 'r1', originOrderId: orderTxn.id});
1666
+ } catch (error) {
1667
+ this.logger.error(
1668
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for invalid order ID ${
1669
+ orderTxn.id
1670
+ } to ${
1671
+ orderTxn.sourceWalletAddress
1672
+ } on chain ${
1673
+ orderTxn.sourceChain
1674
+ } because of error: ${
1675
+ error.message
1676
+ }`
1677
+ );
1678
+ }
1679
+ });
1680
+
1681
+ oversizedOrders.forEach(async (orderTxn) => {
1682
+ let protocolMessage = this._computeProtocolMessage(orderTxn.sourceChain, 'r1', [orderTxn.id], 'Oversized order');
1683
+ try {
1684
+ await this.execRefundTransaction(orderTxn, latestBlockTimestamp, protocolMessage, {type: 'r1', originOrderId: orderTxn.id});
1685
+ } catch (error) {
1686
+ this.logger.error(
1687
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for oversized order ID ${
1688
+ orderTxn.id
1689
+ } to ${
1690
+ orderTxn.sourceWalletAddress
1691
+ } on chain ${
1692
+ orderTxn.sourceChain
1693
+ } because of error: ${
1694
+ error.message
1695
+ }`
1696
+ );
1697
+ }
1698
+ });
1699
+
1700
+ undersizedOrders.forEach(async (orderTxn) => {
1701
+ let protocolMessage = this._computeProtocolMessage(orderTxn.sourceChain, 'r1', [orderTxn.id], 'Undersized order');
1702
+ try {
1703
+ await this.execRefundTransaction(orderTxn, latestBlockTimestamp, protocolMessage, {type: 'r1', originOrderId: orderTxn.id});
1704
+ } catch (error) {
1705
+ this.logger.error(
1706
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for undersized order ID ${
1707
+ orderTxn.id
1708
+ } to ${
1709
+ orderTxn.sourceWalletAddress
1710
+ } on chain ${
1711
+ orderTxn.sourceChain
1712
+ } because of error: ${
1713
+ error.message
1714
+ }`
1715
+ );
1716
+ }
1717
+ });
1718
+ }
1719
+
1720
+ let expiredOrders;
1721
+ if (chainSymbol === this.baseChainSymbol) {
1722
+ expiredOrders = this.tradeEngine.expireBidOrders(chainHeight);
1723
+ } else {
1724
+ expiredOrders = this.tradeEngine.expireAskOrders(chainHeight);
1725
+ }
1726
+ expiredOrders.forEach(async (expiredOrder) => {
1727
+ this.logger.info(
1728
+ `Chain ${chainSymbol}: Order ${expiredOrder.id} at height ${expiredOrder.height} expired`
1729
+ );
1730
+ if (this.passiveMode) {
1731
+ return;
1732
+ }
1733
+ let protocolMessage = this._computeProtocolMessage(expiredOrder.sourceChain, 'r2', [expiredOrder.id], 'Expired order');
1734
+ try {
1735
+ await this.refundOrder(
1736
+ expiredOrder,
1737
+ latestBlockTimestamp,
1738
+ expiredOrder.expiryHeight,
1739
+ protocolMessage,
1740
+ {type: 'r2', originOrderId: expiredOrder.id}
1741
+ );
1742
+ } catch (error) {
1743
+ this.logger.error(
1744
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for expired order ID ${
1745
+ expiredOrder.id
1746
+ } to ${
1747
+ expiredOrder.sourceWalletAddress
1748
+ } on chain ${
1749
+ expiredOrder.sourceChain
1750
+ } because of error: ${
1751
+ error.message
1752
+ }`
1753
+ );
1754
+ }
1755
+ });
1756
+
1757
+ closeOrders.forEach(async (orderTxn) => {
1758
+ let targetOrder = this.tradeEngine.getOrder(orderTxn.orderIdToClose);
1759
+ if (!targetOrder) {
1760
+ this.logger.warn(
1761
+ `Failed to close order with ID ${orderTxn.orderIdToClose} because it could not be found`
1762
+ );
1763
+ return;
1764
+ }
1765
+ let refundTxn = {
1766
+ sourceChain: targetOrder.sourceChain,
1767
+ sourceWalletAddress: targetOrder.sourceWalletAddress,
1768
+ height: orderTxn.height
1769
+ };
1770
+ if (refundTxn.sourceChain === this.baseChainSymbol) {
1771
+ refundTxn.sourceChainAmount = targetOrder.valueRemaining;
1772
+ } else {
1773
+ refundTxn.sourceChainAmount = targetOrder.sizeRemaining;
1774
+ }
1775
+ // Also send back any amount which was sent as part of the close order.
1776
+ refundTxn.sourceChainAmount += orderTxn.sourceChainAmount;
1777
+
1778
+ let result;
1779
+ try {
1780
+ result = this.tradeEngine.addCloseOrder(orderTxn);
1781
+ } catch (error) {
1782
+ this.logger.error(error);
1783
+ return;
1784
+ }
1785
+ if (this.passiveMode) {
1786
+ return;
1787
+ }
1788
+ let protocolMessage = this._computeProtocolMessage(refundTxn.sourceChain, 'r3', [targetOrder.id, orderTxn.id], 'Closed order');
1789
+ try {
1790
+ await this.execRefundTransaction(
1791
+ refundTxn,
1792
+ latestBlockTimestamp,
1793
+ protocolMessage,
1794
+ {type: 'r3', originOrderId: targetOrder.id, closerOrderId: orderTxn.id}
1795
+ );
1796
+ } catch (error) {
1797
+ this.logger.error(
1798
+ `Chain ${chainSymbol}: Failed to post multisig refund transaction for closed order ID ${
1799
+ targetOrder.id
1800
+ } to ${
1801
+ targetOrder.sourceWalletAddress
1802
+ } on chain ${
1803
+ targetOrder.sourceChain
1804
+ } because of error: ${
1805
+ error.message
1806
+ }`
1807
+ );
1808
+ }
1809
+ });
1810
+
1811
+ limitAndMarketOrders.forEach(async (orderTxn) => {
1812
+ let result;
1813
+ try {
1814
+ result = this.tradeEngine.addOrder(orderTxn);
1815
+ } catch (error) {
1816
+ this.logger.warn(error);
1817
+ return;
1818
+ }
1819
+ this.logger.info(
1820
+ `Chain ${chainSymbol}: Added order ${orderTxn.id} to the trade matching engine`
1821
+ );
1822
+
1823
+ if (this.passiveMode) {
1824
+ return;
1825
+ }
1826
+
1827
+ let takerTargetChain = result.taker.targetChain;
1828
+ let takerChainOptions = this.options.chains[takerTargetChain];
1829
+ let takerTargetChainModuleAlias = takerChainOptions.moduleAlias;
1830
+ let takerAddress = result.taker.targetWalletAddress;
1831
+ let takerAmount = takerTargetChain === this.baseChainSymbol ? result.takeValue : result.takeSize;
1832
+ let feeCalc = this.bigIntFeeCalculators[takerTargetChain];
1833
+ takerAmount -= BigInt(takerChainOptions.exchangeFeeBase);
1834
+ takerAmount -= feeCalc.multiplyBigIntByDecimal(takerAmount, takerChainOptions.exchangeFeeRate);
1835
+
1836
+ let makerCount = 0;
1837
+
1838
+ result.makers.forEach(async (makerOrder) => {
1839
+ let makerChainOptions = this.options.chains[makerOrder.targetChain];
1840
+ let makerAddress = makerOrder.targetWalletAddress;
1841
+ let makerAmount = makerOrder.targetChain === this.baseChainSymbol ? makerOrder.lastValueTaken : makerOrder.lastSizeTaken;
1842
+ let feeCalc = this.bigIntFeeCalculators[makerOrder.targetChain];
1843
+ makerAmount -= BigInt(makerChainOptions.exchangeFeeBase);
1844
+ makerAmount -= feeCalc.multiplyBigIntByDecimal(makerAmount, makerChainOptions.exchangeFeeRate);
1845
+
1846
+ if (makerAmount <= 0n) {
1847
+ this.logger.error(
1848
+ `Chain ${chainSymbol}: Did not post the maker trade order ${makerOrder.id} because the amount after fees was less than or equal to 0`
1849
+ );
1850
+ return;
1851
+ }
1852
+ makerCount++;
1853
+
1854
+ let makerTxn = {
1855
+ recipientAddress: makerAddress,
1856
+ amount: makerAmount.toString(),
1857
+ fee: makerChainOptions.exchangeFeeBase.toString(),
1858
+ timestamp: latestBlockTimestamp,
1859
+ height: latestChainHeights[makerOrder.targetChain]
1860
+ };
1861
+ let protocolMessage = this._computeProtocolMessage(
1862
+ makerOrder.targetChain,
1863
+ 't2',
1864
+ [makerOrder.sourceChain, makerOrder.id, result.taker.id],
1865
+ 'Order made'
1866
+ );
1867
+ try {
1868
+ await this.execMultisigTransaction(
1869
+ makerOrder.targetChain,
1870
+ makerTxn,
1871
+ protocolMessage,
1872
+ {type: 't2', originOrderId: makerOrder.id, makerOrderId: makerOrder.id, takerOrderId: result.taker.id}
1873
+ );
1874
+ } catch (error) {
1875
+ this.logger.error(
1876
+ `Chain ${chainSymbol}: Failed to post multisig transaction of maker ${makerAddress} on chain ${makerOrder.targetChain} because of error: ${error.message}`
1877
+ );
1878
+ }
1879
+ });
1880
+
1881
+ (async () => {
1882
+ if (!makerCount) {
1883
+ return;
1884
+ }
1885
+ if (takerAmount <= 0n) {
1886
+ this.logger.warn(
1887
+ `Chain ${chainSymbol}: Did not post the taker trade order ${orderTxn.id} because the amount after fees was less than or equal to 0`
1888
+ );
1889
+ return;
1890
+ }
1891
+ let takerTxn = {
1892
+ recipientAddress: takerAddress,
1893
+ amount: takerAmount.toString(),
1894
+ fee: takerChainOptions.exchangeFeeBase.toString(),
1895
+ timestamp: latestBlockTimestamp,
1896
+ height: latestChainHeights[takerTargetChain]
1897
+ };
1898
+ let protocolMessage = this._computeProtocolMessage(
1899
+ takerTargetChain,
1900
+ 't1',
1901
+ [result.taker.sourceChain, result.taker.id, makerCount],
1902
+ 'Orders taken'
1903
+ );
1904
+ try {
1905
+ await this.execMultisigTransaction(
1906
+ takerTargetChain,
1907
+ takerTxn,
1908
+ protocolMessage,
1909
+ {type: 't1', originOrderId: result.taker.id, takerOrderId: result.taker.id, makerCount}
1910
+ );
1911
+ } catch (error) {
1912
+ this.logger.error(
1913
+ `Chain ${chainSymbol}: Failed to post multisig transaction of taker ${takerAddress} on chain ${takerTargetChain} because of error: ${error.message}`
1914
+ );
1915
+ }
1916
+ })();
1917
+
1918
+ (async () => {
1919
+ if (orderTxn.type === 'market') {
1920
+ let refundTxn = {
1921
+ sourceChain: result.taker.sourceChain,
1922
+ sourceWalletAddress: result.taker.sourceWalletAddress,
1923
+ height: orderTxn.height
1924
+ };
1925
+ if (result.taker.sourceChain === this.baseChainSymbol) {
1926
+ refundTxn.sourceChainAmount = result.taker.valueRemaining;
1927
+ } else {
1928
+ refundTxn.sourceChainAmount = result.taker.sizeRemaining;
1929
+ }
1930
+ if (refundTxn.sourceChainAmount <= 0n) {
1931
+ return;
1932
+ }
1933
+ let protocolMessage = this._computeProtocolMessage(refundTxn.sourceChain, 'r4', [orderTxn.id], 'Unmatched market order part');
1934
+ try {
1935
+ await this.execRefundTransaction(refundTxn, latestBlockTimestamp, protocolMessage, {type: 'r4', originOrderId: orderTxn.id});
1936
+ } catch (error) {
1937
+ this.logger.error(
1938
+ `Chain ${chainSymbol}: Failed to post multisig market order refund transaction of taker ${takerAddress} on chain ${takerTargetChain} because of error: ${error.message}`
1939
+ );
1940
+ }
1941
+ }
1942
+ })();
1943
+ });
1944
+
1945
+ this.processedHeights = {...latestChainHeights};
1946
+ }
1947
+
1948
+ let processDividends = async ({chainSymbol, chainHeight, toHeight, latestBlockTimestamp}) => {
1949
+ let chainOptions = this.options.chains[chainSymbol];
1950
+ let fromHeight = toHeight - chainOptions.dividendHeightInterval;
1951
+ let { readMaxBlocks } = chainOptions;
1952
+ if (fromHeight < 1) {
1953
+ fromHeight = 1;
1954
+ }
1955
+
1956
+ let contributionData = {};
1957
+ let currentBlock = await this._getBlockAtHeight(chainSymbol, fromHeight);
1958
+
1959
+ while (currentBlock) {
1960
+ let blocksToProcess = await this._getBlocksBetweenHeights(chainSymbol, currentBlock.height, toHeight, readMaxBlocks);
1961
+ this.logger.info(
1962
+ `Chain ${chainSymbol}: Processing blocks between heights ${currentBlock.height} and ${toHeight} as part of dividend calculation`
1963
+ );
1964
+ for (let block of blocksToProcess) {
1965
+ if (block.numberOfTransactions === 0) {
1966
+ continue;
1967
+ }
1968
+ let outboundTxns = await this._getOutboundTransactionsFromBlock(chainSymbol, chainOptions.multisigAddress, block.id);
1969
+ outboundTxns.forEach((txn) => {
1970
+ let contributionList = this._computeContributions(chainSymbol, txn, chainOptions.exchangeFeeRate);
1971
+ contributionList.forEach((contribution) => {
1972
+ if (!contributionData[contribution.walletAddress]) {
1973
+ contributionData[contribution.walletAddress] = 0n;
1974
+ }
1975
+ contributionData[contribution.walletAddress] += BigInt(contribution.amount);
1976
+ });
1977
+ });
1978
+ }
1979
+ currentBlock = blocksToProcess[blocksToProcess.length - 1];
1980
+ }
1981
+ let { memberCount } = this.multisigWalletInfo[chainSymbol];
1982
+ let dividendList = await this.computeDividends({
1983
+ chainSymbol,
1984
+ contributionData,
1985
+ chainOptions,
1986
+ memberCount,
1987
+ fromHeight,
1988
+ toHeight
1989
+ });
1990
+ await Promise.all(
1991
+ dividendList.map(async (dividend) => {
1992
+ let txnAmount = dividend.amount - BigInt(chainOptions.exchangeFeeBase);
1993
+ if (txnAmount <= 0n) {
1994
+ this.logger.debug(
1995
+ `Chain ${chainSymbol}: Skipped dividend distribution to member address ${
1996
+ dividend.walletAddress
1997
+ } because the amount due after fees was less than or equal to 0`
1998
+ );
1999
+ return;
2000
+ }
2001
+ let dividendTxn = {
2002
+ recipientAddress: dividend.walletAddress,
2003
+ amount: txnAmount.toString(),
2004
+ fee: chainOptions.exchangeFeeBase.toString(),
2005
+ timestamp: latestBlockTimestamp,
2006
+ height: chainHeight
2007
+ };
2008
+ let protocolMessage = this._computeProtocolMessage(chainSymbol, 'd1', [fromHeight + 1, toHeight], 'Member dividend');
2009
+ try {
2010
+ await this.execMultisigTransaction(chainSymbol, dividendTxn, protocolMessage);
2011
+ } catch (error) {
2012
+ this.logger.error(
2013
+ `Chain ${chainSymbol}: Failed to post multisig dividend transaction to member address ${dividend.walletAddress} because of error: ${error.message}`
2014
+ );
2015
+ }
2016
+ })
2017
+ );
2018
+ };
2019
+
2020
+ let isInForkRecovery = false;
2021
+
2022
+ let processBlockchains = async () => {
2023
+ if (lastProcessedTimestamp == null) {
2024
+ return 0;
2025
+ }
2026
+ if (isInForkRecovery) {
2027
+ if (this.isForked) {
2028
+ return 0;
2029
+ }
2030
+ if (this.updater.activeUpdate) {
2031
+ // If there was a fork in one of the blockchains during a DEX update,
2032
+ // revert the update. This will cause the module process to restart,
2033
+ // resync from the last safe snapshot and then try to apply the update again.
2034
+ this.logger.error(
2035
+ `DEX module recovered from a blockchain fork while update ${
2036
+ this.updater.activeUpdate.id
2037
+ } was in progress - The incomplete update will be reverted and the DEX module will relaunch and try again`
2038
+ );
2039
+ this.updater.revertActiveUpdate();
2040
+ process.exit();
2041
+ }
2042
+ isInForkRecovery = false;
2043
+ this.pendingTransfers.clear();
2044
+ lastProcessedTimestamp = await this.revertToSafeSnapshot();
2045
+
2046
+ await Promise.all(
2047
+ this.chainSymbols.map(async (chainSymbol) => {
2048
+ let chainCrypto = this.chainCrypto[chainSymbol];
2049
+ if (chainCrypto.reset) {
2050
+ await chainCrypto.reset(lastProcessedTimestamp);
2051
+ }
2052
+ })
2053
+ );
2054
+ }
2055
+ let orderedChainSymbols = [
2056
+ this.baseChainSymbol,
2057
+ this.quoteChainSymbol
2058
+ ];
2059
+
2060
+ let [
2061
+ baseChainLastProcessedHeight,
2062
+ quoteChainLastProcessedHeight,
2063
+ baseChainMaxHeight,
2064
+ quoteChainMaxHeight
2065
+ ] = await Promise.all([
2066
+ ...orderedChainSymbols.map(async (chainSymbol) => {
2067
+ try {
2068
+ let lastProcessedBlock = await this._getLastBlockAtTimestamp(chainSymbol, lastProcessedTimestamp);
2069
+ return lastProcessedBlock.height;
2070
+ } catch (error) {
2071
+ if (error.sourceError && error.sourceError.name === 'BlockDidNotExistError') {
2072
+ return 0;
2073
+ }
2074
+ throw error;
2075
+ }
2076
+ }),
2077
+ ...orderedChainSymbols.map(async (chainSymbol) => this._getMaxBlockHeight(chainSymbol))
2078
+ ]);
2079
+
2080
+ let latestProcessedChainHeights = {
2081
+ [this.baseChainSymbol]: baseChainLastProcessedHeight,
2082
+ [this.quoteChainSymbol]: quoteChainLastProcessedHeight
2083
+ };
2084
+
2085
+ let maxChainHeights = {
2086
+ [this.baseChainSymbol]: baseChainMaxHeight,
2087
+ [this.quoteChainSymbol]: quoteChainMaxHeight
2088
+ };
2089
+
2090
+ let [baseChainBlocks, quoteChainBlocks] = await Promise.all(
2091
+ orderedChainSymbols.map(async (chainSymbol) => {
2092
+ let chainOptions = this.options.chains[chainSymbol];
2093
+ let lastProcessedheight = latestProcessedChainHeights[chainSymbol];
2094
+ let maxBlockHeight = maxChainHeights[chainSymbol];
2095
+ let maxSafeBlockHeight;
2096
+
2097
+ let lastSkippedChainBlock = this.lastSkippedBlocks[chainSymbol];
2098
+ let recentSkippedChainBlock;
2099
+
2100
+ if (
2101
+ lastSkippedChainBlock &&
2102
+ lastSkippedChainBlock.timestamp > lastProcessedTimestamp
2103
+ ) {
2104
+ recentSkippedChainBlock = lastSkippedChainBlock;
2105
+ }
2106
+
2107
+ if (
2108
+ chainSymbol === this.baseChainSymbol &&
2109
+ this.options.dexDisabledFromHeight != null &&
2110
+ maxBlockHeight >= this.options.dexDisabledFromHeight &&
2111
+ maxBlockHeight < this.options.dexDisabledFromHeight + this.options.dexDisabledRefundHeightOffset
2112
+ ) {
2113
+ maxSafeBlockHeight = this.options.dexDisabledFromHeight - 1;
2114
+ } else if (recentSkippedChainBlock) {
2115
+ // Ignore requiredConfirmations if there is a skipped block. The skipped block acts as a checkpoint.
2116
+ maxSafeBlockHeight = maxBlockHeight;
2117
+ } else {
2118
+ maxSafeBlockHeight = maxBlockHeight - chainOptions.requiredConfirmations;
2119
+ }
2120
+
2121
+ if (lastProcessedheight > maxSafeBlockHeight) {
2122
+ return [];
2123
+ }
2124
+
2125
+ let timestampedBlockList = await this._getBlocksBetweenHeights(
2126
+ chainSymbol,
2127
+ lastProcessedheight,
2128
+ maxSafeBlockHeight,
2129
+ chainOptions.readMaxBlocks
2130
+ );
2131
+ let sanitizedBlockList = timestampedBlockList
2132
+ .filter(block => block.timestamp >= lastProcessedTimestamp)
2133
+ .map(block => ({...block, chainSymbol}));
2134
+
2135
+ if (
2136
+ recentSkippedChainBlock &&
2137
+ (
2138
+ !sanitizedBlockList.length ||
2139
+ recentSkippedChainBlock.height === sanitizedBlockList[sanitizedBlockList.length - 1].height + 1
2140
+ )
2141
+ ) {
2142
+ sanitizedBlockList.push({
2143
+ ...recentSkippedChainBlock,
2144
+ chainSymbol,
2145
+ isSkipped: true
2146
+ });
2147
+ }
2148
+
2149
+ return sanitizedBlockList;
2150
+ })
2151
+ );
2152
+
2153
+ if (!baseChainBlocks.length || !quoteChainBlocks.length) {
2154
+ this.logger.debug(
2155
+ `One or more chains had no new blocks - Base chain count: ${
2156
+ baseChainBlocks.length
2157
+ }, quote chain count: ${
2158
+ quoteChainBlocks.length
2159
+ }`
2160
+ );
2161
+ return 0;
2162
+ }
2163
+
2164
+ if (!this._isBlockSequenceValid(baseChainBlocks, baseChainLastProcessedHeight)) {
2165
+ this.logger.error(`The sequence of blocks provided by the ${this.baseChainSymbol} chain was invalid`);
2166
+ return 0;
2167
+ }
2168
+ if (!this._isBlockSequenceValid(quoteChainBlocks, quoteChainLastProcessedHeight)) {
2169
+ this.logger.error(`The sequence of blocks provided by the ${this.quoteChainSymbol} chain was invalid`);
2170
+ return 0;
2171
+ }
2172
+
2173
+ let lastBaseChainBlock = baseChainBlocks[baseChainBlocks.length - 1];
2174
+ let lastQuoteChainBlock = quoteChainBlocks[quoteChainBlocks.length - 1];
2175
+
2176
+ let highestTimestampOfOldestChain = Math.min(lastBaseChainBlock.timestamp, lastQuoteChainBlock.timestamp);
2177
+ while (baseChainBlocks.length > 0 && baseChainBlocks[baseChainBlocks.length - 1].timestamp > highestTimestampOfOldestChain) {
2178
+ baseChainBlocks.pop();
2179
+ }
2180
+ while (quoteChainBlocks.length > 0 && quoteChainBlocks[quoteChainBlocks.length - 1].timestamp > highestTimestampOfOldestChain) {
2181
+ quoteChainBlocks.pop();
2182
+ }
2183
+
2184
+ let orderedBlockList = baseChainBlocks.concat(quoteChainBlocks);
2185
+
2186
+ orderedBlockList.sort((a, b) => {
2187
+ let timestampA = a.timestamp;
2188
+ let timestampB = b.timestamp;
2189
+ if (timestampA < timestampB) {
2190
+ return -1;
2191
+ }
2192
+ if (timestampA > timestampB) {
2193
+ return 1;
2194
+ }
2195
+ if (a.chainSymbol === this.baseChainSymbol) {
2196
+ return -1;
2197
+ }
2198
+ return 1;
2199
+ });
2200
+
2201
+ for (let block of orderedBlockList) {
2202
+ if (isInForkRecovery) {
2203
+ return orderedBlockList.length;
2204
+ }
2205
+ latestProcessedChainHeights[block.chainSymbol] = block.height;
2206
+ try {
2207
+ if (!block.isSkipped) {
2208
+ await processBlock({
2209
+ chainSymbol: block.chainSymbol,
2210
+ chainHeight: block.height,
2211
+ latestChainHeights: {...latestProcessedChainHeights},
2212
+ blockData: {...block}
2213
+ });
2214
+ }
2215
+ if (block.chainSymbol === this.quoteChainSymbol) {
2216
+ lastProcessedTimestamp = block.timestamp;
2217
+ }
2218
+ } catch (error) {
2219
+ this.logger.error(
2220
+ `Encountered the following error while processing block with id ${
2221
+ block.id
2222
+ } on chain ${block.chainSymbol} at height ${block.height}: ${error.stack}`
2223
+ );
2224
+ return orderedBlockList.length;
2225
+ }
2226
+ }
2227
+
2228
+ if (lastQuoteChainBlock.timestamp > highestTimestampOfOldestChain) {
2229
+ lastProcessedTimestamp = highestTimestampOfOldestChain;
2230
+ }
2231
+
2232
+ return orderedBlockList.length;
2233
+ };
2234
+
2235
+ this._processBlockchains = true;
2236
+
2237
+ let startProcessingBlockchains = async () => {
2238
+ while (this._processBlockchains) {
2239
+ let blockCount;
2240
+ try {
2241
+ blockCount = await processBlockchains();
2242
+ } catch (error) {
2243
+ this.logger.error(
2244
+ `Failed to process blockchains because of error: ${error.message}`
2245
+ );
2246
+ blockCount = 0;
2247
+ }
2248
+ if (blockCount <= 0) {
2249
+ await wait(this.options.readBlocksInterval);
2250
+ }
2251
+ }
2252
+ };
2253
+
2254
+ startProcessingBlockchains();
2255
+
2256
+ let progressingChains = {};
2257
+
2258
+ this.chainSymbols.forEach((chainSymbol) => {
2259
+ progressingChains[chainSymbol] = true;
2260
+ });
2261
+
2262
+ let areAllChainsProgressing = () => {
2263
+ return Object.keys(progressingChains).every((chainSymbol) => progressingChains[chainSymbol]);
2264
+ }
2265
+
2266
+ this.chainSymbols.forEach(async (chainSymbol) => {
2267
+ let chainOptions = this.options.chains[chainSymbol];
2268
+ let chainModuleAlias = chainOptions.moduleAlias;
2269
+
2270
+ // This is to detect forks in the underlying blockchains.
2271
+
2272
+ if (chainOptions.useBlocksChangeChannel) {
2273
+ // This approach supports compatibility with older Lisk chain module interface.
2274
+
2275
+ let lastSeenChainHeight = 0;
2276
+
2277
+ channel.subscribe(`${chainModuleAlias}:blocks:change`, async (event) => {
2278
+ let chainHeight = parseInt(event.data.height);
2279
+
2280
+ progressingChains[chainSymbol] = chainHeight > lastSeenChainHeight;
2281
+ lastSeenChainHeight = chainHeight;
2282
+
2283
+ // If starting without a snapshot, use the timestamp of the first new block.
2284
+ if (lastProcessedTimestamp == null) {
2285
+ lastProcessedTimestamp = this._normalizeTimestamp(chainSymbol, parseInt(event.data.timestamp));
2286
+ }
2287
+ if (areAllChainsProgressing()) {
2288
+ this.isForked = false;
2289
+ } else {
2290
+ this.isForked = true;
2291
+ isInForkRecovery = true;
2292
+ }
2293
+ });
2294
+ } else {
2295
+ // This approach is recommended and is the default.
2296
+
2297
+ channel.subscribe(`${chainModuleAlias}:chainChanges`, async (event) => {
2298
+ let type = event.data.type;
2299
+ if (type !== 'addBlock' && type !== 'removeBlock' && type !== 'skipBlock') {
2300
+ return;
2301
+ }
2302
+
2303
+ if (type === 'skipBlock') {
2304
+ let skippedBlock = event.data.block;
2305
+ this.lastSkippedBlocks[chainSymbol] = {
2306
+ timestamp: this._normalizeTimestamp(chainSymbol, parseInt(skippedBlock.timestamp)),
2307
+ height: skippedBlock.height
2308
+ };
2309
+ }
2310
+
2311
+ progressingChains[chainSymbol] = type !== 'removeBlock';
2312
+
2313
+ // If starting without a snapshot, use the timestamp of the first new block.
2314
+ if (lastProcessedTimestamp == null) {
2315
+ lastProcessedTimestamp = this._normalizeTimestamp(chainSymbol, parseInt(event.data.block.timestamp));
2316
+ }
2317
+
2318
+ if (areAllChainsProgressing()) {
2319
+ this.isForked = false;
2320
+ } else {
2321
+ this.isForked = true;
2322
+ isInForkRecovery = true;
2323
+ }
2324
+ });
2325
+ }
2326
+ });
2327
+ channel.publish(`${this.alias}:bootstrap`);
2328
+ }
2329
+
2330
+ _isBlockSequenceValid(blockList, lastProcessedHeight) {
2331
+ let previousHeight = lastProcessedHeight;
2332
+ for (let block of blockList) {
2333
+ if (previousHeight != null && block.height - previousHeight !== 1) {
2334
+ return false;
2335
+ }
2336
+ previousHeight = block.height;
2337
+ }
2338
+ return true;
2339
+ }
2340
+
2341
+ _computeContributions(chainSymbol, transaction, exchangeFeeRate) {
2342
+ transaction = {...transaction};
2343
+ if (!transaction.asset) {
2344
+ transaction.asset = {};
2345
+ }
2346
+ if (!transaction.message) {
2347
+ return [];
2348
+ }
2349
+ let txnData = transaction.message;
2350
+ // Only trade transactions (e.g. t1 and t2) are counted.
2351
+ if (txnData.charAt(0) !== 't') {
2352
+ return [];
2353
+ }
2354
+
2355
+ let feeCalc = this.bigIntFeeCalculators[chainSymbol];
2356
+ let amountBeforeFee = feeCalc.divideBigIntByDecimal(BigInt(transaction.amount), 1 - exchangeFeeRate);
2357
+ let memberSignatures = transaction.signatures || [];
2358
+
2359
+ return memberSignatures.map((signaturePacket) => {
2360
+ let { signerAddress } = signaturePacket;
2361
+ if (!signerAddress) {
2362
+ return null;
2363
+ }
2364
+ return {
2365
+ walletAddress: signerAddress,
2366
+ amount: amountBeforeFee
2367
+ };
2368
+ }).filter(dividend => !!dividend);
2369
+ }
2370
+
2371
+ _isLimitOrderTooSmallToConvert(chainSymbol, amount, price) {
2372
+ if (chainSymbol === this.baseChainSymbol) {
2373
+ let quoteChainValue = this.bigIntPriceCalculator.divideBigIntByDecimal(amount, price);
2374
+ return (
2375
+ quoteChainValue <= this.chainExchangeFeeBases[this.quoteChainSymbol] ||
2376
+ quoteChainValue < this.tradeEngine.quoteMinPartialTake
2377
+ );
2378
+ }
2379
+ let baseChainValue = this.bigIntPriceCalculator.multiplyBigIntByDecimal(amount, price);
2380
+ return (
2381
+ baseChainValue <= this.chainExchangeFeeBases[this.baseChainSymbol] ||
2382
+ baseChainValue < this.tradeEngine.baseMinPartialTake
2383
+ );
2384
+ }
2385
+
2386
+ _isMarketOrderTooSmallToConvert(chainSymbol, amount) {
2387
+ if (chainSymbol === this.baseChainSymbol) {
2388
+ let { price: quoteChainPrice } = this.tradeEngine.peekAsks() || {};
2389
+ let quoteChainValue = this.bigIntPriceCalculator.divideBigIntByDecimal(amount, quoteChainPrice);
2390
+ return (
2391
+ quoteChainValue <= this.chainExchangeFeeBases[this.quoteChainSymbol] ||
2392
+ quoteChainValue < this.tradeEngine.quoteMinPartialTake
2393
+ );
2394
+ }
2395
+ let { price: baseChainPrice } = this.tradeEngine.peekBids() || {};
2396
+ let baseChainValue = this.bigIntPriceCalculator.multiplyBigIntByDecimal(amount, baseChainPrice);
2397
+ return (
2398
+ baseChainValue <= this.chainExchangeFeeBases[this.baseChainSymbol] ||
2399
+ baseChainValue < this.tradeEngine.baseMinPartialTake
2400
+ );
2401
+ }
2402
+
2403
+ _sha1(string) {
2404
+ return crypto.createHash('sha1').update(string).digest('hex');
2405
+ }
2406
+
2407
+ _transactionComparator(a, b) {
2408
+ // The sort order cannot be predicted before the block is forged.
2409
+ if (a.sortKey < b.sortKey) {
2410
+ return -1;
2411
+ }
2412
+ if (a.sortKey > b.sortKey) {
2413
+ return 1;
2414
+ }
2415
+
2416
+ // This should never happen unless there is a hash collision.
2417
+ this.logger.error(
2418
+ `Failed to compare transactions ${
2419
+ a.id
2420
+ } and ${
2421
+ b.id
2422
+ } from block ID ${
2423
+ blockId
2424
+ } because they had the same sortKey - This may lead to nondeterministic output`
2425
+ );
2426
+ return 0;
2427
+ }
2428
+
2429
+ _normalizeListTimestamps(chainSymbol, objectList) {
2430
+ for (let obj of objectList) {
2431
+ obj.timestamp = this._normalizeTimestamp(chainSymbol, obj.timestamp);
2432
+ }
2433
+ }
2434
+
2435
+ _normalizeObjectTimestamp(chainSymbol, obj) {
2436
+ obj.timestamp = this._normalizeTimestamp(chainSymbol, obj.timestamp);
2437
+ }
2438
+
2439
+ // Normalize a timestamp to make it line up with the other chain.
2440
+ _normalizeTimestamp(chainSymbol, timestamp) {
2441
+ if (timestamp == null) {
2442
+ return null;
2443
+ }
2444
+ let transform = this.timestampTransforms[chainSymbol];
2445
+ timestamp = Math.round(timestamp * transform.multiplier);
2446
+ timestamp += transform.offset;
2447
+ return timestamp;
2448
+ }
2449
+
2450
+ // Denormalize a timestamp to put it back in its original state.
2451
+ _denormalizeTimestamp(chainSymbol, timestamp) {
2452
+ if (timestamp == null) {
2453
+ return null;
2454
+ }
2455
+ let transform = this.timestampTransforms[chainSymbol];
2456
+ timestamp -= transform.offset;
2457
+ timestamp = Math.round(timestamp / transform.multiplier);
2458
+ return timestamp;
2459
+ }
2460
+
2461
+ async _getMultisigWalletMembers(chainSymbol, walletAddress) {
2462
+ let chainOptions = this.options.chains[chainSymbol];
2463
+ return this.channel.invoke(`${chainOptions.moduleAlias}:getMultisigWalletMembers`, {walletAddress});
2464
+ }
2465
+
2466
+ async _getMinMultisigRequiredSignatures(chainSymbol, walletAddress) {
2467
+ let chainOptions = this.options.chains[chainSymbol];
2468
+ return this.channel.invoke(`${chainOptions.moduleAlias}:getMinMultisigRequiredSignatures`, {walletAddress});
2469
+ }
2470
+
2471
+ async _getOutboundTransactions(chainSymbol, walletAddress, fromTimestamp, limit) {
2472
+ let chainOptions = this.options.chains[chainSymbol];
2473
+ return this.channel.invoke(`${chainOptions.moduleAlias}:getOutboundTransactions`, {walletAddress, fromTimestamp, limit});
2474
+ }
2475
+
2476
+ async _getInboundTransactionsFromBlock(chainSymbol, walletAddress, blockId) {
2477
+ let chainOptions = this.options.chains[chainSymbol];
2478
+ let txns = await this.channel.invoke(`${chainOptions.moduleAlias}:getInboundTransactionsFromBlock`, {walletAddress, blockId});
2479
+
2480
+ let transactions = txns.map(txn => ({
2481
+ ...txn,
2482
+ sortKey: this._sha1(txn.id + blockId)
2483
+ })).sort((a, b) => this._transactionComparator(a, b));
2484
+
2485
+ this._normalizeListTimestamps(chainSymbol, transactions);
2486
+ return transactions;
2487
+ }
2488
+
2489
+ async _getOutboundTransactionsFromBlock(chainSymbol, walletAddress, blockId) {
2490
+ let chainCache = this.outboundTransactionBlockCaches[chainSymbol];
2491
+ let cacheKey = `${walletAddress},${blockId}`;
2492
+ let cachedTransactions = chainCache.get(cacheKey);
2493
+ if (cachedTransactions) {
2494
+ return cachedTransactions;
2495
+ }
2496
+ let chainOptions = this.options.chains[chainSymbol];
2497
+ let txns = await this.channel.invoke(`${chainOptions.moduleAlias}:getOutboundTransactionsFromBlock`, {walletAddress, blockId});
2498
+
2499
+ let transactions = txns.map(txn => ({
2500
+ ...txn,
2501
+ sortKey: this._sha1(txn.id + blockId)
2502
+ })).sort((a, b) => this._transactionComparator(a, b));
2503
+
2504
+ this._normalizeListTimestamps(chainSymbol, transactions);
2505
+
2506
+ chainCache.set(cacheKey, transactions);
2507
+
2508
+ let cacheSize = chainOptions.outboundTransactionBlockCacheSize == null ?
2509
+ DEFAULT_OUTBOUND_TRANSACTION_BLOCK_CACHE_SIZE : chainOptions.outboundTransactionBlockCacheSize;
2510
+
2511
+ while (chainCache.size > cacheSize) {
2512
+ let nextKey = chainCache.keys().next().value;
2513
+ chainCache.delete(nextKey);
2514
+ }
2515
+ return transactions;
2516
+ }
2517
+
2518
+ async _getLastBlockAtTimestamp(chainSymbol, timestamp) {
2519
+ timestamp = this._denormalizeTimestamp(chainSymbol, timestamp);
2520
+ let chainOptions = this.options.chains[chainSymbol];
2521
+ let block = await this.channel.invoke(`${chainOptions.moduleAlias}:getLastBlockAtTimestamp`, {timestamp});
2522
+ this._normalizeObjectTimestamp(chainSymbol, block);
2523
+ return block;
2524
+ }
2525
+
2526
+ async _getMaxBlockHeight(chainSymbol) {
2527
+ let chainOptions = this.options.chains[chainSymbol];
2528
+ return this.channel.invoke(`${chainOptions.moduleAlias}:getMaxBlockHeight`, {});
2529
+ }
2530
+
2531
+ async _getBlocksBetweenHeights(chainSymbol, fromHeight, toHeight, limit) {
2532
+ let chainOptions = this.options.chains[chainSymbol];
2533
+ let blocks = await this.channel.invoke(`${chainOptions.moduleAlias}:getBlocksBetweenHeights`, {fromHeight, toHeight, limit});
2534
+ this._normalizeListTimestamps(chainSymbol, blocks);
2535
+ return blocks;
2536
+ }
2537
+
2538
+ async _getBlockAtHeight(chainSymbol, height) {
2539
+ let chainOptions = this.options.chains[chainSymbol];
2540
+ let block = await this.channel.invoke(`${chainOptions.moduleAlias}:getBlockAtHeight`, {height});
2541
+ this._normalizeObjectTimestamp(chainSymbol, block);
2542
+ return block;
2543
+ }
2544
+
2545
+ async _getBaseChainBlockTimestamp(height) {
2546
+ let firstBaseChainBlock = await this._getBlockAtHeight(this.baseChainSymbol, height);
2547
+ return firstBaseChainBlock.timestamp;
2548
+ };
2549
+
2550
+ async refundOrderBook(snapshot, timestamp, movedToAddresses) {
2551
+ let allOrders = snapshot.orderBook.bidLimitOrders.concat(snapshot.orderBook.askLimitOrders);
2552
+ await Promise.all(
2553
+ allOrders.map(async (order) => {
2554
+ let movedToAddress = movedToAddresses[order.sourceChain];
2555
+ if (movedToAddress) {
2556
+ let protocolMessage = this._computeProtocolMessage(order.sourceChain, 'r5', [order.id, movedToAddress], 'DEX has moved');
2557
+ await this.refundOrder(
2558
+ order,
2559
+ timestamp,
2560
+ snapshot.chainHeights[order.sourceChain],
2561
+ protocolMessage,
2562
+ {type: 'r5', originOrderId: order.id}
2563
+ );
2564
+ } else {
2565
+ let protocolMessage = this._computeProtocolMessage(order.sourceChain, 'r6', [order.id], 'DEX has been disabled');
2566
+ allOrders.map(async (order) => {
2567
+ await this.refundOrder(
2568
+ order,
2569
+ timestamp,
2570
+ snapshot.chainHeights[order.sourceChain],
2571
+ protocolMessage,
2572
+ {type: 'r6', originOrderId: order.id}
2573
+ );
2574
+ })
2575
+ }
2576
+ })
2577
+ );
2578
+ }
2579
+
2580
+ async refundOrder(order, timestamp, refundHeight, reason, extraTransferData) {
2581
+ let refundTxn = {
2582
+ sourceChain: order.sourceChain,
2583
+ sourceWalletAddress: order.sourceWalletAddress,
2584
+ height: refundHeight
2585
+ };
2586
+ if (order.sourceChain === this.baseChainSymbol) {
2587
+ refundTxn.sourceChainAmount = order.valueRemaining;
2588
+ } else {
2589
+ refundTxn.sourceChainAmount = order.sizeRemaining;
2590
+ }
2591
+ await this.execRefundTransaction(refundTxn, timestamp, reason, extraTransferData);
2592
+ }
2593
+
2594
+ _computeProtocolMessage(chainSymbol, code, args, reasonMessage) {
2595
+ let chainOptions = this.options.chains[chainSymbol];
2596
+ let maxArgLength = chainOptions.protocolMaxArgumentLength || DEFAULT_PROTOCOL_MAX_ARGUMENT_LENGTH;
2597
+ let excludeReason = chainOptions.protocolExcludeReason || DEFAULT_PROTOCOL_EXCLUDE_REASON;
2598
+ let sanitizedArgs = args.map(arg => String(arg).slice(0, maxArgLength));
2599
+ let messageHeaderParts = [code, ...sanitizedArgs];
2600
+ let messageHeader = messageHeaderParts.join(',');
2601
+ let messageParts = [messageHeader];
2602
+ if (!excludeReason && reasonMessage) {
2603
+ messageParts.push(reasonMessage);
2604
+ }
2605
+ return messageParts.join(': ');
2606
+ }
2607
+
2608
+ async execRefundTransaction(txn, timestamp, reason, extraTransferData) {
2609
+ let refundChainOptions = this.options.chains[txn.sourceChain];
2610
+ let refundAmount = txn.sourceChainAmount - BigInt(refundChainOptions.exchangeFeeBase);
2611
+
2612
+ // Refunds do not charge the exchangeFeeRate.
2613
+ if (refundAmount <= 0n) {
2614
+ throw new Error(
2615
+ 'Failed to make refund because amount was less than or equal to 0'
2616
+ );
2617
+ }
2618
+
2619
+ let refundTxn = {
2620
+ recipientAddress: txn.sourceWalletAddress,
2621
+ amount: refundAmount.toString(),
2622
+ fee: refundChainOptions.exchangeFeeBase.toString(),
2623
+ timestamp,
2624
+ height: txn.height
2625
+ };
2626
+ await this.execMultisigTransaction(
2627
+ txn.sourceChain,
2628
+ refundTxn,
2629
+ reason,
2630
+ extraTransferData
2631
+ );
2632
+ }
2633
+
2634
+ // Broadcast the signature to all DEX nodes with a matching baseAddress and quoteAddress
2635
+ async _broadcastSignaturesToSubnet(signatureDataList) {
2636
+ let actionRouteString = `${this.alias}?baseAddress=${this.baseAddress}&quoteAddress=${this.quoteAddress}`;
2637
+ try {
2638
+ await this.channel.invoke('network:emit', {
2639
+ event: `${actionRouteString}:signatures`,
2640
+ data: signatureDataList
2641
+ });
2642
+ } catch (error) {
2643
+ this.logger.error(
2644
+ `Error encountered while attempting to broadcast signatures to the network - ${error.message}`
2645
+ );
2646
+ }
2647
+ }
2648
+
2649
+ async execMultisigTransaction(targetChain, transactionData, message, extraTransferData) {
2650
+ let chainTimestamp = this._denormalizeTimestamp(targetChain, transactionData.timestamp);
2651
+ let chainCrypto = this.chainCrypto[targetChain];
2652
+ let {
2653
+ transaction: preparedTxn,
2654
+ signature: multisigSignaturePacket
2655
+ } = await chainCrypto.prepareTransaction({
2656
+ recipientAddress: transactionData.recipientAddress,
2657
+ amount: transactionData.amount,
2658
+ fee: transactionData.fee,
2659
+ timestamp: chainTimestamp,
2660
+ message
2661
+ });
2662
+
2663
+ let processedSignerAddressSet = new Set([multisigSignaturePacket.signerAddress]);
2664
+ preparedTxn.signatures.push(multisigSignaturePacket);
2665
+
2666
+ // If the pendingTransfers map already has a transaction with the specified id, delete the existing entry so
2667
+ // that when it is re-inserted, it will be added at the end of the queue.
2668
+ // To perform expiry using an iterator, it's essential that the insertion order is maintained.
2669
+ if (this.pendingTransfers.has(preparedTxn.id)) {
2670
+ this.pendingTransfers.delete(preparedTxn.id);
2671
+ }
2672
+ let transfer = {
2673
+ id: preparedTxn.id,
2674
+ transaction: preparedTxn,
2675
+ recipientAddress: transactionData.recipientAddress,
2676
+ targetChain,
2677
+ processedSignerAddressSet,
2678
+ height: transactionData.height,
2679
+ timestamp: Date.now(),
2680
+ ...extraTransferData
2681
+ };
2682
+ this.pendingTransfers.set(preparedTxn.id, transfer);
2683
+ }
2684
+
2685
+ _getUpdateSnapshotFilePath(updateId) {
2686
+ return path.join(this.orderBookUpdateSnapshotDirPath, `snapshot-${updateId}.json`);
2687
+ }
2688
+
2689
+ async loadSnapshot() {
2690
+ let serializedSafeSnapshot = await readFile(
2691
+ this.orderBookSnapshotFilePath,
2692
+ {encoding: 'utf8'}
2693
+ );
2694
+ let safeSnapshot = JSON.parse(serializedSafeSnapshot);
2695
+ let snapshot;
2696
+
2697
+ if (this.updater.activeUpdate) {
2698
+ let updateSnapshotFilePath = this._getUpdateSnapshotFilePath(this.updater.activeUpdate.id);
2699
+ let serializedUpdateSnapshot = await readFile(
2700
+ updateSnapshotFilePath,
2701
+ {encoding: 'utf8'}
2702
+ );
2703
+ snapshot = JSON.parse(serializedUpdateSnapshot);
2704
+ } else {
2705
+ snapshot = safeSnapshot;
2706
+ }
2707
+
2708
+ snapshot.orderBook.askLimitOrders.forEach((order) => {
2709
+ if (order.orderId != null) {
2710
+ order.id = order.orderId;
2711
+ delete order.orderId;
2712
+ }
2713
+ });
2714
+ snapshot.orderBook.bidLimitOrders.forEach((order) => {
2715
+ if (order.orderId != null) {
2716
+ order.id = order.orderId;
2717
+ delete order.orderId;
2718
+ }
2719
+ });
2720
+ this.lastSnapshot = safeSnapshot;
2721
+ this.tradeEngine.setSnapshot(snapshot.orderBook);
2722
+ let baseChainHeight = snapshot.chainHeights[this.baseChainSymbol];
2723
+ return this._getBaseChainBlockTimestamp(baseChainHeight);
2724
+ }
2725
+
2726
+ async revertToSafeSnapshot() {
2727
+ if (this.finalizedSnapshot) {
2728
+ this.lastSnapshot = this.finalizedSnapshot;
2729
+ }
2730
+ if (!this.lastSnapshot) {
2731
+ this.tradeEngine.clear();
2732
+ return;
2733
+ }
2734
+ this.tradeEngine.setSnapshot(this.lastSnapshot.orderBook);
2735
+ let baseChainHeight = this.lastSnapshot.chainHeights[this.baseChainSymbol];
2736
+ return this._getBaseChainBlockTimestamp(baseChainHeight);
2737
+ }
2738
+
2739
+ async saveSnapshot(snapshot, filePath) {
2740
+ if (filePath == null) {
2741
+ filePath = this.orderBookSnapshotFilePath;
2742
+ }
2743
+ this.finalizedSnapshot = snapshot;
2744
+ let baseChainHeight = snapshot.chainHeights[this.baseChainSymbol];
2745
+ let serializedSnapshot = JSON.stringify(snapshot);
2746
+ await writeFile(filePath, serializedSnapshot);
2747
+
2748
+ try {
2749
+ await writeFile(
2750
+ path.join(
2751
+ this.orderBookSnapshotBackupDirPath,
2752
+ `snapshot-${baseChainHeight}.json`
2753
+ ),
2754
+ serializedSnapshot
2755
+ );
2756
+ let allSnapshots = await readdir(this.orderBookSnapshotBackupDirPath);
2757
+ let heightRegex = /[0-9]+/g;
2758
+ allSnapshots.sort((a, b) => {
2759
+ let snapshotHeightA = parseInt(a.match(heightRegex)[0] || 0);
2760
+ let snapshotHeightB = parseInt(b.match(heightRegex)[0] || 0);
2761
+ if (snapshotHeightA > snapshotHeightB) {
2762
+ return -1;
2763
+ }
2764
+ if (snapshotHeightA < snapshotHeightB) {
2765
+ return 1;
2766
+ }
2767
+ return 0;
2768
+ });
2769
+ let snapshotsToDelete = allSnapshots.slice(this.options.orderBookSnapshotBackupMaxCount || 200, allSnapshots.length);
2770
+ await Promise.all(
2771
+ snapshotsToDelete.map(async (fileName) => {
2772
+ await unlink(
2773
+ path.join(this.orderBookSnapshotBackupDirPath, fileName)
2774
+ );
2775
+ })
2776
+ );
2777
+ } catch (error) {
2778
+ this.logger.error(
2779
+ `Failed to backup snapshot in directory ${
2780
+ this.orderBookSnapshotBackupDirPath
2781
+ } because of error: ${
2782
+ error.message
2783
+ }`
2784
+ );
2785
+ }
2786
+ }
2787
+
2788
+ async unload() {
2789
+ this._processBlockchains = false;
2790
+ clearInterval(this._multisigExpiryInterval);
2791
+ clearInterval(this._multisigFlushInterval);
2792
+ clearInterval(this._signatureFlushInterval);
2793
+ clearInterval(this._tradeHistoryUpdateInterval);
2794
+ await Promise.all(
2795
+ this.chainSymbols.map(async (chainSymbol) => {
2796
+ return this.chainCrypto[chainSymbol].unload();
2797
+ })
2798
+ );
2799
+ }
2800
+ };
2801
+
2802
+ function wait(duration) {
2803
+ return new Promise((resolve) => {
2804
+ setTimeout(resolve, duration);
2805
+ });
2806
+ }