Merge branch 'v5' into v5

This commit is contained in:
Josh Harvey 2017-08-30 16:51:10 +03:00 committed by GitHub
commit d254e28e96
15 changed files with 2223 additions and 1468 deletions

View file

@ -150,6 +150,24 @@ app.get('/api/transactions', (req, res, next) => {
.catch(next) .catch(next)
}) })
app.get('/api/transaction/:id', (req, res, next) => {
return transactions.single(req.params.id)
.then(r => {
if (!r) return res.status(404).send({Error: 'Not found'})
return res.send(r)
})
})
app.patch('/api/transaction/:id', (req, res, next) => {
if (!req.query.cancel) return res.status(400).send({Error: 'Requires cancel'})
return transactions.cancel(req.params.id)
.then(r => {
return res.send(r)
})
.catch(() => res.status(404).send({Error: 'Not found'}))
})
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(err) console.error(err)
@ -223,7 +241,6 @@ function establishSocket (ws, token) {
if (!success) return ws.close(1008, 'Authentication error') if (!success) return ws.close(1008, 'Authentication error')
const listener = data => { const listener = data => {
console.log('DEBUG200: %j', data)
ws.send(JSON.stringify(data)) ws.send(JSON.stringify(data))
} }
@ -239,8 +256,6 @@ function establishSocket (ws, token) {
}, REAUTHENTICATE_INTERVAL) }, REAUTHENTICATE_INTERVAL)
socketEmitter.on('message', listener) socketEmitter.on('message', listener)
console.log('DEBUG120: %j', token)
ws.send('Testing123') ws.send('Testing123')
}) })
} }

View file

@ -2,6 +2,8 @@ const _ = require('lodash/fp')
const db = require('../db') const db = require('../db')
const machineLoader = require('../machine-loader') const machineLoader = require('../machine-loader')
const tx = require('../tx')
const cashInTx = require('../cash-in-tx')
const NUM_RESULTS = 20 const NUM_RESULTS = 20
@ -18,21 +20,49 @@ function addNames (txs) {
}) })
} }
const camelize = _.mapKeys(_.camelCase)
function batch () { function batch () {
const camelize = _.mapKeys(_.camelCase)
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']),
_.take(NUM_RESULTS), _.map(camelize), addNames) _.take(NUM_RESULTS), _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class, cash_in_txs.* const cashInSql = `select 'cashIn' as tx_class, cash_in_txs.*,
((not send_confirmed) and (created <= now() - interval $1)) as expired
from cash_in_txs from cash_in_txs
order by created desc limit $1` order by created desc limit $2`
const cashOutSql = `select 'cashOut' as tx_class, cash_out_txs.* const cashOutSql = `select 'cashOut' as tx_class, cash_out_txs.*
from cash_out_txs from cash_out_txs
order by created desc limit $1` order by created desc limit $1`
return Promise.all([db.any(cashInSql, [NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS])]) return Promise.all([db.any(cashInSql, [cashInTx.PENDING_INTERVAL, NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS])])
.then(packager) .then(packager)
} }
module.exports = {batch} function single (txId) {
const packager = _.flow(_.compact, _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class,
((not send_confirmed) and (created <= now() - interval $1)) as expired,
cash_in_txs.*
from cash_in_txs
where id=$2`
const cashOutSql = `select 'cashOut' as tx_class, cash_out_txs.*
from cash_out_txs
where id=$1`
return Promise.all([
db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId]),
db.oneOrNone(cashOutSql, [txId])
])
.then(packager)
.then(_.head)
}
function cancel (txId) {
return tx.cancel(txId)
.then(() => single(txId))
}
module.exports = {batch, single, cancel}

View file

