diff --git a/lib/admin/transactions.js b/lib/admin/transactions.js index 8a3a8878..d7591858 100644 --- a/lib/admin/transactions.js +++ b/lib/admin/transactions.js @@ -3,7 +3,7 @@ const _ = require('lodash/fp') const db = require('../db') const machineLoader = require('../machine-loader') const tx = require('../tx') -const cashInTx = require('../cash-in-tx') +const cashInTx = require('../cash-in/cash-in-tx') const NUM_RESULTS = 20 diff --git a/lib/cash-in-tx.js b/lib/cash-in-tx.js deleted file mode 100644 index 37ce2171..00000000 --- a/lib/cash-in-tx.js +++ /dev/null @@ -1,327 +0,0 @@ -const _ = require('lodash/fp') -const pgp = require('pg-promise')() -const db = require('./db') -const BN = require('./bn') -const plugins = require('./plugins') -const logger = require('./logger') -const T = require('./time') -const E = require('./error') - -const PENDING_INTERVAL = '60 minutes' -const PENDING_INTERVAL_MS = 60 * T.minutes -const MAX_PENDING = 10 - -module.exports = {post, monitorPending, cancel, PENDING_INTERVAL} - -function atomic (machineTx, pi) { - const TransactionMode = pgp.txMode.TransactionMode - const isolationLevel = pgp.txMode.isolationLevel - const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) - - function transaction (t) { - const sql = 'select * from cash_in_txs where id=$1' - const sql2 = 'select * from bills where cash_in_txs_id=$1' - - return t.oneOrNone(sql, [machineTx.id]) - .then(row => { - if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError('Stale tx') - - return t.any(sql2, [machineTx.id]) - .then(billRows => { - const dbTx = toObj(row) - - return preProcess(dbTx, machineTx, pi) - .then(preProcessedTx => upsert(dbTx, preProcessedTx)) - .then(r => { - return insertNewBills(billRows, machineTx) - .then(newBills => _.set('newBills', newBills, r)) - }) - }) - }) - } - - transaction.txMode = tmSRD - - return transaction -} - -function post (machineTx, pi) { - return db.tx(atomic(machineTx, pi)) - .then(r => { - const updatedTx = r.tx - - return postProcess(r, pi) - .then(changes => update(updatedTx, changes)) - .then(tx => _.set('bills', machineTx.bills, tx)) - }) -} - -function nilEqual (a, b) { - if (_.isNil(a) && _.isNil(b)) return true - - return undefined -} - -function isMonotonic (oldField, newField, fieldKey) { - if (_.isNil(newField)) return false - if (_.isBoolean(oldField)) return oldField === newField || !oldField - if (oldField.isBigNumber) return oldField.lte(newField) - if (_.isNumber(oldField)) return oldField <= newField - - throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`) -} - -function ensureRatchet (oldField, newField, fieldKey) { - const monotonic = ['cryptoAtoms', 'fiat', 'cashInFeeCrypto', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion'] - const free = ['sendPending', 'error', 'errorCode', 'customerId'] - - if (_.isNil(oldField)) return true - - if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey) - - if (_.includes(fieldKey, free)) { - if (_.isNil(newField)) return false - return true - } - - if (_.isNil(newField)) return false - if (oldField.isBigNumber && newField.isBigNumber) return BN(oldField).eq(newField) - if (oldField.toString() === newField.toString()) return true - - return false -} - -function diff (oldTx, newTx) { - let updatedTx = {} - - if (!oldTx) throw new Error('oldTx must not be null') - if (!newTx) throw new Error('newTx must not be null') - - _.forEach(fieldKey => { - const oldField = oldTx[fieldKey] - const newField = newTx[fieldKey] - if (fieldKey === 'bills') return - if (_.isEqualWith(nilEqual, oldField, newField)) return - - if (!ensureRatchet(oldField, newField, fieldKey)) { - logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey) - logger.warn('Old tx: %j', oldTx) - logger.warn('New tx: %j', newTx) - throw new E.RatchetError('Value from lamassu-machine would violate ratchet') - } - - updatedTx[fieldKey] = newField - }, _.keys(newTx)) - - return updatedTx -} - -function toObj (row) { - if (!row) return null - - const keys = _.keys(row) - let newObj = {} - - keys.forEach(key => { - const objKey = _.camelCase(key) - if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'cash_in_fee_crypto'])) { - newObj[objKey] = BN(row[key]) - return - } - - newObj[objKey] = row[key] - }) - - newObj.direction = 'cashIn' - - return newObj -} - -function convertBigNumFields (obj) { - const convert = value => value && value.isBigNumber - ? value.toString() - : value - - return _.mapValues(convert, obj) -} - -function pullNewBills (billRows, machineTx) { - if (_.isEmpty(machineTx.bills)) return [] - - const toBill = _.mapKeys(_.camelCase) - const bills = _.map(toBill, billRows) - - return _.differenceBy(_.get('id'), machineTx.bills, bills) -} - -const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills']), convertBigNumFields, _.mapKeys(_.snakeCase)) - -function insertNewBills (billRows, machineTx) { - const bills = pullNewBills(billRows, machineTx) - if (_.isEmpty(bills)) return Promise.resolve([]) - - const dbBills = _.map(massage, bills) - const columns = _.keys(dbBills[0]) - const sql = pgp.helpers.insert(dbBills, columns, 'bills') - - return db.none(sql) - .then(() => bills) -} - -function upsert (dbTx, preProcessedTx) { - if (!dbTx) { - return insert(preProcessedTx) - .then(tx => ({dbTx, tx})) - } - - return update(dbTx, diff(dbTx, preProcessedTx)) - .then(tx => ({dbTx, tx})) -} - -function insert (tx) { - const dbTx = massage(tx) - const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *' - return db.one(sql) - .then(toObj) -} - -function update (tx, changes) { - if (_.isEmpty(changes)) return Promise.resolve(tx) - - const dbChanges = massage(changes) - const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') + - pgp.as.format(' where id=$1', [tx.id]) + ' returning *' - - return db.one(sql) - .then(toObj) -} - -function registerTrades (pi, newBills) { - _.forEach(bill => pi.buy(bill), newBills) -} - -function logAction (rec, tx) { - const action = { - tx_id: tx.id, - action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'), - error: rec.error, - error_code: rec.errorCode, - tx_hash: rec.txHash - } - - const sql = pgp.helpers.insert(action, null, 'cash_in_actions') - - return db.none(sql) - .then(_.constant(rec)) -} - -function logActionById (action, _rec, txId) { - const rec = _.assign(_rec, {action, tx_id: txId}) - const sql = pgp.helpers.insert(rec, null, 'cash_in_actions') - - return db.none(sql) -} - -function isClearToSend (oldTx, newTx) { - const now = Date.now() - - return newTx.send && - (!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) && - (newTx.created > now - PENDING_INTERVAL_MS) -} - -function postProcess (r, pi) { - registerTrades(pi, r.newBills) - - if (!isClearToSend(r.dbTx, r.tx)) return Promise.resolve({}) - - return pi.sendCoins(r.tx) - .then(txHash => ({ - txHash, - sendConfirmed: true, - sendTime: 'now()^', - sendPending: false, - error: null, - errorCode: null - })) - .catch(err => { - // Important: We don't know what kind of error this is - // so not safe to assume that funds weren't sent. - // Therefore, don't set sendPending to false except for - // errors (like InsufficientFundsError) that are guaranteed - // not to send. - const sendPending = err.name !== 'InsufficientFundsError' - - return { - sendTime: 'now()^', - error: err.message, - errorCode: err.name, - sendPending - } - }) - .then(sendRec => logAction(sendRec, r.tx)) -} - -function preProcess (dbTx, machineTx, pi) { - // Note: The way this works is if we're clear to send, - // we mark the transaction as sendPending. - // - // If another process is trying to also mark this as sendPending - // that means that it saw the tx as sendPending=false. - // But if that's true, then it must be serialized before this - // (otherwise it would see sendPending=true), and therefore we can't - // be seeing sendPending=false (a pre-condition of clearToSend()). - // Therefore, one of the conflicting transactions will error, - // which is what we want. - return new Promise(resolve => { - if (!dbTx) return resolve(machineTx) - - if (isClearToSend(dbTx, machineTx)) { - return resolve(_.set('sendPending', true, machineTx)) - } - - return resolve(machineTx) - }) -} - -function monitorPending (settings) { - const sql = `select * from cash_in_txs - where created > now() - interval $1 - and send - and not send_confirmed - and not send_pending - and not operator_completed - order by created - limit $2` - - const processPending = row => { - const tx = toObj(row) - const pi = plugins(settings, tx.deviceId) - - return post(tx, pi) - .catch(logger.error) - } - - return db.any(sql, [PENDING_INTERVAL, MAX_PENDING]) - .then(rows => Promise.all(_.map(processPending, rows))) - .catch(logger.error) -} - -function cancel (txId) { - const updateRec = { - error: 'Operator cancel', - error_code: 'operatorCancel', - operator_completed: true - } - - return Promise.resolve() - .then(() => { - return pgp.helpers.update(updateRec, null, 'cash_in_txs') + - pgp.as.format(' where id=$1', [txId]) - }) - .then(sql => db.result(sql, false)) - .then(res => { - if (res.rowCount !== 1) throw new Error('No such tx-id') - }) - .then(() => logActionById('operatorCompleted', {}, txId)) -} diff --git a/lib/cash-in/cash-in-atomic.js b/lib/cash-in/cash-in-atomic.js new file mode 100644 index 00000000..899ce501 --- /dev/null +++ b/lib/cash-in/cash-in-atomic.js @@ -0,0 +1,83 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() + +const E = require('../error') + +const cashInLow = require('./cash-in-low') + +module.exports = {atomic} + +function atomic (machineTx, pi) { + const TransactionMode = pgp.txMode.TransactionMode + const isolationLevel = pgp.txMode.isolationLevel + const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) + + function transaction (t) { + const sql = 'select * from cash_in_txs where id=$1' + const sql2 = 'select * from bills where cash_in_txs_id=$1' + + return t.oneOrNone(sql, [machineTx.id]) + .then(row => { + if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError('Stale tx') + + return t.any(sql2, [machineTx.id]) + .then(billRows => { + const dbTx = cashInLow.toObj(row) + + return preProcess(dbTx, machineTx, pi) + .then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx)) + .then(r => { + return insertNewBills(t, billRows, machineTx) + .then(newBills => _.set('newBills', newBills, r)) + }) + }) + }) + } + + transaction.txMode = tmSRD + + return transaction +} + +function insertNewBills (t, billRows, machineTx) { + const bills = pullNewBills(billRows, machineTx) + if (_.isEmpty(bills)) return Promise.resolve([]) + + const dbBills = _.map(cashInLow.massage, bills) + const columns = _.keys(dbBills[0]) + const sql = pgp.helpers.insert(dbBills, columns, 'bills') + + return t.none(sql) + .then(() => bills) +} + +function pullNewBills (billRows, machineTx) { + if (_.isEmpty(machineTx.bills)) return [] + + const toBill = _.mapKeys(_.camelCase) + const bills = _.map(toBill, billRows) + + return _.differenceBy(_.get('id'), machineTx.bills, bills) +} + +function preProcess (dbTx, machineTx, pi) { + // Note: The way this works is if we're clear to send, + // we mark the transaction as sendPending. + // + // If another process is trying to also mark this as sendPending + // that means that it saw the tx as sendPending=false. + // But if that's true, then it must be serialized before this + // (otherwise it would see sendPending=true), and therefore we can't + // be seeing sendPending=false (a pre-condition of clearToSend()). + // Therefore, one of the conflicting transactions will error, + // which is what we want. + return new Promise(resolve => { + if (!dbTx) return resolve(machineTx) + + if (cashInLow.isClearToSend(dbTx, machineTx)) { + return resolve(_.set('sendPending', true, machineTx)) + } + + return resolve(machineTx) + }) +} diff --git a/lib/cash-in/cash-in-low.js b/lib/cash-in/cash-in-low.js new file mode 100644 index 00000000..8bb91601 --- /dev/null +++ b/lib/cash-in/cash-in-low.js @@ -0,0 +1,137 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() + +const BN = require('../bn') +const T = require('../time') +const logger = require('../logger') + +const PENDING_INTERVAL_MS = 60 * T.minutes + +const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills']), + convertBigNumFields, _.mapKeys(_.snakeCase)) + +module.exports = {toObj, upsert, insert, update, massage, isClearToSend} + +function convertBigNumFields (obj) { + const convert = value => value && value.isBigNumber + ? value.toString() + : value + + return _.mapValues(convert, obj) +} + +function toObj (row) { + if (!row) return null + + const keys = _.keys(row) + let newObj = {} + + keys.forEach(key => { + const objKey = _.camelCase(key) + if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'cash_in_fee_crypto'])) { + newObj[objKey] = BN(row[key]) + return + } + + newObj[objKey] = row[key] + }) + + newObj.direction = 'cashIn' + + return newObj +} + +function upsert (t, dbTx, preProcessedTx) { + if (!dbTx) { + return insert(t, preProcessedTx) + .then(tx => ({dbTx, tx})) + } + + return update(t, dbTx, diff(dbTx, preProcessedTx)) + .then(tx => ({dbTx, tx})) +} + +function insert (t, tx) { + const dbTx = massage(tx) + const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *' + return t.one(sql) + .then(toObj) +} + +function update (t, tx, changes) { + if (_.isEmpty(changes)) return Promise.resolve(tx) + + const dbChanges = massage(changes) + const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') + + pgp.as.format(' where id=$1', [tx.id]) + ' returning *' + + return t.one(sql) + .then(toObj) +} + +function diff (oldTx, newTx) { + let updatedTx = {} + + if (!oldTx) throw new Error('oldTx must not be null') + if (!newTx) throw new Error('newTx must not be null') + + _.forEach(fieldKey => { + const oldField = oldTx[fieldKey] + const newField = newTx[fieldKey] + if (fieldKey === 'bills') return + if (_.isEqualWith(nilEqual, oldField, newField)) return + + if (!ensureRatchet(oldField, newField, fieldKey)) { + logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey) + logger.warn('Old tx: %j', oldTx) + logger.warn('New tx: %j', newTx) + throw new Error('Value from lamassu-machine would violate ratchet') + } + + updatedTx[fieldKey] = newField + }, _.keys(newTx)) + + return updatedTx +} + +function ensureRatchet (oldField, newField, fieldKey) { + const monotonic = ['cryptoAtoms', 'fiat', 'cashInFeeCrypto', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion'] + const free = ['sendPending', 'error', 'errorCode', 'customerId'] + + if (_.isNil(oldField)) return true + if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey) + + if (_.includes(fieldKey, free)) { + if (_.isNil(newField)) return false + return true + } + + if (_.isNil(newField)) return false + if (oldField.isBigNumber && newField.isBigNumber) return BN(oldField).eq(newField) + if (oldField.toString() === newField.toString()) return true + + return false +} + +function isMonotonic (oldField, newField, fieldKey) { + if (_.isNil(newField)) return false + if (_.isBoolean(oldField)) return oldField === newField || !oldField + if (oldField.isBigNumber) return oldField.lte(newField) + if (_.isNumber(oldField)) return oldField <= newField + + throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`) +} + +function nilEqual (a, b) { + if (_.isNil(a) && _.isNil(b)) return true + + return undefined +} + +function isClearToSend (oldTx, newTx) { + const now = Date.now() + + return newTx.send && + (!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) && + (newTx.created > now - PENDING_INTERVAL_MS) +} diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js new file mode 100644 index 00000000..ce74ac0c --- /dev/null +++ b/lib/cash-in/cash-in-tx.js @@ -0,0 +1,126 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() +const pEachSeries = require('p-each-series') + +const db = require('../db') +const plugins = require('../plugins') +const logger = require('../logger') + +const cashInAtomic = require('./cash-in-atomic') +const cashInLow = require('./cash-in-low') + +const PENDING_INTERVAL = '60 minutes' +const MAX_PENDING = 10 + +module.exports = {post, monitorPending, cancel, PENDING_INTERVAL} + +function post (machineTx, pi) { + return db.tx(cashInAtomic.atomic(machineTx, pi)) + .then(r => { + const updatedTx = r.tx + + return postProcess(r, pi) + .then(changes => cashInLow.update(db, updatedTx, changes)) + .then(tx => _.set('bills', machineTx.bills, tx)) + }) +} + +function registerTrades (pi, newBills) { + _.forEach(bill => pi.buy(bill), newBills) +} + +function logAction (rec, tx) { + const action = { + tx_id: tx.id, + action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'), + error: rec.error, + error_code: rec.errorCode, + tx_hash: rec.txHash + } + + const sql = pgp.helpers.insert(action, null, 'cash_in_actions') + + return db.none(sql) + .then(_.constant(rec)) +} + +function logActionById (action, _rec, txId) { + const rec = _.assign(_rec, {action, tx_id: txId}) + const sql = pgp.helpers.insert(rec, null, 'cash_in_actions') + + return db.none(sql) +} + +function postProcess (r, pi) { + registerTrades(pi, r.newBills) + + if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({}) + + return pi.sendCoins(r.tx) + .then(txHash => ({ + txHash, + sendConfirmed: true, + sendTime: 'now()^', + sendPending: false, + error: null, + errorCode: null + })) + .catch(err => { + // Important: We don't know what kind of error this is + // so not safe to assume that funds weren't sent. + // Therefore, don't set sendPending to false except for + // errors (like InsufficientFundsError) that are guaranteed + // not to send. + const sendPending = err.name !== 'InsufficientFundsError' + + return { + sendTime: 'now()^', + error: err.message, + errorCode: err.name, + sendPending + } + }) + .then(sendRec => logAction(sendRec, r.tx)) +} + +function monitorPending (settings) { + const sql = `select * from cash_in_txs + where created > now() - interval $1 + and send + and not send_confirmed + and not send_pending + and not operator_completed + order by created + limit $2` + + const processPending = row => { + const tx = cashInLow.toObj(row) + const pi = plugins(settings, tx.deviceId) + + return post(tx, pi) + .catch(logger.error) + } + + return db.any(sql, [PENDING_INTERVAL, MAX_PENDING]) + .then(rows => pEachSeries(rows, row => processPending(row))) + .catch(logger.error) +} + +function cancel (txId) { + const updateRec = { + error: 'Operator cancel', + error_code: 'operatorCancel', + operator_completed: true + } + + return Promise.resolve() + .then(() => { + return pgp.helpers.update(updateRec, null, 'cash_in_txs') + + pgp.as.format(' where id=$1', [txId]) + }) + .then(sql => db.result(sql, false)) + .then(res => { + if (res.rowCount !== 1) throw new Error('No such tx-id') + }) + .then(() => logActionById('operatorCompleted', {}, txId)) +} diff --git a/lib/cash-out-tx.js b/lib/cash-out-tx.js deleted file mode 100644 index d13d0953..00000000 --- a/lib/cash-out-tx.js +++ /dev/null @@ -1,410 +0,0 @@ -const _ = require('lodash/fp') -const pgp = require('pg-promise')() - -const db = require('./db') -const billMath = require('./bill-math') -const T = require('./time') -const logger = require('./logger') -const plugins = require('./plugins') -const helper = require('./cash-out-helper') -const socket = require('./socket-client') -const E = require('./error') - -module.exports = { - post, - monitorLiveIncoming, - monitorStaleIncoming, - monitorUnnotified, - cancel -} - -const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed', - 'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt'] - -const STALE_INCOMING_TX_AGE = T.week -const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes -const MAX_NOTIFY_AGE = 2 * T.days -const MIN_NOTIFY_AGE = 5 * T.minutes -const INSUFFICIENT_FUNDS_CODE = 570 - -const toObj = helper.toObj -const toDb = helper.toDb - -function httpError (msg, code) { - const err = new Error(msg) - err.name = 'HTTPError' - err.code = code || 500 - - return err -} - -function selfPost (tx, pi) { - return post(tx, pi, false) -} - -function post (tx, pi, fromClient = true) { - const TransactionMode = pgp.txMode.TransactionMode - const isolationLevel = pgp.txMode.isolationLevel - const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) - - function transaction (t) { - const sql = 'select * from cash_out_txs where id=$1' - - return t.oneOrNone(sql, [tx.id]) - .then(toObj) - .then(oldTx => { - const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion) - if (isStale) throw new E.StaleTxError('Stale tx') - - return preProcess(oldTx, tx, pi) - .then(preProcessedTx => upsert(oldTx, preProcessedTx)) - }) - } - - transaction.txMode = tmSRD - - return db.tx(transaction) - .then(txVector => { - const [, newTx] = txVector - return postProcess(txVector, pi) - .then(changes => update(newTx, changes)) - }) -} - -function logError (action, err, tx) { - return logAction(action, { - error: err.message, - error_code: err.name - }, tx) -} - -function mapDispense (tx) { - const bills = tx.bills - - if (_.isEmpty(bills)) return {} - - return { - provisioned_1: bills[0].provisioned, - provisioned_2: bills[1].provisioned, - dispensed_1: bills[0].dispensed, - dispensed_2: bills[1].dispensed, - rejected_1: bills[0].rejected, - rejected_2: bills[1].rejected, - denomination_1: bills[0].denomination, - denomination_2: bills[1].denomination - } -} - -function logDispense (tx) { - const baseRec = {error: tx.error, error_code: tx.errorCode} - const rec = _.merge(mapDispense(tx), baseRec) - const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError' - return logAction(action, rec, tx) -} - -function logActionById (action, _rec, txId) { - const rec = _.assign(_rec, {action, tx_id: txId, redeem: false}) - const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') - - return db.none(sql) -} - -function logAction (action, _rec, tx) { - const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem}) - const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') - - return db.none(sql) - .then(_.constant(tx)) -} - -function nilEqual (a, b) { - if (_.isNil(a) && _.isNil(b)) return true - - return undefined -} - -function diff (oldTx, newTx) { - let updatedTx = {} - - UPDATEABLE_FIELDS.forEach(fieldKey => { - if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return - - // We never null out an existing field - if (oldTx && _.isNil(newTx[fieldKey])) return - - updatedTx[fieldKey] = newTx[fieldKey] - }) - - return updatedTx -} - -function upsert (oldTx, tx) { - if (!oldTx) { - return insert(tx) - .then(newTx => [oldTx, newTx]) - } - - return update(tx, diff(oldTx, tx)) - .then(newTx => [oldTx, newTx]) -} - -function insert (tx) { - const dbTx = toDb(tx) - - const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *' - return db.one(sql) - .then(toObj) -} - -function update (tx, changes) { - if (_.isEmpty(changes)) return Promise.resolve(tx) - - const dbChanges = toDb(changes) - const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + - pgp.as.format(' where id=$1', [tx.id]) - - const newTx = _.merge(tx, changes) - - return db.none(sql) - .then(() => newTx) -} - -function nextHd (isHd, tx) { - if (!isHd) return Promise.resolve(tx) - - return db.one("select nextval('hd_indices_seq') as hd_index") - .then(row => _.set('hdIndex', row.hd_index, tx)) -} - -function dispenseOccurred (bills) { - return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills) -} - -function updateCassettes (tx) { - if (!dispenseOccurred(tx.bills)) return Promise.resolve() - - const sql = `update devices set - cassette1 = cassette1 - $1, - cassette2 = cassette2 - $2 - where device_id = $3 - returning cassette1, cassette2` - - const values = [ - tx.bills[0].dispensed + tx.bills[0].rejected, - tx.bills[1].dispensed + tx.bills[1].rejected, - tx.deviceId - ] - - return db.one(sql, values) - .then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))) -} - -function wasJustAuthorized (oldTx, newTx, isZeroConf) { - const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published']) && - _.includes(newTx.status, ['authorized', 'instant', 'confirmed']) - - const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized']) && - _.includes(newTx.status, ['instant', 'confirmed']) - - return isZeroConf ? isAuthorized() : isConfirmed() -} - -function preProcess (oldTx, newTx, pi) { - if (!oldTx) { - return pi.isHd(newTx) - .then(isHd => nextHd(isHd, newTx)) - .then(newTxHd => { - return pi.newAddress(newTxHd) - .then(_.set('toAddress', _, newTxHd)) - .then(_.unset('isLightning')) - }) - .then(addressedTx => { - const rec = {to_address: addressedTx.toAddress} - return logAction('provisionAddress', rec, addressedTx) - }) - .catch(err => { - return logError('provisionAddress', err, newTx) - .then(() => { throw err }) - }) - } - - return Promise.resolve(updateStatus(oldTx, newTx)) - .then(updatedTx => { - if (updatedTx.status !== oldTx.status) { - const isZeroConf = pi.isZeroConf(updatedTx) - if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx) - - const rec = { - to_address: updatedTx.toAddress, - tx_hash: updatedTx.txHash - } - - return logAction(updatedTx.status, rec, updatedTx) - } - - const hasError = !oldTx.error && newTx.error - const hasDispenseOccurred = !dispenseOccurred(oldTx.bills) && dispenseOccurred(newTx.bills) - - if (hasError || hasDispenseOccurred) { - return logDispense(updatedTx) - .then(updateCassettes(updatedTx)) - } - - if (!oldTx.phone && newTx.phone) { - return logAction('addPhone', {}, updatedTx) - } - - if (!oldTx.redeem && newTx.redeem) { - return logAction('redeemLater', {}, updatedTx) - } - - return updatedTx - }) -} - -function postProcess (txVector, pi) { - const [oldTx, newTx] = txVector - - if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) { - return pi.buildAvailableCassettes(newTx.id) - .then(cassettes => { - const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat) - - if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE) - return bills - }) - .then(bills => { - const provisioned1 = bills[0].provisioned - const provisioned2 = bills[1].provisioned - const denomination1 = bills[0].denomination - const denomination2 = bills[1].denomination - - const rec = { - provisioned_1: provisioned1, - provisioned_2: provisioned2, - denomination_1: denomination1, - denomination_2: denomination2 - } - - return logAction('provisionNotes', rec, newTx) - .then(_.constant({bills})) - }) - .catch(err => { - return logError('provisionNotesError', err, newTx) - .then(() => { throw err }) - }) - } - - return Promise.resolve({}) -} - -function isPublished (status) { - return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed']) -} - -function isConfirmed (status) { - return status === 'confirmed' -} - -function updateStatus (oldTx, newTx) { - const oldStatus = oldTx.status - const newStatus = ratchetStatus(oldStatus, newTx.status) - - const publishedAt = !oldTx.publishedAt && isPublished(newStatus) - ? 'now()^' - : undefined - - const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus) - ? 'now()^' - : undefined - - const updateRec = { - publishedAt, - confirmedAt, - status: newStatus - } - - return _.merge(newTx, updateRec) -} - -function ratchetStatus (oldStatus, newStatus) { - const statusOrder = ['notSeen', 'published', 'rejected', - 'authorized', 'instant', 'confirmed'] - - if (oldStatus === newStatus) return oldStatus - if (newStatus === 'insufficientFunds') return newStatus - - const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus)) - return statusOrder[idx] -} - -function fetchOpenTxs (statuses, age) { - const sql = `select * - from cash_out_txs - where ((extract(epoch from (now() - created))) * 1000)<$1 - and status in ($2^)` - - const statusClause = _.map(pgp.as.text, statuses).join(',') - - return db.any(sql, [age, statusClause]) - .then(rows => rows.map(toObj)) -} - -function processTxStatus (tx, settings) { - const pi = plugins(settings, tx.deviceId) - - return pi.getStatus(tx) - .then(res => _.assign(tx, {status: res.status})) - .then(_tx => selfPost(_tx, pi)) -} - -function monitorLiveIncoming (settings) { - const statuses = ['notSeen', 'published', 'insufficientFunds'] - - return fetchOpenTxs(statuses, STALE_LIVE_INCOMING_TX_AGE) - .then(txs => Promise.all(txs.map(tx => processTxStatus(tx, settings)))) - .catch(logger.error) -} - -function monitorStaleIncoming (settings) { - const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds'] - - return fetchOpenTxs(statuses, STALE_INCOMING_TX_AGE) - .then(txs => Promise.all(txs.map(tx => processTxStatus(tx, settings)))) - .catch(logger.error) -} - -function monitorUnnotified (settings) { - const sql = `select * - from cash_out_txs - where ((extract(epoch from (now() - created))) * 1000)<$1 - and notified=$2 and dispense=$3 - and phone is not null - and status in ('instant', 'confirmed') - and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)` - - const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx) - return db.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE]) - .then(rows => _.map(toObj, rows)) - .then(txs => Promise.all(txs.map(notify))) - .catch(logger.error) -} - -function cancel (txId) { - const updateRec = { - error: 'Operator cancel', - error_code: 'operatorCancel', - dispense: true - } - - return Promise.resolve() - .then(() => { - return pgp.helpers.update(updateRec, null, 'cash_out_txs') + - pgp.as.format(' where id=$1', [txId]) - }) - .then(sql => db.result(sql, false)) - .then(res => { - if (res.rowCount !== 1) throw new Error('No such tx-id') - }) - .then(() => logActionById('operatorCompleted', {}, txId)) -} diff --git a/lib/cash-out/cash-out-actions.js b/lib/cash-out/cash-out-actions.js new file mode 100644 index 00000000..8899ac09 --- /dev/null +++ b/lib/cash-out/cash-out-actions.js @@ -0,0 +1,50 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() + +module.exports = {logDispense, logActionById, logAction, logError} + +function logDispense (t, tx) { + const baseRec = {error: tx.error, error_code: tx.errorCode} + const rec = _.merge(mapDispense(tx), baseRec) + const action = tx.dispenseConfirmed ? 'dispense' : 'dispenseError' + return logAction(t, action, rec, tx) +} + +function logActionById (t, action, _rec, txId) { + const rec = _.assign(_rec, {action, tx_id: txId, redeem: false}) + const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') + + return t.none(sql) +} + +function logAction (t, action, _rec, tx) { + const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem}) + const sql = pgp.helpers.insert(rec, null, 'cash_out_actions') + + return t.none(sql) + .then(_.constant(tx)) +} + +function logError (t, action, err, tx) { + return logAction(t, action, { + error: err.message, + error_code: err.name + }, tx) +} + +function mapDispense (tx) { + const bills = tx.bills + + if (_.isEmpty(bills)) return {} + + return { + provisioned_1: bills[0].provisioned, + provisioned_2: bills[1].provisioned, + dispensed_1: bills[0].dispensed, + dispensed_2: bills[1].dispensed, + rejected_1: bills[0].rejected, + rejected_2: bills[1].rejected, + denomination_1: bills[0].denomination, + denomination_2: bills[1].denomination + } +} diff --git a/lib/cash-out/cash-out-atomic.js b/lib/cash-out/cash-out-atomic.js new file mode 100644 index 00000000..acb24741 --- /dev/null +++ b/lib/cash-out/cash-out-atomic.js @@ -0,0 +1,160 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() + +const E = require('../error') +const socket = require('../socket-client') + +const helper = require('./cash-out-helper') +const cashOutActions = require('./cash-out-actions') +const cashOutLow = require('./cash-out-low') + +const toObj = helper.toObj + +module.exports = {atomic} + +function atomic (tx, pi, fromClient) { + const TransactionMode = pgp.txMode.TransactionMode + const isolationLevel = pgp.txMode.isolationLevel + const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) + + function transaction (t) { + const sql = 'select * from cash_out_txs where id=$1' + + return t.oneOrNone(sql, [tx.id]) + .then(toObj) + .then(oldTx => { + const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion) + if (isStale) throw new E.StaleTxError('Stale tx') + + return preProcess(t, oldTx, tx, pi) + .then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx)) + }) + } + + transaction.txMode = tmSRD + + return transaction +} + +function preProcess (t, oldTx, newTx, pi) { + if (!oldTx) { + return pi.isHd(newTx) + .then(isHd => nextHd(t, isHd, newTx)) + .then(newTxHd => { + return pi.newAddress(newTxHd) + .then(_.set('toAddress', _, newTxHd)) + }) + .then(addressedTx => { + const rec = {to_address: addressedTx.toAddress} + return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx) + }) + .catch(err => { + return cashOutActions.logError(t, 'provisionAddress', err, newTx) + .then(() => { throw err }) + }) + } + + return Promise.resolve(updateStatus(oldTx, newTx)) + .then(updatedTx => { + if (updatedTx.status !== oldTx.status) { + const isZeroConf = pi.isZeroConf(updatedTx) + if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx) + + const rec = { + to_address: updatedTx.toAddress, + tx_hash: updatedTx.txHash + } + + return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx) + } + + if (!oldTx.dispenseConfirmed && updatedTx.dispenseConfirmed) { + return cashOutActions.logDispense(t, updatedTx) + .then(updateCassettes(t, updatedTx)) + } + + if (!oldTx.phone && newTx.phone) { + return cashOutActions.logAction(t, 'addPhone', {}, updatedTx) + } + + if (!oldTx.redeem && newTx.redeem) { + return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx) + } + + return updatedTx + }) +} + +function nextHd (t, isHd, tx) { + if (!isHd) return Promise.resolve(tx) + + return t.one("select nextval('hd_indices_seq') as hd_index") + .then(row => _.set('hdIndex', row.hd_index, tx)) +} + +function updateCassettes (t, tx) { + const sql = `update devices set + cassette1 = cassette1 - $1, + cassette2 = cassette2 - $2 + where device_id = $3 + returning cassette1, cassette2` + + const values = [ + tx.bills[0].dispensed + tx.bills[0].rejected, + tx.bills[1].dispensed + tx.bills[1].rejected, + tx.deviceId + ] + + return t.one(sql, values) + .then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId}))) +} + +function wasJustAuthorized (oldTx, newTx, isZeroConf) { + const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published']) && + _.includes(newTx.status, ['authorized', 'instant', 'confirmed']) + + const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized']) && + _.includes(newTx.status, ['instant', 'confirmed']) + + return isZeroConf ? isAuthorized() : isConfirmed() +} + +function isPublished (status) { + return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed']) +} + +function isConfirmed (status) { + return status === 'confirmed' +} + +function updateStatus (oldTx, newTx) { + const oldStatus = oldTx.status + const newStatus = ratchetStatus(oldStatus, newTx.status) + + const publishedAt = !oldTx.publishedAt && isPublished(newStatus) + ? 'now()^' + : undefined + + const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus) + ? 'now()^' + : undefined + + const updateRec = { + publishedAt, + confirmedAt, + status: newStatus + } + + return _.merge(newTx, updateRec) +} + +function ratchetStatus (oldStatus, newStatus) { + const statusOrder = ['notSeen', 'published', 'rejected', + 'authorized', 'instant', 'confirmed'] + + if (oldStatus === newStatus) return oldStatus + if (newStatus === 'insufficientFunds') return newStatus + + const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus)) + return statusOrder[idx] +} diff --git a/lib/cash-out-helper.js b/lib/cash-out/cash-out-helper.js similarity index 96% rename from lib/cash-out-helper.js rename to lib/cash-out/cash-out-helper.js index b5edd152..17fa53d7 100644 --- a/lib/cash-out-helper.js +++ b/lib/cash-out/cash-out-helper.js @@ -1,8 +1,8 @@ const _ = require('lodash/fp') -const db = require('./db') -const T = require('./time') -const BN = require('./bn') +const db = require('../db') +const T = require('../time') +const BN = require('../bn') const REDEEMABLE_AGE = T.day diff --git a/lib/cash-out/cash-out-low.js b/lib/cash-out/cash-out-low.js new file mode 100644 index 00000000..1040b5db --- /dev/null +++ b/lib/cash-out/cash-out-low.js @@ -0,0 +1,64 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() + +const helper = require('./cash-out-helper') + +const toDb = helper.toDb +const toObj = helper.toObj + +const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed', + 'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt'] + +module.exports = {upsert, update, insert} + +function upsert (t, oldTx, tx) { + if (!oldTx) { + return insert(t, tx) + .then(newTx => [oldTx, newTx]) + } + + return update(t, tx, diff(oldTx, tx)) + .then(newTx => [oldTx, newTx]) +} + +function insert (t, tx) { + const dbTx = toDb(tx) + + const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *' + return t.one(sql) + .then(toObj) +} + +function update (t, tx, changes) { + if (_.isEmpty(changes)) return Promise.resolve(tx) + + const dbChanges = toDb(changes) + const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + + pgp.as.format(' where id=$1', [tx.id]) + + const newTx = _.merge(tx, changes) + + return t.none(sql) + .then(() => newTx) +} + +function diff (oldTx, newTx) { + let updatedTx = {} + + UPDATEABLE_FIELDS.forEach(fieldKey => { + if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return + + // We never null out an existing field + if (oldTx && _.isNil(newTx[fieldKey])) return + + updatedTx[fieldKey] = newTx[fieldKey] + }) + + return updatedTx +} + +function nilEqual (a, b) { + if (_.isNil(a) && _.isNil(b)) return true + + return undefined +} diff --git a/lib/cash-out/cash-out-tx.js b/lib/cash-out/cash-out-tx.js new file mode 100644 index 00000000..27b3e0af --- /dev/null +++ b/lib/cash-out/cash-out-tx.js @@ -0,0 +1,158 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() +const pEachSeries = require('p-each-series') + +const db = require('../db') +const billMath = require('../bill-math') +const T = require('../time') +const logger = require('../logger') +const plugins = require('../plugins') + +const helper = require('./cash-out-helper') +const cashOutAtomic = require('./cash-out-atomic') +const cashOutActions = require('./cash-out-actions') +const cashOutLow = require('./cash-out-low') + +module.exports = { + post, + monitorLiveIncoming, + monitorStaleIncoming, + monitorUnnotified, + cancel +} + +const STALE_INCOMING_TX_AGE = T.week +const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes +const MAX_NOTIFY_AGE = 2 * T.days +const MIN_NOTIFY_AGE = 5 * T.minutes +const INSUFFICIENT_FUNDS_CODE = 570 + +const toObj = helper.toObj + +function httpError (msg, code) { + const err = new Error(msg) + err.name = 'HTTPError' + err.code = code || 500 + + return err +} + +function selfPost (tx, pi) { + return post(tx, pi, false) +} + +function post (tx, pi, fromClient = true) { + return db.tx(cashOutAtomic.atomic(tx, pi, fromClient)) + .then(txVector => { + const [, newTx] = txVector + return postProcess(txVector, pi) + .then(changes => cashOutLow.update(db, newTx, changes)) + }) +} + +function postProcess (txVector, pi) { + const [oldTx, newTx] = txVector + + if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) { + return pi.buildAvailableCassettes(newTx.id) + .then(cassettes => { + const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat) + + if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE) + return bills + }) + .then(bills => { + const provisioned1 = bills[0].provisioned + const provisioned2 = bills[1].provisioned + const denomination1 = bills[0].denomination + const denomination2 = bills[1].denomination + + const rec = { + provisioned_1: provisioned1, + provisioned_2: provisioned2, + denomination_1: denomination1, + denomination_2: denomination2 + } + + return cashOutActions.logAction(db, 'provisionNotes', rec, newTx) + .then(_.constant({bills})) + }) + .catch(err => { + return cashOutActions.logError(db, 'provisionNotesError', err, newTx) + .then(() => { throw err }) + }) + } + + return Promise.resolve({}) +} + +function fetchOpenTxs (statuses, age) { + const sql = `select * + from cash_out_txs + where ((extract(epoch from (now() - created))) * 1000)<$1 + and status in ($2^)` + + const statusClause = _.map(pgp.as.text, statuses).join(',') + + return db.any(sql, [age, statusClause]) + .then(rows => rows.map(toObj)) +} + +function processTxStatus (tx, settings) { + const pi = plugins(settings, tx.deviceId) + + return pi.getStatus(tx) + .then(res => _.assign(tx, {status: res.status})) + .then(_tx => selfPost(_tx, pi)) +} + +function monitorLiveIncoming (settings) { + const statuses = ['notSeen', 'published', 'insufficientFunds'] + + return fetchOpenTxs(statuses, STALE_LIVE_INCOMING_TX_AGE) + .then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings))) + .catch(logger.error) +} + +function monitorStaleIncoming (settings) { + const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds'] + + return fetchOpenTxs(statuses, STALE_INCOMING_TX_AGE) + .then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings))) + .catch(logger.error) +} + +function monitorUnnotified (settings) { + const sql = `select * + from cash_out_txs + where ((extract(epoch from (now() - created))) * 1000)<$1 + and notified=$2 and dispense=$3 + and phone is not null + and status in ('instant', 'confirmed') + and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)` + + const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx) + return db.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE]) + .then(rows => _.map(toObj, rows)) + .then(txs => Promise.all(txs.map(notify))) + .catch(logger.error) +} + +function cancel (txId) { + const updateRec = { + error: 'Operator cancel', + error_code: 'operatorCancel', + dispense: true + } + + return Promise.resolve() + .then(() => { + return pgp.helpers.update(updateRec, null, 'cash_out_txs') + + pgp.as.format(' where id=$1', [txId]) + }) + .then(sql => db.result(sql, false)) + .then(res => { + if (res.rowCount !== 1) throw new Error('No such tx-id') + }) + .then(() => cashOutActions.logActionById(db, 'operatorCompleted', {}, txId)) +} diff --git a/lib/plugins.js b/lib/plugins.js index 68594b42..480f69c6 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -16,7 +16,7 @@ const wallet = require('./wallet') const exchange = require('./exchange') const sms = require('./sms') const email = require('./email') -const cashOutHelper = require('./cash-out-helper') +const cashOutHelper = require('./cash-out/cash-out-helper') const machineLoader = require('./machine-loader') const coinUtils = require('./coin-utils') diff --git a/lib/plugins/wallet/mock-wallet/mock-wallet.js b/lib/plugins/wallet/mock-wallet/mock-wallet.js index aed5f8a9..e91674b5 100644 --- a/lib/plugins/wallet/mock-wallet/mock-wallet.js +++ b/lib/plugins/wallet/mock-wallet/mock-wallet.js @@ -83,7 +83,7 @@ function getStatus (account, toAddress, cryptoAtoms, cryptoCode) { if (elapsed < AUTHORIZE_TIME) return Promise.resolve({status: 'published'}) if (elapsed < CONFIRM_TIME) return Promise.resolve({status: 'authorized'}) - console.log('[%s] DEBUG: Mock wallet has confirmed transaction', cryptoCode) + console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5)) return Promise.resolve({status: 'confirmed'}) } diff --git a/lib/poller.js b/lib/poller.js index e11806d0..af0f9829 100644 --- a/lib/poller.js +++ b/lib/poller.js @@ -2,8 +2,8 @@ const plugins = require('./plugins') const notifier = require('./notifier') const T = require('./time') const logger = require('./logger') -const cashOutTx = require('./cash-out-tx') -const cashInTx = require('./cash-in-tx') +const cashOutTx = require('./cash-out/cash-out-tx') +const cashInTx = require('./cash-in/cash-in-tx') const INCOMING_TX_INTERVAL = 30 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds diff --git a/lib/tx.js b/lib/tx.js index 32c0bc70..f6ff3d49 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -1,7 +1,7 @@ const _ = require('lodash/fp') const BN = require('./bn') -const CashInTx = require('./cash-in-tx') -const CashOutTx = require('./cash-out-tx') +const CashInTx = require('./cash-in/cash-in-tx') +const CashOutTx = require('./cash-out/cash-out-tx') function process (tx, pi) { const mtx = massage(tx) diff --git a/package.json b/package.json index 9838f139..57506fc4 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,8 @@ "node-hkdf-sync": "^1.0.0", "node-mailjet": "^3.2.1", "numeral": "^2.0.3", - "pg-native": "^2.0.1", - "pg-promise": "^6.3.7", + "pg-native": "^2.2.0", + "pg-promise": "^7.4.1", "pify": "^3.0.0", "pretty-ms": "^2.1.0", "promise-sequential": "^1.1.1", diff --git a/tools/modify.js b/tools/modify.js index d5c34611..307744f3 100644 --- a/tools/modify.js +++ b/tools/modify.js @@ -1,7 +1,7 @@ const settingsLoader = require('../lib/settings-loader') const fields = [ - settingsLoader.configDeleteField({crypto: 'ETH', machine: 'global'}, 'exchange') + settingsLoader.configDeleteField({crypto: 'BTC', machine: 'global'}, 'wallet') ] settingsLoader.modifyConfig(fields)