UNPKG

@abcpros/bitcore-wallet-service

Version:
594 lines 30 kB
"use strict"; 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