@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.
- package/.claude/settings.local.json +8 -0
- package/CONTRIBUTING.md +130 -0
- package/LICENSE +23 -0
- package/README.md +215 -0
- package/__tests__/checkAddresses/checkBitcoinAddressesNegative.test.ts +75 -0
- package/__tests__/checkAddresses/checkBitcoinAddressesPositive.test.ts +87 -0
- package/__tests__/checkAddresses/checkDogeAddressesPositive.test.ts +43 -0
- package/__tests__/checkAddresses/checkLitecoinAddressesNegative.test.ts +75 -0
- package/__tests__/checkAddresses/checkLitecoinAddressesPositive.test.ts +87 -0
- package/__tests__/checkModels/address.test.ts +183 -0
- package/__tests__/checkModels/fakeRawTransactions.json +182 -0
- package/__tests__/deriveAddresses/deriveBitcoinAddresses.test.ts +207 -0
- package/__tests__/deriveAddresses/deriveBitcoinCashAddresses.test copy.ts +79 -0
- package/__tests__/deriveAddresses/deriveDogecoinAddresses.test.ts +43 -0
- package/__tests__/deriveAddresses/deriveEthereumAddresses.test.ts +26 -0
- package/__tests__/deriveAddresses/deriveLitecoinAddresses.test.ts +110 -0
- package/__tests__/helpers.test.ts +274 -0
- package/__tests__/test-utils.ts +3 -0
- package/babel.config.js +6 -0
- package/jest.config.ts +5 -0
- package/ledgerhq-xpub-scan-1.0.4.tgz +0 -0
- package/lib/actions/checkAddress.d.ts +29 -0
- package/lib/actions/checkAddress.js +122 -0
- package/lib/actions/checkBalance.d.ts +20 -0
- package/lib/actions/checkBalance.js +300 -0
- package/lib/actions/deriveAddresses.d.ts +17 -0
- package/lib/actions/deriveAddresses.js +239 -0
- package/lib/actions/processTransactions.d.ts +29 -0
- package/lib/actions/processTransactions.js +289 -0
- package/lib/actions/saveAnalysis.d.ts +2 -0
- package/lib/actions/saveAnalysis.js +800 -0
- package/lib/actions/scanner.d.ts +15 -0
- package/lib/actions/scanner.js +152 -0
- package/lib/api/customProvider.d.ts +19 -0
- package/lib/api/customProvider.js +434 -0
- package/lib/api/defaultProvider.d.ts +23 -0
- package/lib/api/defaultProvider.js +275 -0
- package/lib/comparison/compareOperations.d.ts +13 -0
- package/lib/comparison/compareOperations.js +500 -0
- package/lib/comparison/diffs.d.ts +18 -0
- package/lib/comparison/diffs.js +70 -0
- package/lib/configuration/currencies.d.ts +55 -0
- package/lib/configuration/currencies.js +72 -0
- package/lib/configuration/settings.d.ts +51 -0
- package/lib/configuration/settings.js +113 -0
- package/lib/display.d.ts +12 -0
- package/lib/display.js +251 -0
- package/lib/helpers.d.ts +27 -0
- package/lib/helpers.js +255 -0
- package/lib/input/args.d.ts +6 -0
- package/lib/input/args.js +129 -0
- package/lib/input/check.d.ts +6 -0
- package/lib/input/check.js +217 -0
- package/lib/input/importOperations.d.ts +11 -0
- package/lib/input/importOperations.js +406 -0
- package/lib/models/address.d.ts +40 -0
- package/lib/models/address.js +101 -0
- package/lib/models/comparison.d.ts +8 -0
- package/lib/models/comparison.js +6 -0
- package/lib/models/currency.d.ts +11 -0
- package/lib/models/currency.js +6 -0
- package/lib/models/operation.d.ts +33 -0
- package/lib/models/operation.js +80 -0
- package/lib/models/ownAddresses.d.ts +11 -0
- package/lib/models/ownAddresses.js +31 -0
- package/lib/models/scanLimits.d.ts +7 -0
- package/lib/models/scanLimits.js +6 -0
- package/lib/models/stats.d.ts +7 -0
- package/lib/models/stats.js +6 -0
- package/lib/models/transaction.d.ts +10 -0
- package/lib/models/transaction.js +13 -0
- package/lib/scan.d.ts +2 -0
- package/lib/scan.js +31 -0
- package/lib/templates/logos.base64.d.ts +2 -0
- package/lib/templates/logos.base64.js +9 -0
- package/lib/templates/report.html.d.ts +1 -0
- package/lib/templates/report.html.js +393 -0
- package/lib/tsconfig.tsbuildinfo +1 -0
- package/lib/types.d.ts +55 -0
- package/lib/types.js +2 -0
- package/npm-shrinkwrap.json +12323 -0
- package/package.json +81 -0
- 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
|
+
};
|