From 340b39d47de32bcde92c10424c045c4d80ec77f5 Mon Sep 17 00:00:00 2001 From: Josh Harvey Date: Wed, 15 Mar 2017 22:54:40 +0200 Subject: [PATCH] WIPP --- lib/bill-math.js | 38 +++++++++ lib/cash-in-tx.js | 2 + lib/cash-out-tx.js | 95 +++++++++++++++------ lib/plugins.js | 54 +++++------- lib/route-helpers.js | 5 +- lib/routes.js | 30 +++++-- lib/tx.js | 14 ++- migrations/022-add_cash_in_sent.js | 3 +- migrations/023-add-dispenses-to-cash-out.js | 20 +++++ 9 files changed, 189 insertions(+), 72 deletions(-) create mode 100644 lib/bill-math.js create mode 100644 migrations/023-add-dispenses-to-cash-out.js diff --git a/lib/bill-math.js b/lib/bill-math.js new file mode 100644 index 00000000..2d902cc2 --- /dev/null +++ b/lib/bill-math.js @@ -0,0 +1,38 @@ +const uuid = require('uuid') + +// Custom algorith for two cassettes. For three or more denominations, we'll need +// to rethink this. Greedy algorithm fails to find *any* solution in some cases. +// Dynamic programming may be too inefficient for large amounts. +// +// We can either require canononical denominations for 3+, or try to expand +// this algorithm. +exports.makeChange = function makeChange (cartridges, amount) { + // Note: Everything here is converted to primitive numbers, + // since they're all integers, well within JS number range, + // and this is way more efficient in a tight loop. + + console.log('DEBUG777: %j', cartridges) + const small = cartridges[0] + const large = cartridges[1] + + const largeDenom = large.denomination + const smallDenom = small.denomination + const largeBills = Math.min(large.count, Math.floor(amount / largeDenom)) + const amountNum = amount.toNumber() + + for (let i = largeBills; i >= 0; i--) { + const remainder = amountNum - largeDenom * i + + if (remainder % smallDenom !== 0) continue + + const smallCount = remainder / smallDenom + if (smallCount > small.count) continue + + return [ + {count: smallCount, denomination: small.denomination, id: uuid.v4()}, + {count: i, denomination: largeDenom, id: uuid.v4()} + ] + } + + return null +} diff --git a/lib/cash-in-tx.js b/lib/cash-in-tx.js index 4f93c447..e55fa334 100644 --- a/lib/cash-in-tx.js +++ b/lib/cash-in-tx.js @@ -68,6 +68,8 @@ function toObj (row) { newObj[objKey] = row[key] }) + newObj.direction = 'cashIn' + return newObj } diff --git a/lib/cash-out-tx.js b/lib/cash-out-tx.js index f4e87c19..cfa24f6a 100644 --- a/lib/cash-out-tx.js +++ b/lib/cash-out-tx.js @@ -2,25 +2,11 @@ const _ = require('lodash/fp') const pgp = require('pg-promise')() const db = require('./db') const BN = require('./bn') +const billMath = require('./bill-math') module.exports = {post} -// id | uuid | not null -// device_id | text | not null -// to_address | text | not null -// crypto_atoms | bigint | not null -// crypto_code | text | not null -// fiat | numeric(14,5) | not null -// currency_code | text | not null -// tx_hash | text | -// status | status_stage | not null default 'notSeen'::status_stage -// dispensed | boolean | not null default false -// notified | boolean | not null default false -// redeem | boolean | not null default false -// phone | text | -// error | text | -// created | timestamp with time zone | not null default now() -// confirmation_time | timestamp with time zone | +const mapValuesWithKey = _.mapValues.convert({cap: false}) const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem', 'phone', 'error', 'confirmationTime'] @@ -33,9 +19,13 @@ function post (tx, pi) { function transaction (t) { const sql = 'select * from cash_out_txs where id=$1' - console.log('DEBUG888: %j', tx) + console.log('DEBUG988: %j', tx) return t.oneOrNone(sql, [tx.id]) - .then(row => upsert(row, tx)) + .then(toObj) + .then(oldTx => { + return preProcess(oldTx, tx, pi) + .then(preProcessedTx => upsert(oldTx, preProcessedTx)) + }) } transaction.txMode = tmSRD @@ -86,12 +76,12 @@ function toObj (row) { newObj[objKey] = row[key] }) + newObj.direction = 'cashOut' + return newObj } -function upsert (row, tx) { - const oldTx = toObj(row) - +function upsert (oldTx, tx) { // insert bills if (!oldTx) { @@ -103,10 +93,40 @@ function upsert (row, tx) { .then(newTx => [oldTx, newTx]) } +function mapDispense (tx) { + const bills = tx.bills + + if (_.isEmpty(bills)) return tx + + const extra = { + dispensed1: bills[0].actualDispense, + dispensed2: bills[1].actualDispense, + rejected1: bills[0].rejected, + rejected2: bills[1].rejected, + denomination1: bills[0].denomination, + denomination2: bills[1].denomination, + dispenseTime: 'NOW()^' + } + + return _.assign(tx, extra) +} + +function toDb (tx) { + const mapper = (v, k) => { + if (k === 'fiat' || k === 'cryptoAtoms') return v.toNumber() + return v + } + + const massager = _.flow(mapValuesWithKey(mapper), mapDispense, _.omit(['direction', 'bills']), _.mapKeys(_.snakeCase)) + return massager(tx) +} + function insert (tx) { - const dbTx = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], tx)) + const dbTx = toDb(tx) const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *' + console.log('DEBUG901: %s', sql) + console.log('DEBUG902: %j', dbTx) return db.one(sql) .then(toObj) } @@ -114,17 +134,36 @@ function insert (tx) { function update (tx, changes) { if (_.isEmpty(changes)) return Promise.resolve(tx) - const dbChanges = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], changes)) + const dbChanges = toDb(tx) console.log('DEBUG893: %j', dbChanges) const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + - pgp.as.format(' where id=$1', [tx.id]) + ' returning *' + pgp.as.format(' where id=$1', [tx.id]) - return db.one(sql) - .then(toObj) + const newTx = _.merge(tx, changes) + + return db.none(sql) + .then(() => newTx) +} + +function preProcess (tx, newTx, pi) { + if (!tx) { + console.log('DEBUG910') + return pi.newAddress(newTx) + .then(_.set('toAddress', _, newTx)) + } + + return Promise.resolve(newTx) } function postProcess (txVector, pi) { - const [oldTx, newTx] = txVector + const [, newTx] = txVector - return Promise.resolve({}) + if (newTx.dispensed && !newTx.bills) { + return pi.buildCartridges() + .then(cartridges => { + return _.set('bills', billMath.makeChange(cartridges.cartridges, newTx.fiat), newTx) + }) + } + + return Promise.resolve(newTx) } diff --git a/lib/plugins.js b/lib/plugins.js index 3506a088..aa224af6 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -72,8 +72,14 @@ function plugins (settings, deviceId) { return balances } - function buildCartridges (cartridges, virtualCartridges, rec) { - return { + function buildCartridges () { + const config = configManager.machineScoped(deviceId, settings.config) + const cartridges = [ config.topCashOutDenomination, + config.bottomCashOutDenomination ] + const virtualCartridges = [config.virtualCashOutDenomination] + + return dbm.cartridgeCounts(deviceId) + .then(rec => ({ cartridges: [ { denomination: parseInt(cartridges[0], 10), @@ -85,7 +91,7 @@ function plugins (settings, deviceId) { } ], virtualCartridges - } + })) } function fetchCurrentConfigVersion () { @@ -102,9 +108,6 @@ function plugins (settings, deviceId) { const config = configManager.machineScoped(deviceId, settings.config) const fiatCode = config.fiatCurrency const cryptoCodes = config.cryptoCurrencies - const cartridges = [ config.topCashOutDenomination, - config.bottomCashOutDenomination ] - const virtualCartridges = [config.virtualCashOutDenomination] const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) @@ -112,20 +115,20 @@ function plugins (settings, deviceId) { const currentConfigVersionPromise = fetchCurrentConfigVersion() const promises = [ - dbm.cartridgeCounts(deviceId), + buildCartridges(), pingPromise, currentConfigVersionPromise ].concat(tickerPromises, balancePromises) return Promise.all(promises) .then(arr => { - const cartridgeCounts = arr[0] + const cartridges = arr[0] const currentConfigVersion = arr[2] const tickers = arr.slice(3, cryptoCodes.length + 3) const balances = arr.slice(cryptoCodes.length + 3) return { - cartridges: buildCartridges(cartridges, virtualCartridges, cartridgeCounts), + cartridges, rates: buildRates(tickers), balances: buildBalances(balances), currentConfigVersion @@ -174,29 +177,13 @@ function plugins (settings, deviceId) { return dbm.machineEvent(event) } - function cashOut (tx) { + function newAddress (tx) { const cryptoCode = tx.cryptoCode - - const serialPromise = wallet.supportsHD - ? dbm.nextCashOutSerialHD(tx.id, cryptoCode) - : Promise.resolve() - - return serialPromise - .then(serialNumber => { - const info = { - label: 'TX ' + Date.now(), - account: 'deposit', - serialNumber - } - - return wallet.newAddress(settings, cryptoCode, info) - .then(address => { - const newTx = _.set('toAddress', address, tx) - - return dbm.addInitialIncoming(deviceId, newTx, address) - .then(() => address) - }) - }) + const info = { + label: 'TX ' + Date.now(), + account: 'deposit' + } + return wallet.newAddress(settings, cryptoCode, info) } function dispenseAck (tx) { @@ -486,7 +473,7 @@ function plugins (settings, deviceId) { pollQueries, trade, sendCoins, - cashOut, + newAddress, dispenseAck, getPhoneCode, executeTrades, @@ -498,7 +485,8 @@ function plugins (settings, deviceId) { sweepLiveHD, sweepOldHD, sendMessage, - checkBalances + checkBalances, + buildCartridges } } diff --git a/lib/route-helpers.js b/lib/route-helpers.js index 6cec63e0..50a22d94 100644 --- a/lib/route-helpers.js +++ b/lib/route-helpers.js @@ -71,12 +71,15 @@ function fetchPhoneTx (phone) { } function fetchStatusTx (txId, status) { + console.log('DEBUG444') const sql = 'select * from cash_out_txs where id=$1' - return db.oneOrNone(sql, [txId, status]) + return db.oneOrNone(sql, [txId]) .then(toObj) .then(tx => { + console.log('DEBUG445') if (!tx) throw httpError('No transaction', 404) + console.log('DEBUG446') if (tx.status === status) throw httpError('Not Modified', 304) return tx }) diff --git a/lib/routes.js b/lib/routes.js index 39a704bd..a438555d 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -36,7 +36,7 @@ function poll (req, res, next) { pids[deviceId] = {pid, ts: Date.now()} - pi.pollQueries(deviceTime, req.query) + return pi.pollQueries(deviceTime, req.query) .then(results => { const cartridges = results.cartridges @@ -73,15 +73,32 @@ function poll (req, res, next) { response.idVerificationLimit = config.idVerificationLimit } + console.log('DEBUG22: %j', response) return res.json(response) }) .catch(next) } function getTx (req, res, next) { - if (req.query.phone) return helpers.fetchPhoneTx(req.query.phone) - if (req.query.status) return helpers.fetchStatusTx(req.query.status) - throw httpError('Not Found', 404) + console.log('DEBUG333: %s', req.query.status) + if (req.query.status) { + return helpers.fetchStatusTx(req.params.id, req.query.status) + .then(r => res.json(r)) + .catch(next) + } + + console.log('DEBUG334') + return next(httpError('Not Found', 404)) +} + +function getPhoneTx (req, res, next) { + if (req.query.phone) { + return helpers.fetchPhoneTx(req.query.phone) + .then(r => res.json(r)) + .catch(next) + } + + return next(httpError('Not Found', 404)) } function postTx (req, res, next) { @@ -153,7 +170,7 @@ function phoneCode (req, res, next) { } function errorHandler (err, req, res, next) { - const statusCode = err.name === 'HttpError' + const statusCode = err.name === 'HTTPError' ? err.code || 500 : 500 @@ -244,7 +261,8 @@ app.post('/verify_transaction', verifyTx) app.post('/phone_code', phoneCode) app.post('/tx', postTx) -app.get('/tx', getTx) +app.get('/tx/:id', getTx) +app.get('/tx', getPhoneTx) app.use(errorHandler) app.use((req, res) => res.status(404).json({err: 'No such route'})) diff --git a/lib/tx.js b/lib/tx.js index 44cb3c48..540deee8 100644 --- a/lib/tx.js +++ b/lib/tx.js @@ -1,10 +1,18 @@ +const _ = require('lodash/fp') +const BN = require('./bn') const CashInTx = require('./cash-in-tx') +const CashOutTx = require('./cash-out-tx') function post (tx, pi) { - if (tx.direction === 'cashIn') return CashInTx.post(tx, pi) - if (tx.direction === 'cashOut') throw new Error('not implemented') + const mtx = massage(tx) + if (mtx.direction === 'cashIn') return CashInTx.post(mtx, pi) + if (mtx.direction === 'cashOut') return CashOutTx.post(mtx, pi) - return Promise.reject(new Error('No such tx direction: %s', tx.direction)) + return Promise.reject(new Error('No such tx direction: ' + mtx.direction)) +} + +function massage (tx) { + return _.assign(tx, {cryptoAtoms: BN(tx.cryptoAtoms), fiat: BN(tx.fiat)}) } module.exports = {post} diff --git a/migrations/022-add_cash_in_sent.js b/migrations/022-add_cash_in_sent.js index 13e80201..41aeb0c9 100644 --- a/migrations/022-add_cash_in_sent.js +++ b/migrations/022-add_cash_in_sent.js @@ -3,7 +3,8 @@ var db = require('./db') exports.up = function (next) { var sql = [ 'alter table cash_in_txs add column send boolean not null default false', - 'alter table cash_in_txs rename currency_code to fiat_code' + 'alter table cash_in_txs rename currency_code to fiat_code', + 'alter table cash_out_txs rename currency_code to fiat_code' ] db.multi(sql, next) } diff --git a/migrations/023-add-dispenses-to-cash-out.js b/migrations/023-add-dispenses-to-cash-out.js new file mode 100644 index 00000000..3229514d --- /dev/null +++ b/migrations/023-add-dispenses-to-cash-out.js @@ -0,0 +1,20 @@ +var db = require('./db') + +exports.up = function (next) { + var sql = [ + 'alter table cash_out_txs add column dispensed_1 integer', + 'alter table cash_out_txs add column dispensed_2 integer', + 'alter table cash_out_txs add column rejected_1 integer', + 'alter table cash_out_txs add column rejected_2 integer', + 'alter table cash_out_txs add column denomination_1 integer', + 'alter table cash_out_txs add column denomination_2 integer', + 'alter table cash_out_txs add column dispense_error text', + 'alter table cash_out_txs add column dispense_time timestamptz', + 'drop table dispenses' + ] + db.multi(sql, next) +} + +exports.down = function (next) { + next() +}