This commit is contained in:
Josh Harvey 2017-03-15 22:54:40 +02:00
parent b4d8f3cd4c
commit 340b39d47d
9 changed files with 189 additions and 72 deletions

38
lib/bill-math.js Normal file
View file

@ -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
}

View file

@ -68,6 +68,8 @@ function toObj (row) {
newObj[objKey] = row[key] newObj[objKey] = row[key]
}) })
newObj.direction = 'cashIn'
return newObj return newObj
} }

View file

@ -2,25 +2,11 @@ const _ = require('lodash/fp')
const pgp = require('pg-promise')() const pgp = require('pg-promise')()
const db = require('./db') const db = require('./db')
const BN = require('./bn') const BN = require('./bn')
const billMath = require('./bill-math')
module.exports = {post} module.exports = {post}
// id | uuid | not null const mapValuesWithKey = _.mapValues.convert({cap: false})
// 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 UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem', const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem',
'phone', 'error', 'confirmationTime'] 'phone', 'error', 'confirmationTime']
@ -33,9 +19,13 @@ function post (tx, pi) {
function transaction (t) { function transaction (t) {
const sql = 'select * from cash_out_txs where id=$1' 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]) 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 transaction.txMode = tmSRD
@ -86,12 +76,12 @@ function toObj (row) {
newObj[objKey] = row[key] newObj[objKey] = row[key]
}) })
newObj.direction = 'cashOut'
return newObj return newObj
} }
function upsert (row, tx) { function upsert (oldTx, tx) {
const oldTx = toObj(row)
// insert bills // insert bills
if (!oldTx) { if (!oldTx) {
@ -103,10 +93,40 @@ function upsert (row, tx) {
.then(newTx => [oldTx, newTx]) .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) { 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 *' 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) return db.one(sql)
.then(toObj) .then(toObj)
} }
@ -114,17 +134,36 @@ function insert (tx) {
function update (tx, changes) { function update (tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx) if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], changes)) const dbChanges = toDb(tx)
console.log('DEBUG893: %j', dbChanges) console.log('DEBUG893: %j', dbChanges)
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + 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) const newTx = _.merge(tx, changes)
.then(toObj)
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) { 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)
} }

View file

