Lots of development

This commit is contained in:
Josh Harvey 2017-03-31 16:45:14 +03:00
parent 5cbec6bd23
commit 3a244f691e
19 changed files with 594 additions and 837 deletions

19
bin/lamassu-hd-address Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env node
const HKDF = require('node-hkdf-sync')
const wallet = require('lamassu-geth')
const pify = require('pify')
const fs = pify(require('fs'))
const options = require('../lib/options')
function computeSeed (masterSeed) {
const hkdf = new HKDF('sha256', 'lamassu-server-salt', masterSeed)
return hkdf.derive('wallet-seed', 32)
}
fs.readFile(options.seedPath, 'utf8')
.then(hex => {
const masterSeed = Buffer.from(hex.trim(), 'hex')
console.log(wallet.defaultAddress({seed: computeSeed(masterSeed)}))
})

View file

@ -244,6 +244,7 @@ function fetchData () {
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']}, {code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']}, {code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']},
{code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ['BTC', 'ETH']}, {code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ['BTC', 'ETH']},
{code: 'mock-exchange', display: 'Mock exchange', class: 'exchange', cryptos: ['BTC', 'ETH']},
{code: 'mock-sms', display: 'Mock SMS', class: 'sms'}, {code: 'mock-sms', display: 'Mock SMS', class: 'sms'},
{code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'}, {code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
{code: 'twilio', display: 'Twilio', class: 'sms'}, {code: 'twilio', display: 'Twilio', class: 'sms'},

View file

@ -22,15 +22,10 @@ function unpair (rec) {
return pairing.unpair(rec.deviceId) return pairing.unpair(rec.deviceId)
} }
function repair (rec) {
return pairing.repair(rec.deviceId)
}
function setMachine (rec) { function setMachine (rec) {
switch (rec.action) { switch (rec.action) {
case 'resetCashOutBills': return resetCashOutBills(rec) case 'resetCashOutBills': return resetCashOutBills(rec)
case 'unpair': return unpair(rec) case 'unpair': return unpair(rec)
case 'repair': return repair(rec)
default: throw new Error('No such action: ' + rec.action) default: throw new Error('No such action: ' + rec.action)
} }
} }

View file

@ -11,14 +11,7 @@ const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
const bsAlpha = baseX(ALPHA_BASE) const bsAlpha = baseX(ALPHA_BASE)
function unpair (deviceId) { function unpair (deviceId) {
const sql = 'update devices set paired=FALSE where device_id=$1' const sql = 'delete from devices where device_id=$1'
return db.none(sql, [deviceId])
}
function repair (deviceId) {
const sql = 'update devices set paired=TRUE where device_id=$1'
return db.none(sql, [deviceId]) return db.none(sql, [deviceId])
} }
@ -39,4 +32,4 @@ function totem (hostname, name) {
}) })
} }
module.exports = {totem, unpair, repair} module.exports = {totem, unpair}

View file

