bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
404 lines • 16.7 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BlockchainMonitor = void 0;
const async = __importStar(require("async"));
const lodash_1 = __importDefault(require("lodash"));
require("source-map-support/register");
const blockchainexplorer_1 = require("./blockchainexplorer");
const index_1 = require("./chain/index");
const common_1 = require("./common");
const utils_1 = require("./common/utils");
const lock_1 = require("./lock");
const logger_1 = __importDefault(require("./logger"));
const messagebroker_1 = require("./messagebroker");
const model_1 = require("./model");
const server_1 = require("./server");
const storage_1 = require("./storage");
const $ = require('preconditions').singleton();
const Constants = common_1.Common.Constants;
const throttle = (fn) => {
let lastCalled = 0;
return (...args) => {
const bcmContext = args[0], chain = args[1], network = args[2], hash = args[3];
let msDelay = bcmContext.getChainThrottleSetting(chain, network) * 1000;
let now = new Date().getTime();
if (now - lastCalled < msDelay) {
return;
}
lastCalled = now;
return fn(bcmContext, chain, network, hash);
};
};
const throttledNewBlocks = throttle((bcmContext, chain, network, hash) => {
bcmContext._notifyNewBlock(chain, network, hash);
bcmContext._handleTxConfirmations(chain, network, hash);
});
class BlockchainMonitor {
constructor() {
this.blockThrottleSettings = Constants.CHAIN_NEW_BLOCK_THROTTLE_TIME_SECONDS;
}
start(opts, cb) {
opts = opts || {};
this.N = opts.N || 100;
this.Ni = this.Nix = 0;
this.last = this.lastTx = [];
async.parallel([
done => {
this.explorers = {
btc: {},
bch: {},
eth: {},
matic: {},
xrp: {},
doge: {},
ltc: {},
arb: {},
base: {},
op: {},
sol: {},
};
const chainNetworkPairs = [];
lodash_1.default.each(lodash_1.default.values(Constants.CHAINS), chain => {
lodash_1.default.each(lodash_1.default.values(Constants.NETWORKS[chain]), network => {
chainNetworkPairs.push({
chain,
network
});
});
});
lodash_1.default.each(chainNetworkPairs, pair => {
let explorer;
if (opts.blockchainExplorers &&
opts.blockchainExplorers[pair.chain] &&
opts.blockchainExplorers[pair.chain][pair.network]) {
explorer = opts.blockchainExplorers[pair.chain][pair.network];
}
else {
let config = {};
if (opts.blockchainExplorerOpts &&
opts.blockchainExplorerOpts[pair.chain] &&
opts.blockchainExplorerOpts[pair.chain][pair.network]) {
config = opts.blockchainExplorerOpts[pair.chain][pair.network];
}
else {
return;
}
const bcNetwork = utils_1.Utils.getNetworkType(pair.network) === 'testnet' && config.regtestEnabled ? 'regtest' : pair.network;
explorer = (0, blockchainexplorer_1.BlockChainExplorer)({
provider: config.provider,
chain: pair.chain,
network: bcNetwork,
url: config.url,
userAgent: server_1.WalletService.getServiceVersion()
});
}
$.checkState(explorer, 'Failed State: explorer undefined at <start()>');
this._initExplorer(pair.chain, pair.network, explorer);
this.explorers[pair.chain][pair.network] = explorer;
});
done();
},
done => {
if (opts.storage) {
this.storage = opts.storage;
done();
}
else {
this.storage = new storage_1.Storage();
this.storage.connect({
...opts.storageOpts,
secondaryPreferred: true
}, done);
}
},
done => {
this.messageBroker = opts.messageBroker || new messagebroker_1.MessageBroker(opts.messageBrokerOpts);
done();
},
done => {
this.lock = opts.lock || new lock_1.Lock(this.storage);
done();
}
], err => {
if (err) {
logger_1.default.error('%o', err);
}
return cb(err);
});
}
_initExplorer(chain, network, explorer) {
explorer.initSocket({
onBlock: lodash_1.default.bind(this._handleNewBlock, this, chain, network),
onIncomingPayments: lodash_1.default.bind(this._handleIncomingPayments, this, chain, network)
});
}
_handleThirdPartyBroadcasts(chain, network, data, processIt) {
if (!data || !data.txid)
return;
if (!processIt) {
if (this.lastTx.indexOf(data.txid) >= 0) {
return;
}
this.lastTx[this.Nix++] = data.txid;
if (this.Nix >= this.N)
this.Nix = 0;
logger_1.default.debug(`\tChecking ${chain}/${network} txid: ${data.txid}`);
}
this.storage.fetchTxByHash(data.txid, (err, txp) => {
if (err) {
logger_1.default.error('Could not fetch tx from the db');
return;
}
if (!txp || txp.status != 'accepted')
return;
const walletId = txp.walletId;
if (!processIt) {
logger_1.default.debug('Detected broadcast ' +
data.txid +
' of an accepted txp [' +
txp.id +
'] for wallet ' +
walletId +
' [' +
txp.amount +
'sat ]');
return setTimeout(this._handleThirdPartyBroadcasts.bind(this, chain, network, data, true), 20 * 1000);
}
logger_1.default.debug('Processing accepted txp [' + txp.id + '] for wallet ' + walletId + ' [' + txp.amount + 'sat ]');
txp.setBroadcasted();
this.storage.storeTx(this.walletId, txp, err => {
if (err)
logger_1.default.error('Could not save TX for wallet %o, %o', this.walletId, err);
const args = {
txProposalId: txp.id,
txid: data.txid,
amount: txp.getTotalAmount()
};
const notification = model_1.Notification.create({
type: 'NewOutgoingTxByThirdParty',
data: args,
walletId
});
this._storeAndBroadcastNotification(notification);
});
});
}
_handleIncomingPayments(chain, network, data) {
if (!data)
return;
let out = data.out;
if (!out || !out.address || out.address.length < 10)
return;
if (!Constants.EVM_CHAINS[chain.toUpperCase()]) {
if (!(out.amount > 0))
return;
if (this.last.indexOf(out.address) >= 0) {
logger_1.default.debug('The incoming tx"s out ' + out.address + ' was already processed');
return;
}
this.last[this.Ni++] = out.address;
if (this.Ni >= this.N)
this.Ni = 0;
}
else {
if (this.lastTx.indexOf(data.txid) >= 0) {
logger_1.default.debug('The incoming tx ' + data.txid + ' was already processed');
return;
}
this.lastTx[this.Nix++] = data.txid;
if (this.Nix >= this.N)
this.Nix = 0;
}
logger_1.default.debug(`Checking ${chain}:${network}:${out.address} ${out.amount}`);
this.storage.fetchAddressByChain(chain, out.address, (err, address) => {
if (err) {
logger_1.default.error('Could not fetch addresses from the db %o', err);
return;
}
if (!address || address.isChange) {
return this._handleThirdPartyBroadcasts(chain, network, data, null);
}
const walletId = address.walletId;
const fromTs = Date.now() - 24 * 3600 * 1000;
this.storage.fetchNotifications(walletId, null, fromTs, (err, notifications) => {
if (err)
return;
const alreadyNotified = lodash_1.default.some(notifications, n => {
return n.type == 'NewIncomingTx' && n.data && n.data.txid == data.txid;
});
if (alreadyNotified) {
logger_1.default.debug('The incoming tx ' + data.txid + ' was already notified');
return;
}
logger_1.default.debug('Incoming tx for wallet ' + walletId + ' [' + out.amount + 'amount -> ' + out.address + ']');
const notification = model_1.Notification.create({
type: 'NewIncomingTx',
data: {
txid: data.txid,
address: out.address,
amount: out.amount,
tokenAddress: out.tokenAddress,
multisigContractAddress: out.multisigContractAddress,
network
},
walletId
});
if (utils_1.Utils.getNetworkType(network) !== 'testnet') {
this.storage.fetchWallet(walletId, (err, wallet) => {
if (err)
return;
async.each(wallet.copayers, (c, next) => {
const sub = model_1.TxConfirmationSub.create({
copayerId: c.id,
walletId,
txid: data.txid,
amount: out.amount,
isCreator: false
});
this.storage.storeTxConfirmationSub(sub, next);
}, err => {
if (err)
logger_1.default.error('%o', err);
});
});
}
this._storeAndBroadcastNotification(notification, () => {
return;
});
});
});
}
_notifyNewBlock(chain, network, hash) {
logger_1.default.debug(` ** NOTIFY New ${chain}/${network} block ${hash}`);
const notification = model_1.Notification.create({
type: 'NewBlock',
walletId: `${chain}:${network}`,
data: {
hash,
chain,
network
}
});
this._storeAndBroadcastNotification(notification, () => { });
}
_handleTxConfirmations(chain, network, hash) {
if (!index_1.ChainService.notifyConfirmations(chain, network))
return;
const processTriggeredSub = (sub, cb) => {
logger_1.default.debug('New tx confirmation ' + sub.txid);
sub.isActive = false;
async.waterfall([
next => {
this.storage.storeTxConfirmationSub(sub, err => {
if (err)
return cb(err);
const notification = model_1.Notification.create({
type: 'TxConfirmation',
walletId: sub.walletId,
creatorId: sub.copayerId,
isCreator: sub.isCreator,
data: {
txid: sub.txid,
chain,
network,
amount: sub.amount
}
});
next(null, notification);
});
},
(notification, next) => {
this._storeAndBroadcastNotification(notification, next);
}
], cb);
};
const explorer = this.explorers[chain][network];
if (!explorer)
return;
explorer.getTxidsInBlock(hash, async (err, txids) => {
if (err) {
logger_1.default.error('Could not fetch txids from block %o %o', hash, err);
return;
}
const stream = this.storage.streamActiveTxConfirmationSubs(null, txids);
let txSub = await stream.next();
while (txSub != null) {
processTriggeredSub(txSub, err => {
if (err) {
logger_1.default.error('Could not process tx confirmation %o', err);
}
return;
});
txSub = await stream.next();
}
});
}
_handleNewBlock(chain, network, hash) {
const cacheKey = storage_1.Storage.BCHEIGHT_KEY + ':' + chain + ':' + network;
this.storage.clearGlobalCache(cacheKey, () => { });
if (chain == 'xrp') {
return;
}
if (this.getChainThrottleSetting(chain, network) > 0) {
throttledNewBlocks(this, chain, network, hash);
}
else {
this._notifyNewBlock(chain, network, hash);
this._handleTxConfirmations(chain, network, hash);
}
}
_storeAndBroadcastNotification(notification, cb) {
this.storage.storeNotification(notification.walletId, notification, () => {
this.messageBroker.send(notification);
if (cb)
return cb();
});
}
getChainThrottleSetting(chain, network) {
const config = this.blockThrottleSettings;
if (typeof config[chain] === 'object') {
if (typeof config[chain][network] === 'number') {
return config[chain][network];
}
}
return 0;
}
}
exports.BlockchainMonitor = BlockchainMonitor;
//# sourceMappingURL=blockchainmonitor.js.map