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.direction = 'cashIn'
return newObj
}

View file

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

View file

@ -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
account: 'deposit'
}
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) {
@ -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
}
}

View file

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

View file

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

View file

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

View file

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

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