@abcpros/bitcore-wallet-service
Version:
A service for Mutisig HD Bitcoin Wallets
594 lines • 30 kB
JavaScript
;
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (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 (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator = (this && this.__generator) || function (thisArg, body) {
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
function verb(n) { return function (v) { return step([n, v]); }; }
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while (_) try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0: case 1: t = op; break;
case 4: _.label++; return { value: op[1], done: false };
case 5: _.label++; y = op[1]; op = [0]; continue;
case 7: op = _.ops.pop(); _.trys.pop(); continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
if (t[2]) _.ops.pop();
_.trys.pop(); continue;
}
op = body.call(thisArg, _);
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
}
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PushNotificationsService = void 0;
var async = __importStar(require("async"));
var fs = __importStar(require("fs"));
var lodash_1 = __importDefault(require("lodash"));
require("source-map-support/register");
var chain_1 = require("./chain");
var logger_1 = __importDefault(require("./logger"));
var messagebroker_1 = require("./messagebroker");
var storage_1 = require("./storage");
var Mustache = require('mustache');
var defaultRequest = require('request');
var path = require('path');
var Utils = require('./common/utils');
var Defaults = require('./common/defaults');
var Constants = require('./common/constants');
var sjcl = require('sjcl');
var config = require('../config');
var PUSHNOTIFICATIONS_TYPES = {
NewCopayer: {
filename: 'new_copayer'
},
WalletComplete: {
filename: 'wallet_complete'
},
NewTxProposal: {
filename: 'new_tx_proposal'
},
NewOutgoingTx: {
filename: 'new_outgoing_tx'
},
NewIncomingTx: {
filename: ['new_incoming_tx_testnet', 'new_incoming_tx']
},
TxProposalFinallyRejected: {
filename: 'txp_finally_rejected'
},
TxConfirmation: {
filename: ['tx_confirmation_sender', 'tx_confirmation_receiver']
},
NewAddress: {
dataOnly: true
},
NewBlock: {
dataOnly: true,
broadcastToActiveUsers: true
},
TxProposalAcceptedBy: {
dataOnly: true
},
TxProposalFinallyAccepted: {
dataOnly: true
},
TxProposalRejectedBy: {
dataOnly: true
},
TxProposalRemoved: {
dataOnly: true
}
};
var PushNotificationsService = (function () {
function PushNotificationsService() {
}
PushNotificationsService.prototype.start = function (opts, cb) {
var _this = this;
opts = opts || {};
this.request = opts.request || defaultRequest;
var _readDirectories = function (basePath, cb) {
fs.readdir(basePath, function (err, files) {
if (err)
return cb(err);
async.filter(files, function (file, next) {
fs.stat(path.join(basePath, file), function (err, stats) {
return next(!err && stats.isDirectory());
});
}, function (dirs) {
return cb(null, dirs);
});
});
};
this.templatePath = path.normalize((opts.pushNotificationsOpts.templatePath || __dirname + '../../../templates') + '/');
this.defaultLanguage = opts.pushNotificationsOpts.defaultLanguage || 'en';
this.defaultUnit = opts.pushNotificationsOpts.defaultUnit || 'btc';
this.subjectPrefix = opts.pushNotificationsOpts.subjectPrefix || '';
this.pushServerUrl = opts.pushNotificationsOpts.pushServerUrl;
this.authorizationKey = opts.pushNotificationsOpts.authorizationKey;
if (!this.authorizationKey)
return cb(new Error('Missing authorizationKey attribute in configuration.'));
async.parallel([
function (done) {
_readDirectories(_this.templatePath, function (err, res) {
if (err) {
_this.templatePath = path.normalize(__dirname + '../../../templates/');
_readDirectories(_this.templatePath, function (err, res) {
_this.availableLanguages = res;
done(err);
});
}
else {
_this.availableLanguages = res;
done(err);
}
});
},
function (done) {
if (opts.storage) {
_this.storage = opts.storage;
done();
}
else {
_this.storage = new storage_1.Storage();
_this.storage.connect(opts.storageOpts, done);
}
},
function (done) {
_this.messageBroker = opts.messageBroker || new messagebroker_1.MessageBroker(opts.messageBrokerOpts);
_this.messageBroker.onMessage(lodash_1.default.bind(_this._sendPushNotifications, _this));
done();
}
], function (err) {
if (err) {
logger_1.default.error('ERROR:' + err);
}
return cb(err);
});
};
PushNotificationsService.prototype._sendPushNotifications = function (notification, cb) {
var _this = this;
cb = cb || function () { };
var notifType = lodash_1.default.cloneDeep(PUSHNOTIFICATIONS_TYPES[notification.type]);
if (!notifType)
return cb();
if (notification.type === 'NewIncomingTx') {
notifType.filename = notification.data.network === 'testnet' ? notifType.filename[0] : notifType.filename[1];
}
else if (notification.type === 'TxConfirmation') {
if (notification.data && !notification.data.amount) {
notifType.filename = 'tx_confirmation';
}
else {
notifType.filename = notification.isCreator ? notifType.filename[0] : notifType.filename[1];
}
}
logger_1.default.debug('Notification received: ' + notification.type);
logger_1.default.debug(JSON.stringify(notification));
this._checkShouldSendNotif(notification, function (err, should) {
if (err)
return cb(err);
logger_1.default.debug('Should send notification: ' + should);
if (!should)
return cb();
_this._getRecipientsList(notification, notifType, function (err, recipientsList) {
if (err)
return cb(err);
async.waterfall([
function (next) {
_this._readAndApplyTemplates(notification, notifType, recipientsList, next);
},
function (contents, next) {
_this._getSubscriptions(notification, notifType, recipientsList, contents, next);
},
function (subs, next) {
var notifications = lodash_1.default.map(subs, function (sub) {
var _a, _b, _c, _d, _e, _f, _g;
if (notification.type === 'NewTxProposal' && sub.copayerId === notification.creatorId)
return;
var tokenAddress = notification.data && notification.data.tokenAddress ? notification.data.tokenAddress : null;
var multisigContractAddress = notification.data && notification.data.multisigContractAddress
? notification.data.multisigContractAddress
: null;
var notificationData = {
to: sub.token,
priority: 'high',
restricted_package_name: sub.packageName,
data: {
walletId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(notification.walletId || sub.walletId)),
tokenAddress: tokenAddress,
multisigContractAddress: multisigContractAddress,
copayerId: sjcl.codec.hex.fromBits(sjcl.hash.sha256.hash(sub.copayerId)),
notification_type: notification.type,
coin: (_a = notification === null || notification === void 0 ? void 0 : notification.data) === null || _a === void 0 ? void 0 : _a.coin,
network: (_b = notification === null || notification === void 0 ? void 0 : notification.data) === null || _b === void 0 ? void 0 : _b.network,
tokenId: (_c = notification === null || notification === void 0 ? void 0 : notification.data) === null || _c === void 0 ? void 0 : _c.tokenId
}
};
if (!notifType.dataOnly) {
notificationData.data.title = (_d = sub === null || sub === void 0 ? void 0 : sub.plain) === null || _d === void 0 ? void 0 : _d.subject;
notificationData.data.body = (_e = sub === null || sub === void 0 ? void 0 : sub.plain) === null || _e === void 0 ? void 0 : _e.body;
notificationData.notification = {
title: (_f = sub === null || sub === void 0 ? void 0 : sub.plain) === null || _f === void 0 ? void 0 : _f.subject,
body: (_g = sub === null || sub === void 0 ? void 0 : sub.plain) === null || _g === void 0 ? void 0 : _g.body,
sound: 'default',
click_action: 'FCM_PLUGIN_ACTIVITY',
icon: 'fcm_push_icon'
};
}
return notificationData;
});
if (notifications &&
notifications[0] &&
notifications[0].notification &&
subs.length > Defaults.PUSH_NOTIFICATION_LIMIT) {
logger_1.default.warn("The recipient list for this push notification is greater than the established limit (" + Defaults.PUSH_NOTIFICATION_LIMIT + ")");
}
return next(err, notifications);
},
function (notifications, next) {
async.each(notifications, function (notification, next) {
_this._makeRequest(notification, function (err, response) {
if (err)
logger_1.default.error('ERROR:' + err);
if (response) {
}
next();
});
}, function (err) {
return next(err);
});
}
], function (err) {
if (err) {
logger_1.default.error('An error ocurred generating notification:' + err);
}
return cb(err);
});
});
});
};
PushNotificationsService.prototype._checkShouldSendNotif = function (notification, cb) {
if (notification.type != 'NewTxProposal')
return cb(null, true);
this.storage.fetchWallet(notification.walletId, function (err, wallet) {
return cb(err, wallet && wallet.m > 1);
});
};
PushNotificationsService.prototype._getRecipientsList = function (notification, notificationType, cb) {
var _this = this;
if (notificationType.broadcastToActiveUsers)
return cb(null, []);
this.storage.fetchWallet(notification.walletId, function (err, wallet) { return __awaiter(_this, void 0, void 0, function () {
var unit, tokenId, tokenName, tokenDecimals, chronikClient_1, txDetail;
var _this = this;
var _a, _b;
return __generator(this, function (_c) {
switch (_c.label) {
case 0:
if (err)
return [2, cb(err)];
if (!wallet)
return [2, cb(null, [])];
if (wallet.coin != Defaults.COIN) {
unit = wallet.coin;
}
if (!wallet.isSlpToken) return [3, 2];
chronikClient_1 = chain_1.ChainService.getChronikClient(wallet.coin);
return [4, chronikClient_1.tx(notification.data.txid)];
case 1:
txDetail = _c.sent();
tokenId = ((_b = (_a = txDetail === null || txDetail === void 0 ? void 0 : txDetail.slpTxData) === null || _a === void 0 ? void 0 : _a.slpMeta) === null || _b === void 0 ? void 0 : _b.tokenId) || null;
if (tokenId) {
this.storage.fetchTokenInfoById(tokenId, function (err, tokenInfo) { return __awaiter(_this, void 0, void 0, function () {
var tokenInfoChronik, tokenInfo_1;
var _a;
return __generator(this, function (_b) {
switch (_b.label) {
case 0:
if (err)
logger_1.default.error(err);
if (!lodash_1.default.isEmpty(tokenInfo)) return [3, 2];
return [4, chronikClient_1.tx(tokenId)];
case 1:
tokenInfoChronik = _b.sent();
tokenInfo_1 = (_a = tokenInfoChronik === null || tokenInfoChronik === void 0 ? void 0 : tokenInfoChronik.slpTxData) === null || _a === void 0 ? void 0 : _a.genesisInfo;
tokenId = tokenId;
unit = tokenInfo_1 === null || tokenInfo_1 === void 0 ? void 0 : tokenInfo_1.tokenTicker;
tokenName = tokenInfo_1 === null || tokenInfo_1 === void 0 ? void 0 : tokenInfo_1.tokenName;
tokenDecimals = tokenInfo_1 === null || tokenInfo_1 === void 0 ? void 0 : tokenInfo_1.decimals;
return [3, 3];
case 2:
tokenId = tokenId;
unit = tokenInfo === null || tokenInfo === void 0 ? void 0 : tokenInfo.symbol;
tokenName = tokenInfo === null || tokenInfo === void 0 ? void 0 : tokenInfo.name;
tokenDecimals = tokenInfo === null || tokenInfo === void 0 ? void 0 : tokenInfo.decimals;
_b.label = 3;
case 3: return [2];
}
});
}); });
notification.data.amount = Number(txDetail.outputs[1].slpToken.amount) || null;
notification.data.tokenId = tokenId || null;
}
_c.label = 2;
case 2:
this.storage.fetchPreferences(notification.walletId, null, function (err, preferences) {
if (err)
logger_1.default.error(err);
if (lodash_1.default.isEmpty(preferences))
preferences = [];
var recipientPreferences = lodash_1.default.compact(lodash_1.default.map(preferences, function (p) {
if (!lodash_1.default.includes(_this.availableLanguages, p.language)) {
if (p.language)
logger_1.default.warn('Language for notifications "' + p.language + '" not available.');
p.language = _this.defaultLanguage;
}
return {
copayerId: p.copayerId,
language: p.language || _this.defaultLanguage,
unit: unit || p.unit || _this.defaultUnit
};
}));
var copayers = lodash_1.default.keyBy(recipientPreferences, 'copayerId');
var recipientsList = lodash_1.default.compact(lodash_1.default.map(wallet.copayers, function (copayer) {
var p = copayers[copayer.id] || {
language: _this.defaultLanguage,
unit: _this.defaultUnit
};
return {
walletId: notification.walletId,
copayerId: copayer.id,
language: p.language || _this.defaultLanguage,
unit: unit || p.unit || _this.defaultUnit,
tokenId: tokenId || null,
tokenName: tokenName || null,
tokenDecimals: tokenDecimals || null
};
}));
return cb(null, recipientsList);
});
return [2];
}
});
}); });
};
PushNotificationsService.prototype._readAndApplyTemplates = function (notification, notifType, recipientsList, cb) {
var _this = this;
if (!notifType.filename)
return cb(null, []);
async.map(recipientsList, function (recipient, next) {
async.waterfall([
function (next) {
_this._getDataForTemplate(notification, recipient, next);
},
function (data, next) {
async.map(['plain', 'html'], function (type, next) {
_this._loadTemplate(notifType, recipient, '.' + type, function (err, template) {
if (err && type == 'html')
return next();
if (err)
return next(err);
_this._applyTemplate(template, data, function (err, res) {
return next(err, [type, res]);
});
});
}, function (err, res) {
return next(err, lodash_1.default.fromPairs(res.filter(Boolean)));
});
},
function (result, next) {
next(null, result);
}
], function (err, res) {
next(err, [recipient.language, res]);
});
}, function (err, res) {
return cb(err, lodash_1.default.fromPairs(res.filter(Boolean)));
});
};
PushNotificationsService.prototype._getDataForTemplate = function (notification, recipient, cb) {
var UNIT_LABELS = {
btc: 'BTC',
bit: 'bits',
bch: 'BCH',
xec: 'XEC',
eth: 'ETH',
xrp: 'XRP',
doge: 'DOGE',
ltc: 'LTC',
usdc: 'USDC',
pax: 'PAX',
gusd: 'GUSD',
busd: 'BUSD',
wbtc: 'WBTC',
dai: 'DAI',
xpi: 'XPI'
};
var data = lodash_1.default.cloneDeep(notification.data);
data.subjectPrefix = lodash_1.default.trim(this.subjectPrefix + ' ');
if (data.amount) {
try {
var unit = recipient.unit.toLowerCase();
var label = recipient.tokenName || UNIT_LABELS[unit];
if (data.tokenAddress) {
var tokenAddress = data.tokenAddress.toLowerCase();
if (Constants.TOKEN_OPTS[tokenAddress]) {
unit = Constants.TOKEN_OPTS[tokenAddress].symbol.toLowerCase();
label = UNIT_LABELS[unit];
}
else {
label = 'tokens';
throw new Error('Notifications for unsupported token are not allowed');
}
}
if (recipient.tokenId && recipient.tokenDecimals) {
var caculateAmountToken = function (amount, decimals) {
return amount / Math.pow(10, decimals);
};
data.amount = caculateAmountToken(data.amount, recipient.tokenDecimals) + ' ' + label;
}
else {
data.amount = Utils.formatAmount(+data.amount, unit) + ' ' + label;
}
}
catch (ex) {
return cb(new Error('Could not format amount' + ex));
}
}
this.storage.fetchWallet(notification.walletId, function (err, wallet) {
if (err || !wallet)
return cb(err);
data.walletId = wallet.id;
data.walletName = wallet.name;
data.walletM = wallet.m;
data.walletN = wallet.n;
var copayer = wallet.copayers.find(function (c) { return c.id === notification.creatorId; });
if (copayer) {
data.copayerId = copayer.id;
data.copayerName = copayer.name;
}
if (notification.type == 'TxProposalFinallyRejected' && data.rejectedBy) {
var rejectors = lodash_1.default.map(data.rejectedBy, function (copayerId) {
return wallet.copayers.find(function (c) { return c.id === copayerId; }).name;
});
data.rejectorsNames = rejectors.join(', ');
}
return cb(null, data);
});
};
PushNotificationsService.prototype._applyTemplate = function (template, data, cb) {
if (!data)
return cb(new Error('Could not apply template to empty data'));
var error;
var result = lodash_1.default.mapValues(template, function (t) {
try {
return Mustache.render(t, data);
}
catch (e) {
logger_1.default.error('Could not apply data to template:' + e);
error = e;
}
});
if (error)
return cb(error);
return cb(null, result);
};
PushNotificationsService.prototype._loadTemplate = function (notifType, recipient, extension, cb) {
var _this = this;
this._readTemplateFile(recipient.language, notifType.filename + extension, function (err, template) {
if (err)
return cb(err);
return cb(null, _this._compileTemplate(template, extension));
});
};
PushNotificationsService.prototype._readTemplateFile = function (language, filename, cb) {
var fullFilename = path.join(this.templatePath, language, filename);
fs.readFile(fullFilename, 'utf8', function (err, template) {
if (err) {
return cb(new Error('Could not read template file ' + fullFilename + err));
}
return cb(null, template);
});
};
PushNotificationsService.prototype._compileTemplate = function (template, extension) {
var lines = template.split('\n');
if (extension == '.html') {
lines.unshift('');
}
return {
subject: lines[0],
body: lodash_1.default.tail(lines).join('\n')
};
};
PushNotificationsService.prototype._getSubscriptions = function (notification, notifType, recipientsList, contents, cb) {
var _this = this;
if (notifType.broadcastToActiveUsers) {
this.storage.fetchLatestPushNotificationSubs(function (err, subs) {
if (err)
return cb(err);
var allSubs = lodash_1.default.uniqBy(lodash_1.default.reject(subs, function (sub) { return !sub.walletId; }), 'token');
logger_1.default.info("Sending " + notification.type + " [" + notification.data.coin + "/" + notification.data.network + "] notifications to: " + allSubs.length + " devices");
return cb(null, allSubs);
});
}
else {
async.map(recipientsList, function (recipient, next) {
var content = contents ? contents[recipient.language] : null;
_this.storage.fetchPushNotificationSubs(recipient.copayerId, function (err, subs) {
if (err)
return next(err);
var subscriptions = subs && subs.length ? subs.map(function (obj) { return (__assign(__assign({}, obj), { plain: content === null || content === void 0 ? void 0 : content.plain })); }) : subs;
return next(err, subscriptions);
});
}, function (err, allSubs) {
if (err)
return cb(err);
return cb(null, lodash_1.default.flatten(allSubs));
});
}
};
PushNotificationsService.prototype._makeRequest = function (opts, cb) {
this.request({
url: this.pushServerUrl + '/send',
method: 'POST',
json: true,
headers: {
'Content-Type': 'application/json',
Authorization: 'key=' + this.authorizationKey
},
body: opts
}, cb);
};
return PushNotificationsService;
}());
exports.PushNotificationsService = PushNotificationsService;
//# sourceMappingURL=pushnotificationsservice.js.map