@hackerhouse/xpub-scan 1.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.
Files changed (83) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/CONTRIBUTING.md +130 -0
  3. package/LICENSE +23 -0
  4. package/README.md +215 -0
  5. package/__tests__/checkAddresses/checkBitcoinAddressesNegative.test.ts +75 -0
  6. package/__tests__/checkAddresses/checkBitcoinAddressesPositive.test.ts +87 -0
  7. package/__tests__/checkAddresses/checkDogeAddressesPositive.test.ts +43 -0
  8. package/__tests__/checkAddresses/checkLitecoinAddressesNegative.test.ts +75 -0
  9. package/__tests__/checkAddresses/checkLitecoinAddressesPositive.test.ts +87 -0
  10. package/__tests__/checkModels/address.test.ts +183 -0
  11. package/__tests__/checkModels/fakeRawTransactions.json +182 -0
  12. package/__tests__/deriveAddresses/deriveBitcoinAddresses.test.ts +207 -0
  13. package/__tests__/deriveAddresses/deriveBitcoinCashAddresses.test copy.ts +79 -0
  14. package/__tests__/deriveAddresses/deriveDogecoinAddresses.test.ts +43 -0
  15. package/__tests__/deriveAddresses/deriveEthereumAddresses.test.ts +26 -0
  16. package/__tests__/deriveAddresses/deriveLitecoinAddresses.test.ts +110 -0
  17. package/__tests__/helpers.test.ts +274 -0
  18. package/__tests__/test-utils.ts +3 -0
  19. package/babel.config.js +6 -0
  20. package/jest.config.ts +5 -0
  21. package/ledgerhq-xpub-scan-1.0.4.tgz +0 -0
  22. package/lib/actions/checkAddress.d.ts +29 -0
  23. package/lib/actions/checkAddress.js +122 -0
  24. package/lib/actions/checkBalance.d.ts +20 -0
  25. package/lib/actions/checkBalance.js +300 -0
  26. package/lib/actions/deriveAddresses.d.ts +17 -0
  27. package/lib/actions/deriveAddresses.js +239 -0
  28. package/lib/actions/processTransactions.d.ts +29 -0
  29. package/lib/actions/processTransactions.js +289 -0
  30. package/lib/actions/saveAnalysis.d.ts +2 -0
  31. package/lib/actions/saveAnalysis.js +800 -0
  32. package/lib/actions/scanner.d.ts +15 -0
  33. package/lib/actions/scanner.js +152 -0
  34. package/lib/api/customProvider.d.ts +19 -0
  35. package/lib/api/customProvider.js +434 -0
  36. package/lib/api/defaultProvider.d.ts +23 -0
  37. package/lib/api/defaultProvider.js +275 -0
  38. package/lib/comparison/compareOperations.d.ts +13 -0
  39. package/lib/comparison/compareOperations.js +500 -0
  40. package/lib/comparison/diffs.d.ts +18 -0
  41. package/lib/comparison/diffs.js +70 -0
  42. package/lib/configuration/currencies.d.ts +55 -0
  43. package/lib/configuration/currencies.js +72 -0
  44. package/lib/configuration/settings.d.ts +51 -0
  45. package/lib/configuration/settings.js +113 -0
  46. package/lib/display.d.ts +12 -0
  47. package/lib/display.js +251 -0
  48. package/lib/helpers.d.ts +27 -0
  49. package/lib/helpers.js +255 -0
  50. package/lib/input/args.d.ts +6 -0
  51. package/lib/input/args.js +129 -0
  52. package/lib/input/check.d.ts +6 -0
  53. package/lib/input/check.js +217 -0
  54. package/lib/input/importOperations.d.ts +11 -0
  55. package/lib/input/importOperations.js +406 -0
  56. package/lib/models/address.d.ts +40 -0
  57. package/lib/models/address.js +101 -0
  58. package/lib/models/comparison.d.ts +8 -0
  59. package/lib/models/comparison.js +6 -0
  60. package/lib/models/currency.d.ts +11 -0
  61. package/lib/models/currency.js +6 -0
  62. package/lib/models/operation.d.ts +33 -0
  63. package/lib/models/operation.js +80 -0
  64. package/lib/models/ownAddresses.d.ts +11 -0
  65. package/lib/models/ownAddresses.js +31 -0
  66. package/lib/models/scanLimits.d.ts +7 -0
  67. package/lib/models/scanLimits.js +6 -0
  68. package/lib/models/stats.d.ts +7 -0
  69. package/lib/models/stats.js +6 -0
  70. package/lib/models/transaction.d.ts +10 -0
  71. package/lib/models/transaction.js +13 -0
  72. package/lib/scan.d.ts +2 -0
  73. package/lib/scan.js +31 -0
  74. package/lib/templates/logos.base64.d.ts +2 -0
  75. package/lib/templates/logos.base64.js +9 -0
  76. package/lib/templates/report.html.d.ts +1 -0
  77. package/lib/templates/report.html.js +393 -0
  78. package/lib/tsconfig.tsbuildinfo +1 -0
  79. package/lib/types.d.ts +55 -0
  80. package/lib/types.js +2 -0
  81. package/npm-shrinkwrap.json +12323 -0
  82. package/package.json +81 -0
  83. package/sonar-project.properties +15 -0