@ -2,6 +2,7 @@ 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 logger = require('./logger')
const mapValuesWithKey = _.mapValues.convert({cap: false}) const mapValuesWithKey = _.mapValues.convert({cap: false})
@ -25,7 +26,7 @@ function post (tx, pi) {
return upsert(row, tx) return upsert(row, tx)
.then(vector => { .then(vector => {
return insertNewBills(billRows, tx) return insertNewBills(billRows, tx)
.then(newBills => _.concat(vector, [billRows])) .then(newBills => _.concat(vector, [newBills]))
}) })
}) })
}) })
@ -51,7 +52,6 @@ function diff (oldTx, newTx) {
let updatedTx = {} let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => { UPDATEABLE_FIELDS.forEach(fieldKey => {
console.log('DEBUG80: %j', [oldTx[fieldKey], newTx[fieldKey]])
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return
// We never null out an existing field // We never null out an existing field
@ -139,15 +139,6 @@ function insert (tx) {
.then(toObj) .then(toObj)
} }
// const tx = JSON.parse('{"id":"677ec2b7-8e7a-4efc-99fc-1c1aa1b6a3a6","fiat":"1","cryptoAtoms":"73100","bills":[{"id":"afc6103f-b8bf-4ef3-aa28-6bd14f0c2633","fiat":"1","fiatCode":"USD","cryptoAtoms":"73100","cryptoCode":"BTC","deviceTime":1489642154270,"cashInTxsId":"677ec2b7-8e7a-4efc-99fc-1c1aa1b6a3a6"}],"fiatCode":"USD","cryptoCode":"BTC","direction":"cashIn","toAddress":"1MyRmwUVffy5QC5NEbdu9u1Lb9pZkwcNGg","deviceId":"F2:9C:7F:2C:59:F6:3C:EB:C5:A7:AE:4D:C0:59:32:70:0B:9D:3D:FE"}')
// insert(tx)
// .then(console.log)
// .catch(err => {
// console.log(err.stack)
// process.exit(1)
// })
function update (tx, changes) { function update (tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx) if (_.isEmpty(changes)) return Promise.resolve(tx)
@ -159,13 +150,21 @@ function update (tx, changes) {
.then(toObj) .then(toObj)
} }
function registerTrades (pi, txVector) {
console.log('DEBUG400')
const newBills = _.last(txVector)
console.log('DEBUG401: %j', newBills)
_.forEach(bill => pi.buy(bill), newBills)
}
function postProcess (txVector, pi) { function postProcess (txVector, pi) {
const [oldTx, newTx] = txVector const [oldTx, newTx] = txVector
registerTrades(pi, txVector)
if (newTx.send && !oldTx.send) { if (newTx.send && !oldTx.send) {
return pi.sendCoins(newTx) return pi.sendCoins(newTx)
.then(txHash => ({txHash})) .then(txHash => ({txHash}))
.catch(error => ({error}))
} }
return Promise.resolve({}) return Promise.resolve({})

View file

@ -1,15 +1,29 @@
const _ = require('lodash/fp') 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') const billMath = require('./bill-math')
const T = require('./time')
const logger = require('./logger')
const plugins = require('./plugins')
module.exports = {post} module.exports = {
post,
monitorLiveIncoming,
monitorStaleIncoming,
monitorUnnotified
}
const mapValuesWithKey = _.mapValues.convert({cap: false}) const mapValuesWithKey = _.mapValues.convert({cap: false})
const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem', const UPDATEABLE_FIELDS = ['txHash', 'status', 'dispensed', 'notified', 'redeem',
'phone', 'error', 'confirmationTime'] 'phone', 'error', 'confirmationTime', 'swept']
const STALE_INCOMING_TX_AGE = T.week
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
const MAX_NOTIFY_AGE = 2 * T.days
const MIN_NOTIFY_AGE = 5 * T.minutes
function post (tx, pi) { function post (tx, pi) {
const TransactionMode = pgp.txMode.TransactionMode const TransactionMode = pgp.txMode.TransactionMode
@ -47,7 +61,6 @@ function diff (oldTx, newTx) {
let updatedTx = {} let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => { UPDATEABLE_FIELDS.forEach(fieldKey => {
console.log('DEBUG80: %j', [oldTx[fieldKey], newTx[fieldKey]])
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return
// We never null out an existing field // We never null out an existing field
@ -122,8 +135,12 @@ function convertBigNumFields (obj) {
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj)) return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
} }
function convertField (key) {
return _.snakeCase(key)
}
function toDb (tx) { function toDb (tx) {
const massager = _.flow(convertBigNumFields, mapDispense, _.omit(['direction', 'bills']), _.mapKeys(_.snakeCase)) const massager = _.flow(convertBigNumFields, mapDispense, _.omit(['direction', 'bills']), _.mapKeys(convertField))
return massager(tx) return massager(tx)
} }
@ -148,17 +165,32 @@ function update (tx, changes) {
.then(() => newTx) .then(() => newTx)
} }
function nextHd (isHd, tx) {
console.log('DEBUG160: %s', isHd)
if (!isHd) return Promise.resolve(tx)
console.log('DEBUG161: %s', isHd)
return db.one("select nextval('hd_indices_seq') as hd_index")
.then(row => _.set('hdIndex', row.hd_index, tx))
}
function preProcess (tx, newTx, pi) { function preProcess (tx, newTx, pi) {
if (!tx) { if (!tx) {
return pi.newAddress(newTx) return pi.isHd(newTx)
.then(_.set('toAddress', _, newTx)) .then(isHd => nextHd(isHd, newTx))
.then(newTxHd => {
return pi.newAddress(newTxHd)
.then(_.set('toAddress', _, newTxHd))
})
} }
return Promise.resolve(newTx) return Promise.resolve(updateStatus(tx, newTx))
} }
function postProcess (txVector, pi) { function postProcess (txVector, pi) {
const [, newTx] = txVector const [oldTx, newTx] = txVector
if (!oldTx) pi.sell(newTx)
if (newTx.dispensed && !newTx.bills) { if (newTx.dispensed && !newTx.bills) {
return pi.buildCartridges() return pi.buildCartridges()
@ -169,3 +201,75 @@ function postProcess (txVector, pi) {
return Promise.resolve(newTx) return Promise.resolve(newTx)
} }
function updateStatus (oldTx, newTx) {
const tx = _.set('status', ratchetStatus(oldTx.status, newTx.status), newTx)
const isConfirmed = _.includes(tx.status, ['instant', 'confirmed'])
if (tx.status === oldTx.status || !isConfirmed) return tx
return _.set('confirmationTime', 'now()^', tx)
}
function ratchetStatus (oldStatus, newStatus) {
const statusOrder = ['notSeen', 'published', 'rejected',
'authorized', 'instant', 'confirmed']
if (oldStatus === newStatus) return oldStatus
if (newStatus === 'insufficientFunds') return newStatus
const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus))
return statusOrder[idx]
}
function fetchOpenTxs (statuses, age) {
const sql = `select *
from cash_out_txs
where ((extract(epoch from (now() - created))) * 1000)<$1
and status in ($2^)`
const statusClause = _.map(pgp.as.text, statuses).join(',')
return db.any(sql, [age, statusClause])
.then(rows => rows.map(toObj))
}
function processTxStatus (tx, settings) {
const pi = plugins(settings, tx.deviceId)
return pi.getStatus(tx)
.then(res => _.set('status', res.status, tx))
.then(_tx => post(_tx, pi))
}
function monitorLiveIncoming (settings) {
const statuses = ['notSeen', 'published', 'insufficientFunds']
return fetchOpenTxs(statuses, STALE_LIVE_INCOMING_TX_AGE)
.then(txs => Promise.all(txs.map(tx => processTxStatus(tx, settings))))
.catch(logger.error)
}
function monitorStaleIncoming (settings) {
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
return fetchOpenTxs(statuses, STALE_INCOMING_TX_AGE)
.then(txs => Promise.all(txs.map(tx => processTxStatus(tx, settings))))
.catch(logger.error)
}
function monitorUnnotified (settings) {
const sql = `select *
from cash_out_txs
where ((extract(epoch from (now() - created))) * 1000)<$1
and notified=$2 and dispensed=$3
and phone is not null
and status in ('instant', 'confirmed')
and (redeem=$4 or ((extract(epoch from (now() - created))) * 1000)>$5)`
const notify = tx => plugins(settings, tx.deviceId).notifyConfirmation(tx)
return db.any(sql, [MAX_NOTIFY_AGE, false, false, true, MIN_NOTIFY_AGE])
.then(rows => _.map(toObj, rows))
.then(txs => Promise.all(txs.map(notify)))
.catch(logger.error)
}

View file

@ -14,7 +14,7 @@ function lookupExchange (settings, cryptoCode) {
function fetchExchange (settings, cryptoCode) { function fetchExchange (settings, cryptoCode) {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
const plugin = lookupExchange(cryptoCode) const plugin = lookupExchange(settings, cryptoCode)
if (!plugin) throw noExchangeError(cryptoCode) if (!plugin) throw noExchangeError(cryptoCode)
const account = settings.accounts[plugin] const account = settings.accounts[plugin]
const exchange = require('lamassu-' + plugin) const exchange = require('lamassu-' + plugin)
@ -33,7 +33,7 @@ function sell (settings, cryptoAtoms, fiatCode, cryptoCode) {
.then(r => r.exchange.sell(r.account, cryptoAtoms, fiatCode, cryptoCode)) .then(r => r.exchange.sell(r.account, cryptoAtoms, fiatCode, cryptoCode))
} }
function active (settings, fiatCode, cryptoCode) { function active (settings, cryptoCode) {
return !!lookupExchange(settings, cryptoCode) return !!lookupExchange(settings, cryptoCode)
} }

View file

@ -2,6 +2,7 @@ const uuid = require('uuid')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const argv = require('minimist')(process.argv.slice(2)) const argv = require('minimist')(process.argv.slice(2))
const crypto = require('crypto') const crypto = require('crypto')
const pgp = require('pg-promise')()
const BN = require('./bn') const BN = require('./bn')
const dbm = require('./postgresql_interface') const dbm = require('./postgresql_interface')
@ -15,10 +16,8 @@ const exchange = require('./exchange')
const sms = require('./sms') const sms = require('./sms')
const email = require('./email') const email = require('./email')
const STALE_INCOMING_TX_AGE = T.week const mapValuesWithKey = _.mapValues.convert({cap: false})
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
const MAX_NOTIFY_AGE = 2 * T.days
const MIN_NOTIFY_AGE = 5 * T.minutes
const TRADE_TTL = 2 * T.minutes const TRADE_TTL = 2 * T.minutes
const STALE_TICKER = 3 * T.minutes const STALE_TICKER = 3 * T.minutes
const STALE_BALANCE = 3 * T.minutes const STALE_BALANCE = 3 * T.minutes
@ -42,13 +41,13 @@ function plugins (settings, deviceId) {
const cryptoConfig = configManager.scoped(cryptoCode, deviceId, settings.config) const cryptoConfig = configManager.scoped(cryptoCode, deviceId, settings.config)
const rateRec = tickers[i] const rateRec = tickers[i]
const cashInCommission = BN(1).minus(BN(cryptoConfig.cashInCommission).div(100)) const cashInCommission = BN(1).add(BN(cryptoConfig.cashInCommission).div(100))
const cashOutCommission = cashOut && BN(1).plus(BN(cryptoConfig.cashOutCommission).div(100)) const cashOutCommission = cashOut && BN(1).add(BN(cryptoConfig.cashOutCommission).div(100))
if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode) if (Date.now() - rateRec.timestamp > STALE_TICKER) return logger.warn('Stale rate for ' + cryptoCode)
const rate = rateRec.rates const rate = rateRec.rates
rates[cryptoCode] = { rates[cryptoCode] = {
cashIn: rate.ask.div(cashInCommission), cashIn: rate.ask.mul(cashInCommission),
cashOut: cashOut ? rate.bid.div(cashOutCommission) : undefined cashOut: cashOut ? rate.bid.div(cashOutCommission) : undefined
} }
}) })
@ -160,36 +159,10 @@ function plugins (settings, deviceId) {
}) })
} }
// NOTE: This will fail if we have already sent coins because there will be
// a unique dbm record in the table already.
function sendCoins (tx) { function sendCoins (tx) {
return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode) return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
} }
function trade (rawTrade) {
// TODO: move this to dbm, too
// add bill to trader queue (if trader is enabled)
const cryptoCode = rawTrade.cryptoCode
const fiatCode = rawTrade.fiatCode
const cryptoAtoms = rawTrade.cryptoAtoms
return dbm.recordBill(deviceId, rawTrade)
.then(() => {
const market = [fiatCode, cryptoCode].join('')
if (!exchange.active(settings, cryptoCode)) return
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
if (!tradesQueues[market]) tradesQueues[market] = []
tradesQueues[market].push({
fiatCode,
cryptoAtoms,
cryptoCode,
timestamp: Date.now()
})
})
}
function recordPing (deviceTime, rec) { function recordPing (deviceTime, rec) {
const event = { const event = {
id: uuid.v4(), id: uuid.v4(),
@ -201,13 +174,22 @@ function plugins (settings, deviceId) {
return dbm.machineEvent(event) return dbm.machineEvent(event)
} }
function isHd (tx) {
return wallet.isHd(settings, tx.cryptoCode)
}
function getStatus (tx) {
return wallet.getStatus(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
}
function newAddress (tx) { function newAddress (tx) {
const cryptoCode = tx.cryptoCode
const info = { const info = {
cryptoCode: tx.cryptoCode,
label: 'TX ' + Date.now(), label: 'TX ' + Date.now(),
account: 'deposit' account: 'deposit',
hdIndex: tx.hdIndex
} }
return wallet.newAddress(settings, cryptoCode, info) return wallet.newAddress(settings, info)
} }
function dispenseAck (tx) { function dispenseAck (tx) {
@ -244,11 +226,6 @@ function plugins (settings, deviceId) {
}) })
} }
function processTxStatus (tx) {
return wallet.getStatus(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
.then(res => dbm.updateTxStatus(tx, res.status))
}
function notifyConfirmation (tx) { function notifyConfirmation (tx) {
logger.debug('notifyConfirmation') logger.debug('notifyConfirmation')
@ -256,34 +233,17 @@ function plugins (settings, deviceId) {
const rec = { const rec = {
sms: { sms: {
toNumber: phone, toNumber: phone,
body: 'Your cash is waiting! Go to the Cryptomat and press Redeem.' body: 'Your cash is waiting! Go to the Cryptomat and press Redeem within 24 hours.'
} }
} }
return sms.sendMessage(settings, rec) return sms.sendMessage(settings, rec)
.then(() => dbm.updateNotify(tx)) .then(() => {
} const sql = 'update cash_out_txs set notified=$1 where id=$2'
const values = [true, tx.id]
function monitorLiveIncoming () { return db.none(sql, values)
const statuses = ['notSeen', 'published', 'insufficientFunds'] })
return dbm.fetchOpenTxs(statuses, STALE_LIVE_INCOMING_TX_AGE)
.then(txs => Promise.all(txs.map(processTxStatus)))
.catch(logger.error)
}
function monitorIncoming () {
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
return dbm.fetchOpenTxs(statuses, STALE_INCOMING_TX_AGE)
.then(txs => Promise.all(txs.map(processTxStatus)))
.catch(logger.error)
}
function monitorUnnotified () {
dbm.fetchUnnotifiedTxs(MAX_NOTIFY_AGE, MIN_NOTIFY_AGE)
.then(txs => Promise.all(txs.map(notifyConfirmation)))
.catch(logger.error)
} }
function pong () { function pong () {
@ -304,6 +264,35 @@ function plugins (settings, deviceId) {
* Trader functions * Trader functions
*/ */
function buy (rec) {
return buyAndSell(rec, true)
}
function sell (rec) {
return buyAndSell(rec, false)
}
function buyAndSell (rec, doBuy) {
const cryptoCode = rec.cryptoCode
const fiatCode = rec.fiatCode
const cryptoAtoms = doBuy ? rec.cryptoAtoms : rec.cryptoAtoms.neg()
const market = [fiatCode, cryptoCode].join('')
console.log('DEBUG333')
if (!exchange.active(settings, cryptoCode)) return
console.log('DEBUG334')
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
if (!tradesQueues[market]) tradesQueues[market] = []
tradesQueues[market].push({
fiatCode,
cryptoAtoms,
cryptoCode,
timestamp: Date.now()
})
}
function consolidateTrades (cryptoCode, fiatCode) { function consolidateTrades (cryptoCode, fiatCode) {
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
@ -371,27 +360,56 @@ function plugins (settings, deviceId) {
if (!exchange.active(settings, cryptoCode)) return if (!exchange.active(settings, cryptoCode)) return
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
logger.debug('[%s] checking for trades', market)
const tradeEntry = consolidateTrades(cryptoCode, fiatCode) const tradeEntry = consolidateTrades(cryptoCode, fiatCode)
if (tradeEntry === null) return logger.debug('[%s] no trades', market)
if (tradeEntry.cryptoAtoms.eq(0)) { if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
logger.debug('[%s] rejecting 0 trade', market)
return
}
logger.debug('[%s] making a trade: %d', market, tradeEntry.cryptoAtoms.toString()) return executeTradeForType(tradeEntry)
return exchange.buy(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => logger.debug('[%s] Successful trade.', market))
.catch(err => { .catch(err => {
tradesQueues[market].push(tradeEntry) tradesQueues[market].push(tradeEntry)
if (err.name === 'NoExchangeError') return logger.debug(err.message) if (err.name === 'NoExchangeError') return logger.debug(err.message)
if (err.name === 'orderTooSmall') return logger.debug(err.message)
logger.error(err) logger.error(err)
}) })
} }
function executeTradeForType (_tradeEntry) {
const expand = te => _.assign(te, {
cryptoAtoms: te.cryptoAtoms.abs(),
type: te.cryptoAtoms.gte(0) ? 'buy' : 'sell'
})
const tradeEntry = expand(_tradeEntry)
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => recordTrade(tradeEntry))
}
function convertBigNumFields (obj) {
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
? value.toString()
: value
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
? key + '#'
: key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
function recordTrade (_tradeEntry) {
const massage = _.flow(
_.pick(['cryptoCode', 'cryptoAtoms', 'fiatCode', 'type']),
convertBigNumFields,
_.mapKeys(_.snakeCase)
)
const tradeEntry = massage(_tradeEntry)
const sql = pgp.helpers.insert(tradeEntry, null, 'trades')
return db.none(sql)
}
function sendMessage (rec) { function sendMessage (rec) {
const config = configManager.unscoped(settings.config) const config = configManager.unscoped(settings.config)
@ -466,49 +484,51 @@ function plugins (settings, deviceId) {
.then(() => code) .then(() => code)
} }
function sweepHD (row) { function sweepHdRow (row) {
const cryptoCode = row.crypto_code const cryptoCode = row.crypto_code
return wallet.sweep(settings, row.hd_serial) console.log('DEBUG200')
return wallet.sweep(settings, cryptoCode, row.hd_index)
.then(txHash => { .then(txHash => {
if (txHash) { if (txHash) {
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash) logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
return dbm.markSwept(row.tx_id)
const sql = `update cash_out_txs set swept='t'
where id=$1`
return db.none(sql, row.id)
} }
}) })
.catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message)) .catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
} }
function sweepLiveHD () { function sweepHd () {
return dbm.fetchLiveHD() const sql = `select id, crypto_code, hd_index from cash_out_txs
.then(rows => Promise.all(rows.map(sweepHD))) where hd_index is not null and not swept and status in ('confirmed', 'instant')`
.catch(err => logger.error(err))
}
function sweepOldHD () { return db.any(sql)
return dbm.fetchOldHD() .then(rows => Promise.all(rows.map(sweepHdRow)))
.then(rows => Promise.all(rows.map(sweepHD)))
.catch(err => logger.error(err)) .catch(err => logger.error(err))
} }
return { return {
pollQueries, pollQueries,
trade,
sendCoins, sendCoins,
newAddress, newAddress,
isHd,
getStatus,
dispenseAck, dispenseAck,
getPhoneCode, getPhoneCode,
executeTrades, executeTrades,
pong, pong,
pongClear, pongClear,
monitorLiveIncoming, notifyConfirmation,
monitorIncoming, sweepHd,
monitorUnnotified,
sweepLiveHD,
sweepOldHD,
sendMessage, sendMessage,
checkBalances, checkBalances,
buildCartridges buildCartridges,
buy,
sell
} }
} }

View file

@ -2,12 +2,12 @@ const plugins = require('./plugins')
const notifier = require('./notifier') const notifier = require('./notifier')
const T = require('./time') const T = require('./time')
const logger = require('./logger') const logger = require('./logger')
const cashOutTx = require('./cash-out-tx')
const INCOMING_TX_INTERVAL = 30 * T.seconds const INCOMING_TX_INTERVAL = 30 * T.seconds
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
const UNNOTIFIED_INTERVAL = 10 * T.seconds const UNNOTIFIED_INTERVAL = 10 * T.seconds
const SWEEP_LIVE_HD_INTERVAL = T.minute const SWEEP_HD_INTERVAL = T.minute
const SWEEP_OLD_HD_INTERVAL = 2 * T.minutes
const TRADE_INTERVAL = 10 * T.seconds const TRADE_INTERVAL = 10 * T.seconds
const PONG_INTERVAL = 10 * T.seconds const PONG_INTERVAL = 10 * T.seconds
const PONG_CLEAR_INTERVAL = 1 * T.day const PONG_CLEAR_INTERVAL = 1 * T.day
@ -26,19 +26,17 @@ function start (settings) {
pi.executeTrades() pi.executeTrades()
pi.pong() pi.pong()
pi.pongClear() pi.pongClear()
pi.monitorLiveIncoming() cashOutTx.monitorLiveIncoming(settings)
pi.monitorIncoming() cashOutTx.monitorStaleIncoming(settings)
pi.monitorUnnotified() cashOutTx.monitorUnnotified(settings)
pi.sweepLiveHD() pi.sweepHd()
pi.sweepOldHD()
notifier.checkNotification(pi) notifier.checkNotification(pi)
setInterval(() => pi.executeTrades(), TRADE_INTERVAL) setInterval(() => pi.executeTrades(), TRADE_INTERVAL)
setInterval(() => pi.monitorLiveIncoming(), LIVE_INCOMING_TX_INTERVAL) setInterval(() => cashOutTx.monitorLiveIncoming(settings), LIVE_INCOMING_TX_INTERVAL)
setInterval(() => pi.monitorIncoming(), INCOMING_TX_INTERVAL) setInterval(() => cashOutTx.monitorStaleIncoming(settings), INCOMING_TX_INTERVAL)
setInterval(() => pi.monitorUnnotified(), UNNOTIFIED_INTERVAL) setInterval(() => cashOutTx.monitorUnnotified(settings), UNNOTIFIED_INTERVAL)
setInterval(() => pi.sweepLiveHD(), SWEEP_LIVE_HD_INTERVAL) setInterval(() => pi.sweepHd(), SWEEP_HD_INTERVAL)
setInterval(() => pi.sweepOldHD(), SWEEP_OLD_HD_INTERVAL)
setInterval(() => pi.pong(), PONG_INTERVAL) setInterval(() => pi.pong(), PONG_INTERVAL)
setInterval(() => pi.pongClear(), PONG_CLEAR_INTERVAL) setInterval(() => pi.pongClear(), PONG_CLEAR_INTERVAL)
setInterval(() => notifier.checkNotification(pi), CHECK_NOTIFICATION_INTERVAL) setInterval(() => notifier.checkNotification(pi), CHECK_NOTIFICATION_INTERVAL)

View file

@ -1,17 +1,4 @@
// @flow weak
'use strict'
const BigNumber = require('bignumber.js')
const db = require('./db') const db = require('./db')
const pgp = require('pg-promise')()
const logger = require('./logger')
const LIVE_SWEEP_TTL = 48 * 60 * 60 * 1000
function isUniqueViolation (err) {
return err.code === '23505'
}
function getInsertQuery (tableName, fields) { function getInsertQuery (tableName, fields) {
// outputs string like: '$1, $2, $3...' with proper No of items // outputs string like: '$1, $2, $3...' with proper No of items
@ -27,39 +14,6 @@ function getInsertQuery (tableName, fields) {
return query return query
} }
// logs inputted bill and overall tx status (if available)
exports.recordBill = function recordBill (deviceId, rec) {
const fields = [
'id',
'device_id',
'currency_code',
'crypto_code',
'to_address',
'cash_in_txs_id',
'device_time',
'crypto_atoms',
'denomination'
]
const values = [
rec.uuid,
deviceId,
rec.fiatCode,
rec.cryptoCode,
rec.toAddress,
rec.txId,
rec.deviceTime,
rec.cryptoAtoms.toString(),
rec.fiat
]
return db.none(getInsertQuery('bills', fields), values)
.catch(err => {
if (isUniqueViolation(err)) return logger.warn('Attempt to report bill twice')
throw err
})
}
exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) { exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
const sql = 'INSERT INTO device_events (device_id, event_type, ' + const sql = 'INSERT INTO device_events (device_id, event_type, ' +
'note, device_time) VALUES ($1, $2, $3, $4)' 'note, device_time) VALUES ($1, $2, $3, $4)'
@ -69,170 +23,6 @@ exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
return db.none(sql, values) return db.none(sql, values)
} }
// NOTE: This will fail if we have already sent coins because there will be
// a unique cash_in_txs record in the table already keyed by txId.
exports.addOutgoingTx = function addOutgoingTx (deviceId, tx) {
const fields = ['id', 'device_id', 'to_address',
'crypto_atoms', 'crypto_code', 'currency_code', 'fiat', 'tx_hash',
'fee', 'phone', 'error'
]
const values = [
tx.id,
deviceId,
tx.toAddress,
tx.cryptoAtoms.toString(),
tx.cryptoCode,
tx.fiatCode,
tx.fiat,
tx.txHash,
null,
tx.phone,
tx.error
]
return db.none(getInsertQuery('cash_in_txs', fields), values)
}
exports.sentCoins = function sentCoins (tx, toSend, fee, error, txHash) {
const sql = 'update cash_in_txs set tx_hash=$1, error=$2 where id=$3'
return db.none(sql, [txHash, error, tx.id])
}
exports.addInitialIncoming = function addInitialIncoming (deviceId, tx) {
const fields = ['id', 'device_id', 'to_address',
'crypto_atoms', 'crypto_code', 'currency_code', 'fiat', 'tx_hash',
'phone', 'error'
]
const values = [
tx.id,
deviceId,
tx.toAddress,
tx.cryptoAtoms.toString(),
tx.cryptoCode,
tx.fiatCode,
tx.fiat,
tx.txHash,
tx.phone,
tx.error
]
return db.none(getInsertQuery('cash_out_txs', fields), values)
}
function insertDispense (deviceId, tx, cartridges) {
const fields = [
'device_id', 'cash_out_txs_id',
'dispense1', 'reject1',
'dispense2', 'reject2', 'error'
]
const sql = getInsertQuery('dispenses', fields)
const dispense1 = tx.bills[0].actualDispense
const dispense2 = tx.bills[1].actualDispense
const reject1 = tx.bills[0].rejected
const reject2 = tx.bills[1].rejected
const values = [
deviceId, tx.id,
dispense1, reject1, dispense2, reject2,
false, tx.error
]
const sql2 = `update devices set cassette1=cassette1-$1, cassette2=cassette2-$2
where device_id=$3`
const pulled1 = dispense1 + reject1
const pulled2 = dispense2 + reject2
return db.none(sql, values)
.then(() => db.none(sql2, [pulled1, pulled2, deviceId]))
}
exports.addIncomingPhone = function addIncomingPhone (tx, notified) {
const sql = `UPDATE cash_out_txs SET phone=$1, notified=$2
WHERE id=$3
AND phone IS NULL`
const values = [tx.phone, notified, tx.id]
return db.result(sql, values)
.then(results => {
const noPhone = results.rowCount === 0
const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
if (noPhone) return {noPhone: noPhone}
return db.none(sql2, [tx.id, 'addedPhone'])
.then(() => ({noPhone: noPhone}))
})
}
function normalizeTx (tx) {
tx.toAddress = tx.to_address
tx.fiatCode = tx.currency_code
tx.txHash = tx.tx_hash
tx.cryptoCode = tx.crypto_code
tx.cryptoAtoms = new BigNumber(tx.crypto_atoms)
tx.to_address = undefined
tx.currency_code = undefined
tx.tx_hash = undefined
tx.crypto_code = undefined
// Eventually turn this into BigDecimal, for now, integer
tx.fiat = parseInt(tx.fiat, 10)
return tx
}
function normalizeTxs (txs) {
return txs.map(normalizeTx)
}
exports.fetchPhoneTxs = function fetchPhoneTxs (phone, dispenseTimeout) {
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, dispenseTimeout]
return db.any(sql, values)
.then(rows => normalizeTxs(rows))
}
exports.fetchTx = function fetchTx (txId) {
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1'
return db.one(sql, [txId])
.then(row => normalizeTx(row))
}
exports.addDispenseRequest = function addDispenseRequest (tx) {
const sql = 'update cash_out_txs set dispensed=$1 where id=$2 and dispensed=$3'
const values = [true, tx.id, false]
return db.result(sql, values)
.then(results => {
const alreadyDispensed = results.rowCount === 0
if (alreadyDispensed) return {dispense: false, reason: 'alreadyDispensed'}
const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
return db.none(sql2, [tx.id, 'dispenseRequested'])
.then(() => ({dispense: true, txId: tx.id}))
})
}
exports.addDispense = function addDispense (deviceId, tx, cartridges) {
return insertDispense(deviceId, tx, cartridges)
.then(() => {
const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
return db.none(sql2, [tx.id, 'dispensed'])
})
}
exports.cartridgeCounts = function cartridgeCounts (deviceId) { exports.cartridgeCounts = function cartridgeCounts (deviceId) {
const sql = 'SELECT cassette1, cassette2 FROM devices ' + const sql = 'SELECT cassette1, cassette2 FROM devices ' +
'WHERE device_id=$1' 'WHERE device_id=$1'
@ -271,165 +61,3 @@ exports.machineEvents = function machineEvents () {
return db.any(sql, []) return db.any(sql, [])
} }
function singleQuotify (item) { return '\'' + item + '\'' }
exports.fetchOpenTxs = function fetchOpenTxs (statuses, age) {
const _statuses = '(' + statuses.map(singleQuotify).join(',') + ')'
const sql = 'SELECT * ' +
'FROM cash_out_txs ' +
'WHERE ((EXTRACT(EPOCH FROM (now() - created))) * 1000)<$1 ' +
'AND status IN ' + _statuses
return db.any(sql, [age])
.then(rows => normalizeTxs(rows))
}
exports.fetchUnnotifiedTxs = function fetchUnnotifiedTxs (age, waitPeriod) {
const sql = `SELECT *
FROM cash_out_txs
WHERE ((EXTRACT(EPOCH FROM (now() - created))) * 1000)<$1
AND notified=$2 AND dispensed=$3
AND phone IS NOT NULL
AND status IN ('instant', 'confirmed')
AND (redeem=$4 OR ((EXTRACT(EPOCH FROM (now() - created))) * 1000)>$5)`
return db.any(sql, [age, false, false, true, waitPeriod])
.then(rows => normalizeTxs(rows))
}
function ratchetStatus (oldStatus, newStatus) {
const statusOrder = ['notSeen', 'published', 'rejected',
'authorized', 'instant', 'confirmed']
if (oldStatus === newStatus) return oldStatus
if (newStatus === 'insufficientFunds') return newStatus
const idx = Math.max(statusOrder.indexOf(oldStatus), statusOrder.indexOf(newStatus))
return statusOrder[idx]
}
exports.updateTxStatus = function updateTxStatus (tx, status) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select status, confirmation_time from cash_out_txs where id=$1'
return t.one(sql, [tx.id])
.then(row => {
const newStatus = ratchetStatus(row.status, status)
if (row.status === newStatus) return
const setConfirmationTime = !row.confirmation_time &&
(newStatus === 'instant' || newStatus === 'confirmed')
const sql2 = setConfirmationTime
? 'UPDATE cash_out_txs SET status=$1, confirmation_time=now() WHERE id=$2'
: 'UPDATE cash_out_txs SET status=$1 WHERE id=$2'
const values2 = [newStatus, tx.id]
return t.none(sql2, values2)
.then(() => ({status: newStatus}))
})
}
transaction.txMode = tmSRD
// Note: don't worry about retrying failed transaction here
// It will be tried again on the next status check
return db.tx(transaction)
.then(r => {
if (!r) return
const sql3 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
return db.none(sql3, [tx.id, r.status])
.then(() => {
if (r.status === 'confirmed') {
const sql4 = 'update cash_out_hds set confirmed=true where id=$1'
return db.none(sql4, [tx.id])
}
})
})
}
exports.registerRedeem = function registerRedeem (txId) {
const sql = 'UPDATE cash_out_txs SET redeem=$1 WHERE id=$2'
const values = [true, txId]
return db.none(sql, values)
.then(() => {
const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
return db.none(sql2, [txId, 'redeem'])
})
}
exports.updateNotify = function updateNotify (tx) {
const sql = 'UPDATE cash_out_txs SET notified=$1 WHERE id=$2'
const values = [true, tx.id]
return db.none(sql, values)
.then(() => {
const sql2 = 'insert into cash_out_actions (cash_out_txs_id, action) values ($1, $2)'
return db.none(sql2, [tx.id, 'notified'])
})
}
exports.cacheResponse = function (deviceId, txId, path, method, body) {
const sql = `update cached_responses
set body=$1
where device_id=$2
and tx_id=$3
and path=$4
and method=$5`
const values = [body, deviceId, txId, path, method]
return db.none(sql, values)
}
exports.nextCashOutSerialHD = function nextCashOutSerialHD (txId, cryptoCode) {
const sql = `select hd_serial from cash_out_hds
where crypto_code=$1 order by hd_serial desc limit 1`
const attempt = () => db.oneOrNone(sql, [cryptoCode])
.then(row => {
const serialNumber = row ? row.hd_serial + 1 : 0
const fields2 = ['id', 'crypto_code', 'hd_serial']
const sql2 = getInsertQuery('cash_out_hds', fields2)
const values2 = [txId, cryptoCode, serialNumber]
return db.none(sql2, values2)
.then(() => serialNumber)
})
// TODO: retry on failure
return attempt()
}
exports.fetchLiveHD = function fetchLiveHD () {
const sql = `select * from cash_out_txs, cash_out_hds
where cash_out_txs.id=cash_out_hds.id
and status=$1 and swept=$2 and
((extract(epoch from (now() - cash_out_txs.created))) * 1000)<$3`
const values = ['confirmed', false, LIVE_SWEEP_TTL]
return db.any(sql, values)
}
exports.fetchOldHD = function fetchLiveHD () {
const sql = `select * from cash_out_hds
where confirmed
order by last_checked
limit 10`
return db.any(sql)
}
exports.markSwept = function markSwept (txId) {
const sql = 'update cash_out_hds set swept=$1 where id=$2'
return db.none(sql, [true, txId])
}

View file

@ -73,7 +73,6 @@ 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)
@ -225,9 +224,7 @@ function authorize (req, res, next) {
.catch(next) .catch(next)
} }
const skip = options.logLevel === 'debug' const skip = (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200
? () => false
: (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200
const configRequiredRoutes = [ const configRequiredRoutes = [
'/poll', '/poll',
@ -309,7 +306,8 @@ function populateDeviceId (req, res, next) {
function populateSettings (req, res, next) { function populateSettings (req, res, next) {
const versionId = req.headers['config-version'] const versionId = req.headers['config-version']
logger.debug('versionId: %s', versionId)
console.log('DEBUG300: %s', versionId)
if (!versionId) { if (!versionId) {
return settingsLoader.loadLatest() return settingsLoader.loadLatest()

View file

@ -1,27 +1,38 @@
const _ = require('lodash/fp')
const mem = require('mem') const mem = require('mem')
const HKDF = require('node-hkdf-sync')
const configManager = require('./config-manager') const configManager = require('./config-manager')
const pify = require('pify')
const fs = pify(require('fs'))
const options = require('./options')
const FETCH_INTERVAL = 5000 const FETCH_INTERVAL = 5000
const INSUFFICIENT_FUNDS_CODE = 570
const INSUFFICIENT_FUNDS_NAME = 'InsufficientFunds'
function httpError (msg, code) {
const err = new Error(msg)
err.name = 'HTTPError'
err.code = code || 500
return err
}
function computeSeed (masterSeed) {
const hkdf = new HKDF('sha256', 'lamassu-server-salt', masterSeed)
return hkdf.derive('wallet-seed', 32)
}
function fetchWallet (settings, cryptoCode) { function fetchWallet (settings, cryptoCode) {
return Promise.resolve() return fs.readFile(options.seedPath, 'utf8')
.then(() => { .then(hex => {
console.log('DEBUG44') const masterSeed = Buffer.from(hex.trim(), 'hex')
console.log('DEBUG44.0.0: %j', cryptoCode)
try {
console.log('DEBUG44.0: %j', configManager.cryptoScoped(cryptoCode, settings.config).wallet)
} catch (err) {
console.log('DEBUG44.0.e: %s', err.stack)
}
const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet
console.log('DEBUG44.1')
const account = settings.accounts[plugin] const account = settings.accounts[plugin]
console.log('DEBUG44.2')
const wallet = require('lamassu-' + plugin) const wallet = require('lamassu-' + plugin)
console.log('DEBUG45: %j', {wallet, account}) return {wallet, account: _.set('seed', computeSeed(masterSeed), account)}
return {wallet, account}
}) })
} }
@ -44,11 +55,18 @@ function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) {
return res return res
}) })
}) })
.catch(err => {
if (err.name === INSUFFICIENT_FUNDS_NAME) {
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
}
throw err
})
} }
function newAddress (settings, cryptoCode, info) { function newAddress (settings, info) {
return fetchWallet(settings, cryptoCode) return fetchWallet(settings, info.cryptoCode)
.then(r => r.wallet.newAddress(r.account, cryptoCode, info)) .then(r => r.wallet.newAddress(r.account, info))
} }
function getStatus (settings, toAddress, cryptoAtoms, cryptoCode) { function getStatus (settings, toAddress, cryptoAtoms, cryptoCode) {
@ -56,9 +74,21 @@ function getStatus (settings, toAddress, cryptoAtoms, cryptoCode) {
.then(r => r.wallet.getStatus(r.account, toAddress, cryptoAtoms, cryptoCode)) .then(r => r.wallet.getStatus(r.account, toAddress, cryptoAtoms, cryptoCode))
} }
function sweep (settings, cryptoCode, hdIndex) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.sweep(r.account, cryptoCode, hdIndex))
}
function isHd (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.supportsHd)
}
module.exports = { module.exports = {
balance: mem(balance, {maxAge: FETCH_INTERVAL}), balance: mem(balance, {maxAge: FETCH_INTERVAL}),
sendCoins, sendCoins,
newAddress, newAddress,
getStatus getStatus,
sweep,
isHd
} }

