const _ = require('lodash/fp') const mem = require('mem') const hkdf = require('futoin-hkdf') const configManager = require('./new-config-manager') const pify = require('pify') const fs = pify(require('fs')) const mnemonicHelpers = require('./mnemonic-helpers') const options = require('./options') const ph = require('./plugin-helper') const layer2 = require('./layer2') const FETCH_INTERVAL = 5000 const INSUFFICIENT_FUNDS_CODE = 570 const INSUFFICIENT_FUNDS_NAME = 'InsufficientFunds' const ZERO_CONF_EXPIRATION = 60000 function httpError (msg, code) { const err = new Error(msg) err.name = 'HTTPError' err.code = code || 500 return err } function computeSeed (masterSeed) { return hkdf(masterSeed, 32, {salt: 'lamassu-server-salt', info: 'wallet-seed'}) } function fetchWallet (settings, cryptoCode) { return fs.readFile(options.mnemonicPath, 'utf8') .then(mnemonic => { const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic) const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet const wallet = ph.load(ph.WALLET, plugin) const rawAccount = settings.accounts[plugin] const account = _.set('seed', computeSeed(masterSeed), rawAccount) if (_.isFunction(wallet.run)) wallet.run(account) return { wallet, account } }) } const lastBalance = {} function _balance (settings, cryptoCode) { return fetchWallet(settings, cryptoCode) .then(r => r.wallet.balance(r.account, cryptoCode)) .then(balance => ({ balance, timestamp: Date.now() })) .then(r => { lastBalance[cryptoCode] = r return r }) .catch(err => { console.error(err) return lastBalance[cryptoCode] }) } function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) { return fetchWallet(settings, cryptoCode) .then(r => { return r.wallet.sendCoins(r.account, toAddress, cryptoAtoms, cryptoCode) .then(res => { mem.clear(module.exports.balance) return res }) }) .catch(err => { if (err.name === INSUFFICIENT_FUNDS_NAME) { throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE) } throw err }) } function newAddress (settings, info) { const walletAddressPromise = fetchWallet(settings, info.cryptoCode) .then(r => r.wallet.newAddress(r.account, info)) return Promise.all([ walletAddressPromise, layer2.newAddress(settings, info) ]) .then(([toAddress, layer2Address]) => ({ toAddress, layer2Address })) } function newFunding (settings, cryptoCode, address) { return fetchWallet(settings, cryptoCode) .then(r => { const wallet = r.wallet const account = r.account return wallet.newFunding(account, cryptoCode) }) } function mergeStatus (a, b) { if (!a) return b if (!b) return a return { receivedCryptoAtoms: a.receivedCryptoAtoms, status: mergeStatusMode(a.status, b.status) } } function mergeStatusMode (a, b) { const cleared = ['authorized', 'confirmed', 'instant'] if (_.includes(a, cleared)) return a if (_.includes(b, cleared)) return b if (a === 'published') return a if (b === 'published') return b if (a === 'rejected') return a if (b === 'rejected') return b return 'notSeen' } function getWalletStatus (settings, tx) { const fudgeFactorEnabled = configManager.getWalletSettings(tx.cryptoCode, settings.config).fudgeFactorActive const fudgeFactor = fudgeFactorEnabled ? 100 : 0 const walletStatusPromise = fetchWallet(settings, tx.cryptoCode) .then(r => r.wallet.getStatus(r.account, tx.toAddress, tx.cryptoAtoms.minus(fudgeFactor), tx.cryptoCode)) return Promise.all([ walletStatusPromise, layer2.getStatus(settings, tx) ]) .then(([walletStatus, layer2Status]) => { return mergeStatus(walletStatus, layer2Status) }) } function authorizeZeroConf (settings, tx, machineId) { const plugin = configManager.getWalletSettings(tx.cryptoCode, settings.config).zeroConf const cashOutConfig = configManager.getCashOut(machineId, settings.config) const zeroConfLimit = cashOutConfig.zeroConfLimit if (!_.isObject(tx.fiat)) { return Promise.reject(new Error('tx.fiat is undefined!')) } if (plugin === 'no-zero-conf' || tx.fiat.gt(zeroConfLimit)) { return Promise.resolve(false) } if (plugin === 'all-zero-conf') return Promise.resolve(true) const zeroConf = ph.load(ph.ZERO_CONF, plugin) const account = settings.accounts[plugin] return zeroConf.authorize(account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode) } function getStatus (settings, tx, machineId) { return getWalletStatus(settings, tx) .then((statusRec) => { if (statusRec.status === 'authorized') { return authorizeZeroConf(settings, tx, machineId) .then(isAuthorized => { const publishAge = Date.now() - tx.publishedAt const unauthorizedStatus = publishAge < ZERO_CONF_EXPIRATION ? 'published' : 'rejected' const status = isAuthorized ? 'authorized' : unauthorizedStatus return { receivedCryptoAtoms: statusRec.receivedCryptoAtoms, status } }) } return statusRec }) } function sweep (settings, cryptoCode, hdIndex) { return fetchWallet(settings, cryptoCode) .then(r => r.wallet.sweep(r.account, cryptoCode, hdIndex)) } function isHd (settings, cryptoCode) { return fetchWallet(settings, cryptoCode) .then(r => r.wallet.supportsHd) } function cryptoNetwork (settings, cryptoCode) { const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet const wallet = ph.load(ph.WALLET, plugin) const account = settings.accounts[plugin] if (!wallet.cryptoNetwork) return Promise.resolve(false) return wallet.cryptoNetwork(account, cryptoCode) } function isStrictAddress (settings, cryptoCode, toAddress) { // Note: For now, only for wallets that specifically check for this. return fetchWallet(settings, cryptoCode) .then(r => { if (!r.wallet.isStrictAddress) return true return r.wallet.isStrictAddress(cryptoCode, toAddress) }) } const coinFilter = ['ETH'] const balance = (settings, cryptoCode) => { if (_.includes(coinFilter, cryptoCode)) return balanceFiltered(settings, cryptoCode) return balanceUnfiltered(settings, cryptoCode) } const balanceUnfiltered = mem(_balance, { maxAge: FETCH_INTERVAL, cacheKey: (settings, cryptoCode) => cryptoCode }) const balanceFiltered = mem(_balance, { maxAge: 3 * FETCH_INTERVAL, cacheKey: (settings, cryptoCode) => cryptoCode }) module.exports = { balance, sendCoins, newAddress, getStatus, isStrictAddress, sweep, isHd, newFunding, cryptoNetwork }