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/LICENSE +675 -0
- package/README.md +89 -0
- package/big-int-calculator.js +30 -0
- package/defaults/config.js +39 -0
- package/index.js +2806 -0
- package/package.json +32 -0
- package/test/trade-engine.js +563 -0
- package/trade-engine.js +400 -0
- package/utils.js +16 -0
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}"eAddress=${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
|
+
}
|