@ -4,13 +4,15 @@ const db = require('./db')
const BN = require('./bn') const BN = require('./bn')
const plugins = require('./plugins') const plugins = require('./plugins')
const logger = require('./logger') const logger = require('./logger')
const pp = require('./pp') const T = require('./time')
const E = require('./error')
module.exports = {post, monitorPending} const PENDING_INTERVAL = '60 minutes'
const PENDING_INTERVAL_MS = 60 * T.minutes
const PENDING_INTERVAL = '1 day'
const MAX_PENDING = 10 const MAX_PENDING = 10
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL}
function atomic (machineTx, pi) { function atomic (machineTx, pi) {
const TransactionMode = pgp.txMode.TransactionMode const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel const isolationLevel = pgp.txMode.isolationLevel
@ -22,6 +24,8 @@ function atomic (machineTx, pi) {
return t.oneOrNone(sql, [machineTx.id]) return t.oneOrNone(sql, [machineTx.id])
.then(row => { .then(row => {
if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError('Stale tx')
return t.any(sql2, [machineTx.id]) return t.any(sql2, [machineTx.id])
.then(billRows => { .then(billRows => {
const dbTx = toObj(row) const dbTx = toObj(row)
@ -29,10 +33,8 @@ function atomic (machineTx, pi) {
return preProcess(dbTx, machineTx, pi) return preProcess(dbTx, machineTx, pi)
.then(preProcessedTx => upsert(dbTx, preProcessedTx)) .then(preProcessedTx => upsert(dbTx, preProcessedTx))
.then(r => { .then(r => {
pp('DEBUG701.5')(r)
return insertNewBills(billRows, machineTx) return insertNewBills(billRows, machineTx)
.then(newBills => _.set('newBills', newBills, r)) .then(newBills => _.set('newBills', newBills, r))
.then(pp('DEBUG702'))
}) })
}) })
}) })
@ -44,7 +46,6 @@ function atomic (machineTx, pi) {
} }
function post (machineTx, pi) { function post (machineTx, pi) {
console.log('DEBUG700: %j', machineTx)
return db.tx(atomic(machineTx, pi)) return db.tx(atomic(machineTx, pi))
.then(r => { .then(r => {
const updatedTx = r.tx const updatedTx = r.tx
@ -67,11 +68,11 @@ function isMonotonic (oldField, newField, fieldKey) {
if (oldField.isBigNumber) return oldField.lte(newField) if (oldField.isBigNumber) return oldField.lte(newField)
if (_.isNumber(oldField)) return oldField <= newField if (_.isNumber(oldField)) return oldField <= newField
throw new Error(`Unexpected value: ${oldField}`) throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
} }
function ensureRatchet (oldField, newField, fieldKey) { function ensureRatchet (oldField, newField, fieldKey) {
const monotonic = ['cryptoAtoms', 'fiat', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout'] const monotonic = ['cryptoAtoms', 'fiat', 'cashInFeeCrypto', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion']
const free = ['sendPending', 'error', 'errorCode', 'customerId'] const free = ['sendPending', 'error', 'errorCode', 'customerId']
if (_.isNil(oldField)) return true if (_.isNil(oldField)) return true
@ -121,16 +122,11 @@ function toObj (row) {
keys.forEach(key => { keys.forEach(key => {
const objKey = _.camelCase(key) const objKey = _.camelCase(key)
if (key === 'crypto_atoms' || key === 'fiat' || key === 'cash_in_fee') { if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'cash_in_fee_crypto'])) {
newObj[objKey] = BN(row[key]) newObj[objKey] = BN(row[key])
return return
} }
if (key === 'device_time') {
newObj[objKey] = parseInt(row[key], 10)
return
}
newObj[objKey] = row[key] newObj[objKey] = row[key]
}) })
@ -200,14 +196,13 @@ function update (tx, changes) {
} }
function registerTrades (pi, newBills) { function registerTrades (pi, newBills) {
console.log('DEBUG600: %j', newBills)
_.forEach(bill => pi.buy(bill), newBills) _.forEach(bill => pi.buy(bill), newBills)
} }
function logAction (rec, tx) { function logAction (rec, tx) {
const action = { const action = {
tx_id: tx.id, tx_id: tx.id,
action: rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError', action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
error: rec.error, error: rec.error,
error_code: rec.errorCode, error_code: rec.errorCode,
tx_hash: rec.txHash tx_hash: rec.txHash
@ -219,13 +214,22 @@ function logAction (rec, tx) {
.then(_.constant(rec)) .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) { function isClearToSend (oldTx, newTx) {
const now = Date.now()
return newTx.send && return newTx.send &&
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) (!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
(newTx.created > now - PENDING_INTERVAL_MS)
} }
function postProcess (r, pi) { function postProcess (r, pi) {
console.log('DEBUG701: %j', r)
registerTrades(pi, r.newBills) registerTrades(pi, r.newBills)
if (isClearToSend(r.dbTx, r.tx)) { if (isClearToSend(r.dbTx, r.tx)) {
@ -303,3 +307,22 @@ function monitorPending (settings) {
.then(rows => Promise.all(_.map(processPending, rows))) .then(rows => Promise.all(_.map(processPending, rows)))
.catch(logger.error) .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))
}

View file

@ -22,3 +22,4 @@ function register (errorName) {
register('BadNumberError') register('BadNumberError')
register('NoDataError') register('NoDataError')
register('InsufficientFundsError') register('InsufficientFundsError')
register('StaleTxError')

View file

@ -20,7 +20,6 @@ function fetchExchange (settings, cryptoCode) {
} }
function buy (settings, cryptoAtoms, fiatCode, cryptoCode) { function buy (settings, cryptoAtoms, fiatCode, cryptoCode) {
console.log('DEBUG600')
return fetchExchange(settings, cryptoCode) return fetchExchange(settings, cryptoCode)
.then(r => r.exchange.buy(r.account, cryptoAtoms, fiatCode, cryptoCode)) .then(r => r.exchange.buy(r.account, cryptoAtoms, fiatCode, cryptoCode))
} }

View file

@ -184,7 +184,7 @@ function plugins (settings, deviceId) {
cryptoCode, cryptoCode,
display: cryptoRec.display, display: cryptoRec.display,
minimumTx: BN.max(minimumTx, cashInFee), minimumTx: BN.max(minimumTx, cashInFee),
cashInFee: cashInFee, cashInFee,
cryptoNetwork cryptoNetwork
} }
} }
@ -216,8 +216,6 @@ function plugins (settings, deviceId) {
const testNets = arr.slice(2 * cryptoCodesCount + 3) const testNets = arr.slice(2 * cryptoCodesCount + 3)
const coinParams = _.zip(cryptoCodes, testNets) const coinParams = _.zip(cryptoCodes, testNets)
console.log('DEBUG200: %j', cryptoCodes)
console.log('DEBUG201: %j', coinParams)
return { return {
cassettes, cassettes,
rates: buildRates(tickers), rates: buildRates(tickers),
@ -351,7 +349,6 @@ function plugins (settings, deviceId) {
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
console.log('DEBUG505')
if (!exchange.active(settings, cryptoCode)) return if (!exchange.active(settings, cryptoCode)) return
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
@ -368,7 +365,6 @@ function plugins (settings, deviceId) {
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
const marketTradesQueues = tradesQueues[market] const marketTradesQueues = tradesQueues[market]
console.log('DEBUG504: %j', marketTradesQueues)
if (!marketTradesQueues || marketTradesQueues.length === 0) return null if (!marketTradesQueues || marketTradesQueues.length === 0) return null
logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length) logger.debug('[%s] tradesQueues size: %d', market, marketTradesQueues.length)
@ -409,7 +405,6 @@ function plugins (settings, deviceId) {
} }
function executeTrades () { function executeTrades () {
console.log('DEBUG500')
return machineLoader.getMachines() return machineLoader.getMachines()
.then(devices => { .then(devices => {
const deviceIds = devices.map(device => device.deviceId) const deviceIds = devices.map(device => device.deviceId)
@ -430,7 +425,6 @@ function plugins (settings, deviceId) {
} }
function executeTradesForMarket (settings, fiatCode, cryptoCode) { function executeTradesForMarket (settings, fiatCode, cryptoCode) {
console.log('DEBUG501: %s, %j', cryptoCode, exchange.active(settings, cryptoCode))
if (!exchange.active(settings, cryptoCode)) return if (!exchange.active(settings, cryptoCode)) return
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
@ -438,8 +432,6 @@ function plugins (settings, deviceId) {
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
console.log('DEBUG502')
return executeTradeForType(tradeEntry) return executeTradeForType(tradeEntry)
.catch(err => { .catch(err => {
tradesQueues[market].push(tradeEntry) tradesQueues[market].push(tradeEntry)
@ -457,8 +449,6 @@ function plugins (settings, deviceId) {
const tradeEntry = expand(_tradeEntry) const tradeEntry = expand(_tradeEntry)
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
console.log('DEBUG503')
return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode) return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => recordTrade(tradeEntry)) .then(() => recordTrade(tradeEntry))
} }

View file

@ -33,7 +33,6 @@ function trade (account, type, cryptoAtoms, fiatCode, cryptoCode) {
kraken.api('AddOrder', orderInfo, (error, response) => { kraken.api('AddOrder', orderInfo, (error, response) => {
if (error) return reject(error) if (error) return reject(error)
console.log('DEBUG900: %j', response)
return resolve() return resolve()
}) })
}) })

View file

@ -15,6 +15,7 @@ const plugins = require('./plugins')
const helpers = require('./route-helpers') const helpers = require('./route-helpers')
const poller = require('./poller') const poller = require('./poller')
const Tx = require('./tx') const Tx = require('./tx')
const E = require('./error')
const customers = require('./customers') const customers = require('./customers')
const argv = require('minimist')(process.argv.slice(2)) const argv = require('minimist')(process.argv.slice(2))
@ -109,6 +110,10 @@ function postTx (req, res, next) {
return res.json(tx) return res.json(tx)
}) })
.catch(err => {
if (err instanceof E.StaleTxError) return res.status(404).json({})
throw err
})
.catch(next) .catch(next)
} }
@ -289,7 +294,11 @@ app.get('/tx/:id', getTx)
app.get('/tx', getPhoneTx) app.get('/tx', getPhoneTx)
app.use(errorHandler) app.use(errorHandler)
app.use((req, res) => res.status(404).json({error: 'No such route'})) app.use((req, res) => {
console.log('DEBUG98')
console.log(req.route)
res.status(404).json({error: 'No such route'})
})
localApp.get('/pid', (req, res) => { localApp.get('/pid', (req, res) => {
const deviceId = req.query.device_id const deviceId = req.query.device_id

View file

@ -23,9 +23,13 @@ function massage (tx) {
const transformDates = r => mapValuesWithKey(transformDate, r) const transformDates = r => mapValuesWithKey(transformDate, r)
const mapBN = r => { const mapBN = r => {
const update = r.direction === 'cashIn' const update = {
? {cryptoAtoms: BN(r.cryptoAtoms), fiat: BN(r.fiat), cashInFee: BN(r.cashInFee)} cryptoAtoms: BN(r.cryptoAtoms),
: {cryptoAtoms: BN(r.cryptoAtoms), fiat: BN(r.fiat)} fiat: BN(r.fiat),
cashInFee: BN(r.cashInFee),
cashInFeeCrypto: BN(r.cashInFeeCrypto),
minimumTx: BN(r.minimumTx)
}
return _.assign(r, update) return _.assign(r, update)
} }
@ -35,4 +39,17 @@ function massage (tx) {
return mapper(tx) return mapper(tx)
} }
module.exports = {post} function cancel (txId) {
const promises = [
CashInTx.cancel(txId).then(() => true).catch(() => false),
CashOutTx.cancel(txId).then(() => true).catch(() => false)
]
return Promise.all(promises)
.then(r => {
if (_.some(r)) return
throw new Error('No such transaction')
})
}
module.exports = {post, cancel}

View file

@ -1,10 +1,9 @@
var db = require('./db') var db = require('./db')
var anonymous = require('../lib/constants').anonymousCustomer var anonymous = require('../lib/constants').anonymousCustomer
exports.up = function(next) { exports.up = function (next) {
const sql = const sql =
[ [`create table customers (
`create table customers (
id uuid PRIMARY KEY, id uuid PRIMARY KEY,
phone text unique, phone text unique,
phone_at timestamptz, phone_at timestamptz,
@ -23,12 +22,12 @@ exports.up = function(next) {
created timestamptz NOT NULL DEFAULT now() )`, created timestamptz NOT NULL DEFAULT now() )`,
`insert into customers (id, name) VALUES ( '${anonymous.uuid}','${anonymous.name}' )`, `insert into customers (id, name) VALUES ( '${anonymous.uuid}','${anonymous.name}' )`,
`alter table cash_in_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`, `alter table cash_in_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`,
`alter table cash_out_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`, `alter table cash_out_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`
] ]
db.multi(sql, next) db.multi(sql, next)
}; }
exports.down = function(next) { exports.down = function (next) {
next(); next()
}; }

View file

@ -1,19 +1,18 @@
var db = require('./db') var db = require('./db')
exports.up = function(next) { exports.up = function (next) {
const sql = const sql =
[`create type compliance_types as enum ('manual', 'sanctions', 'sanctions_override') `, [ "create type compliance_types as enum ('manual', 'sanctions', 'sanctions_override')",
`create table compliance_authorizations ( `create table compliance_authorizations (
id uuid PRIMARY KEY, id uuid PRIMARY KEY,
customer_id uuid REFERENCES customers (id), customer_id uuid REFERENCES customers (id),
compliance_type compliance_types NOT NULL, compliance_type compliance_types NOT NULL,
authorized_at timestamptz NOT NULL, authorized_at timestamptz NOT NULL,
authorized_by text REFERENCES user_tokens (token) )` ] authorized_by text REFERENCES user_tokens (token) )` ]
db.multi(sql, next) db.multi(sql, next)
}; }
exports.down = function(next) { exports.down = function (next) {
next(); next()
}; }

View file

@ -0,0 +1,14 @@
var db = require('./db')
exports.up = function (next) {
const sql = [
'alter table cash_in_txs drop column device_time',
'alter table cash_out_txs drop column device_time'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,14 @@
var db = require('./db')
exports.up = function (next) {
const sql = [
'alter table cash_in_txs add column tx_version integer not null',
'alter table cash_out_txs add column tx_version integer not null'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

File diff suppressed because it is too large Load diff

View file

@ -204,6 +204,12 @@ p {
background-color: #ffffff; background-color: #ffffff;
} }
.lamassuAdminTxTable a {
text-decoration: none;
color: #5f5f56;
border-bottom: 1px solid #37e8d7;
}
.lamassuAdminTxTable .lamassuAdminNumberColumn { .lamassuAdminTxTable .lamassuAdminNumberColumn {
text-align: right; text-align: right;
width: 10em; width: 10em;
@ -215,6 +221,10 @@ p {
font-size: 90%; font-size: 90%;
} }
.lamassuAdminTxTable .lamassuAdminTxCancelled {
background-color: #efd1d2;
}
.lamassuAdminTxTable tbody { .lamassuAdminTxTable tbody {
font-family: Inconsolata, monospace; font-family: Inconsolata, monospace;
color: #5f5f56; color: #5f5f56;