@ -72,8 +72,14 @@ function plugins (settings, deviceId) {
return balances return balances
} }
function buildCartridges (cartridges, virtualCartridges, rec) { function buildCartridges () {
return { const config = configManager.machineScoped(deviceId, settings.config)
const cartridges = [ config.topCashOutDenomination,
config.bottomCashOutDenomination ]
const virtualCartridges = [config.virtualCashOutDenomination]
return dbm.cartridgeCounts(deviceId)
.then(rec => ({
cartridges: [ cartridges: [
{ {
denomination: parseInt(cartridges[0], 10), denomination: parseInt(cartridges[0], 10),
@ -85,7 +91,7 @@ function plugins (settings, deviceId) {
} }
], ],
virtualCartridges virtualCartridges
} }))
} }
function fetchCurrentConfigVersion () { function fetchCurrentConfigVersion () {
@ -102,9 +108,6 @@ function plugins (settings, deviceId) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const fiatCode = config.fiatCurrency const fiatCode = config.fiatCurrency
const cryptoCodes = config.cryptoCurrencies 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 tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
@ -112,20 +115,20 @@ function plugins (settings, deviceId) {
const currentConfigVersionPromise = fetchCurrentConfigVersion() const currentConfigVersionPromise = fetchCurrentConfigVersion()
const promises = [ const promises = [
dbm.cartridgeCounts(deviceId), buildCartridges(),
pingPromise, pingPromise,
currentConfigVersionPromise currentConfigVersionPromise
].concat(tickerPromises, balancePromises) ].concat(tickerPromises, balancePromises)
return Promise.all(promises) return Promise.all(promises)
.then(arr => { .then(arr => {
const cartridgeCounts = arr[0] const cartridges = arr[0]
const currentConfigVersion = arr[2] const currentConfigVersion = arr[2]
const tickers = arr.slice(3, cryptoCodes.length + 3) const tickers = arr.slice(3, cryptoCodes.length + 3)
const balances = arr.slice(cryptoCodes.length + 3) const balances = arr.slice(cryptoCodes.length + 3)
return { return {
cartridges: buildCartridges(cartridges, virtualCartridges, cartridgeCounts), cartridges,
rates: buildRates(tickers), rates: buildRates(tickers),
balances: buildBalances(balances), balances: buildBalances(balances),
currentConfigVersion currentConfigVersion
@ -174,29 +177,13 @@ function plugins (settings, deviceId) {
return dbm.machineEvent(event) return dbm.machineEvent(event)
} }
function cashOut (tx) { function newAddress (tx) {
const cryptoCode = tx.cryptoCode const cryptoCode = tx.cryptoCode
const info = {
const serialPromise = wallet.supportsHD label: 'TX ' + Date.now(),
? dbm.nextCashOutSerialHD(tx.id, cryptoCode) account: 'deposit'
: Promise.resolve() }
return wallet.newAddress(settings, cryptoCode, info)
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)
})
})
} }
function dispenseAck (tx) { function dispenseAck (tx) {
@ -486,7 +473,7 @@ function plugins (settings, deviceId) {
pollQueries, pollQueries,
trade, trade,
sendCoins, sendCoins,
cashOut, newAddress,
dispenseAck, dispenseAck,
getPhoneCode, getPhoneCode,
executeTrades, executeTrades,
@ -498,7 +485,8 @@ function plugins (settings, deviceId) {
sweepLiveHD, sweepLiveHD,
sweepOldHD, sweepOldHD,
sendMessage, sendMessage,
checkBalances checkBalances,
buildCartridges
} }
} }

View file

@ -71,12 +71,15 @@ function fetchPhoneTx (phone) {
} }
function fetchStatusTx (txId, status) { function fetchStatusTx (txId, status) {
console.log('DEBUG444')
const sql = 'select * from cash_out_txs where id=$1' const sql = 'select * from cash_out_txs where id=$1'
return db.oneOrNone(sql, [txId, status]) return db.oneOrNone(sql, [txId])
.then(toObj) .then(toObj)
.then(tx => { .then(tx => {
console.log('DEBUG445')
if (!tx) throw httpError('No transaction', 404) if (!tx) throw httpError('No transaction', 404)
console.log('DEBUG446')
if (tx.status === status) throw httpError('Not Modified', 304) if (tx.status === status) throw httpError('Not Modified', 304)
return tx return tx
}) })

View file

@ -36,7 +36,7 @@ function poll (req, res, next) {
pids[deviceId] = {pid, ts: Date.now()} pids[deviceId] = {pid, ts: Date.now()}
pi.pollQueries(deviceTime, req.query) return pi.pollQueries(deviceTime, req.query)
.then(results => { .then(results => {
const cartridges = results.cartridges const cartridges = results.cartridges
@ -73,15 +73,32 @@ function poll (req, res, next) {
response.idVerificationLimit = config.idVerificationLimit response.idVerificationLimit = config.idVerificationLimit
} }
console.log('DEBUG22: %j', response)
return res.json(response) return res.json(response)
}) })
.catch(next) .catch(next)
} }
function getTx (req, res, next) { function getTx (req, res, next) {
if (req.query.phone) return helpers.fetchPhoneTx(req.query.phone) console.log('DEBUG333: %s', req.query.status)
if (req.query.status) return helpers.fetchStatusTx(req.query.status) if (req.query.status) {
throw httpError('Not Found', 404) 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) { function postTx (req, res, next) {
@ -153,7 +170,7 @@ function phoneCode (req, res, next) {
} }
function errorHandler (err, req, res, next) { function errorHandler (err, req, res, next) {
const statusCode = err.name === 'HttpError' const statusCode = err.name === 'HTTPError'
? err.code || 500 ? err.code || 500
: 500 : 500
@ -244,7 +261,8 @@ app.post('/verify_transaction', verifyTx)
app.post('/phone_code', phoneCode) app.post('/phone_code', phoneCode)
app.post('/tx', postTx) app.post('/tx', postTx)
app.get('/tx', getTx) app.get('/tx/:id', getTx)
app.get('/tx', getPhoneTx)
app.use(errorHandler) app.use(errorHandler)
app.use((req, res) => res.status(404).json({err: 'No such route'})) app.use((req, res) => res.status(404).json({err: 'No such route'}))

View file

@ -1,10 +1,18 @@
const _ = require('lodash/fp')
const BN = require('./bn')
const CashInTx = require('./cash-in-tx') const CashInTx = require('./cash-in-tx')
const CashOutTx = require('./cash-out-tx')
function post (tx, pi) { function post (tx, pi) {
if (tx.direction === 'cashIn') return CashInTx.post(tx, pi) const mtx = massage(tx)
if (tx.direction === 'cashOut') throw new Error('not implemented') 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} module.exports = {post}

View file

@ -3,7 +3,8 @@ var db = require('./db')
exports.up = function (next) { exports.up = function (next) {
var sql = [ var sql = [
'alter table cash_in_txs add column send boolean not null default false', '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) db.multi(sql, next)
} }

View file

@ -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()
}