View file

@ -0,0 +1,23 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
'create sequence hd_indices_seq minvalue 0 maxvalue 2147483647',
'alter table cash_out_txs add column hd_index integer',
'alter sequence hd_indices_seq owned by cash_out_txs.hd_index',
"alter table cash_out_txs add column swept boolean not null default 'f'",
'alter table cash_out_txs drop column tx_hash',
'create unique index on cash_out_txs (hd_index)',
'drop table cash_out_hds',
'drop table cash_out_actions',
'drop table transactions',
'drop table idempotents',
'drop table machine_configs',
'drop table pending_transactions'
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,20 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
"create type trade_type as enum ('buy', 'sell')",
`create table trades (
id serial PRIMARY KEY,
type trade_type not null,
crypto_code text not null,
crypto_atoms bigint not null,
fiat_code text not null,
created timestamptz NOT NULL default now()
)`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -33,8 +33,8 @@
"node-hkdf-sync": "^1.0.0", "node-hkdf-sync": "^1.0.0",
"numeral": "^2.0.3", "numeral": "^2.0.3",
"pg": "^6.1.2", "pg": "^6.1.2",
"pg-native": "^1.10.0", "pg-native": "latest",
"pg-promise": "^5.5.0", "pg-promise": "^5.6.4",
"pify": "^2.3.0", "pify": "^2.3.0",
"pretty-ms": "^2.1.0", "pretty-ms": "^2.1.0",
"ramda": "^0.22.1", "ramda": "^0.22.1",

