From b4d8f3cd4c07f3a3604b12b27099cfec708dc676 Mon Sep 17 00:00:00 2001 From: Josh Harvey Date: Wed, 8 Mar 2017 15:39:29 +0200 Subject: [PATCH] WIPP --- .vscode/settings.json | 4 +- lib/cash-out-tx.js | 130 ++++++++++++++++++++++++++++++++++++++++++ lib/route-helpers.js | 59 +++++++++++++++++-- lib/routes.js | 7 +++ 4 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 lib/cash-out-tx.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef42..48d232fe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,3 @@ -{} +{ + "vsicons.presets.angular": false +} diff --git a/lib/cash-out-tx.js b/lib/cash-out-tx.js new file mode 100644 index 00000000..f4e87c19 --- /dev/null +++ b/lib/cash-out-tx.js @@ -0,0 +1,130 @@ +const _ = require('lodash/fp') +const pgp = require('pg-promise')() +const db = require('./db') +const BN = require('./bn') + +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 UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem', + 'phone', 'error', 'confirmationTime'] + +function post (tx, 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_out_txs where id=$1' + + console.log('DEBUG888: %j', tx) + return t.oneOrNone(sql, [tx.id]) + .then(row => upsert(row, tx)) + } + + transaction.txMode = tmSRD + + return db.tx(transaction) + .then(txVector => { + const [, newTx] = txVector + return postProcess(txVector, pi) + .then(changes => update(newTx, changes)) + }) +} + +function nilEqual (a, b) { + if (_.isNil(a) && _.isNil(b)) return true + + return undefined +} + +function diff (oldTx, newTx) { + let updatedTx = {} + + UPDATEABLE_FIELDS.forEach(fieldKey => { + console.log('DEBUG80: %j', [oldTx[fieldKey], newTx[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 toObj (row) { + if (!row) return null + + const keys = _.keys(row) + let newObj = {} + + keys.forEach(key => { + const objKey = _.camelCase(key) + if (key === 'crypto_atoms' || key === 'fiat') { + newObj[objKey] = BN(row[key]) + return + } + + newObj[objKey] = row[key] + }) + + return newObj +} + +function upsert (row, tx) { + const oldTx = toObj(row) + + // insert bills + + if (!oldTx) { + return insert(tx) + .then(newTx => [oldTx, newTx]) + } + + return update(tx, diff(oldTx, tx)) + .then(newTx => [oldTx, newTx]) +} + +function insert (tx) { + const dbTx = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], 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 = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], changes)) + console.log('DEBUG893: %j', dbChanges) + const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') + + pgp.as.format(' where id=$1', [tx.id]) + ' returning *' + + return db.one(sql) + .then(toObj) +} + +function postProcess (txVector, pi) { + const [oldTx, newTx] = txVector + + return Promise.resolve({}) +} diff --git a/lib/route-helpers.js b/lib/route-helpers.js index f15c7951..6cec63e0 100644 --- a/lib/route-helpers.js +++ b/lib/route-helpers.js @@ -1,10 +1,21 @@ +const _ = require('lodash/fp') const R = require('ramda') const db = require('./db') const dbm = require('./postgresql_interface') const T = require('./time') +const BN = require('./bn') + const TRANSACTION_EXPIRATION = 2 * T.days +function httpError (msg, code) { + const err = new Error(msg) + err.name = 'HTTPError' + err.code = code || 500 + + return err +} + function stateChange (deviceId, deviceTime, rec) { const event = { id: rec.uuid, @@ -16,8 +27,34 @@ function stateChange (deviceId, deviceTime, rec) { return dbm.machineEvent(event) } +function toObj (row) { + if (!row) return null + + const keys = _.keys(row) + let newObj = {} + + keys.forEach(key => { + const objKey = _.camelCase(key) + if (key === 'crypto_atoms' || key === 'fiat') { + newObj[objKey] = BN(row[key]) + return + } + + newObj[objKey] = row[key] + }) + + return newObj +} + function fetchPhoneTx (phone) { - return dbm.fetchPhoneTxs(phone, TRANSACTION_EXPIRATION) + const sql = `select * from cash_out_txs + where phone=$1 and dispensed=$2 + and (extract(epoch from (coalesce(confirmation_time, now()) - created))) * 1000 < $3` + + const values = [phone, false, TRANSACTION_EXPIRATION] + + return db.any(sql, values) + .then(_.map(toObj)) .then(txs => { const confirmedTxs = txs.filter(tx => R.contains(tx.status, ['instant', 'confirmed'])) if (confirmedTxs.length > 0) { @@ -25,11 +62,23 @@ function fetchPhoneTx (phone) { return !acc || val.cryptoAtoms.gt(acc.cryptoAtoms) ? val : acc }, null, confirmedTxs) - return {tx: maxTx} + return maxTx } - if (txs.length > 0) return {pending: true} - return {} + if (txs.length > 0) throw httpError('Pending transactions', 412) + throw httpError('No transactions', 404) + }) +} + +function fetchStatusTx (txId, status) { + const sql = 'select * from cash_out_txs where id=$1' + + return db.oneOrNone(sql, [txId, status]) + .then(toObj) + .then(tx => { + if (!tx) throw httpError('No transaction', 404) + if (tx.status === status) throw httpError('Not Modified', 304) + return tx }) } @@ -37,4 +86,4 @@ function updateDeviceConfigVersion (versionId) { return db.none('update devices set user_config_id=$1', [versionId]) } -module.exports = {stateChange, fetchPhoneTx, updateDeviceConfigVersion} +module.exports = {stateChange, fetchPhoneTx, fetchStatusTx, updateDeviceConfigVersion} diff --git a/lib/routes.js b/lib/routes.js index 4eda34a6..39a704bd 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -78,6 +78,12 @@ function poll (req, res, next) { .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) +} + function postTx (req, res, next) { const pi = plugins(req.settings, req.deviceId) @@ -238,6 +244,7 @@ app.post('/verify_transaction', verifyTx) app.post('/phone_code', phoneCode) app.post('/tx', postTx) +app.get('/tx', getTx) app.use(errorHandler) app.use((req, res) => res.status(404).json({err: 'No such route'}))