@@ -0,0 +1,500 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkImportedOperations = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const settings_1 = require("../configuration/settings");
9
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
10
+ /**
11
+ * Sort by amount and, then, if needed, by address
12
+ * @param {Operation} A
13
+ * The first operation to compare
14
+ * @param {Operation} B
15
+ * The second operation to compare
16
+ * @returns number
17
+ * -1 if A > B
18
+ * 1 if A < B
19
+ * 0 if A == B
20
+ */
21
+ const compareOps = (A, B) => {
22
+ // date
23
+ if (A.date > B.date) {
24
+ return -1;
25
+ }
26
+ if (A.date < B.date) {
27
+ return 1;
28
+ }
29
+ // amount
30
+ if (A.amount > B.amount) {
31
+ return -1;
32
+ }
33
+ if (A.amount < B.amount) {
34
+ return 1;
35
+ }
36
+ // address
37
+ if (A.address > B.address) {
38
+ return -1;
39
+ }
40
+ if (A.address < B.address) {
41
+ return 1;
42
+ }
43
+ return 0;
44
+ };
45
+ /**
46
+ * Check whether imported and actual operation are matching or not
47
+ * @param {Operation} importedOperation
48
+ * An imported operation
49
+ * @param {Operation} actualOperation
50
+ * An actual operation
51
+ * @returns boolean
52
+ * `true` if operations are matching
53
+ * `false` if operations are not matching
54
+ */
55
+ const areMatching = (importedOperation, actualOperation) => {
56
+ // ┏━━━━━━━━━━━━━━━━━━━━━┓
57
+ // ┃ 1 | CHECK ADDRESSES ┃
58
+ // ┗━━━━━━━━━━━━━━━━━━━━━┛
59
+ var _a, _b, _c;
60
+ // 1. Check addresses (general case)
61
+ // only check if imported address is set (not always the case: Live Desktpop CSVs)
62
+ // besides, imported address can be a superset of actual address as the
63
+ // imported operation can have several addresses; therefore, `includes` has to
64
+ // be used
65
+ let mismatchingAddresses = false;
66
+ const importedAddress = (_a = importedOperation.getAddress()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
67
+ const actualAddress = actualOperation.getAddress().toLowerCase();
68
+ if (importedAddress &&
69
+ !importedAddress.includes(actualAddress) &&
70
+ !actualAddress.includes(importedAddress)) {
71
+ mismatchingAddresses = true;
72
+ }
73
+ // 1b. Check addresses (Bitcoin Cash)
74
+ const importedCashAddress = (_b = importedOperation.getCashAddress()) === null || _b === void 0 ? void 0 : _b.toLowerCase();
75
+ const actualCashAddress = (_c = actualOperation.getCashAddress()) === null || _c === void 0 ? void 0 : _c.toLowerCase();
76
+ if (importedCashAddress &&
77
+ actualCashAddress &&
78
+ !importedCashAddress.includes(actualCashAddress) &&
79
+ !actualCashAddress.includes(importedCashAddress)) {
80
+ mismatchingAddresses = true;
81
+ }
82
+ if (mismatchingAddresses) {
83
+ return "Mismatch: addresses";
84
+ }
85
+ // ┏━━━━━━━━━━━━━━━━━━━┓
86
+ // ┃ 2 | CHECK AMOUNTS ┃
87
+ // ┗━━━━━━━━━━━━━━━━━━━┛
88
+ // Note: absolute values are compared because one of the amounts can be negative (i.e., swap)
89
+ if (!importedOperation.amount
90
+ .absoluteValue()
91
+ .isEqualTo(actualOperation.amount.absoluteValue())) {
92
+ return "Mismatch: amounts";
93
+ }
94
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
95
+ // ┃ 3 | CHECK TOKENS (OPTIONAL) ┃
96
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
97
+ // 3. (if applicable: augmented mode) check tokens
98
+ const importedToken = importedOperation.token;
99
+ const actualToken = actualOperation.token;
100
+ if (typeof importedToken !== "undefined" &&
101
+ typeof actualToken !== "undefined") {
102
+ if (!importedToken.amount.isEqualTo(actualToken.amount)) {
103
+ return "Mismatch: token amounts";
104
+ }
105
+ if (importedToken.symbol.toLocaleLowerCase() !==
106
+ actualToken.symbol.toLocaleLowerCase()) {
107
+ return "Mismatch: token tickers";
108
+ }
109
+ }
110
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
111
+ // ┃ 4 | CHECK DAPPS (OPTIONAL) ┃
112
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
113
+ // 4. (if applicable: augmented mode) check dapp
114
+ const importedDapp = importedOperation.dapp;
115
+ const actualDapp = actualOperation.token; // currently, as far as the external provider is concerned, token == Dapp
116
+ if (typeof importedDapp !== "undefined" &&
117
+ typeof actualDapp !== "undefined") {
118
+ if (importedDapp.contract_name.toLocaleLowerCase() !==
119
+ actualDapp.name.toLocaleLowerCase()) {
120
+ return "Mismatch: Dapp";
121
+ }
122
+ }
123
+ return "Match";
124
+ };
125
+ /**
126
+ * Make addresses displayable
127
+ * @param {string} address
128
+ * An address to display
129
+ * @returns string
130
+ * empty string if no address
131
+ * partial address + ellipsis if long address
132
+ * the address itself otherwise
133
+ */
134
+ const renderAddress = (address) => {
135
+ const maxLength = 35;
136
+ if (!address) {
137
+ return "".padEnd(maxLength + 4, " ");
138
+ }
139
+ if (address.length < maxLength) {
140
+ return address.padEnd(maxLength + 4, " ");
141
+ }
142
+ return address
143
+ .substring(0, maxLength - 3)
144
+ .concat("...")
145
+ .padEnd(maxLength + 4, " ");
146
+ };
147
+ /**
148
+ * Display the comparison between two operations or, if an operation
149
+ * is missing, display one operation with the indication that one operation
150
+ * is missing
151
+ * @param {ComparisonStatus} status
152
+ * Status of the comparison (Match, Mismatch, etc.)
153
+ * @param {Operation} A
154
+ * An operation
155
+ * @param {Operation} B?
156
+ * An optional operation
157
+ * @returns void
158
+ */
159
+ const showOperations = (status, A, B) => {
160
+ var _a, _b;
161
+ if (settings_1.configuration.silent) {
162
+ return;
163
+ }
164
+ const halfColorPadding = 84;
165
+ const fullColorPadding = 85;
166
+ let imported = "";
167
+ let actual = "";
168
+ switch (status) {
169
+ case "Match":
170
+ /* fallthrough */
171
+ case (_a = status.match(/^Mismatch.*/)) === null || _a === void 0 ? void 0 : _a.input:
172
+ /* fallthrough */
173
+ imported = A.date
174
+ .padEnd(24, " ")
175
+ .concat(renderAddress(A.address))
176
+ .concat(String(A.amount));
177
+ if (B) {
178
+ actual = B.date
179
+ .padEnd(24, " ")
180
+ .concat(renderAddress(B.address))
181
+ .concat(String(B.amount));
182
+ }
183
+ break;
184
+ case "Extra Operation":
185
+ actual = "(missing operation)";
186
+ imported = A.date
187
+ .padEnd(24, " ")
188
+ .concat(renderAddress(A.address))
189
+ .concat(String(A.amount));
190
+ break;
191
+ case "Missing Operation":
192
+ imported = "(missing operation)";
193
+ actual = A.date
194
+ .padEnd(24, " ")
195
+ .concat(renderAddress(A.address))
196
+ .concat(String(A.amount));
197
+ break;
198
+ case "Missing (aggregated)":
199
+ imported = "(aggregated operation)";
200
+ actual = A.date
201
+ .padEnd(24, " ")
202
+ .concat(renderAddress(A.address))
203
+ .concat(String(A.amount));
204
+ break;
205
+ }
206
+ if (A.operationType === "Failed to send" ||
207
+ (B === null || B === void 0 ? void 0 : B.operationType) === "Failed to send") {
208
+ actual = chalk_1.default.blueBright(actual.concat("\t[failed]"));
209
+ }
210
+ if (A.operationType.includes("token") || (B === null || B === void 0 ? void 0 : B.operationType.includes("token"))) {
211
+ actual = chalk_1.default.white(actual.concat("\t[token]"));
212
+ }
213
+ if (A.operationType.includes("dapp") || (B === null || B === void 0 ? void 0 : B.operationType.includes("dapp"))) {
214
+ actual = chalk_1.default.white(actual.concat("\t[dapp]"));
215
+ }
216
+ if (A.operationType.includes("SCI") || (B === null || B === void 0 ? void 0 : B.operationType.includes("SCI"))) {
217
+ actual = chalk_1.default.white(actual.concat("\t[sci]"));
218
+ }
219
+ if (A.operationType.includes("Swapped") ||
220
+ (B === null || B === void 0 ? void 0 : B.operationType.includes("Swapped"))) {
221
+ actual = chalk_1.default.white(actual.concat("\t[swap]"));
222
+ }
223
+ switch (status) {
224
+ case "Match":
225
+ console.log(chalk_1.default.greenBright(imported.padEnd(halfColorPadding, " ")), actual);
226
+ break;
227
+ case "Match (aggregated)":
228
+ /* fallthrough */
229
+ case "Missing (aggregated)":
230
+ console.log(chalk_1.default.green(imported.padEnd(halfColorPadding, " ")), actual);
231
+ break;
232
+ case (_b = status.match(/^Mismatch.*/)) === null || _b === void 0 ? void 0 : _b.input:
233
+ /* fallthrough */
234
+ case "Missing Operation":
235
+ /* fallthrough */
236
+ case "Extra Operation":
237
+ console.log(chalk_1.default.redBright(imported.padEnd(fullColorPadding, " ").concat(actual)));
238
+ break;
239
+ }
240
+ };
241
+ /**
242
+ * Check whether operations are aggregated or not
243
+ * @param {Operation} importedOp
244
+ * An imported operation
245
+ * @param {Array<Operation>} actualOps
246
+ * List of actual operations
247
+ * @returns boolean
248
+ * `true` if operations are aggregated
249
+ * `false` if operations are not aggregated
250
+ */
251
+ const areAggregated = (importedOp, actualOps) => {
252
+ if (typeof importedOp === "undefined") {
253
+ return false;
254
+ }
255
+ const actual = actualOps.filter((op) => op.txid === importedOp.txid);
256
+ if (actual.length < 2) {
257
+ return false;
258
+ }
259
+ let actualAmountTotal = new bignumber_js_1.default(0);
260
+ for (const actualOp of actualOps) {
261
+ actualAmountTotal = actualAmountTotal.plus(actualOp.amount);
262
+ }
263
+ const importedAmount = new bignumber_js_1.default(importedOp.amount);
264
+ return actualAmountTotal.isEqualTo(importedAmount);
265
+ };
266
+ /**
267
+ * Compare the imported operations with the actual ones
268
+ * @param importedOperations operations from the product
269
+ * @param actualOperations operations from the provider (source of truth)
270
+ * @param actualAddresses actual addresses
271
+ * @param partialComparison (optional) partial comparison
272
+ * @returns list of comparisons
273
+ */
274
+ const checkImportedOperations = (importedOperations, actualOperations, actualAddresses, partialComparison) => {
275
+ if (!settings_1.configuration.silent) {
276
+ console.log(chalk_1.default.bold.whiteBright("\nComparison between imported and actual operations\n"));
277
+ console.log(chalk_1.default.grey("imported operations" + "\t".repeat(8) + " actual operations"));
278
+ }
279
+ const allComparingCriteria = [];
280
+ const comparisons = [];
281
+ const blockHeightUpperLimit = settings_1.configuration.blockHeightUpperLimit;
282
+ // filter imported operations if scan is limited (range scan)
283
+ if (partialComparison) {
284
+ const rangeAddresses = actualAddresses.map((address) => address.toString());
285
+ importedOperations = importedOperations.filter((op) => op.address.split(",").find((a) => rangeAddresses.includes(a)));
286
+ }
287
+ // create a list of comparing criterion containing all elements that can be used
288
+ // to compare transactions. That is: date, txid, and/or block number
289
+ importedOperations.concat(actualOperations).forEach((op) => {
290
+ if (!allComparingCriteria.some((t) => t.hash === op.txid)) {
291
+ // (ignore duplicates)
292
+ allComparingCriteria.push({
293
+ date: op.date,
294
+ hash: op.txid,
295
+ block: op.block,
296
+ });
297
+ }
298
+ });
299
+ // sort by reverse chronological order
300
+ allComparingCriteria.sort((a, b) => (a.date > b.date ? -1 : 1));
301
+ for (const comparingCriterion of allComparingCriteria) {
302
+ let importedOps;
303
+ let actualOps;
304
+ if (importedOperations.some((op) => typeof op.txid !== "undefined")) {
305
+ // case 1. tx id is set
306
+ importedOps = importedOperations.filter((op) => op.txid === comparingCriterion.hash);
307
+ actualOps = actualOperations.filter((op) => op.txid === comparingCriterion.hash);
308
+ }
309
+ else {
310
+ // case 2. tx id is NOT set: compare by block number instead
311
+ importedOps = importedOperations.filter((op) => op.block === comparingCriterion.block);
312
+ actualOps = actualOperations.filter((op) => op.block === comparingCriterion.block);
313
+ }
314
+ // the imported operations can have multiple concatenated addresses
315
+ // that have to be reduced to only one:
316
+ // the one corresponding to that of an actual operation from the same
317
+ // block and with the same amount
318
+ for (const imported of importedOps) {
319
+ // do not continue if no address (see: Live Desktop CSVs): not relevant
320
+ if (!imported.address) {
321
+ break;
322
+ }
323
+ // the actual operation address must already be present in the
324
+ // imported operations' addresses and the amount must match
325
+ const actual = actualOperations.filter((op) => imported.address.includes(op.address) &&
326
+ op.amount === imported.amount);
327
+ if (Object.keys(actual).length === 1) {
328
+ imported.setAddress(actual[0].address);
329
+ }
330
+ }
331
+ // sort I (common case):
332
+ // compare by date, amount, address
333
+ importedOps.sort(compareOps);
334
+ actualOps.sort(compareOps);
335
+ // sort II (edge case):
336
+ // sort operations with same txid, same date, and same amount
337
+ // that cannot be sorted by address
338
+ for (const criterion of allComparingCriteria) {
339
+ const imported = importedOps.filter((op) => op.txid === criterion.hash);
340
+ // if only one imported operation have this txid, skip...
341
+ if (imported.length < 2 || typeof criterion.hash === "undefined") {
342
+ continue;
343
+ }
344
+ // ... otherwise, sort the imported operations
345
+ for (const importedOp of imported) {
346
+ for (let i = 0; i < actualOps.length; ++i) {
347
+ // if an actual operation is having the same txid,
348
+ // but the operation types or addresses differ...
349
+ if (
350
+ // [ an actual operation has the same txid,
351
+ (actualOps[i].txid === importedOp.txid &&
352
+ // and:
353
+ // 1. the operation type is the same {use of
354
+ // `startsWith` as the actual operation types are
355
+ // a superset of imported operation types:
356
+ // - Send (to self, to sibling);
357
+ // - Received ((non-sibling to change))},
358
+ !actualOps[i]
359
+ .getOperationType()
360
+ .startsWith(importedOp.getOperationType())) ||
361
+ // and
362
+ // 2. the imported operation does include the actual
363
+ // address (`includes`: imported addresses can be aggregated) ]
364
+ (importedOp.address &&
365
+ !importedOp.address.includes(actualOps[i].address))) {
366
+ // ... then swap it with first actual operation
367
+ [actualOps[0], actualOps[i]] = [actualOps[i], actualOps[0]];
368
+ break;
369
+ }
370
+ }
371
+ }
372
+ }
373
+ const aggregatedTxids = [];
374
+ // Math.max(...) used here because the imported and actual arrays do not
375
+ // necessarily have the same size (i.e. missing operations)
376
+ for (let i = 0; i < Math.max(importedOps.length, actualOps.length); ++i) {
377
+ const importedOp = importedOps[i];
378
+ const actualOp = actualOps[i];
379
+ // aggregated operations
380
+ // (fixes https://github.com/LedgerHQ/xpub-scan/issues/23)
381
+ if (typeof actualOp !== "undefined" &&
382
+ (aggregatedTxids.includes(actualOp.txid) ||
383
+ areAggregated(importedOp, actualOps))) {
384
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
385
+ // ┃ CASE 1 | AGGREGATED OPERATIONS ┃
386
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
387
+ aggregatedTxids.push(actualOp.txid);
388
+ if (typeof importedOp !== "undefined") {
389
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
390
+ // ┃ CASE 1A | MATCHING AGGREGATED OPERATIONS ┃
391
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
392
+ showOperations("Match (aggregated)", importedOp, actualOp);
393
+ comparisons.push({
394
+ imported: importedOp,
395
+ actual: actualOp,
396
+ status: "Match (aggregated)",
397
+ });
398
+ }
399
+ else {
400
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
401
+ // ┃ CASE 1B | MISSING AGGREGATED OPERATIONS ┃
402
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
403
+ showOperations("Missing (aggregated)", actualOp);
404
+ comparisons.push({
405
+ imported: importedOp,
406
+ actual: actualOp,
407
+ status: "Missing (aggregated)",
408
+ });
409
+ }
410
+ continue;
411
+ }
412
+ // actual operation with no corresponding imported operation
413
+ if (typeof importedOp === "undefined") {
414
+ // if the block height upper limit is reached, skip the comparison...
415
+ if (blockHeightUpperLimit > 0 &&
416
+ actualOp.getBlockNumber() > blockHeightUpperLimit) {
417
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
418
+ // ┃ CASE 2 | SKIPPED OPERATIONS (BLOCK HEIGHT UPPER LIMIT MODE) ┃
419
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
420
+ comparisons.push({
421
+ imported: undefined,
422
+ actual: actualOp,
423
+ status: "Skipped",
424
+ });
425
+ continue;
426
+ }
427
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
428
+ // ┃ CASE 3 | MISSING OPERATION ┃
429
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
430
+ // ...else, this is a missing operation
431
+ showOperations("Missing Operation", actualOp);
432
+ comparisons.push({
433
+ imported: undefined,
434
+ actual: actualOp,
435
+ status: "Missing Operation",
436
+ });
437
+ continue;
438
+ }
439
+ // imported operation with no corresponding actual operation
440
+ if (typeof actualOp === "undefined") {
441
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓
442
+ // ┃ CASE 4 | EXTRA OPERATION ┃
443
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛
444
+ showOperations("Extra Operation", importedOp);
445
+ comparisons.push({
446
+ imported: importedOp,
447
+ actual: undefined,
448
+ status: "Extra Operation",
449
+ });
450
+ continue;
451
+ }
452
+ const comparisonResult = areMatching(importedOp, actualOp);
453
+ if (comparisonResult !== "Match") {
454
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
455
+ // ┃ CASE 5 | MISMATCHING OPERATIONS ┃
456
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
457
+ showOperations(comparisonResult, importedOp, actualOp);
458
+ comparisons.push({
459
+ imported: importedOp,
460
+ actual: actualOp,
461
+ status: comparisonResult,
462
+ });
463
+ }
464
+ else {
465
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
466
+ // ┃ CASE 6 | MATCHING OPERATIONS ┃
467
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
468
+ showOperations("Match", importedOp, actualOp);
469
+ comparisons.push({
470
+ imported: importedOp,
471
+ actual: actualOp,
472
+ status: "Match",
473
+ });
474
+ }
475
+ }
476
+ }
477
+ // Sort the comparisons
478
+ // 1. Split the comparisons in two segments:
479
+ // first segment (can be empty): skipped comparisons
480
+ // second segment: unskipped comparisons
481
+ const skippedComparisons = [];
482
+ for (let i = comparisons.length - 1; i >= 0; i--) {
483
+ const comparison = comparisons[i];
484
+ if (comparison.status === "Skipped") {
485
+ skippedComparisons.push(comparison);
486
+ comparisons.splice(i, 1);
487
+ }
488
+ }
489
+ // 2. Sort the skipped comparisons by date
490
+ skippedComparisons.sort((a, b) => {
491
+ return a.actual.date > b.actual.date ? -1 : 1;
492
+ });
493
+ // 3. Merge the two segments into one.
494
+ // The comparisons are then sorted this way:
495
+ // first: all skipped comparisons, sorted by date
496
+ // second: all unskipped comparisons, sorted following the
497
+ // imported operations ordering
498
+ return skippedComparisons.concat(comparisons);
499
+ };
500
+ exports.checkImportedOperations = checkImportedOperations;
@@ -0,0 +1,18 @@
1
+ import { Comparison } from "../models/comparison";
2
+ /**
3
+ * Show differences between imported and actual data
4
+ * @param {number} actualBalance
5
+ * Actual balance
6
+ * @param {number} importedBalance?
7
+ * Optional imported balance
8
+ * @param {Array<Comparison>} comparisons?
9
+ * Optional list of comparisons
10
+ * @param {boolean} diff?
11
+ * Optional diff boolean
12
+ * @returns number
13
+ * An exist code:
14
+ * - zero if no diff
15
+ * - non-zero otherwise
16
+ */
17
+ declare const showDiff: (actualBalance: number, importedBalance?: string, comparisons?: Array<Comparison>, diff?: boolean) => number;
18
+ export { showDiff };
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.showDiff = void 0;
7
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const helpers_1 = require("../helpers");
10
+ /**
11
+ * Show differences between imported and actual data
12
+ * @param {number} actualBalance
13
+ * Actual balance
14
+ * @param {number} importedBalance?
15
+ * Optional imported balance
16
+ * @param {Array<Comparison>} comparisons?
17
+ * Optional list of comparisons
18
+ * @param {boolean} diff?
19
+ * Optional diff boolean
20
+ * @returns number
21
+ * An exist code:
22
+ * - zero if no diff
23
+ * - non-zero otherwise
24
+ */
25
+ const showDiff = (actualBalance, importedBalance, comparisons, diff) => {
26
+ let exitCode = 0;
27
+ // check operations
28
+ if (comparisons && diff) {
29
+ const operationsMismatches = comparisons.filter((comparison) => !comparison.status.startsWith("Match"));
30
+ const mismatches = [];
31
+ for (const o of operationsMismatches) {
32
+ mismatches.push({
33
+ imported: typeof o.imported !== "undefined"
34
+ ? Object.assign(Object.assign({}, o.imported), { amount: o.imported.amount.toFixed() }) : undefined,
35
+ actual: typeof o.actual !== "undefined"
36
+ ? Object.assign(Object.assign({}, o.actual), { cashAddress: (0, helpers_1.toUnprefixedCashAddress)(o.actual.address), amount: o.actual.amount.toFixed() }) : undefined,
37
+ status: o.status,
38
+ });
39
+ }
40
+ if (mismatches.length > 0) {
41
+ console.log(chalk_1.default.redBright("Diff [ KO ]: operations mismatches"));
42
+ console.dir(JSON.parse(JSON.stringify(mismatches)));
43
+ exitCode += 1;
44
+ }
45
+ else {
46
+ console.log(chalk_1.default.greenBright("Diff [ OK ]: operations match"));
47
+ }
48
+ }
49
+ // check balance
50
+ if (importedBalance) {
51
+ const imported = new bignumber_js_1.default(importedBalance).toFixed(0);
52
+ const actual = (0, helpers_1.toBaseUnit)(new bignumber_js_1.default(actualBalance));
53
+ if (imported !== actual) {
54
+ console.log(chalk_1.default.redBright("Diff [ KO ]: balances mismatch"));
55
+ console.log("| imported balance: ", imported);
56
+ console.log("| actual balance: ", actual);
57
+ exitCode += 2;
58
+ }
59
+ else {
60
+ console.log(chalk_1.default.greenBright("Diff [ OK ]: balances match: ".concat(actual)));
61
+ }
62
+ }
63
+ // exit codes:
64
+ // 0: OK
65
+ // 1: operation(s) mismatch(es)
66
+ // 2: balance mismatch
67
+ // 3: operation(s) _and_ balance mismatches
68
+ return exitCode;
69
+ };
70
+ exports.showDiff = showDiff;
@@ -0,0 +1,55 @@
1
+ export declare enum DerivationMode {
2
+ LEGACY = "Legacy",
3
+ NATIVE = "Native SegWit",
4
+ SEGWIT = "SegWit",
5
+ TAPROOT = "Taproot",
6
+ BCH = "Bitcoin Cash",
7
+ ETHEREUM = "Ethereum",
8
+ DOGECOIN = "Dogecoin",
9
+ UNKNOWN = "Unknown"
10
+ }
11
+ export declare const currencies: {
12
+ btc: {
13
+ name: string;
14
+ symbol: string;
15
+ network_mainnet: any;
16
+ network_testnet: any;
17
+ derivationModes: DerivationMode[];
18
+ precision: number;
19
+ utxo_based: boolean;
20
+ };
21
+ bch: {
22
+ name: string;
23
+ symbol: string;
24
+ network_mainnet: any;
25
+ network_testnet: any;
26
+ derivationModes: DerivationMode[];
27
+ precision: number;
28
+ utxo_based: boolean;
29
+ };
30
+ ltc: {
31
+ name: string;
32
+ symbol: string;
33
+ network_mainnet: any;
34
+ network_testnet: any;
35
+ derivationModes: DerivationMode[];
36
+ precision: number;
37
+ utxo_based: boolean;
38
+ };
39
+ eth: {
40
+ name: string;
41
+ symbol: string;
42
+ precision: number;
43
+ utxo_based: boolean;
44
+ derivationModes: DerivationMode[];
45
+ };
46
+ doge: {
47
+ name: string;
48
+ symbol: string;
49
+ network_mainnet: any;
50
+ network_testnet: any;
51
+ precision: number;
52
+ utxo_based: boolean;
53
+ derivationModes: DerivationMode[];
54
+ };
55
+ };