View file

@ -26920,6 +26920,8 @@ var _user$project$Css_Admin$className = function ($class) {
return A2(_rtfeldman$elm_css_util$Css_Helpers$identifierToString, _user$project$Css_Admin$name, $class); return A2(_rtfeldman$elm_css_util$Css_Helpers$identifierToString, _user$project$Css_Admin$name, $class);
}; };
var _user$project$Css_Classes$CashIn = {ctor: 'CashIn'};
var _user$project$Css_Classes$CashOut = {ctor: 'CashOut'};
var _user$project$Css_Classes$QrCode = {ctor: 'QrCode'}; var _user$project$Css_Classes$QrCode = {ctor: 'QrCode'};
var _user$project$Css_Classes$TxAddress = {ctor: 'TxAddress'}; var _user$project$Css_Classes$TxAddress = {ctor: 'TxAddress'};
var _user$project$Css_Classes$TxPhone = {ctor: 'TxPhone'}; var _user$project$Css_Classes$TxPhone = {ctor: 'TxPhone'};
@ -32711,9 +32713,7 @@ var _user$project$TransactionTypes$CashOutTxRec = function (a) {
return function (m) { return function (m) {
return function (n) { return function (n) {
return function (o) { return function (o) {
return function (p) { return {id: a, machineName: b, toAddress: c, cryptoAtoms: d, cryptoCode: e, fiat: f, fiatCode: g, status: h, dispensed: i, notified: j, redeemed: k, phone: l, error: m, created: n, confirmed: o};
return {id: a, machineName: b, toAddress: c, cryptoAtoms: d, cryptoCode: e, fiat: f, fiatCode: g, txHash: h, status: i, dispensed: j, notified: k, redeemed: l, phone: m, error: n, created: o, confirmed: p};
};
}; };
}; };
}; };
@ -32841,37 +32841,33 @@ var _user$project$TransactionDecoder$cashOutTxDecoder = A3(
_elm_lang$core$Json_Decode$string, _elm_lang$core$Json_Decode$string,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'txHash', 'fiatCode',
_elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string), _elm_lang$core$Json_Decode$string,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'fiatCode', 'fiat',
_elm_lang$core$Json_Decode$string, _user$project$TransactionDecoder$floatString,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'fiat', 'cryptoCode',
_user$project$TransactionDecoder$floatString, _elm_lang$core$Json_Decode$string,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'cryptoCode', 'cryptoAtoms',
_elm_lang$core$Json_Decode$string, _user$project$TransactionDecoder$intString,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'cryptoAtoms', 'toAddress',
_user$project$TransactionDecoder$intString, _elm_lang$core$Json_Decode$string,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'toAddress', 'machineName',
_elm_lang$core$Json_Decode$string, _elm_lang$core$Json_Decode$string,
A3( A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required, _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'machineName', 'id',
_elm_lang$core$Json_Decode$string, _elm_lang$core$Json_Decode$string,
A3( _NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$TransactionTypes$CashOutTxRec))))))))))))))));
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'id',
_elm_lang$core$Json_Decode$string,
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$TransactionTypes$CashOutTxRec)))))))))))))))));
var _user$project$TransactionDecoder$txDecode = function (txClass) { var _user$project$TransactionDecoder$txDecode = function (txClass) {
var _p3 = txClass; var _p3 = txClass;
switch (_p3) { switch (_p3) {
@ -32893,95 +32889,104 @@ var _user$project$TransactionDecoder$txsDecoder = A2(
'transactions', 'transactions',
_elm_lang$core$Json_Decode$list(_user$project$TransactionDecoder$txDecoder)); _elm_lang$core$Json_Decode$list(_user$project$TransactionDecoder$txDecoder));
var _user$project$Transaction$multiplier = function (code) {
var _p0 = code;
switch (_p0) {
case 'BTC':
return 1.0e8;
case 'ETH':
return 1.0e18;
default:
return 1.0;
}
};
var _user$project$Transaction$rowView = function (tx) { var _user$project$Transaction$rowView = function (tx) {
var _p0 = tx; var _p1 = tx;
if (_p0.ctor === 'CashInTx') { if (_p1.ctor === 'CashInTx') {
var _p1 = _p0._0; var _p2 = _p1._0;
return A2( return A2(
_elm_lang$html$Html$tr, _elm_lang$html$Html$tr,
{ctor: '[]'}, {
ctor: '::',
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$CashIn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{ {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text('Cash in'),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'yyyy-MM-dd HH:mm', _p1.created)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p1.machineName), _0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'yyyy-MM-dd HH:mm', _p2.created)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text(_p2.machineName),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(
_ggb$numeral_elm$Numeral$format,
'0,0.000000',
_elm_lang$core$Basics$negate(
_elm_lang$core$Basics$toFloat(_p1.cryptoAtoms)) / 1.0e8)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p1.cryptoCode), _0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(
_ggb$numeral_elm$Numeral$format,
'0,0.000000',
_elm_lang$core$Basics$toFloat(_p2.cryptoAtoms) / _user$project$Transaction$multiplier(_p2.cryptoCode))),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text(_p2.cryptoCode),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(_ggb$numeral_elm$Numeral$format, '0,0.00', _p1.fiat)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33001,7 +33006,7 @@ var _user$project$Transaction$rowView = function (tx) {
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text( _0: _elm_lang$html$Html$text(
A2(_elm_lang$core$Maybe$withDefault, '', _p1.phone)), A2(_ggb$numeral_elm$Numeral$format, '0,0.00', _p2.fiat)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33013,17 +33018,38 @@ var _user$project$Transaction$rowView = function (tx) {
_0: _user$project$Css_Admin$class( _0: _user$project$Css_Admin$class(
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Classes$TxAddress, _0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p1.toAddress), _0: _elm_lang$html$Html$text(
A2(_elm_lang$core$Maybe$withDefault, '', _p2.phone)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {
ctor: '::',
_0: A2(
_elm_lang$html$Html$td,
{
ctor: '::',
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$TxAddress,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(_p2.toAddress),
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
}
} }
} }
} }
@ -33032,98 +33058,94 @@ var _user$project$Transaction$rowView = function (tx) {
} }
}); });
} else { } else {
var _p2 = _p0._0; var _p3 = _p1._0;
return A2( return A2(
_elm_lang$html$Html$tr, _elm_lang$html$Html$tr,
{ctor: '[]'}, {
ctor: '::',
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$CashOut,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{ {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text('Cash out'),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {
ctor: '::',
_0: _user$project$Css_Classes$DateColumn,
_1: {ctor: '[]'}
}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'yyyy-MM-dd HH:mm', _p2.created)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p2.machineName), _0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {
ctor: '::',
_0: _user$project$Css_Classes$DateColumn,
_1: {ctor: '[]'}
}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(_justinmimbs$elm_date_extra$Date_Extra$toFormattedString, 'yyyy-MM-dd HH:mm', _p3.created)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text(_p3.machineName),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(
_ggb$numeral_elm$Numeral$format,
'0,0.000000',
_elm_lang$core$Basics$toFloat(_p2.cryptoAtoms) / 1.0e8)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p2.cryptoCode), _0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(
_ggb$numeral_elm$Numeral$format,
'0,0.000000',
_elm_lang$core$Basics$toFloat(_p3.cryptoAtoms) / _user$project$Transaction$multiplier(_p3.cryptoCode))),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html$text(_p3.cryptoCode),
{
ctor: '::',
_0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(
A2(
_ggb$numeral_elm$Numeral$format,
'0,0.00',
_elm_lang$core$Basics$negate(_p2.fiat))),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33143,7 +33165,7 @@ var _user$project$Transaction$rowView = function (tx) {
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text( _0: _elm_lang$html$Html$text(
A2(_elm_lang$core$Maybe$withDefault, '', _p2.phone)), A2(_ggb$numeral_elm$Numeral$format, '0,0.00', _p3.fiat)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33155,17 +33177,38 @@ var _user$project$Transaction$rowView = function (tx) {
_0: _user$project$Css_Admin$class( _0: _user$project$Css_Admin$class(
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Classes$TxAddress, _0: _user$project$Css_Classes$NumberColumn,
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text(_p2.toAddress), _0: _elm_lang$html$Html$text(
A2(_elm_lang$core$Maybe$withDefault, '', _p3.phone)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {
ctor: '::',
_0: A2(
_elm_lang$html$Html$td,
{
ctor: '::',
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$TxAddress,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text(_p3.toAddress),
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
}
} }
} }
} }
@ -33209,21 +33252,8 @@ var _user$project$Transaction$tableView = function (txs) {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ {ctor: '[]'},
ctor: '::', {ctor: '[]'}),
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$TxDate,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text('Time'),
_1: {ctor: '[]'}
}),
_1: { _1: {
ctor: '::', ctor: '::',
_0: A2( _0: A2(
@ -33233,14 +33263,14 @@ var _user$project$Transaction$tableView = function (txs) {
_0: _user$project$Css_Admin$class( _0: _user$project$Css_Admin$class(
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Classes$TxMachine, _0: _user$project$Css_Classes$TxDate,
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text('Machine'), _0: _elm_lang$html$Html$text('Time'),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33249,12 +33279,17 @@ var _user$project$Transaction$tableView = function (txs) {
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html_Attributes$colspan(2), _0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$TxMachine,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text('Crypto'), _0: _elm_lang$html$Html$text('Machine'),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33263,17 +33298,12 @@ var _user$project$Transaction$tableView = function (txs) {
_elm_lang$html$Html$td, _elm_lang$html$Html$td,
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Admin$class( _0: _elm_lang$html$Html_Attributes$colspan(2),
{
ctor: '::',
_0: _user$project$Css_Classes$TxAmount,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text('Fiat'), _0: _elm_lang$html$Html$text('Crypto'),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33285,14 +33315,14 @@ var _user$project$Transaction$tableView = function (txs) {
_0: _user$project$Css_Admin$class( _0: _user$project$Css_Admin$class(
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Classes$TxPhone, _0: _user$project$Css_Classes$TxAmount,
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text('Phone'), _0: _elm_lang$html$Html$text('Fiat'),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: { _1: {
@ -33304,17 +33334,37 @@ var _user$project$Transaction$tableView = function (txs) {
_0: _user$project$Css_Admin$class( _0: _user$project$Css_Admin$class(
{ {
ctor: '::', ctor: '::',
_0: _user$project$Css_Classes$TxAddress, _0: _user$project$Css_Classes$TxPhone,
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}, },
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text('To address'), _0: _elm_lang$html$Html$text('Phone'),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}), }),
_1: {ctor: '[]'} _1: {
ctor: '::',
_0: A2(
_elm_lang$html$Html$td,
{
ctor: '::',
_0: _user$project$Css_Admin$class(
{
ctor: '::',
_0: _user$project$Css_Classes$TxAddress,
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
},
{
ctor: '::',
_0: _elm_lang$html$Html$text('To address'),
_1: {ctor: '[]'}
}),
_1: {ctor: '[]'}
}
} }
} }
} }
@ -33334,8 +33384,8 @@ var _user$project$Transaction$tableView = function (txs) {
}); });
}; };
var _user$project$Transaction$view = function (model) { var _user$project$Transaction$view = function (model) {
var _p3 = model; var _p4 = model;
switch (_p3.ctor) { switch (_p4.ctor) {
case 'NotAsked': case 'NotAsked':
return A2( return A2(
_elm_lang$html$Html$div, _elm_lang$html$Html$div,
@ -33357,7 +33407,7 @@ var _user$project$Transaction$view = function (model) {
{ {
ctor: '::', ctor: '::',
_0: _elm_lang$html$Html$text( _0: _elm_lang$html$Html$text(
_elm_lang$core$Basics$toString(_p3._0)), _elm_lang$core$Basics$toString(_p4._0)),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}); });
default: default:
@ -33366,17 +33416,17 @@ var _user$project$Transaction$view = function (model) {
{ctor: '[]'}, {ctor: '[]'},
{ {
ctor: '::', ctor: '::',
_0: _user$project$Transaction$tableView(_p3._0), _0: _user$project$Transaction$tableView(_p4._0),
_1: {ctor: '[]'} _1: {ctor: '[]'}
}); });
} }
}; };
var _user$project$Transaction$update = F2( var _user$project$Transaction$update = F2(
function (msg, model) { function (msg, model) {
var _p4 = msg; var _p5 = msg;
return A2( return A2(
_elm_lang$core$Platform_Cmd_ops['!'], _elm_lang$core$Platform_Cmd_ops['!'],
_p4._0, _p5._0,
{ctor: '[]'}); {ctor: '[]'});
}); });
var _user$project$Transaction$init = _krisajenkins$remotedata$RemoteData$NotAsked; var _user$project$Transaction$init = _krisajenkins$remotedata$RemoteData$NotAsked;

View file

@ -33,6 +33,10 @@ p {
width: 100%; width: 100%;
} }
.lamassuAdminCashOut {
background-color: #f6f6f4;
}
.lamassuAdminFormRow { .lamassuAdminFormRow {
margin: 20px 0; margin: 20px 0;
} }

127
todo.txt
View file

@ -1,126 +1 @@
- l-m shouldn't keep polling l-s when not on pending screen (low priority) - migrate rest of postgresql_interface
- scrutinize hkdf, maybe use own simplified version
- test bitcoind cash-out
- test eth cash out, check that HD is still working
- throttle status check for 3rd services like bitgo
- load stuff from master config file
- add default configuration
- either remove lamassu-config or fix link in package.json
------------------------
schemas:
- global crypto / global machine
- global crypto / specific machine
- specific crypto / global machine
- specific crypto / specific machine
- We'll have one group for config that applies to all 4
- Machine config applies to only global crypto (machine config screen)
- Crypto config applies to only specific crypto
v update migrate-config to match lamassu.json schema
- update machine name
---------------------------
- are plugins loaded globally or per machine?
- list all used plugins:
ticker, trade, wallet, info (?), idVerifier, email, sms
- wallet doesn't care about fiat
- look into how each plugin is used
- info plugin not used
- need either transitive closure of all cryptos across machines,
or add new plugin when needed
- currently we're looking at all cryptos, so this is probably easier
- different machines can have different coins, currencies, etc
- necessitates different plugins
- do plugins require initialization and state?
probably not
- require caches, so probably not big penalty to require whenever a plugin is needed
- essentially, plugins just need their account info, plus some additional
special info, such as masterSeed, fiatCurrencies, etc
- deviceCurrency needs to be fixed. currently we're assuming one currency
globally
load ticker plugin (not really, remove this), pollRate (can supply actual deviceCurrency),
purchase/trader (also device specific), consolidateTrades (see what calls this),
checkBalances (*check callee)
* pollRate -- this sets lastRates which is keyed by crypto only, may need
to be keyed by fiat as well; probably better to fetch on demand, with throttle
see: https://github.com/sindresorhus/mem
* consolidateTrades: one trader for each crypto, or separate per crypto/fiat?
does consolidateTrades need fiat currency at all? even so, shouldn't this
be in the trade record?
example: operator may have machines in US and China; might want different exchanges
for china machines; same for ticker
options: configure per machine; configure per crypto/fiat
- crypto/fiat requires more admin dev work design
- also restricts configurability
- but global ticker/exchange doesn't make sense if fiats are different
- which means it would be required to define ticker/exchange for each
machine, even if they all have the same crypto/fiat
* checkBalances: used for notifier, to indicate if balances are too low;
how to do this generalized across all machines/currencies?
* possibly configure home currency for admin; probably a good idea for
transactions, statistics, etc
-> For now, keep it as is -- one exchange per crypto, see how we handle fiat
currencies now. (passed in on trade -- we assume that we're using machine currency, which may not be true)
- consider defining exchange fiat currency in exchange account config, we don't pass in fiat amount, anyway
-----------------------------
- default values
- server side validation, including required
- need to think hard about how to do required checks for scopes
- what to do if validation fails?
- need to rethink cachedConfig, don't use global variables [later]
- cartridge counts -- where to store? already in db, not ideal but can fix later
- twoWayMode should be per crypto
- add cassette count handling in machines/actions in admin
--------------------------------
v need to create CA: http://stackoverflow.com/questions/19665863/how-do-i-use-a-self-signed-certificate-for-a-https-node-js-server
--------------------------------
v consistent error handling
v usage of http status codes (good for got)
v finish idempotency for all calls
-------------
- test pending action (action needs to take a while so we can test)
- defaults and validation
- tweak install script
---------------
- clean up lamassu-server script, app, routes

View file

@ -3421,7 +3421,7 @@ pg-minify@0.4:
version "0.4.1" version "0.4.1"
resolved "https://registry.yarnpkg.com/pg-minify/-/pg-minify-0.4.1.tgz#a642c6bd256c7da833066590b1e414334a1f6e19" resolved "https://registry.yarnpkg.com/pg-minify/-/pg-minify-0.4.1.tgz#a642c6bd256c7da833066590b1e414334a1f6e19"
pg-native@^1.10.0: pg-native@latest:
version "1.10.0" version "1.10.0"
resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-1.10.0.tgz#abe299214afa2be51db5f5104e14770c738230fd" resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-1.10.0.tgz#abe299214afa2be51db5f5104e14770c738230fd"
dependencies: dependencies:
@ -3436,14 +3436,14 @@ pg-pool@1.*:
generic-pool "2.4.2" generic-pool "2.4.2"
object-assign "4.1.0" object-assign "4.1.0"
pg-promise@^5.5.0: pg-promise@^5.6.4:
version "5.5.0" version "5.6.4"
resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-5.5.0.tgz#a89c7e25e8695c343a51f7821d4e16bb5f46d5cc" resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-5.6.4.tgz#80b18a2a1bdd9af7fb0087e01a1b87ada8559a71"
dependencies: dependencies:
manakin "0.4" manakin "0.4"
pg "5.1" pg "5.1"
pg-minify "0.4" pg-minify "0.4"
spex "1.1" spex "1.2"
pg-types@1.*, pg-types@1.6.0: pg-types@1.*, pg-types@1.6.0:
version "1.6.0" version "1.6.0"
@ -4266,9 +4266,9 @@ spdx-license-ids@^1.0.2:
version "1.2.2" version "1.2.2"
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
spex@1.1: spex@1.2:
version "1.1.1" version "1.2.0"
resolved "https://registry.yarnpkg.com/spex/-/spex-1.1.1.tgz#ceede36b128e7dcb26100b89e2b049a5f4477d50" resolved "https://registry.yarnpkg.com/spex/-/spex-1.2.0.tgz#6264b3b8acbc444477f06dbb66d425c0ee1074c0"
split@^1.0.0: split@^1.0.0:
version "1.0.0" version "1.0.0"