@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,800 @@
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.save = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const html_minifier_1 = __importDefault(require("html-minifier"));
9
+ const settings_1 = require("../configuration/settings");
10
+ const report_html_1 = require("../templates/report.html");
11
+ const logos_base64_1 = require("../templates/logos.base64");
12
+ const helpers_1 = require("../helpers");
13
+ const currencies_1 = require("../configuration/currencies");
14
+ const bignumber_js_1 = __importDefault(require("bignumber.js"));
15
+ function renderToken(token, status) {
16
+ const renderedAmount = new bignumber_js_1.default(token.amount).toFormat(6);
17
+ let renderedToken = `<br><span class="token_details` +
18
+ (typeof status == "undefined" || status.includes("Match")
19
+ ? ``
20
+ : ` token_mismatch`) +
21
+ `">`;
22
+ renderedToken += `${parseFloat(renderedAmount)} ${token.symbol}<br>${token.name}</span>`;
23
+ return renderedToken;
24
+ }
25
+ // align float numbers or zeros
26
+ function renderAmount(amount) {
27
+ const decimalPrecision = 8;
28
+ const filler = "¤"; // (any non-numeric non-dot char)
29
+ let renderedAmount;
30
+ const n = new bignumber_js_1.default(amount);
31
+ if (n.isZero()) {
32
+ // align '0': insert filler on the right
33
+ renderedAmount = "0".padEnd(decimalPrecision, filler);
34
+ }
35
+ else {
36
+ renderedAmount = (0, helpers_1.toAccountUnit)(n, 8);
37
+ }
38
+ // align any number: insert filler on the left
39
+ renderedAmount = renderedAmount.padStart(decimalPrecision, filler);
40
+ renderedAmount = renderedAmount.replace(/^0+(\d)|(\d)0+$/gm, "$1$2"); // remove trailing zeros
41
+ // insert non-breaking spaces by replacing the filler with `&nbsp;`
42
+ return ('<span class="monospaced">' +
43
+ renderedAmount.split(filler).join("&nbsp;") +
44
+ "</span>");
45
+ }
46
+ // generate the url to an external explorer allowing to get more info
47
+ // regarding an item (address or transaction)
48
+ function getUrl(itemType, item) {
49
+ // general case (Bitcoin, Litecoin)
50
+ // --------------------------------
51
+ let url = settings_1.EXTERNAL_EXPLORERS_URLS.general;
52
+ const itemTypes = {
53
+ address: "address",
54
+ transaction: "tx",
55
+ };
56
+ // exception(s)
57
+ // ------------
58
+ // Testnet
59
+ if (settings_1.configuration.testnet) {
60
+ url = url.replace("{currency}", "{currency}-testnet");
61
+ }
62
+ // Bitcoin Cash
63
+ //
64
+ // coin: "bitcoin-cash"
65
+ // item types: "address" | "transaction"
66
+ if (settings_1.configuration.currency.symbol === currencies_1.currencies.bch.symbol) {
67
+ url = settings_1.EXTERNAL_EXPLORERS_URLS.bch;
68
+ url = url.replace("{currency}", "bitcoin-cash");
69
+ itemTypes.address = "address";
70
+ itemTypes.transaction = "transaction";
71
+ }
72
+ // Ethereum
73
+ //
74
+ // item types: "address" | "tx"
75
+ if (settings_1.configuration.currency.symbol === currencies_1.currencies.eth.symbol) {
76
+ url = settings_1.EXTERNAL_EXPLORERS_URLS.eth;
77
+ itemTypes.address = "address";
78
+ itemTypes.transaction = "tx";
79
+ if (settings_1.configuration.testnet) {
80
+ // https://etherscan.io -> https://ropsten.etherscan.io
81
+ url = url.replace("https://", "https://ropsten.");
82
+ }
83
+ }
84
+ // specify item type
85
+ switch (itemType) {
86
+ case "address":
87
+ url = url.replace("{type}", itemTypes.address);
88
+ break;
89
+ case "transaction":
90
+ url = url.replace("{type}", itemTypes.transaction);
91
+ break;
92
+ default:
93
+ throw new Error('Unrecognized item type "' +
94
+ itemType +
95
+ "\" (expected: 'address' or 'transaction')");
96
+ }
97
+ return url
98
+ .replace("{currency}", settings_1.configuration.currency.symbol.toLowerCase())
99
+ .replace("{item}", item);
100
+ }
101
+ // make address clickable
102
+ function addressAsLink(address) {
103
+ // if no address, return empty string
104
+ // (used for CSV files that do not contain any address)
105
+ if (typeof address === "undefined") {
106
+ return "";
107
+ }
108
+ const url = getUrl("address", address);
109
+ address = address.length < 45 ? address : address.substring(0, 45) + "...";
110
+ return ('<a class="monospaced" href="' + url + '" target=_blank>' + address + "</a>");
111
+ }
112
+ function renderAddress(address, cashAddress) {
113
+ const renderedAddress = addressAsLink(address);
114
+ if (settings_1.configuration.currency.symbol !== currencies_1.currencies.bch.symbol || !cashAddress) {
115
+ return renderedAddress;
116
+ }
117
+ else {
118
+ // Bitcoin Cash: handle Legacy/Cash address duality:
119
+ // {legacy}
120
+ // {Cash address}
121
+ return renderedAddress.concat("</br>").concat(addressAsLink(cashAddress));
122
+ }
123
+ }
124
+ // make TXID clickable
125
+ function renderTxid(txid) {
126
+ if (!txid) {
127
+ return "(no txid)";
128
+ }
129
+ const url = getUrl("transaction", txid);
130
+ txid = txid.substring(0, 10) + "...";
131
+ return ('<a class="monospaced" href="' + url + '" target=_blank>' + txid + "</a>");
132
+ }
133
+ // explain some operation types
134
+ function createTooltip(opType) {
135
+ if (opType === "Sent" || opType === "Received") {
136
+ return opType;
137
+ }
138
+ let tooltip = "";
139
+ if (opType === "Received (non-sibling to change)") {
140
+ tooltip = `
141
+ <span class="tooltiptext">
142
+ Change address that received funds from an address NOT belonging to the same xpub
143
+ </span>
144
+ `;
145
+ }
146
+ else if (opType === "Sent to self") {
147
+ tooltip = `
148
+ <span class="tooltiptext">
149
+ Sent to itself (same address)
150
+ </span>
151
+ `;
152
+ }
153
+ else if (opType === "Sent to sibling") {
154
+ tooltip = `
155
+ <span class="tooltiptext">
156
+ Sent to another address, belonging to the same xpub (sibling)
157
+ </span>
158
+ `;
159
+ }
160
+ else if (opType === "Failed to send") {
161
+ tooltip = `
162
+ <span class="tooltiptext">
163
+ Send operation failed (it can impact the balance)
164
+ </span>
165
+ `;
166
+ }
167
+ else if (opType.includes("token")) {
168
+ tooltip = `
169
+ <span class="tooltiptext">
170
+ Ethereum token (e.g. ERC20) related operation
171
+ </span>
172
+ `;
173
+ }
174
+ else if (opType.includes("dapp")) {
175
+ tooltip = `
176
+ <span class="tooltiptext">
177
+ Ethereum Dapp related operation
178
+ </span>
179
+ `;
180
+ }
181
+ else if (opType.includes("SCI")) {
182
+ tooltip = `
183
+ <span class="tooltiptext">
184
+ Ethereum smart contract interaction (not a token transfer)
185
+ </span>
186
+ `;
187
+ }
188
+ else if (opType === "Swapped") {
189
+ tooltip = `
190
+ <span class="tooltiptext">
191
+ Swapped Ethers for tokens (note that it is expected for the imported ETH amount to be positive and for the actual one to be negative)
192
+ </span>
193
+ `;
194
+ }
195
+ else {
196
+ return opType;
197
+ }
198
+ return '<div class="tooltip">' + opType + tooltip + "</div>";
199
+ }
200
+ function makeTransactionsTable(outputData) {
201
+ // balance only mode: do not display the transaction table
202
+ if (outputData.meta.balanceOnly) {
203
+ return "";
204
+ }
205
+ const transactionsTableHead = `
206
+ <thead>
207
+ <tr>
208
+ <th>Date</th>
209
+ <th>Block</th>
210
+ <th>Tx id</th>
211
+ <th>Address</th>
212
+ <th>Amount</th>
213
+ <th>Type</th>
214
+ </tr>
215
+ </thead>
216
+ `;
217
+ let transactionsTemplate = `
218
+ {paginationStyle}
219
+ <li class="tab">
220
+ <input type="radio" name="tabs" id="tab4" />
221
+ <label for="tab4">${outputData.transactions.length} Transactions</label>
222
+ <div id="tab-content4" class="content">
223
+ <div class="warning">{warning}</div>
224
+ {paginationRadios}
225
+ {transactions}
226
+ {paginationSlider}
227
+ </div>
228
+ </li>
229
+ `;
230
+ // display warning if default provider is being used
231
+ if (outputData.meta.provider === "default") {
232
+ transactionsTemplate = transactionsTemplate.replace("{warning}", "Default provider used: only the last ~50 operations by address are displayed");
233
+ }
234
+ else {
235
+ transactionsTemplate = transactionsTemplate.replace("{warning}", "");
236
+ }
237
+ const transactions = [];
238
+ for (const e of outputData.transactions) {
239
+ let rowStyle = "<tr>";
240
+ const transactionRow = [];
241
+ if (e.operationType === "Failed to send") {
242
+ rowStyle = '<tr class="failed_operation">';
243
+ }
244
+ else if (e.operationType.includes("token") ||
245
+ e.operationType === "Swapped") {
246
+ rowStyle = '<tr class="token_operation">';
247
+ }
248
+ else if (e.operationType.includes("SCI")) {
249
+ rowStyle = '<tr class="sci_operation">';
250
+ }
251
+ transactionRow.push(rowStyle);
252
+ transactionRow.push("<td>" + e.date + "</td>");
253
+ transactionRow.push("<td>" + e.block + "</td>");
254
+ transactionRow.push("<td>" + renderTxid(e.txid) + "</td>");
255
+ transactionRow.push("<td>" + renderAddress(e.address, e.cashAddress) + "</td>");
256
+ let amount = renderAmount(e.amount);
257
+ if (typeof e.token !== "undefined") {
258
+ amount += renderToken(e.token);
259
+ }
260
+ if (typeof e.dapp !== "undefined") {
261
+ amount += `<br><span class="dapp_details">${e.dapp.contract_name}</span>`;
262
+ }
263
+ transactionRow.push("<td>" + amount + "</td>");
264
+ transactionRow.push("<td>" + createTooltip(e.operationType) + "</td></tr>");
265
+ transactions.push(transactionRow);
266
+ }
267
+ return makePaginatedTable(transactionsTableHead, transactionsTemplate, transactions, 100, "transactions");
268
+ }
269
+ function makeUTXOSTable(outputData) {
270
+ if (typeof outputData.utxos === "undefined" ||
271
+ outputData.utxos.length === 0) {
272
+ return "";
273
+ }
274
+ const UTXOSTableHead = `
275
+ <thead>
276
+ <tr>
277
+ <th>Type</th>
278
+ <th>Derivation</th>
279
+ <th>Address</th>
280
+ <th>Balance</th>
281
+ <th>Funded</th>
282
+ <th>Spent</th>
283
+ </tr>
284
+ </thead>`;
285
+ const UTXOSTemplate = `
286
+ {paginationStyle}
287
+ <li class="tab">
288
+ <input type="radio" name="tabs" id="tab3" />
289
+ <label for="tab3">${outputData.utxos.length} UTXO${outputData.utxos.length > 1 ? "S" : ""}</label>
290
+ <div id="tab-content3" class="content">
291
+ {paginationRadios}
292
+ {utxos}
293
+ {paginationSlider}
294
+ </div>
295
+ </li>
296
+ `;
297
+ const utxos = [];
298
+ for (const e of outputData.utxos) {
299
+ const utxoRow = [];
300
+ utxoRow.push("<tr><td>" + e.derivationMode + "</td>");
301
+ const derivationPath = "m/" + e.derivation.account + "/" + e.derivation.index;
302
+ utxoRow.push("<td>" + derivationPath + "</td>");
303
+ utxoRow.push("<td>" + renderAddress(e.address, e.cashAddress) + "</td>");
304
+ const balance = renderAmount(e.balance);
305
+ const funded = renderAmount(e.funded);
306
+ const spent = renderAmount(e.spent);
307
+ utxoRow.push("<td>" + balance + "</td>");
308
+ utxoRow.push("<td>" + funded + "</td>");
309
+ utxoRow.push("<td>" + spent + "</td></tr>");
310
+ utxos.push(utxoRow);
311
+ }
312
+ return makePaginatedTable(UTXOSTableHead, UTXOSTemplate, utxos, 100, "utxos");
313
+ }
314
+ function makeComparisonsTable(outputData, onlyDiff) {
315
+ const getLabel = (status) => {
316
+ if (status.includes("Match")) {
317
+ return "match_label";
318
+ }
319
+ else if (status === "Skipped") {
320
+ return "skipped_label";
321
+ }
322
+ else {
323
+ return "mismatch_label";
324
+ }
325
+ };
326
+ const comparisonsTableHead = `
327
+ <thead>
328
+ <tr style="text-align: center">
329
+ <th rowspan="1" colspan="3" class="right_sep">IMPORTED OPERATION, FROM PRODUCT</th>
330
+ <th rowspan="1" colspan="3" class="right_sep">ACTUAL OPERATION, FROM EXTERNAL PROVIDER</th>
331
+ <th rowspan="2" colspan="1">TXID</th>
332
+ <th rowspan="2" colspan="1">TYPE</th>
333
+ <th rowspan="2" colspan="1">STATUS</th>
334
+ </tr>
335
+ <tr>
336
+ <th>Date</th>
337
+ <th>Address</th>
338
+ <th class="right_sep">Amount</th>
339
+ <th>Date</th>
340
+ <th>Address</th>
341
+ <th class="right_sep">Amount</th>
342
+ </tr>
343
+ </thead>
344
+ `;
345
+ let comparisonsTemplate = `
346
+ {paginationStyle}
347
+ <li class="tab">
348
+ <input type="radio" name="tabs" id="tab{id}" />
349
+ <label for="tab{id}">${onlyDiff ? outputData.diffs.length : outputData.comparisons.length} {label}</label>
350
+ <div id="tab-content{id}" class="content">
351
+ {paginationRadios}
352
+ ${onlyDiff ? "{diffs}" : "{comparisons}"}
353
+ {paginationSlider}
354
+ </div>
355
+ </li>
356
+ `;
357
+ let comp;
358
+ if (!onlyDiff) {
359
+ comp = outputData.comparisons;
360
+ comparisonsTemplate = comparisonsTemplate.replace("{label}", `Comparison${outputData.comparisons.length > 1 ? "s" : ""}`);
361
+ comparisonsTemplate = comparisonsTemplate.split("{id}").join("5"); // comparisons have id 5
362
+ }
363
+ else {
364
+ comp = outputData.diffs;
365
+ comparisonsTemplate = comparisonsTemplate.replace("{label}", `Difference${outputData.diffs.length > 1 ? "s" : ""}`);
366
+ comparisonsTemplate = comparisonsTemplate.split("{id}").join("6"); // differences have id 6
367
+ }
368
+ const comparisons = [];
369
+ if (typeof comp !== "undefined") {
370
+ for (const e of comp) {
371
+ const comparisonRow = [];
372
+ let txid = "";
373
+ let opType = "";
374
+ // by default: no imported operation
375
+ const imported = {
376
+ date: "",
377
+ address: "(no operation)",
378
+ amount: "",
379
+ token: undefined,
380
+ dapp: undefined,
381
+ };
382
+ if (typeof e.imported !== "undefined") {
383
+ imported.date = e.imported.date;
384
+ imported.address = renderAddress(e.imported.address);
385
+ imported.amount = renderAmount(e.imported.amount);
386
+ imported.token = e.imported.token;
387
+ imported.dapp = e.imported.dapp;
388
+ txid = e.imported.txid;
389
+ opType = e.imported.operationType;
390
+ }
391
+ // by default: no actual operation
392
+ const actual = {
393
+ date: "",
394
+ address: "(no operation)",
395
+ amount: "",
396
+ token: undefined,
397
+ dapp: undefined,
398
+ };
399
+ if (typeof e.actual !== "undefined") {
400
+ actual.date = e.actual.date;
401
+ actual.address = renderAddress(e.actual.address, e.actual.cashAddress);
402
+ actual.amount = renderAmount(e.actual.amount);
403
+ actual.token = e.actual.token;
404
+ actual.dapp = e.actual.dapp;
405
+ txid = e.actual.txid;
406
+ opType = e.actual.operationType;
407
+ }
408
+ if (e.status === "Match") {
409
+ if (onlyDiff) {
410
+ continue; // if diff: ignore matches
411
+ }
412
+ if (opType === "Failed to send") {
413
+ comparisonRow.push('<tr class="failed_operation">');
414
+ }
415
+ else if (opType.includes("token") || opType === "Swapped") {
416
+ comparisonRow.push('<tr class="token_operation">');
417
+ }
418
+ else if (opType.includes("SCI")) {
419
+ comparisonRow.push('<tr class="sci_operation">');
420
+ }
421
+ else {
422
+ comparisonRow.push('<tr class="comparison_match">');
423
+ }
424
+ }
425
+ else if (e.status.includes("aggregated")) {
426
+ if (onlyDiff) {
427
+ continue; // if diff: ignore aggregated operations
428
+ }
429
+ comparisonRow.push('<tr class="comparison_aggregated">');
430
+ }
431
+ else if (e.status === "Skipped") {
432
+ comparisonRow.push('<tr class="skipped_comparison">');
433
+ }
434
+ else {
435
+ comparisonRow.push('<tr class="comparison_mismatch">');
436
+ }
437
+ comparisonRow.push("<td>" + imported.date + "</td>");
438
+ comparisonRow.push("<td>" + imported.address + "</td>");
439
+ let importedAmount = imported.amount;
440
+ if (typeof imported.token !== "undefined") {
441
+ importedAmount += renderToken(e.imported.token, e.status);
442
+ }
443
+ if (typeof imported.dapp !== "undefined") {
444
+ importedAmount += `<br><span class="dapp_details">${e.imported.dapp.contract_name}</span>`;
445
+ }
446
+ comparisonRow.push('<td class="right_sep">' + importedAmount + "</td>");
447
+ comparisonRow.push("<td>" + actual.date + "</td>");
448
+ comparisonRow.push("<td>" + actual.address + "</td>");
449
+ let actualAmount = actual.amount;
450
+ if (typeof actual.token !== "undefined") {
451
+ actualAmount += renderToken(actual.token, e.status);
452
+ }
453
+ comparisonRow.push('<td class="right_sep">' + actualAmount + "</td>");
454
+ comparisonRow.push("<td>" + renderTxid(txid) + "</td>");
455
+ comparisonRow.push("<td>" + createTooltip(opType) + "</td>");
456
+ comparisonRow.push('<td><span class="label ' + getLabel(e.status) + '">');
457
+ comparisonRow.push(e.status +
458
+ (e.status === "Skipped"
459
+ ? ` (> block #${settings_1.configuration.blockHeightUpperLimit})`
460
+ : "") +
461
+ "</span></td></tr>");
462
+ comparisons.push(comparisonRow);
463
+ }
464
+ }
465
+ if (comparisons.length > 0) {
466
+ return makePaginatedTable(comparisonsTableHead, comparisonsTemplate, comparisons, 100, onlyDiff ? "diffs" : "comparisons");
467
+ }
468
+ else {
469
+ return "";
470
+ }
471
+ }
472
+ function makePaginatedTable(tableHead, template, rowsData, pageSize, key) {
473
+ if (rowsData.length > pageSize) {
474
+ const pageCount = Math.ceil(rowsData.length / 100);
475
+ const pageArray = [...Array(pageCount).keys()].map((i) => i + 1);
476
+ template = template
477
+ .replace("{paginationStyle}", `<style type="text/css">
478
+ ${pageArray.map((i) => `#${key}-radio${i}`).join(", ")} {
479
+ display: none;
480
+ }
481
+ .page-slider {
482
+ display: flex;
483
+ flex-wrap: wrap;
484
+ justify-content: center;
485
+ max-width: 1000px;
486
+ margin: auto;
487
+ text-align: center;
488
+ }
489
+ .${key}-page-label {
490
+ cursor: pointer;
491
+ color: white;
492
+ background-color: #303030;
493
+ padding: 6px;
494
+ width: 50px;
495
+ margin: 0px;
496
+ }
497
+ ${pageArray.map((i) => `#${key}-page${i}`).join(", ")} {
498
+ display: none;
499
+ }
500
+ ${pageArray
501
+ .map((i) => `#${key}-radio${i}:checked ~ .page-slider #${key}-label${i}`)
502
+ .join(", ")} {
503
+ background-color: #4a83fd;
504
+ }
505
+ ${pageArray
506
+ .map((i) => `#${key}-radio${i}:checked ~ #${key}-page${i}`)
507
+ .join(", ")} {
508
+ display: table;
509
+ }
510
+ </style>`)
511
+ .replace("{paginationRadios}", pageArray
512
+ .map((i) => `<input type="radio" name="${key}-page-radio" id="${key}-radio${i}" ${i === 1 ? "checked" : ""} />`)
513
+ .join(""))
514
+ .replace("{paginationSlider}", `<div class="page-slider">
515
+ ${pageArray
516
+ .map((i) => `<label for="${key}-radio${i}" id="${key}-label${i}" class="${key}-page-label">
517
+ ${i}
518
+ </label>`)
519
+ .join("")}
520
+ </div>`)
521
+ .replace(`{${key}}`, pageArray
522
+ .map((i) => `<table id="${key}-page${i}">
523
+ ${tableHead}
524
+ <tbody>
525
+ ${rowsData
526
+ .slice(i * 100 - 100, i * 100)
527
+ .map((rowData) => rowData.join(""))
528
+ .join("")}
529
+ </tbody>
530
+ </table>`)
531
+ .join(""));
532
+ }
533
+ else {
534
+ template = template
535
+ .replace("{paginationStyle}", "")
536
+ .replace("{paginationRadios}", "")
537
+ .replace("{paginationSlider}", "")
538
+ .replace(`{${key}}`, `<table>
539
+ ${tableHead}
540
+ <tbody>
541
+ ${rowsData.map((rowData) => rowData.join("")).join("")}
542
+ </tbody>
543
+ </table>`);
544
+ }
545
+ return template;
546
+ }
547
+ function makeAddressesTable(outputData) {
548
+ const addressesTableHead = `
549
+ <thead>
550
+ <tr>
551
+ <th>Type</th>
552
+ <th>Derivation</th>
553
+ <th>Address</th>
554
+ <th>Balance</th>
555
+ <th>Funded</th>
556
+ <th>Spent</th>
557
+ </tr>
558
+ </thead>
559
+ `;
560
+ let addressesTemplate = `
561
+ {paginationStyle}
562
+ <li class="tab">
563
+ <input type="radio" name="tabs" id="tab2" />
564
+ <label for="tab2">{addresses_count} Address{addresses_plural}</label>
565
+ <div id="tab-content2" class="content">
566
+ {paginationRadios}
567
+ {addresses}
568
+ {paginationSlider}
569
+ </div>
570
+ </li>`;
571
+ const addresses = [];
572
+ for (const e of outputData.addresses) {
573
+ const addressRow = [];
574
+ if (typeof e.derivation.account !== "undefined") {
575
+ addressRow.push("<tr><td>" + e.derivationMode + "</td>");
576
+ const derivationPath = "m/" + e.derivation.account + "/" + e.derivation.index;
577
+ addressRow.push("<td>" + derivationPath + "</td>");
578
+ }
579
+ else {
580
+ addressRow.push("<tr><td>" + settings_1.configuration.currency.name + "</td><td>-</td>");
581
+ }
582
+ addressRow.push("<td>" + renderAddress(e.address, e.cashAddress) + "</td>");
583
+ const balance = renderAmount(e.balance);
584
+ const funded = renderAmount(e.funded);
585
+ const spent = renderAmount(e.spent);
586
+ addressRow.push("<td>" + balance + "</td>");
587
+ addressRow.push("<td>" + funded + "</td>");
588
+ addressRow.push("<td>" + spent + "</td></tr>");
589
+ addresses.push(addressRow);
590
+ }
591
+ addressesTemplate = addressesTemplate.replace("{addresses_count}", outputData.addresses.length.toFixed());
592
+ addressesTemplate = addressesTemplate.replace("{addresses_plural}", outputData.addresses.length > 1 ? "es" : "");
593
+ return makePaginatedTable(addressesTableHead, addressesTemplate, addresses, 100, "addresses");
594
+ }
595
+ function saveHTML(outputData, filepath) {
596
+ let report = report_html_1.reportTemplate;
597
+ // background color and logo
598
+ if (settings_1.configuration.testnet) {
599
+ // yellow if testnet
600
+ report = report.replace("{body_background_color}", "#f7f48a");
601
+ report = report.replace("{logo_base_64}", logos_base64_1.base64YellowLogo.substring(0, 28685));
602
+ }
603
+ else {
604
+ // white otherwise
605
+ report = report.replace("{body_background_color}", "#ffffff");
606
+ report = report.replace("{logo_base_64}", !filepath.includes(`/${"e".repeat(2)}${"g".repeat(2)}/`)
607
+ ? logos_base64_1.base64WhiteLogo.substring(0, 24557)
608
+ : logos_base64_1.base64WhiteLogo.substring(24557, logos_base64_1.base64WhiteLogo.length - 1));
609
+ }
610
+ // meta
611
+ if (typeof outputData.meta.preDerivationSize === "undefined") {
612
+ report = report.replace("{pre_derivation_size}", "");
613
+ }
614
+ else {
615
+ report = report.replace("{pre_derivation_size}", `| pre-derivation size: ${outputData.meta.preDerivationSize}`);
616
+ }
617
+ if (typeof outputData.meta.derivationMode === "undefined") {
618
+ report = report.replace("{derivation_mode}", "");
619
+ }
620
+ else {
621
+ report = report.replace("{derivation_mode}", `| specific derivation mode: ${outputData.meta.derivationMode}`);
622
+ }
623
+ for (const key of Object.keys(outputData.meta)) {
624
+ report = report.split("{" + key + "}").join(outputData.meta[key]);
625
+ }
626
+ // warning range
627
+ if (!outputData.meta.mode.startsWith("Full")) {
628
+ report = report.replace("{warning_range}", `<div id='warning_range'>The data is based on a partial scan:<br/> ${outputData.meta.mode}</div>`);
629
+ }
630
+ else {
631
+ report = report.replace("{warning_range}", "");
632
+ }
633
+ // summary
634
+ const summary = [];
635
+ for (const e of outputData.summary) {
636
+ if (typeof e.derivationMode !== "undefined") {
637
+ summary.push("<tr><td>" + e.derivationMode + "</td>");
638
+ }
639
+ else {
640
+ summary.push("<tr><td>" + settings_1.configuration.currency.name + "</td>");
641
+ }
642
+ const balance = (0, helpers_1.toAccountUnit)(new bignumber_js_1.default(e.balance));
643
+ if (balance === "0") {
644
+ summary.push('<td class="summary_empty">');
645
+ }
646
+ else {
647
+ summary.push('<td class="summary_non_empty">');
648
+ }
649
+ summary.push(balance + "</td></tr>");
650
+ }
651
+ report = report.replace("{summary}", summary.join(""));
652
+ // addresses
653
+ report = report.replace("{addresses_table}", makeAddressesTable(outputData));
654
+ // UTXOs
655
+ report = report.replace("{utxos_table}", makeUTXOSTable(outputData));
656
+ // transactions
657
+ report = report.replace("{transactions_table}", makeTransactionsTable(outputData));
658
+ // comparisons and diff
659
+ if (typeof outputData.comparisons === "undefined" ||
660
+ outputData.comparisons.length === 0) {
661
+ report = report.replace("{comparisons_table}", "");
662
+ report = report.replace("{diff_table}", "");
663
+ }
664
+ else {
665
+ report = report.replace("{comparisons_table}", makeComparisonsTable(outputData));
666
+ report = report.replace("{diff_table}", makeComparisonsTable(outputData, true));
667
+ }
668
+ filepath += ".html";
669
+ const minifiedReport = html_minifier_1.default.minify(report, {
670
+ removeAttributeQuotes: true,
671
+ minifyCSS: true,
672
+ removeComments: true,
673
+ useShortDoctype: true,
674
+ removeRedundantAttributes: true,
675
+ removeOptionalTags: true,
676
+ removeEmptyAttributes: true,
677
+ removeEmptyElements: false, // do NOT remove empty elements (e.g., empty addresses)
678
+ });
679
+ fs_1.default.writeFileSync(filepath, minifiedReport);
680
+ console.log("\nHTML report saved: ".concat(filepath));
681
+ }
682
+ function saveJSON(outputData, filepath) {
683
+ // stringify -> parse -> stringify to remove `undefined` in final JSON
684
+ const JSONobject = JSON.stringify(JSON.parse(JSON.stringify(outputData)), null, 2);
685
+ if (filepath.toLocaleLowerCase() === "stdout") {
686
+ // display
687
+ console.log(JSONobject);
688
+ }
689
+ else {
690
+ // save file
691
+ filepath += ".json";
692
+ fs_1.default.writeFileSync(filepath, JSONobject);
693
+ console.log("\nJSON export saved: ".concat(filepath));
694
+ }
695
+ }
696
+ function save(meta, data, directory) {
697
+ const balanceOnly = meta.balanceOnly;
698
+ // convert amounts into base unit
699
+ const addresses = data.addresses.map((e) => {
700
+ return {
701
+ derivationMode: e.derivationMode,
702
+ derivation: e.getDerivation(),
703
+ address: e.toString(),
704
+ cashAddress: e.asCashAddress(),
705
+ balance: (0, helpers_1.toBaseUnit)(e.balance),
706
+ funded: (0, helpers_1.toBaseUnit)(e.stats.funded),
707
+ spent: (0, helpers_1.toBaseUnit)(e.stats.spent),
708
+ };
709
+ });
710
+ let utxos = [];
711
+ if (settings_1.configuration.currency.utxo_based) {
712
+ utxos = data.addresses
713
+ .filter((a) => a.isUTXO())
714
+ .map((e) => {
715
+ return {
716
+ derivationMode: e.derivationMode,
717
+ derivation: e.getDerivation(),
718
+ address: e.toString(),
719
+ cashAddress: e.asCashAddress(),
720
+ balance: (0, helpers_1.toBaseUnit)(e.balance),
721
+ funded: (0, helpers_1.toBaseUnit)(e.stats.funded),
722
+ spent: (0, helpers_1.toBaseUnit)(e.stats.spent),
723
+ // balance only mode: ignore the following fields
724
+ txid: balanceOnly || typeof e.transactions[0] === "undefined"
725
+ ? undefined
726
+ : e.transactions[0].txid,
727
+ height: balanceOnly || typeof e.transactions[0] === "undefined"
728
+ ? undefined
729
+ : e.transactions[0].blockHeight,
730
+ time: balanceOnly || typeof e.transactions[0] === "undefined"
731
+ ? undefined
732
+ : e.transactions[0].date,
733
+ };
734
+ });
735
+ }
736
+ const summary = data.summary.map((e) => {
737
+ return Object.assign(Object.assign({}, e), { balance: (0, helpers_1.toBaseUnit)(new bignumber_js_1.default(e.balance)) });
738
+ });
739
+ const transactions = !balanceOnly
740
+ ? data.transactions.map((e) => {
741
+ return Object.assign(Object.assign({}, e), { cashAddress: (0, helpers_1.toUnprefixedCashAddress)(e.address), amount: (0, helpers_1.toBaseUnit)(e.amount) });
742
+ })
743
+ : [];
744
+ const comparisons = typeof data.comparisons !== "undefined"
745
+ ? data.comparisons.map((e) => {
746
+ return Object.assign(Object.assign({}, e), { imported: typeof e.imported !== "undefined"
747
+ ? Object.assign(Object.assign({}, e.imported), { amount: (0, helpers_1.toBaseUnit)(e.imported.amount) }) : undefined, actual: typeof e.actual !== "undefined"
748
+ ? Object.assign(Object.assign({}, e.actual), { cashAddress: (0, helpers_1.toUnprefixedCashAddress)(e.actual.address), amount: (0, helpers_1.toBaseUnit)(e.actual.amount) }) : undefined });
749
+ })
750
+ : undefined;
751
+ let diffs = [];
752
+ if (typeof comparisons !== "undefined") {
753
+ diffs =
754
+ comparisons.filter((comparison) => !comparison.status.startsWith("Match") &&
755
+ comparison.status !== "Skipped") || [];
756
+ }
757
+ let warningRange;
758
+ if (!meta.mode.startsWith("Full")) {
759
+ warningRange = `! The data is based on a partial scan: ${meta.mode} !`;
760
+ }
761
+ const providerUrl = settings_1.configuration.externalProviderURL.split("/");
762
+ const providerBaseUrl = providerUrl[0] + "//" + providerUrl[2];
763
+ const outputData = {
764
+ meta: {
765
+ by: "xpub scan <https://github.com/LedgerHQ/xpub-scan>",
766
+ version: meta.version,
767
+ xpub: meta.xpub,
768
+ analysis_date: meta.date,
769
+ currency: `${settings_1.configuration.currency.name} (${(0, helpers_1.getNetworkLabel)()})`,
770
+ provider: settings_1.configuration.providerType,
771
+ provider_url: providerBaseUrl,
772
+ gap_limit: settings_1.configuration.gap_limit,
773
+ unit: "Base unit (i.e., satoshis or equivalent unit)",
774
+ mode: meta.mode,
775
+ preDerivationSize: meta.preDerivationSize,
776
+ derivationMode: meta.derivationMode,
777
+ warningRange,
778
+ balanceOnly,
779
+ },
780
+ addresses,
781
+ utxos,
782
+ summary,
783
+ transactions: balanceOnly ? undefined : transactions,
784
+ comparisons,
785
+ diffs: balanceOnly ? undefined : diffs, // ignore in balance only mode
786
+ };
787
+ // if no filepath/filename specify -> set to current directory
788
+ if (directory === "") {
789
+ directory = __dirname;
790
+ }
791
+ let filepath = directory;
792
+ if (filepath.toLocaleLowerCase() !== "stdout") {
793
+ filepath += `/${meta.xpub}`;
794
+ saveHTML(outputData, filepath); // do not save HTML if stdout
795
+ }
796
+ saveJSON(outputData, filepath);
797
+ // add empty line to separate this text block from potential check results
798
+ console.log();
799
+ }
800
+ exports.save = save;