@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,15 @@
|
|
|
1
|
+
import { ScannerArguments, ScanResult } from "../types";
|
|
2
|
+
export declare class Scanner {
|
|
3
|
+
args: ScannerArguments;
|
|
4
|
+
scanLimits: import("../models/scanLimits").ScanLimits | undefined;
|
|
5
|
+
address: string | undefined;
|
|
6
|
+
currency: string | undefined;
|
|
7
|
+
testnet: boolean | undefined;
|
|
8
|
+
derivationMode: string | undefined;
|
|
9
|
+
itemToScan: string;
|
|
10
|
+
balanceOnly: boolean;
|
|
11
|
+
now: Date;
|
|
12
|
+
exitCode: number;
|
|
13
|
+
constructor(args: ScannerArguments);
|
|
14
|
+
scan(): Promise<ScanResult>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
26
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
27
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
28
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
29
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
30
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
31
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.Scanner = void 0;
|
|
36
|
+
const checkBalances = __importStar(require("./checkBalance"));
|
|
37
|
+
const compare = __importStar(require("./checkAddress"));
|
|
38
|
+
const display = __importStar(require("../display"));
|
|
39
|
+
const processTransactions_1 = require("./processTransactions");
|
|
40
|
+
const diffs_1 = require("../comparison/diffs");
|
|
41
|
+
const compareOperations_1 = require("../comparison/compareOperations");
|
|
42
|
+
const importOperations_1 = require("../input/importOperations");
|
|
43
|
+
const saveAnalysis_1 = require("./saveAnalysis");
|
|
44
|
+
const settings_1 = require("../configuration/settings");
|
|
45
|
+
const helpers_1 = require("../helpers");
|
|
46
|
+
// eslint-disable-next-line
|
|
47
|
+
const { version } = require("../../package.json"); // do not modify: get the version of Xpub Scan from `package.json`
|
|
48
|
+
class Scanner {
|
|
49
|
+
constructor(args) {
|
|
50
|
+
this.now = new Date();
|
|
51
|
+
this.exitCode = 0;
|
|
52
|
+
this.args = args;
|
|
53
|
+
this.scanLimits = args.scanLimits;
|
|
54
|
+
this.address = args.address;
|
|
55
|
+
this.currency = args.currency;
|
|
56
|
+
this.testnet = args.testnet;
|
|
57
|
+
this.derivationMode = args.derivationMode;
|
|
58
|
+
this.itemToScan = args.itemToScan; // xpub or address
|
|
59
|
+
this.balanceOnly = args.balanceOnly;
|
|
60
|
+
(0, helpers_1.init)(this.itemToScan, args.silent, args.quiet, this.currency, this.testnet, this.derivationMode);
|
|
61
|
+
}
|
|
62
|
+
scan() {
|
|
63
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
+
// library mode: suppress all outputs
|
|
65
|
+
if (!settings_1.configuration.commandLineMode) {
|
|
66
|
+
/* eslint-disable */
|
|
67
|
+
console.log = function () { };
|
|
68
|
+
/* eslint-enable */
|
|
69
|
+
settings_1.configuration.silent = true;
|
|
70
|
+
}
|
|
71
|
+
if (this.address) {
|
|
72
|
+
// mode A: `--address {address}`:
|
|
73
|
+
// an address has been provided by the user: check whether its belongs or not to the xpub
|
|
74
|
+
compare.run(this.itemToScan, this.address);
|
|
75
|
+
return { exitCode: this.exitCode };
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// mode B: scan mode
|
|
79
|
+
let importedTransactions;
|
|
80
|
+
if (this.args.operations) {
|
|
81
|
+
// a file path has been provided: import its transactions
|
|
82
|
+
importedTransactions = (0, importOperations_1.importOperations)(this.args.operations);
|
|
83
|
+
}
|
|
84
|
+
const scanResult = yield checkBalances.run(this.itemToScan, this.balanceOnly, this.scanLimits);
|
|
85
|
+
const actualAddresses = scanResult.addresses; // active addresses belonging to the xpub
|
|
86
|
+
const actualUTXOs = (0, processTransactions_1.getSortedUTXOS)(actualAddresses); // UTXOs (if any) belonging to the xpub
|
|
87
|
+
const summary = scanResult.summary; // summary: balance per derivation path
|
|
88
|
+
const actualTransactions = (0, processTransactions_1.getSortedOperations)(actualAddresses); // transactions related to the xpub
|
|
89
|
+
display.showResults(actualUTXOs, actualTransactions, summary, this.balanceOnly);
|
|
90
|
+
const partialScan = typeof this.scanLimits !== "undefined";
|
|
91
|
+
let comparisonResults;
|
|
92
|
+
if (typeof importedTransactions !== "undefined") {
|
|
93
|
+
// transactions have been imported: comparison mode enabled
|
|
94
|
+
// — compare imported transactions with actual ones
|
|
95
|
+
comparisonResults = (0, compareOperations_1.checkImportedOperations)(importedTransactions, actualTransactions, actualAddresses, // scan limits
|
|
96
|
+
partialScan);
|
|
97
|
+
}
|
|
98
|
+
// full v. partial scan
|
|
99
|
+
let mode;
|
|
100
|
+
if (typeof this.args.account !== "undefined" &&
|
|
101
|
+
typeof this.args.index !== "undefined") {
|
|
102
|
+
mode = `Specific derivation path - m/${this.args.account}/${this.args.index}`;
|
|
103
|
+
}
|
|
104
|
+
else if (typeof this.scanLimits !== "undefined") {
|
|
105
|
+
let upperLimit = "∞";
|
|
106
|
+
if (typeof this.scanLimits.indexTo !== "undefined") {
|
|
107
|
+
upperLimit = this.scanLimits.indexTo;
|
|
108
|
+
}
|
|
109
|
+
mode = `Partial range — account ${this.scanLimits.account}, indices ${this.scanLimits.indexFrom}⟶${upperLimit}`;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
mode = "Full scan";
|
|
113
|
+
}
|
|
114
|
+
// (special mode for Custom W)
|
|
115
|
+
if (settings_1.configuration.augmentedImport) {
|
|
116
|
+
// Augmented import mode:
|
|
117
|
+
// Use of an augmented JSON to compare smart contract interactions
|
|
118
|
+
mode += " | Augmented Import";
|
|
119
|
+
}
|
|
120
|
+
// balance only mode
|
|
121
|
+
if (this.balanceOnly) {
|
|
122
|
+
// Balance only mode
|
|
123
|
+
mode += " | Balance Only";
|
|
124
|
+
}
|
|
125
|
+
const meta = {
|
|
126
|
+
xpub: this.itemToScan,
|
|
127
|
+
date: this.now,
|
|
128
|
+
version,
|
|
129
|
+
mode,
|
|
130
|
+
preDerivationSize: this.args.preDerivationSize,
|
|
131
|
+
derivationMode: settings_1.configuration.specificDerivationMode,
|
|
132
|
+
balanceOnly: this.balanceOnly,
|
|
133
|
+
};
|
|
134
|
+
const data = {
|
|
135
|
+
summary,
|
|
136
|
+
addresses: actualAddresses,
|
|
137
|
+
transactions: actualTransactions,
|
|
138
|
+
comparisons: comparisonResults,
|
|
139
|
+
};
|
|
140
|
+
if (this.args.save || this.args.save === "" /* allow empty arg */) {
|
|
141
|
+
(0, saveAnalysis_1.save)(meta, data, this.args.save);
|
|
142
|
+
}
|
|
143
|
+
if (this.args.diff || this.args.balance || this.args.balance === "0") {
|
|
144
|
+
const actualBalance = summary.reduce((accumulator, s) => accumulator + s.balance.toNumber(), 0);
|
|
145
|
+
this.exitCode = (0, diffs_1.showDiff)(actualBalance, this.args.balance, comparisonResults, this.args.diff);
|
|
146
|
+
}
|
|
147
|
+
return { meta, data, exitCode: this.exitCode };
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
exports.Scanner = Scanner;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Address } from "../models/address";
|
|
2
|
+
/**
|
|
3
|
+
* fetch the structured basic stats related to an address
|
|
4
|
+
* its balance, funded and spend sums and counts
|
|
5
|
+
* @param address the address being analyzed
|
|
6
|
+
* @param balanceOnly an option to return only the balance
|
|
7
|
+
*/
|
|
8
|
+
declare function getStats(address: Address, balanceOnly: boolean): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* get all structured transactions related to an address
|
|
11
|
+
* @param address the address being analyzed
|
|
12
|
+
*/
|
|
13
|
+
declare function getTransactions(address: Address): void;
|
|
14
|
+
/**
|
|
15
|
+
* get all normalized transactions related to an account-based address
|
|
16
|
+
* @param address an account-based address (typically Ethereum)
|
|
17
|
+
*/
|
|
18
|
+
declare function getAccountBasedTransactions(address: Address): void;
|
|
19
|
+
export { getStats, getTransactions, getAccountBasedTransactions };
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Here, the raw data is fetched from Crypto APIs (i.e., Crypto APIs):
|
|
3
|
+
// - balance,
|
|
4
|
+
// - total spent and received, and
|
|
5
|
+
// - operations
|
|
6
|
+
// per address
|
|
7
|
+
//
|
|
8
|
+
// Crypto APIs 2.0 <https://cryptoapis.io/>
|
|
9
|
+
// https://developers.cryptoapis.io/technical-documentation/general-information/overview
|
|
10
|
+
//
|
|
11
|
+
// In order to enable Crypto APIs, an API key has to be provided (see: README.md)
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.getAccountBasedTransactions = exports.getTransactions = exports.getStats = void 0;
|
|
49
|
+
const helpers = __importStar(require("../helpers"));
|
|
50
|
+
const currencies_1 = require("../configuration/currencies");
|
|
51
|
+
const settings_1 = require("../configuration/settings");
|
|
52
|
+
const transaction_1 = require("../models/transaction");
|
|
53
|
+
const operation_1 = require("../models/operation");
|
|
54
|
+
const date_fns_1 = require("date-fns");
|
|
55
|
+
const bchaddrjs_1 = __importDefault(require("bchaddrjs"));
|
|
56
|
+
const bignumber_js_1 = __importDefault(require("bignumber.js"));
|
|
57
|
+
const object_hash_1 = __importDefault(require("object-hash"));
|
|
58
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
59
|
+
// ┃ FETCH RAW DATA FROM CRYPTO APIS ┃
|
|
60
|
+
// ┃ just fetch the JSON responses ┃
|
|
61
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
62
|
+
/**
|
|
63
|
+
* fetch the response associated with the request (basic data, transactions)
|
|
64
|
+
* @param currency the currency being analyzed
|
|
65
|
+
* @param address the address being analyzed
|
|
66
|
+
* @param endpoint the endpoint to call
|
|
67
|
+
* @returns an array of transactions (if any)
|
|
68
|
+
*/
|
|
69
|
+
function fetchPayloads(currency, address, endpoint) {
|
|
70
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
71
|
+
// limit the number of transactions per request
|
|
72
|
+
const maxItemsPerRequest = 50;
|
|
73
|
+
const payloads = [];
|
|
74
|
+
const getTxsURLTemplate = settings_1.configuration.externalProviderURL
|
|
75
|
+
.concat("/addresses/")
|
|
76
|
+
.concat(address.toString())
|
|
77
|
+
.replace("{currency}", currency)
|
|
78
|
+
.concat(endpoint
|
|
79
|
+
.concat("?limit=")
|
|
80
|
+
.concat(maxItemsPerRequest.toString())
|
|
81
|
+
.concat("&offset={offset}"));
|
|
82
|
+
// to handle large number of transactions by address, use the index+limit logic
|
|
83
|
+
// offered by Crypto APIs
|
|
84
|
+
let offset = 0;
|
|
85
|
+
let itemsRemainingToBeFetched = true;
|
|
86
|
+
while (itemsRemainingToBeFetched) {
|
|
87
|
+
const url = getTxsURLTemplate.replace("{offset}", String(offset));
|
|
88
|
+
const txs = yield helpers.getJSON(url, settings_1.configuration.APIKey);
|
|
89
|
+
const payload = txs.data.items;
|
|
90
|
+
// when the limit includes the total number of transactions,
|
|
91
|
+
// no need to go further
|
|
92
|
+
if (payload.length === 0) {
|
|
93
|
+
itemsRemainingToBeFetched = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
payloads.push(payload);
|
|
97
|
+
offset += maxItemsPerRequest;
|
|
98
|
+
}
|
|
99
|
+
return payloads;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* fetch the transactions associated with a transaction hash
|
|
104
|
+
* @param currency the currency being analyzed
|
|
105
|
+
* @param transactionHash a transaction hash
|
|
106
|
+
* @returns the transactions associated with a transaction hash
|
|
107
|
+
*/
|
|
108
|
+
function fetchTransactionPayload(currency, transactionHash) {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
const url = settings_1.configuration.externalProviderURL
|
|
111
|
+
.replace("{currency}", currency)
|
|
112
|
+
.concat("/transactions/")
|
|
113
|
+
.concat(transactionHash);
|
|
114
|
+
// important: a valid API key has to be provided
|
|
115
|
+
const txs = yield helpers.getJSON(url, settings_1.configuration.APIKey);
|
|
116
|
+
return txs.data.item;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* fetch the raw basic stats (balance, transactions count...) associated with an address
|
|
121
|
+
* @param currency the currency being analyzed
|
|
122
|
+
* @param address the address being analyzed
|
|
123
|
+
* @returns the basic data associated with an address
|
|
124
|
+
*/
|
|
125
|
+
function fetchOperationsPayloads(currency, address) {
|
|
126
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
127
|
+
return fetchPayloads(currency, address.toString(), "/transactions");
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* fetch the raw token-related transactions related to an address
|
|
132
|
+
* @param currency the currency being analyzed (account-based; typically Ethereum)
|
|
133
|
+
* @param address the address being analyzed (account-based; typically Ethereum)
|
|
134
|
+
* @returns the token-related transactions related to an address
|
|
135
|
+
*/
|
|
136
|
+
function fetchTokenPayloads(currency, address) {
|
|
137
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
const rawTokenOperations = yield fetchPayloads(currency, address.toString(), "/tokens-transfers");
|
|
139
|
+
const tokenOperations = [].concat(...rawTokenOperations);
|
|
140
|
+
// augment token operations with transaction data
|
|
141
|
+
for (const tokenOperation of tokenOperations) {
|
|
142
|
+
const transaction = yield fetchTransactionPayload(currency, tokenOperation.transactionHash);
|
|
143
|
+
// add data related to recipients and senders
|
|
144
|
+
tokenOperation.recipients = transaction.recipients;
|
|
145
|
+
tokenOperation.senders = transaction.senders;
|
|
146
|
+
tokenOperation.fee = transaction.fee;
|
|
147
|
+
}
|
|
148
|
+
return tokenOperations;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* fetch the raw internal transactions related to an address
|
|
153
|
+
* @param currency the currency being analyzed (account-based; typically Ethereum)
|
|
154
|
+
* @param address the address being analyzed (account-based; typically Ethereum)
|
|
155
|
+
* @returns the internal transactions related to an address
|
|
156
|
+
*/
|
|
157
|
+
function fetchInternalTransactionsPayloads(currency, address) {
|
|
158
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
159
|
+
return fetchPayloads(currency, address.toString(), "/internal");
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
163
|
+
// ┃ NORMALIZE TRANSACTIONS FROM CRYPTO APIS ┃
|
|
164
|
+
// ┃ transform JSONs into stats and Operations objects ┃
|
|
165
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
166
|
+
/**
|
|
167
|
+
* fetch the structured basic stats related to an address
|
|
168
|
+
* its balance, funded and spend sums and counts
|
|
169
|
+
* @param address the address being analyzed
|
|
170
|
+
* @param balanceOnly an option to return only the balance
|
|
171
|
+
*/
|
|
172
|
+
function getStats(address, balanceOnly) {
|
|
173
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
174
|
+
// important: currency name is required to be lower case for Crypto APIs
|
|
175
|
+
const currency = settings_1.configuration.currency.name.toLowerCase().replace(" ", "-");
|
|
176
|
+
const url = settings_1.configuration.externalProviderURL
|
|
177
|
+
.concat("/addresses/")
|
|
178
|
+
.concat(address.toString())
|
|
179
|
+
.replace("{currency}", currency);
|
|
180
|
+
const res = yield helpers.getJSON(url, settings_1.configuration.APIKey);
|
|
181
|
+
const item = res.data.item;
|
|
182
|
+
const fundedSum = item.totalReceived.amount;
|
|
183
|
+
const spentSum = item.totalSpent.amount;
|
|
184
|
+
const balance = item.confirmedBalance.amount;
|
|
185
|
+
const txCount = item.transactionsCount;
|
|
186
|
+
address.setStats(txCount, fundedSum, spentSum);
|
|
187
|
+
address.setBalance(balance);
|
|
188
|
+
// get transactions (when applicable)
|
|
189
|
+
if (!balanceOnly) {
|
|
190
|
+
let payloads = yield fetchOperationsPayloads(currency, address);
|
|
191
|
+
// Ethereum: add token-related and internal transactions
|
|
192
|
+
if (settings_1.configuration.currency.symbol === currencies_1.currencies.eth.symbol) {
|
|
193
|
+
payloads = payloads.concat(yield fetchTokenPayloads(currency, address));
|
|
194
|
+
payloads = payloads.concat(payloads, yield fetchInternalTransactionsPayloads(currency, address));
|
|
195
|
+
}
|
|
196
|
+
// flatten the payloads
|
|
197
|
+
const rawTransactions = [].concat(...payloads);
|
|
198
|
+
// Remove duplicates
|
|
199
|
+
// (related to a bug from Crypto APIs)
|
|
200
|
+
const uniqueRawTransactions = [];
|
|
201
|
+
for (let i = rawTransactions.length - 1; i >= 0; i--) {
|
|
202
|
+
const transaction = rawTransactions[i];
|
|
203
|
+
const h = (0, object_hash_1.default)(transaction);
|
|
204
|
+
if (!uniqueRawTransactions.includes(h)) {
|
|
205
|
+
uniqueRawTransactions.push(h);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// remove duplicate
|
|
209
|
+
rawTransactions.splice(i, 1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
address.setRawTransactions(rawTransactions);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
exports.getStats = getStats;
|
|
217
|
+
/**
|
|
218
|
+
* get all structured transactions related to an address
|
|
219
|
+
* @param address the address being analyzed
|
|
220
|
+
*/
|
|
221
|
+
function getTransactions(address) {
|
|
222
|
+
const rawTransactions = address.getRawTransactions();
|
|
223
|
+
const transactions = [];
|
|
224
|
+
// Bitcoin Cash addresses are expressed as cash addresses by Crypto APIs:
|
|
225
|
+
// they have to be converted into legacy ones (if needed)
|
|
226
|
+
const processAddress = (originalAddress) => {
|
|
227
|
+
if (settings_1.configuration.currency.symbol === currencies_1.currencies.bch.symbol) {
|
|
228
|
+
return bchaddrjs_1.default.toLegacyAddress(originalAddress);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return originalAddress;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
// transforms raw transactions associated with an address
|
|
235
|
+
// into an array of processed transactions:
|
|
236
|
+
// [ { blockHeight, txid, ins: [ { address, value }... ], outs: [ { address, value }...] } ]
|
|
237
|
+
rawTransactions.forEach((tx) => {
|
|
238
|
+
const ins = [];
|
|
239
|
+
const outs = [];
|
|
240
|
+
// identify whether the address belongs to the list of transactors or not
|
|
241
|
+
const addressBelongsToTransactors = (transactors) => {
|
|
242
|
+
return transactors.some((t) => processAddress(t.address).includes(address.toString()));
|
|
243
|
+
};
|
|
244
|
+
// the address currently being analyzed is a — recipient —
|
|
245
|
+
if (addressBelongsToTransactors(tx.recipients)) {
|
|
246
|
+
for (const recipient of tx.recipients) {
|
|
247
|
+
if (processAddress(recipient.address).includes(address.toString())) {
|
|
248
|
+
// add one operation per sender
|
|
249
|
+
for (const sender of tx.senders) {
|
|
250
|
+
const op = new operation_1.Operation(String(tx.timestamp), new bignumber_js_1.default(recipient.amount));
|
|
251
|
+
op.setAddress(processAddress(sender.address));
|
|
252
|
+
op.setTxid(tx.transactionId);
|
|
253
|
+
op.setOperationType("Received");
|
|
254
|
+
ins.push(op);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// the address currently being analyzed is a — sender —
|
|
260
|
+
if (addressBelongsToTransactors(tx.senders)) {
|
|
261
|
+
for (let i = 0; i < tx.recipients.length; i++) {
|
|
262
|
+
const recipient = tx.recipients[i];
|
|
263
|
+
// note: the amount sent is specified in blockchainSpecific.vout
|
|
264
|
+
// _at the same index as the recipient_
|
|
265
|
+
const op = new operation_1.Operation(String(tx.timestamp), new bignumber_js_1.default(tx.blockchainSpecific.vout[i].value));
|
|
266
|
+
op.setAddress(processAddress(recipient.address));
|
|
267
|
+
op.setTxid(tx.transactionId);
|
|
268
|
+
op.setOperationType("Sent");
|
|
269
|
+
outs.push(op);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
transactions.push(new transaction_1.Transaction(tx.minedInBlockHeight, (0, date_fns_1.format)(new Date(tx.timestamp * 1000), "yyyy-MM-dd HH:mm:ss"), tx.transactionId, ins, outs));
|
|
273
|
+
});
|
|
274
|
+
address.setTransactions(transactions);
|
|
275
|
+
}
|
|
276
|
+
exports.getTransactions = getTransactions;
|
|
277
|
+
/**
|
|
278
|
+
* get all normalized transactions related to an account-based address
|
|
279
|
+
* @param address an account-based address (typically Ethereum)
|
|
280
|
+
*/
|
|
281
|
+
function getAccountBasedTransactions(address) {
|
|
282
|
+
const rawTransactions = address.getRawTransactions();
|
|
283
|
+
rawTransactions.forEach((tx) => {
|
|
284
|
+
// skip non-basic operations
|
|
285
|
+
if (typeof tx.blockchainSpecific === "undefined") {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const isSender = tx.senders.some((t) => t.address.toLowerCase() === address.toString().toLowerCase());
|
|
289
|
+
const isRecipient = tx.recipients.some((t) => t.address.toLowerCase() === address.toString().toLowerCase());
|
|
290
|
+
const isFailedOperation = tx.blockchainSpecific.transactionStatus !== "0x1";
|
|
291
|
+
// ignore failed *incoming* transactions
|
|
292
|
+
if (isFailedOperation && isRecipient) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const timestamp = (0, date_fns_1.format)(new Date(tx.timestamp * 1000), "yyyy-MM-dd HH:mm:ss");
|
|
296
|
+
// the address currently being analyzed is a — recipient —
|
|
297
|
+
if (isRecipient) {
|
|
298
|
+
const amount = tx.recipients.reduce((a, b) => +a + +b.amount, 0);
|
|
299
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
300
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
301
|
+
op.setAddress(address.toString());
|
|
302
|
+
op.setTxid(tx.transactionId);
|
|
303
|
+
op.setOperationType("Received");
|
|
304
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
305
|
+
address.addFundedOperation(op);
|
|
306
|
+
}
|
|
307
|
+
// the address currently being analyzed is a — sender —
|
|
308
|
+
if (isSender) {
|
|
309
|
+
const amount = new bignumber_js_1.default(tx.recipients.reduce((a, b) => +a + +b.amount, 0));
|
|
310
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
311
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
312
|
+
op.setAddress(address.toString());
|
|
313
|
+
op.setTxid(tx.transactionId);
|
|
314
|
+
if (!isFailedOperation) {
|
|
315
|
+
op.setOperationType(isRecipient ? "Sent to self" : "Sent");
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// failed outgoing operation
|
|
319
|
+
op.setOperationType("Failed to send");
|
|
320
|
+
}
|
|
321
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
322
|
+
address.addSentOperation(op);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
getTokenTransactions(address);
|
|
326
|
+
getInternalTransactions(address);
|
|
327
|
+
}
|
|
328
|
+
exports.getAccountBasedTransactions = getAccountBasedTransactions;
|
|
329
|
+
/**
|
|
330
|
+
* get all normalized token-related transactions associated with an account-based address
|
|
331
|
+
* @param address an account-based address (typically Ethereum)
|
|
332
|
+
*/
|
|
333
|
+
function getTokenTransactions(address) {
|
|
334
|
+
const rawTransactions = address.getRawTransactions();
|
|
335
|
+
rawTransactions.forEach((tx) => {
|
|
336
|
+
// skip non-token operations
|
|
337
|
+
if (typeof tx.senderAddress === "undefined") {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const isSender = tx.senderAddress.toLocaleLowerCase() ===
|
|
341
|
+
address.toString().toLocaleLowerCase();
|
|
342
|
+
const isRecipient = tx.recipientAddress.toLocaleLowerCase() ===
|
|
343
|
+
address.toString().toLocaleLowerCase();
|
|
344
|
+
const tokenAmount = new bignumber_js_1.default(tx.tokensAmount);
|
|
345
|
+
const tokenName = tx.tokenName;
|
|
346
|
+
const tokenSymbol = tx.tokenSymbol;
|
|
347
|
+
const timestamp = (0, date_fns_1.format)(new Date(tx.transactionTimestamp * 1000), "yyyy-MM-dd HH:mm:ss");
|
|
348
|
+
// compute amount
|
|
349
|
+
// (note: the dualities isSender/isRecipient and has sent/has received do not necessarily
|
|
350
|
+
// overlap (e.g., a recipient can also have sent in the swapping context)
|
|
351
|
+
const fees = new bignumber_js_1.default(tx.fee.amount);
|
|
352
|
+
let amount = new bignumber_js_1.default(0);
|
|
353
|
+
let hasSent = false;
|
|
354
|
+
// the address currently being analyzed is a — recipient —
|
|
355
|
+
for (const recipient of tx.recipients) {
|
|
356
|
+
if (recipient.address.toLocaleLowerCase() ===
|
|
357
|
+
address.toString().toLocaleLowerCase()) {
|
|
358
|
+
amount = amount.plus(recipient.amount);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// the address currently being analyzed is a — sender —
|
|
362
|
+
for (const sender of tx.senders) {
|
|
363
|
+
if (sender.address.toLocaleLowerCase() ===
|
|
364
|
+
address.toString().toLocaleLowerCase()) {
|
|
365
|
+
amount = amount.minus(sender.amount);
|
|
366
|
+
hasSent = true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (hasSent) {
|
|
370
|
+
amount = amount.plus(fees); // if has sent, add fees
|
|
371
|
+
}
|
|
372
|
+
// the address currently being analyzed is a — recipient —
|
|
373
|
+
if (isRecipient) {
|
|
374
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
375
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
376
|
+
op.setAddress(address.toString());
|
|
377
|
+
op.setTxid(tx.transactionHash);
|
|
378
|
+
// operation type: if is recipient but has sent: swap operation
|
|
379
|
+
op.setOperationType(hasSent ? "Swapped" : "Received (token)");
|
|
380
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
381
|
+
op.addToken(tokenSymbol, tokenName, tokenAmount);
|
|
382
|
+
address.addFundedOperation(op);
|
|
383
|
+
}
|
|
384
|
+
// the address currently being analyzed is a — sender —
|
|
385
|
+
if (isSender) {
|
|
386
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
387
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
388
|
+
op.setAddress(address.toString());
|
|
389
|
+
op.setTxid(tx.transactionHash);
|
|
390
|
+
op.setOperationType("Sent (token)");
|
|
391
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
392
|
+
op.addToken(tokenSymbol, tokenName, tokenAmount);
|
|
393
|
+
address.addSentOperation(op);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* get all normalized internal transactions associated with an account-based address
|
|
399
|
+
* @param address an account-based address (typically Ethereum)
|
|
400
|
+
*/
|
|
401
|
+
function getInternalTransactions(address) {
|
|
402
|
+
const rawTransactions = address.getRawTransactions();
|
|
403
|
+
rawTransactions.forEach((tx) => {
|
|
404
|
+
// skip non-internal transactions
|
|
405
|
+
if (typeof tx.operationType === "undefined") {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const isSender = tx.sender.toLocaleLowerCase() === address.toString().toLocaleLowerCase();
|
|
409
|
+
const isRecipient = tx.recipient.toLocaleLowerCase() ===
|
|
410
|
+
address.toString().toLocaleLowerCase();
|
|
411
|
+
const amount = new bignumber_js_1.default(0);
|
|
412
|
+
const timestamp = (0, date_fns_1.format)(new Date(tx.timestamp * 1000), "yyyy-MM-dd HH:mm:ss");
|
|
413
|
+
// the address currently being analyzed is a — recipient —
|
|
414
|
+
if (isRecipient) {
|
|
415
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
416
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
417
|
+
op.setAddress(address.toString());
|
|
418
|
+
op.setTxid(tx.parentHash);
|
|
419
|
+
op.setOperationType("SCI (recipient)");
|
|
420
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
421
|
+
address.addFundedOperation(op);
|
|
422
|
+
}
|
|
423
|
+
// the address currently being analyzed is a — sender —
|
|
424
|
+
if (isSender) {
|
|
425
|
+
const fixedAmount = amount.toFixed(settings_1.ETH_FIXED_PRECISION);
|
|
426
|
+
const op = new operation_1.Operation(timestamp, new bignumber_js_1.default(fixedAmount)); // ETH: use fixed-point notation
|
|
427
|
+
op.setAddress(address.toString());
|
|
428
|
+
op.setTxid(tx.parentHash);
|
|
429
|
+
op.setOperationType("SCI (caller)");
|
|
430
|
+
op.setBlockNumber(tx.minedInBlockHeight);
|
|
431
|
+
address.addSentOperation(op);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Address } from "../models/address";
|
|
2
|
+
/**
|
|
3
|
+
* fetch the structured basic stats related to an address
|
|
4
|
+
* its balance, funded and spend sums and counts
|
|
5
|
+
* @param address the address being analyzed
|
|
6
|
+
*/
|
|
7
|
+
declare function getStats(address: Address): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* get all structured transactions related to an address
|
|
10
|
+
* @param address the address being analyzed
|
|
11
|
+
*/
|
|
12
|
+
declare function getTransactions(address: Address): void;
|
|
13
|
+
/**
|
|
14
|
+
* get all structured transactions related to a Bitcoin Cash address
|
|
15
|
+
* @param address the address being analyzed
|
|
16
|
+
*/
|
|
17
|
+
declare function getBitcoinCashTransactions(address: Address): void;
|
|
18
|
+
/**
|
|
19
|
+
* get all structured transactions related to an account-based address (generally Ethereum)
|
|
20
|
+
* @param address the address being analyzed
|
|
21
|
+
*/
|
|
22
|
+
declare function getAccountBasedTransactions(address: Address): void;
|
|
23
|
+
export { getStats, getTransactions, getBitcoinCashTransactions, getAccountBasedTransactions, };
|