const _ = require('lodash/fp') const crypto = require('crypto') const pgp = require('pg-promise')() const { getTimezoneOffset } = require('date-fns-tz') const { millisecondsToMinutes } = require('date-fns/fp') const { skip2fa } = require('./environment-helper') const BN = require('./bn') const dbm = require('./postgresql_interface') const db = require('./db') const logger = require('./logger') const logs = require('./logs') const T = require('./time') const configManager = require('./new-config-manager') const settingsLoader = require('./new-settings-loader') const ticker = require('./ticker') const wallet = require('./wallet') const walletScoring = require('./wallet-scoring') const exchange = require('./exchange') const sms = require('./sms') const email = require('./email') const cashOutHelper = require('./cash-out/cash-out-helper') const machineLoader = require('./machine-loader') const commissionMath = require('./commission-math') const loyalty = require('./loyalty') const transactionBatching = require('./tx-batching') const { CASH_OUT_DISPENSE_READY, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS, CASH_UNIT_CAPACITY, CONFIRMATION_CODE, } = require('./constants') const notifier = require('./notifier') const { utils: coinUtils } = require('@lamassu/coins') const mapValuesWithKey = _.mapValues.convert({ cap: false, }) const TRADE_TTL = 2 * T.minutes const STALE_TICKER = 3 * T.minutes const STALE_BALANCE = 3 * T.minutes const tradesQueues = {} function plugins(settings, deviceId) { function internalBuildRates(tickers, withCommission = true) { const localeConfig = configManager.getLocale(deviceId, settings.config) const cryptoCodes = localeConfig.cryptoCurrencies const rates = {} cryptoCodes.forEach((cryptoCode, i) => { const rateRec = tickers[i] const commissions = configManager.getCommissions( cryptoCode, deviceId, settings.config, ) if (!rateRec) return const cashInCommission = new BN(1).plus( new BN(commissions.cashIn).div(100), ) const cashOutCommission = _.isNil(commissions.cashOut) ? undefined : new BN(1).plus(new BN(commissions.cashOut).div(100)) if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) const rate = rateRec.rates withCommission ? (rates[cryptoCode] = { cashIn: rate.ask.times(cashInCommission).decimalPlaces(5), cashOut: cashOutCommission && rate.bid.div(cashOutCommission).decimalPlaces(5), }) : (rates[cryptoCode] = { cashIn: rate.ask.decimalPlaces(5), cashOut: rate.bid.decimalPlaces(5), }) }) return rates } function buildRatesNoCommission(tickers) { return internalBuildRates(tickers, false) } function buildRates(tickers) { return internalBuildRates(tickers, true) } function getNotificationConfig() { return configManager.getGlobalNotifications(settings.config) } function buildBalances(balanceRecs) { const localeConfig = configManager.getLocale(deviceId, settings.config) const cryptoCodes = localeConfig.cryptoCurrencies const balances = {} cryptoCodes.forEach((cryptoCode, i) => { const balanceRec = balanceRecs[i] if (!balanceRec) return logger.warn('No balance for ' + cryptoCode + ' yet') if (Date.now() - balanceRec.timestamp > STALE_BALANCE) return logger.warn('Stale balance for ' + cryptoCode) balances[cryptoCode] = balanceRec.balance }) return balances } function isZeroConf(tx) { const walletSettings = configManager.getWalletSettings( tx.cryptoCode, settings.config, ) const zeroConfLimit = walletSettings.zeroConfLimit || 0 return tx.fiat.lte(zeroConfLimit) } const accountProvisioned = (cashUnitType, cashUnits, redeemableTxs) => { const kons = (cashUnits, tx) => { // cash-out-helper sends 0 as fallback value, need to filter it out as there are no '0' denominations const cashUnitsBills = _.flow( _.get(['bills']), _.filter( it => _.includes(cashUnitType, it.name) && it.denomination > 0, ), _.zip(cashUnits), )(tx) const sameDenominations = ([cashUnit, bill]) => cashUnit?.denomination === bill?.denomination if (!_.every(sameDenominations, cashUnitsBills)) throw new Error( `Denominations don't add up, ${cashUnitType}s were changed.`, ) return _.map( ([cashUnit, { provisioned }]) => _.set('count', cashUnit.count - provisioned, cashUnit), cashUnitsBills, ) } return _.reduce(kons, cashUnits, redeemableTxs) } function computeAvailableCassettes(cassettes, redeemableTxs) { if (_.isEmpty(redeemableTxs)) return cassettes cassettes = accountProvisioned('cassette', cassettes, redeemableTxs) if (_.some(({ count }) => count < 0, cassettes)) throw new Error( `Negative note count detected: ${JSON.stringify(cassettes)}`, ) return cassettes } function computeAvailableRecyclers(recyclers, redeemableTxs) { if (_.isEmpty(redeemableTxs)) return recyclers recyclers = accountProvisioned('recycler', recyclers, redeemableTxs) if (_.some(({ count }) => count < 0, recyclers)) throw new Error( `Negative note count detected: ${JSON.stringify(recyclers)}`, ) return recyclers } function buildAvailableCassettes(excludeTxId) { const cashOutConfig = configManager.getCashOut(deviceId, settings.config) if (!cashOutConfig.active) return Promise.resolve() return Promise.all([ dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId), ]).then(([{ counts, numberOfCassettes }, redeemableTxs]) => { redeemableTxs = _.reject( _.matchesProperty('id', excludeTxId), redeemableTxs, ) const denominations = _.map( it => cashOutConfig[`cassette${it}`], _.range(1, numberOfCassettes + 1), ) if (counts.length !== denominations.length) throw new Error('Denominations and respective counts do not match!') const cassettes = _.map( it => ({ name: `cassette${it + 1}`, denomination: parseInt(denominations[it], 10), count: parseInt(counts[it], 10), }), _.range(0, numberOfCassettes), ) const virtualCassettes = denominations.length ? [Math.max(...denominations) * 2] : [] try { return { cassettes: computeAvailableCassettes(cassettes, redeemableTxs), virtualCassettes, } } catch (err) { logger.error(err) return { cassettes, virtualCassettes, } } }) } function buildAvailableRecyclers(excludeTxId) { const cashOutConfig = configManager.getCashOut(deviceId, settings.config) if (!cashOutConfig.active) return Promise.resolve() return Promise.all([ dbm.recyclerCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId), ]).then(([{ counts, numberOfRecyclers }, redeemableTxs]) => { redeemableTxs = _.reject( _.matchesProperty('id', excludeTxId), redeemableTxs, ) const denominations = _.map( it => cashOutConfig[`recycler${it}`], _.range(1, numberOfRecyclers + 1), ) if (counts.length !== denominations.length) throw new Error('Denominations and respective counts do not match!') const recyclers = _.map( it => ({ number: it + 1, name: `recycler${it + 1}`, denomination: parseInt(denominations[it], 10), count: parseInt(counts[it], 10), }), _.range(0, numberOfRecyclers), ) const virtualRecyclers = denominations.length ? [Math.max(..._.flatten(denominations)) * 2] : [] try { return { recyclers: computeAvailableRecyclers(recyclers, redeemableTxs), virtualRecyclers, } } catch (err) { logger.error(err) return { recyclers, virtualRecyclers, } } }) } function buildAvailableUnits(excludeTxId) { return Promise.all([ buildAvailableCassettes(excludeTxId), buildAvailableRecyclers(excludeTxId), ]).then(([cassettes, recyclers]) => ({ cassettes: cassettes.cassettes, recyclers: recyclers.recyclers, })) } function mapCoinSettings(coinParams) { const [cryptoCode, cryptoNetwork] = coinParams const commissions = configManager.getCommissions( cryptoCode, deviceId, settings.config, ) const minimumTx = new BN(commissions.minimumTx) const cashInFee = new BN(commissions.fixedFee) const cashOutFee = new BN(commissions.cashOutFixedFee) const cashInCommission = new BN(commissions.cashIn) const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const cryptoUnits = configManager.getCryptoUnits( cryptoCode, settings.config, ) return { cryptoCode, cryptoCodeDisplay: cryptoRec.cryptoCodeDisplay ?? cryptoCode, display: cryptoRec.display, isCashInOnly: Boolean(cryptoRec.isCashinOnly), minimumTx: BN.max(minimumTx, cashInFee), cashInFee, cashOutFee, cashInCommission, cashOutCommission, cryptoNetwork, cryptoUnits, } } function getTickerRates(fiatCode, cryptoCode) { return ticker.getRates(settings, fiatCode, cryptoCode) } function pollQueries() { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies const machineScreenOpts = configManager.getAllMachineScreenOpts( settings.config, ) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) const networkPromises = cryptoCodes.map(c => wallet.cryptoNetwork(settings, c), ) const supportsBatchingPromise = cryptoCodes.map(c => wallet.supportsBatching(settings, c), ) return Promise.all([ buildAvailableCassettes(), buildAvailableRecyclers(), settingsLoader.fetchCurrentConfigVersion(), millisecondsToMinutes(getTimezoneOffset(localeConfig.timezone)), loyalty.getNumberOfAvailablePromoCodes(), Promise.all(supportsBatchingPromise), Promise.all(tickerPromises), Promise.all(balancePromises), Promise.all(networkPromises), ]).then( ([ cassettes, recyclers, configVersion, timezone, numberOfAvailablePromoCodes, batchableCoins, tickers, balances, networks, ]) => { const coinsWithoutRate = _.flow( _.zip(cryptoCodes), _.map(mapCoinSettings), )(networks) const coins = _.flow( _.map(it => ({ batchable: it })), _.zipWith(_.assign, _.zipWith(_.assign, coinsWithoutRate, tickers)), )(batchableCoins) return { cassettes, recyclers: recyclers, rates: buildRates(tickers), balances: buildBalances(balances), coins, configVersion, areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, timezone, screenOptions: machineScreenOpts, } }, ) } function sendCoins(tx) { return wallet .supportsBatching(settings, tx.cryptoCode) .then(supportsBatching => { if (supportsBatching) { return transactionBatching.addTransactionToBatch(tx).then(() => ({ batched: true, sendPending: false, error: null, errorCode: null, })) } return wallet.sendCoins(settings, tx) }) } function recordPing(deviceTime, version, model) { const devices = { version, model, last_online: deviceTime, } return Promise.all([ db.none( `insert into machine_pings(device_id, device_time) values ($1, $2) ON CONFLICT (device_id) DO UPDATE SET device_time = $2, updated = now()`, [deviceId, deviceTime], ), db.none( pgp.helpers.update(devices, null, 'devices') + 'WHERE device_id = ${deviceId}', { deviceId, }, ), ]) } function pruneMachinesHeartbeat() { const sql = `DELETE FROM machine_network_heartbeat h USING (SELECT device_id, max(created) as lastEntry FROM machine_network_heartbeat GROUP BY device_id) d WHERE d.device_id = h.device_id AND h.created < d.lastEntry` return db.none(sql) } function isHd(tx) { return wallet.isHd(settings, tx) } function getStatus(tx) { return wallet.getStatus(settings, tx) } function newAddress(tx) { const info = { cryptoCode: tx.cryptoCode, label: 'TX ' + Date.now(), account: 'deposit', hdIndex: tx.hdIndex, cryptoAtoms: tx.cryptoAtoms, isLightning: tx.isLightning, } return wallet.newAddress(settings, info, tx) } function fiatBalance(fiatCode, cryptoCode) { const commissions = configManager.getCommissions( cryptoCode, deviceId, settings.config, ) return Promise.all([ getTickerRates(fiatCode, cryptoCode), wallet.balance(settings, cryptoCode), ]).then(([rates, balanceRec]) => { if (!rates || !balanceRec) return null const rawRate = rates.rates.ask const cashInCommission = new BN(1).minus( new BN(commissions.cashIn).div(100), ) const balance = balanceRec.balance if (!rawRate || !balance) return null const rate = rawRate.div(cashInCommission) const lowBalanceMargin = new BN(0.95) const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const unitScale = cryptoRec.unitScale const shiftedRate = rate.shiftedBy(-unitScale) const fiatTransferBalance = balance .times(shiftedRate) .times(lowBalanceMargin) return { timestamp: balanceRec.timestamp, balance: fiatTransferBalance.integerValue(BN.ROUND_DOWN).toString(), } }) } function notifyConfirmation(tx) { logger.debug('notifyConfirmation') const phone = tx.phone const timestamp = `${new Date().toISOString().substring(11, 19)} UTC` return sms .getSms(CASH_OUT_DISPENSE_READY, phone, { timestamp }) .then(smsObj => { const rec = { sms: smsObj, } return sms.sendMessage(settings, rec).then(() => { const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2' const values = [true, tx.id] return db.none(sql, values) }) }) } function notifyOperator(tx, rec) { // notify operator about new transaction and add high volume txs to database return notifier.transactionNotify(tx, rec) } function clearOldLogs() { return logs.clearOldLogs().catch(logger.error) } /* * Trader functions */ function toMarketString(fiatCode, cryptoCode) { return [fiatCode, cryptoCode].join('-') } function fromMarketString(market) { const [fiatCode, cryptoCode] = market.split('-') return { fiatCode, cryptoCode } } function buy(rec, tx) { return buyAndSell(rec, true, tx) } function sell(rec) { return buyAndSell(rec, false) } function buyAndSell(rec, doBuy, tx) { const cryptoCode = rec.cryptoCode if (!exchange.active(settings, cryptoCode)) return return exchange.fetchExchange(settings, cryptoCode).then(_exchange => { const fiatCode = _exchange.account.currencyMarket const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() const market = toMarketString(fiatCode, cryptoCode) const direction = doBuy ? 'cashIn' : 'cashOut' const internalTxId = tx ? tx.id : rec.id logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) if (!tradesQueues[market]) tradesQueues[market] = [] tradesQueues[market].push({ direction, internalTxId, fiatCode, cryptoAtoms, cryptoCode, timestamp: Date.now(), }) }) } function consolidateTrades(cryptoCode, fiatCode) { const market = toMarketString(fiatCode, cryptoCode) const marketTradesQueues = tradesQueues[market] if (!marketTradesQueues || marketTradesQueues.length === 0) return null logger.debug( '[%s] tradesQueues size: %d', market, marketTradesQueues.length, ) logger.debug('[%s] tradesQueues head: %j', market, marketTradesQueues[0]) const t1 = Date.now() const filtered = marketTradesQueues.filter(tradeEntry => { return t1 - tradeEntry.timestamp < TRADE_TTL }) const filteredCount = marketTradesQueues.length - filtered.length if (filteredCount > 0) { tradesQueues[market] = filtered logger.debug('[%s] expired %d trades', market, filteredCount) } if (filtered.length === 0) return null const partitionByDirection = _.partition( ({ direction }) => direction === 'cashIn', ) const [cashInTxs, cashOutTxs] = _.compose( partitionByDirection, _.uniqBy('internalTxId'), )(filtered) const cryptoAtoms = filtered.reduce( (prev, current) => prev.plus(current.cryptoAtoms), new BN(0), ) const timestamp = filtered .map(r => r.timestamp) .reduce((acc, r) => Math.max(acc, r), 0) const consolidatedTrade = { cashInTxs, cashOutTxs, fiatCode, cryptoAtoms, cryptoCode, timestamp, } tradesQueues[market] = [] logger.debug('[%s] consolidated: %j', market, consolidatedTrade) return consolidatedTrade } function executeTrades() { const pairs = _.map(fromMarketString)(_.keys(tradesQueues)) pairs.forEach(({ fiatCode, cryptoCode }) => { try { executeTradesForMarket(settings, fiatCode, cryptoCode) } catch (err) { logger.error(err) } }) // Poller expects a promise return Promise.resolve() } function executeTradesForMarket(settings, fiatCode, cryptoCode) { if (!exchange.active(settings, cryptoCode)) return const market = toMarketString(fiatCode, cryptoCode) const tradeEntry = consolidateTrades(cryptoCode, fiatCode) if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return return executeTradeForType(tradeEntry).catch(err => { tradesQueues[market].push(tradeEntry) if (err.name === 'orderTooSmall') return logger.debug(err.message) logger.error(err) }) } function executeTradeForType(_tradeEntry) { const expand = te => _.assign(te, { cryptoAtoms: te.cryptoAtoms.abs(), type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell', }) const tradeEntry = expand(_tradeEntry) const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell return recordTrade(tradeEntry).then(newEntry => { tradeEntry.tradeId = newEntry.id return execute(settings, tradeEntry).catch(err => { updateTradeEntry(tradeEntry, newEntry, err).then(() => { logger.error(err) throw err }) }) }) } function updateTradeEntry(tradeEntry, newEntry, err) { const data = mergeTradeEntryAndError(tradeEntry, err) const sql = pgp.helpers.update(data, ['error'], 'trades') + ` WHERE id = ${newEntry.id}` return db.none(sql) } function recordTradeAndTx(tradeId, { cashInTxs, cashOutTxs }, dbTx) { const columnSetCashIn = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashin_tx_trades', }) const columnSetCashOut = new pgp.helpers.ColumnSet(['tx_id', 'trade_id'], { table: 'cashout_tx_trades', }) const mapToEntry = _.map(tx => ({ tx_id: tx.internalTxId, trade_id: tradeId, })) const queries = [] if (!_.isEmpty(cashInTxs)) { const query = pgp.helpers.insert(mapToEntry(cashInTxs), columnSetCashIn) queries.push(dbTx.none(query)) } if (!_.isEmpty(cashOutTxs)) { const query = pgp.helpers.insert(mapToEntry(cashOutTxs), columnSetCashOut) queries.push(dbTx.none(query)) } return Promise.all(queries) } function convertBigNumFields(obj) { const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat']) ? value.toString() : value const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat']) ? key + '#' : key return _.mapKeys(convertKey, mapValuesWithKey(convert, obj)) } function mergeTradeEntryAndError(tradeEntry, error) { if (error && error.message) { return Object.assign({}, tradeEntry, { error: error.message.slice(0, 200), }) } return tradeEntry } function recordTrade(_tradeEntry, error) { const massage = _.flow( mergeTradeEntryAndError, _.pick(['cryptoCode', 'cryptoAtoms', 'fiatCode', 'type', 'error']), convertBigNumFields, _.mapKeys(_.snakeCase), ) const tradeEntry = massage(_tradeEntry, error) const sql = pgp.helpers.insert(tradeEntry, null, 'trades') + 'RETURNING *' return db.tx(t => { return t.oneOrNone(sql).then(newTrade => { return recordTradeAndTx(newTrade.id, _tradeEntry, t).then( () => newTrade, ) }) }) } function sendMessage(rec) { const notifications = configManager.getGlobalNotifications(settings.config) let promises = [] if (notifications.email.active && rec.email) promises.push(email.sendMessage(settings, rec)) if (notifications.sms.active && rec.sms) promises.push(sms.sendMessage(settings, rec)) return Promise.all(promises) } function checkDevicesCashBalances(fiatCode, devices) { return _.map(device => checkDeviceCashBalances(fiatCode, device), devices) } function getCashUnitCapacity(model, device) { if (!CASH_UNIT_CAPACITY[model]) { return CASH_UNIT_CAPACITY.default[device] } return CASH_UNIT_CAPACITY[model][device] } function checkDeviceCashBalances(fiatCode, device) { const deviceId = device.deviceId const machineName = device.name const notifications = configManager.getNotifications( null, deviceId, settings.config, ) const cashInAlerts = device.cashUnits.cashbox > notifications.cashInAlertThreshold ? [ { code: 'CASH_BOX_FULL', machineName, deviceId, notes: device.cashUnits.cashbox, }, ] : [] const cashOutConfig = configManager.getCashOut(deviceId, settings.config) const cashOutEnabled = cashOutConfig.active const isUnitLow = (have, max, limit) => (have / max) * 100 < limit if (!cashOutEnabled) return cashInAlerts const cassetteCapacity = getCashUnitCapacity(device.model, 'cassette') const cassetteAlerts = Array( Math.min( device.numberOfCassettes ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, ), ) .fill(null) .flatMap((_elem, idx) => { const nth = idx + 1 const cassetteField = `cassette${nth}` const notes = device.cashUnits[cassetteField] const denomination = cashOutConfig[cassetteField] const limit = notifications[`fillingPercentageCassette${nth}`] return isUnitLow(notes, cassetteCapacity, limit) ? [ { code: 'LOW_CASH_OUT', cassette: nth, machineName, deviceId, notes, denomination, fiatCode, }, ] : [] }) const recyclerCapacity = getCashUnitCapacity(device.model, 'recycler') const recyclerAlerts = Array( Math.min( device.numberOfRecyclers ?? 0, CASH_OUT_MAXIMUM_AMOUNT_OF_RECYCLERS, ), ) .fill(null) .flatMap((_elem, idx) => { const nth = idx + 1 const recyclerField = `recycler${nth}` const notes = device.cashUnits[recyclerField] const denomination = cashOutConfig[recyclerField] const limit = notifications[`fillingPercentageRecycler${nth}`] return isUnitLow(notes, recyclerCapacity, limit) ? [ { code: 'LOW_RECYCLER_STACKER', cassette: nth, // @see DETAIL_TEMPLATE in /lib/notifier/utils.js machineName, deviceId, notes, denomination, fiatCode, }, ] : [] }) return [].concat(cashInAlerts, cassetteAlerts, recyclerAlerts) } function checkCryptoBalances(fiatCode, devices) { const fiatBalancePromises = cryptoCodes => _.map(c => fiatBalance(fiatCode, c), cryptoCodes) const fetchCryptoCodes = _deviceId => { const localeConfig = configManager.getLocale(_deviceId, settings.config) return localeConfig.cryptoCurrencies } const union = _.flow(_.map(fetchCryptoCodes), _.flatten, _.uniq) const cryptoCodes = union(devices) const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode]) return Promise.all(fiatBalancePromises(cryptoCodes)).then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)), ) } function checkCryptoBalance(fiatCode, rec) { const [cryptoCode, fiatBalance] = rec if (!fiatBalance) return null const notifications = configManager.getNotifications( cryptoCode, null, settings.config, ) const lowAlertThreshold = notifications.cryptoLowBalance const highAlertThreshold = notifications.cryptoHighBalance const req = { cryptoCode, fiatBalance, fiatCode, } if ( _.isFinite(lowAlertThreshold) && new BN(fiatBalance.balance).lt(lowAlertThreshold) ) { return _.set('code')('LOW_CRYPTO_BALANCE')(req) } if ( _.isFinite(highAlertThreshold) && new BN(fiatBalance.balance).gt(highAlertThreshold) ) { return _.set('code')('HIGH_CRYPTO_BALANCE')(req) } return null } function checkBalances() { const localeConfig = configManager.getGlobalLocale(settings.config) const fiatCode = localeConfig.fiatCurrency return machineLoader .getMachines() .then(devices => Promise.all([ checkCryptoBalances(fiatCode, devices), checkDevicesCashBalances(fiatCode, devices), ]), ) .then(_.flow(_.flattenDeep, _.compact)) } function randomCode() { return new BN(crypto.randomBytes(3).toString('hex'), 16) .shiftedBy(-6) .toFixed(6) .slice(-6) } function getPhoneCode(phone) { const code = settings.config.notifications_thirdParty_sms === 'mock-sms' ? '123' : randomCode() const timestamp = `${new Date().toISOString().substring(11, 19)} UTC` return sms .getSms(CONFIRMATION_CODE, phone, { code, timestamp }) .then(smsObj => { const rec = { sms: smsObj, } if (skip2fa) return '123' return sms.sendMessage(settings, rec).then(() => code) }) } function getEmailCode(toEmail) { const code = settings.config.notifications_thirdParty_email === 'mock-email' ? '123' : randomCode() const rec = { email: { toEmail, subject: 'Your cryptomat code', body: `Your cryptomat code: ${code}`, }, } return email.sendCustomerMessage(settings, rec).then(() => code) } function sweepHdRow(row) { const txId = row.id const cryptoCode = row.crypto_code return wallet .sweep(settings, txId, cryptoCode, row.hd_index) .then(txHash => { if (txHash) { logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash) const sql = `update cash_out_txs set swept='t' where id = $1` return db.none(sql, row.id) } }) .catch(err => logger.error( '[%s] [Session ID: %s] Sweep error: %s', cryptoCode, row.id, err.message, ), ) } function sweepHd() { const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs WHERE hd_index IS NOT NULL AND NOT swept AND status IN ('confirmed', 'instant') AND created > now() - interval '1 week'` return db .any(sql) .then(rows => Promise.all(rows.map(sweepHdRow))) .catch(logger.error) } function getMachineNames() { return machineLoader.getMachineNames(settings.config) } function getRawRates() { const localeConfig = configManager.getGlobalLocale(settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = configManager.getAllCryptoCurrencies(settings.config) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) return Promise.all(tickerPromises) } function getRates() { return getRawRates().then(buildRates) } function rateAddress(cryptoCode, address) { return walletScoring.rateAddress(settings, cryptoCode, address) } function rateTransaction(cryptoCode, address) { return walletScoring.rateTransaction(settings, cryptoCode, address) } function isWalletScoringEnabled(tx) { return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode) } function probeLN(cryptoCode, address) { return wallet.probeLN(settings, cryptoCode, address) } return { getRates, recordPing, buildRates, getRawRates, buildRatesNoCommission, pollQueries, sendCoins, newAddress, isHd, isZeroConf, getStatus, getPhoneCode, getEmailCode, executeTrades, clearOldLogs, notifyConfirmation, sweepHd, sendMessage, checkBalances, getMachineNames, buy, sell, getNotificationConfig, notifyOperator, pruneMachinesHeartbeat, rateAddress, rateTransaction, isWalletScoringEnabled, probeLN, buildAvailableUnits, } } module.exports = plugins