Lots of development
This commit is contained in:
parent
5cbec6bd23
commit
3a244f691e
19 changed files with 594 additions and 837 deletions
19
bin/lamassu-hd-address
Executable file
19
bin/lamassu-hd-address
Executable 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)}))
|
||||
})
|
||||
|
|
@ -244,6 +244,7 @@ function fetchData () {
|
|||
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
|
||||
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['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-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
|
||||
{code: 'twilio', display: 'Twilio', class: 'sms'},
|
||||
|
|
|
|||
|
|
@ -22,15 +22,10 @@ function unpair (rec) {
|
|||
return pairing.unpair(rec.deviceId)
|
||||
}
|
||||
|
||||
function repair (rec) {
|
||||
return pairing.repair(rec.deviceId)
|
||||
}
|
||||
|
||||
function setMachine (rec) {
|
||||
switch (rec.action) {
|
||||
case 'resetCashOutBills': return resetCashOutBills(rec)
|
||||
case 'unpair': return unpair(rec)
|
||||
case 'repair': return repair(rec)
|
||||
default: throw new Error('No such action: ' + rec.action)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,14 +11,7 @@ const ALPHA_BASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'
|
|||
const bsAlpha = baseX(ALPHA_BASE)
|
||||
|
||||
function unpair (deviceId) {
|
||||
const sql = 'update devices set paired=FALSE where device_id=$1'
|
||||
|
||||
return db.none(sql, [deviceId])
|
||||
}
|
||||
|
||||
function repair (deviceId) {
|
||||
const sql = 'update devices set paired=TRUE where device_id=$1'
|
||||
|
||||
const sql = 'delete from devices where device_id=$1'
|
||||
return db.none(sql, [deviceId])
|
||||
}
|
||||
|
||||
|
|
@ -39,4 +32,4 @@ function totem (hostname, name) {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = {totem, unpair, repair}
|
||||
module.exports = {totem, unpair}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ const _ = require('lodash/fp')
|
|||
const pgp = require('pg-promise')()
|
||||
const db = require('./db')
|
||||
const BN = require('./bn')
|
||||
const logger = require('./logger')
|
||||
|
||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
||||
|
|
@ -25,7 +26,7 @@ function post (tx, pi) {
|
|||
return upsert(row, tx)
|
||||
.then(vector => {
|
||||
return insertNewBills(billRows, tx)
|
||||
.then(newBills => _.concat(vector, [billRows]))
|
||||
.then(newBills => _.concat(vector, [newBills]))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -51,7 +52,6 @@ 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
|
||||
|
|
@ -139,15 +139,6 @@ function insert (tx) {
|
|||
.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) {
|
||||
if (_.isEmpty(changes)) return Promise.resolve(tx)
|
||||
|
||||
|
|
@ -159,13 +150,21 @@ function update (tx, changes) {
|
|||
.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) {
|
||||
const [oldTx, newTx] = txVector
|
||||
|
||||
registerTrades(pi, txVector)
|
||||
|
||||
if (newTx.send && !oldTx.send) {
|
||||
return pi.sendCoins(newTx)
|
||||
.then(txHash => ({txHash}))
|
||||
.catch(error => ({error}))
|
||||
}
|
||||
|
||||
return Promise.resolve({})
|
||||
|
|
|
|||
|
|
@ -1,15 +1,29 @@
|
|||
const _ = require('lodash/fp')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const db = require('./db')
|
||||
const BN = require('./bn')
|
||||
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 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) {
|
||||
const TransactionMode = pgp.txMode.TransactionMode
|
||||
|
|
@ -47,7 +61,6 @@ 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
|
||||
|
|
@ -122,8 +135,12 @@ function convertBigNumFields (obj) {
|
|||
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
|
||||
}
|
||||
|
||||
function convertField (key) {
|
||||
return _.snakeCase(key)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -148,17 +165,32 @@ function update (tx, changes) {
|
|||
.then(() => newTx)
|
||||
}
|
||||
|
||||
function preProcess (tx, newTx, pi) {
|
||||
if (!tx) {
|
||||
return pi.newAddress(newTx)
|
||||
.then(_.set('toAddress', _, 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))
|
||||
}
|
||||
|
||||
return Promise.resolve(newTx)
|
||||
function preProcess (tx, newTx, pi) {
|
||||
if (!tx) {
|
||||
return pi.isHd(newTx)
|
||||
.then(isHd => nextHd(isHd, newTx))
|
||||
.then(newTxHd => {
|
||||
return pi.newAddress(newTxHd)
|
||||
.then(_.set('toAddress', _, newTxHd))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve(updateStatus(tx, newTx))
|
||||
}
|
||||
|
||||
function postProcess (txVector, pi) {
|
||||
const [, newTx] = txVector
|
||||
const [oldTx, newTx] = txVector
|
||||
|
||||
if (!oldTx) pi.sell(newTx)
|
||||
|
||||
if (newTx.dispensed && !newTx.bills) {
|
||||
return pi.buildCartridges()
|
||||
|
|
@ -169,3 +201,75 @@ function postProcess (txVector, pi) {
|
|||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function lookupExchange (settings, cryptoCode) {
|
|||
function fetchExchange (settings, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const plugin = lookupExchange(cryptoCode)
|
||||
const plugin = lookupExchange(settings, cryptoCode)
|
||||
if (!plugin) throw noExchangeError(cryptoCode)
|
||||
const account = settings.accounts[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))
|
||||
}
|
||||
|
||||
function active (settings, fiatCode, cryptoCode) {
|
||||
function active (settings, cryptoCode) {
|
||||
return !!lookupExchange(settings, cryptoCode)
|
||||
}
|
||||
|
||||
|
|
|
|||
212
lib/plugins.js
212
lib/plugins.js
|
|
@ -2,6 +2,7 @@ const uuid = require('uuid')
|
|||
const _ = require('lodash/fp')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
const crypto = require('crypto')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const BN = require('./bn')
|
||||
const dbm = require('./postgresql_interface')
|
||||
|
|
@ -15,10 +16,8 @@ const exchange = require('./exchange')
|
|||
const sms = require('./sms')
|
||||
const email = require('./email')
|
||||
|
||||
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
|
||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
||||
const TRADE_TTL = 2 * T.minutes
|
||||
const STALE_TICKER = 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 rateRec = tickers[i]
|
||||
|
||||
const cashInCommission = BN(1).minus(BN(cryptoConfig.cashInCommission).div(100))
|
||||
const cashOutCommission = cashOut && BN(1).plus(BN(cryptoConfig.cashOutCommission).div(100))
|
||||
const cashInCommission = BN(1).add(BN(cryptoConfig.cashInCommission).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)
|
||||
const rate = rateRec.rates
|
||||
rates[cryptoCode] = {
|
||||
cashIn: rate.ask.div(cashInCommission),
|
||||
cashIn: rate.ask.mul(cashInCommission),
|
||||
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) {
|
||||
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) {
|
||||
const event = {
|
||||
id: uuid.v4(),
|
||||
|
|
@ -201,13 +174,22 @@ function plugins (settings, deviceId) {
|
|||
return dbm.machineEvent(event)
|
||||
}
|
||||
|
||||
function newAddress (tx) {
|
||||
const cryptoCode = tx.cryptoCode
|
||||
const info = {
|
||||
label: 'TX ' + Date.now(),
|
||||
account: 'deposit'
|
||||
function isHd (tx) {
|
||||
return wallet.isHd(settings, tx.cryptoCode)
|
||||
}
|
||||
return wallet.newAddress(settings, cryptoCode, info)
|
||||
|
||||
function getStatus (tx) {
|
||||
return wallet.getStatus(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
|
||||
}
|
||||
|
||||
function newAddress (tx) {
|
||||
const info = {
|
||||
cryptoCode: tx.cryptoCode,
|
||||
label: 'TX ' + Date.now(),
|
||||
account: 'deposit',
|
||||
hdIndex: tx.hdIndex
|
||||
}
|
||||
return wallet.newAddress(settings, info)
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.debug('notifyConfirmation')
|
||||
|
||||
|
|
@ -256,34 +233,17 @@ function plugins (settings, deviceId) {
|
|||
const rec = {
|
||||
sms: {
|
||||
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)
|
||||
.then(() => dbm.updateNotify(tx))
|
||||
}
|
||||
.then(() => {
|
||||
const sql = 'update cash_out_txs set notified=$1 where id=$2'
|
||||
const values = [true, tx.id]
|
||||
|
||||
function monitorLiveIncoming () {
|
||||
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)
|
||||
return db.none(sql, values)
|
||||
})
|
||||
}
|
||||
|
||||
function pong () {
|
||||
|
|
@ -304,6 +264,35 @@ function plugins (settings, deviceId) {
|
|||
* 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) {
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
|
||||
|
|
@ -371,27 +360,56 @@ function plugins (settings, deviceId) {
|
|||
if (!exchange.active(settings, cryptoCode)) return
|
||||
|
||||
const market = [fiatCode, cryptoCode].join('')
|
||||
logger.debug('[%s] checking for trades', market)
|
||||
|
||||
const tradeEntry = consolidateTrades(cryptoCode, fiatCode)
|
||||
if (tradeEntry === null) return logger.debug('[%s] no trades', market)
|
||||
|
||||
if (tradeEntry.cryptoAtoms.eq(0)) {
|
||||
logger.debug('[%s] rejecting 0 trade', market)
|
||||
return
|
||||
}
|
||||
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
|
||||
|
||||
logger.debug('[%s] making a trade: %d', market, tradeEntry.cryptoAtoms.toString())
|
||||
|
||||
return exchange.buy(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
|
||||
.then(() => logger.debug('[%s] Successful trade.', market))
|
||||
return executeTradeForType(tradeEntry)
|
||||
.catch(err => {
|
||||
tradesQueues[market].push(tradeEntry)
|
||||
if (err.name === 'NoExchangeError') return logger.debug(err.message)
|
||||
if (err.name === 'orderTooSmall') return logger.debug(err.message)
|
||||
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) {
|
||||
const config = configManager.unscoped(settings.config)
|
||||
|
||||
|
|
@ -466,49 +484,51 @@ function plugins (settings, deviceId) {
|
|||
.then(() => code)
|
||||
}
|
||||
|
||||
function sweepHD (row) {
|
||||
function sweepHdRow (row) {
|
||||
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 => {
|
||||
if (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))
|
||||
}
|
||||
|
||||
function sweepLiveHD () {
|
||||
return dbm.fetchLiveHD()
|
||||
.then(rows => Promise.all(rows.map(sweepHD)))
|
||||
.catch(err => logger.error(err))
|
||||
}
|
||||
function sweepHd () {
|
||||
const sql = `select id, crypto_code, hd_index from cash_out_txs
|
||||
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
|
||||
|
||||
function sweepOldHD () {
|
||||
return dbm.fetchOldHD()
|
||||
.then(rows => Promise.all(rows.map(sweepHD)))
|
||||
return db.any(sql)
|
||||
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
||||
.catch(err => logger.error(err))
|
||||
}
|
||||
|
||||
return {
|
||||
pollQueries,
|
||||
trade,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
isHd,
|
||||
getStatus,
|
||||
dispenseAck,
|
||||
getPhoneCode,
|
||||
executeTrades,
|
||||
pong,
|
||||
pongClear,
|
||||
monitorLiveIncoming,
|
||||
monitorIncoming,
|
||||
monitorUnnotified,
|
||||
sweepLiveHD,
|
||||
sweepOldHD,
|
||||
notifyConfirmation,
|
||||
sweepHd,
|
||||
sendMessage,
|
||||
checkBalances,
|
||||
buildCartridges
|
||||
buildCartridges,
|
||||
buy,
|
||||
sell
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ const plugins = require('./plugins')
|
|||
const notifier = require('./notifier')
|
||||
const T = require('./time')
|
||||
const logger = require('./logger')
|
||||
const cashOutTx = require('./cash-out-tx')
|
||||
|
||||
const INCOMING_TX_INTERVAL = 30 * T.seconds
|
||||
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
|
||||
const UNNOTIFIED_INTERVAL = 10 * T.seconds
|
||||
const SWEEP_LIVE_HD_INTERVAL = T.minute
|
||||
const SWEEP_OLD_HD_INTERVAL = 2 * T.minutes
|
||||
const SWEEP_HD_INTERVAL = T.minute
|
||||
const TRADE_INTERVAL = 10 * T.seconds
|
||||
const PONG_INTERVAL = 10 * T.seconds
|
||||
const PONG_CLEAR_INTERVAL = 1 * T.day
|
||||
|
|
@ -26,19 +26,17 @@ function start (settings) {
|
|||
pi.executeTrades()
|
||||
pi.pong()
|
||||
pi.pongClear()
|
||||
pi.monitorLiveIncoming()
|
||||
pi.monitorIncoming()
|
||||
pi.monitorUnnotified()
|
||||
pi.sweepLiveHD()
|
||||
pi.sweepOldHD()
|
||||
cashOutTx.monitorLiveIncoming(settings)
|
||||
cashOutTx.monitorStaleIncoming(settings)
|
||||
cashOutTx.monitorUnnotified(settings)
|
||||
pi.sweepHd()
|
||||
notifier.checkNotification(pi)
|
||||
|
||||
setInterval(() => pi.executeTrades(), TRADE_INTERVAL)
|
||||
setInterval(() => pi.monitorLiveIncoming(), LIVE_INCOMING_TX_INTERVAL)
|
||||
setInterval(() => pi.monitorIncoming(), INCOMING_TX_INTERVAL)
|
||||
setInterval(() => pi.monitorUnnotified(), UNNOTIFIED_INTERVAL)
|
||||
setInterval(() => pi.sweepLiveHD(), SWEEP_LIVE_HD_INTERVAL)
|
||||
setInterval(() => pi.sweepOldHD(), SWEEP_OLD_HD_INTERVAL)
|
||||
setInterval(() => cashOutTx.monitorLiveIncoming(settings), LIVE_INCOMING_TX_INTERVAL)
|
||||
setInterval(() => cashOutTx.monitorStaleIncoming(settings), INCOMING_TX_INTERVAL)
|
||||
setInterval(() => cashOutTx.monitorUnnotified(settings), UNNOTIFIED_INTERVAL)
|
||||
setInterval(() => pi.sweepHd(), SWEEP_HD_INTERVAL)
|
||||
setInterval(() => pi.pong(), PONG_INTERVAL)
|
||||
setInterval(() => pi.pongClear(), PONG_CLEAR_INTERVAL)
|
||||
setInterval(() => notifier.checkNotification(pi), CHECK_NOTIFICATION_INTERVAL)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,4 @@
|
|||
// @flow weak
|
||||
'use strict'
|
||||
|
||||
const BigNumber = require('bignumber.js')
|
||||
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) {
|
||||
// outputs string like: '$1, $2, $3...' with proper No of items
|
||||
|
|
@ -27,39 +14,6 @@ function getInsertQuery (tableName, fields) {
|
|||
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) {
|
||||
const sql = 'INSERT INTO device_events (device_id, event_type, ' +
|
||||
'note, device_time) VALUES ($1, $2, $3, $4)'
|
||||
|
|
@ -69,170 +23,6 @@ exports.recordDeviceEvent = function recordDeviceEvent (deviceId, event) {
|
|||
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) {
|
||||
const sql = 'SELECT cassette1, cassette2 FROM devices ' +
|
||||
'WHERE device_id=$1'
|
||||
|
|
@ -271,165 +61,3 @@ exports.machineEvents = function machineEvents () {
|
|||
|
||||
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])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ function poll (req, res, next) {
|
|||
response.idVerificationLimit = config.idVerificationLimit
|
||||
}
|
||||
|
||||
console.log('DEBUG22: %j', response)
|
||||
return res.json(response)
|
||||
})
|
||||
.catch(next)
|
||||
|
|
@ -225,9 +224,7 @@ function authorize (req, res, next) {
|
|||
.catch(next)
|
||||
}
|
||||
|
||||
const skip = options.logLevel === 'debug'
|
||||
? () => false
|
||||
: (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200
|
||||
const skip = (req, res) => _.includes(req.path, ['/poll', '/state']) && res.statusCode === 200
|
||||
|
||||
const configRequiredRoutes = [
|
||||
'/poll',
|
||||
|
|
@ -309,7 +306,8 @@ function populateDeviceId (req, res, next) {
|
|||
|
||||
function populateSettings (req, res, next) {
|
||||
const versionId = req.headers['config-version']
|
||||
logger.debug('versionId: %s', versionId)
|
||||
|
||||
console.log('DEBUG300: %s', versionId)
|
||||
|
||||
if (!versionId) {
|
||||
return settingsLoader.loadLatest()
|
||||
|
|
|
|||
|
|
@ -1,27 +1,38 @@
|
|||
const _ = require('lodash/fp')
|
||||
const mem = require('mem')
|
||||
const HKDF = require('node-hkdf-sync')
|
||||
|
||||
const configManager = require('./config-manager')
|
||||
const pify = require('pify')
|
||||
const fs = pify(require('fs'))
|
||||
const options = require('./options')
|
||||
|
||||
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) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
console.log('DEBUG44')
|
||||
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)
|
||||
}
|
||||
return fs.readFile(options.seedPath, 'utf8')
|
||||
.then(hex => {
|
||||
const masterSeed = Buffer.from(hex.trim(), 'hex')
|
||||
const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet
|
||||
console.log('DEBUG44.1')
|
||||
const account = settings.accounts[plugin]
|
||||
console.log('DEBUG44.2')
|
||||
const wallet = require('lamassu-' + plugin)
|
||||
|
||||
console.log('DEBUG45: %j', {wallet, account})
|
||||
|
||||
return {wallet, account}
|
||||
return {wallet, account: _.set('seed', computeSeed(masterSeed), account)}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -44,11 +55,18 @@ function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) {
|
|||
return res
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === INSUFFICIENT_FUNDS_NAME) {
|
||||
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
|
||||
}
|
||||
|
||||
function newAddress (settings, cryptoCode, info) {
|
||||
return fetchWallet(settings, cryptoCode)
|
||||
.then(r => r.wallet.newAddress(r.account, cryptoCode, info))
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
function newAddress (settings, info) {
|
||||
return fetchWallet(settings, info.cryptoCode)
|
||||
.then(r => r.wallet.newAddress(r.account, info))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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 = {
|
||||
balance: mem(balance, {maxAge: FETCH_INTERVAL}),
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus
|
||||
getStatus,
|
||||
sweep,
|
||||
isHd
|
||||
}
|
||||
|
|
|
|||
23
migrations/024-consolidate-hd.js
Normal file
23
migrations/024-consolidate-hd.js
Normal 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()
|
||||
}
|
||||
20
migrations/025-create_trades.js
Normal file
20
migrations/025-create_trades.js
Normal 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()
|
||||
}
|
||||
|
|
@ -33,8 +33,8 @@
|
|||
"node-hkdf-sync": "^1.0.0",
|
||||
"numeral": "^2.0.3",
|
||||
"pg": "^6.1.2",
|
||||
"pg-native": "^1.10.0",
|
||||
"pg-promise": "^5.5.0",
|
||||
"pg-native": "latest",
|
||||
"pg-promise": "^5.6.4",
|
||||
"pify": "^2.3.0",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"ramda": "^0.22.1",
|
||||
|
|
|
|||
340
public/elm.js
340
public/elm.js
|
|
@ -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);
|
||||
};
|
||||
|
||||
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$TxAddress = {ctor: 'TxAddress'};
|
||||
var _user$project$Css_Classes$TxPhone = {ctor: 'TxPhone'};
|
||||
|
|
@ -32711,9 +32713,7 @@ var _user$project$TransactionTypes$CashOutTxRec = function (a) {
|
|||
return function (m) {
|
||||
return function (n) {
|
||||
return function (o) {
|
||||
return function (p) {
|
||||
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};
|
||||
};
|
||||
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};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
@ -32839,10 +32839,6 @@ var _user$project$TransactionDecoder$cashOutTxDecoder = A3(
|
|||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||
'status',
|
||||
_elm_lang$core$Json_Decode$string,
|
||||
A3(
|
||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||
'txHash',
|
||||
_elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string),
|
||||
A3(
|
||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
|
||||
'fiatCode',
|
||||
|
|
@ -32871,7 +32867,7 @@ var _user$project$TransactionDecoder$cashOutTxDecoder = A3(
|
|||
_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)))))))))))))))));
|
||||
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$TransactionTypes$CashOutTxRec))))))))))))))));
|
||||
var _user$project$TransactionDecoder$txDecode = function (txClass) {
|
||||
var _p3 = txClass;
|
||||
switch (_p3) {
|
||||
|
|
@ -32893,41 +32889,41 @@ var _user$project$TransactionDecoder$txsDecoder = A2(
|
|||
'transactions',
|
||||
_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 _p0 = tx;
|
||||
if (_p0.ctor === 'CashInTx') {
|
||||
var _p1 = _p0._0;
|
||||
var _p1 = tx;
|
||||
if (_p1.ctor === 'CashInTx') {
|
||||
var _p2 = _p1._0;
|
||||
return A2(
|
||||
_elm_lang$html$Html$tr,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _user$project$Css_Admin$class(
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _user$project$Css_Classes$NumberColumn,
|
||||
_0: _user$project$Css_Classes$CashIn,
|
||||
_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: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(_p1.machineName),
|
||||
_0: _elm_lang$html$Html$text('Cash in'),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
|
|
@ -32944,116 +32940,6 @@ var _user$project$Transaction$rowView = function (tx) {
|
|||
}),
|
||||
_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: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(_p1.cryptoCode),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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.00', _p1.fiat)),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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(_elm_lang$core$Maybe$withDefault, '', _p1.phone)),
|
||||
_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(_p1.toAddress),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {ctor: '[]'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var _p2 = _p0._0;
|
||||
return A2(
|
||||
_elm_lang$html$Html$tr,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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(
|
||||
|
|
@ -33090,7 +32976,7 @@ var _user$project$Transaction$rowView = function (tx) {
|
|||
A2(
|
||||
_ggb$numeral_elm$Numeral$format,
|
||||
'0,0.000000',
|
||||
_elm_lang$core$Basics$toFloat(_p2.cryptoAtoms) / 1.0e8)),
|
||||
_elm_lang$core$Basics$toFloat(_p2.cryptoAtoms) / _user$project$Transaction$multiplier(_p2.cryptoCode))),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
|
|
@ -33120,10 +33006,7 @@ var _user$project$Transaction$rowView = function (tx) {
|
|||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(
|
||||
A2(
|
||||
_ggb$numeral_elm$Numeral$format,
|
||||
'0,0.00',
|
||||
_elm_lang$core$Basics$negate(_p2.fiat))),
|
||||
A2(_ggb$numeral_elm$Numeral$format, '0,0.00', _p2.fiat)),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
|
|
@ -33172,6 +33055,166 @@ var _user$project$Transaction$rowView = function (tx) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
var _p3 = _p1._0;
|
||||
return A2(
|
||||
_elm_lang$html$Html$tr,
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _user$project$Css_Admin$class(
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _user$project$Css_Classes$CashOut,
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {ctor: '[]'}
|
||||
},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text('Cash out'),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(_p3.machineName),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(_p3.cryptoCode),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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.00', _p3.fiat)),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{
|
||||
ctor: '::',
|
||||
_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(_elm_lang$core$Maybe$withDefault, '', _p3.phone)),
|
||||
_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: '[]'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -33206,6 +33249,12 @@ var _user$project$Transaction$tableView = function (txs) {
|
|||
_elm_lang$html$Html$tr,
|
||||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
{ctor: '[]'},
|
||||
{ctor: '[]'}),
|
||||
_1: {
|
||||
ctor: '::',
|
||||
_0: A2(
|
||||
_elm_lang$html$Html$td,
|
||||
|
|
@ -33320,6 +33369,7 @@ var _user$project$Transaction$tableView = function (txs) {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
_1: {ctor: '[]'}
|
||||
}),
|
||||
|
|
@ -33334,8 +33384,8 @@ var _user$project$Transaction$tableView = function (txs) {
|
|||
});
|
||||
};
|
||||
var _user$project$Transaction$view = function (model) {
|
||||
var _p3 = model;
|
||||
switch (_p3.ctor) {
|
||||
var _p4 = model;
|
||||
switch (_p4.ctor) {
|
||||
case 'NotAsked':
|
||||
return A2(
|
||||
_elm_lang$html$Html$div,
|
||||
|
|
@ -33357,7 +33407,7 @@ var _user$project$Transaction$view = function (model) {
|
|||
{
|
||||
ctor: '::',
|
||||
_0: _elm_lang$html$Html$text(
|
||||
_elm_lang$core$Basics$toString(_p3._0)),
|
||||
_elm_lang$core$Basics$toString(_p4._0)),
|
||||
_1: {ctor: '[]'}
|
||||
});
|
||||
default:
|
||||
|
|
@ -33366,17 +33416,17 @@ var _user$project$Transaction$view = function (model) {
|
|||
{ctor: '[]'},
|
||||
{
|
||||
ctor: '::',
|
||||
_0: _user$project$Transaction$tableView(_p3._0),
|
||||
_0: _user$project$Transaction$tableView(_p4._0),
|
||||
_1: {ctor: '[]'}
|
||||
});
|
||||
}
|
||||
};
|
||||
var _user$project$Transaction$update = F2(
|
||||
function (msg, model) {
|
||||
var _p4 = msg;
|
||||
var _p5 = msg;
|
||||
return A2(
|
||||
_elm_lang$core$Platform_Cmd_ops['!'],
|
||||
_p4._0,
|
||||
_p5._0,
|
||||
{ctor: '[]'});
|
||||
});
|
||||
var _user$project$Transaction$init = _krisajenkins$remotedata$RemoteData$NotAsked;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ p {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.lamassuAdminCashOut {
|
||||
background-color: #f6f6f4;
|
||||
}
|
||||
|
||||
.lamassuAdminFormRow {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
|
|
|||
127
todo.txt
127
todo.txt
|
|
@ -1,126 +1 @@
|
|||
- l-m shouldn't keep polling l-s when not on pending screen (low priority)
|
||||
|
||||
- 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
|
||||
- migrate rest of postgresql_interface
|
||||
|
|
|
|||
16
yarn.lock
16
yarn.lock
|
|
@ -3421,7 +3421,7 @@ pg-minify@0.4:
|
|||
version "0.4.1"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-1.10.0.tgz#abe299214afa2be51db5f5104e14770c738230fd"
|
||||
dependencies:
|
||||
|
|
@ -3436,14 +3436,14 @@ pg-pool@1.*:
|
|||
generic-pool "2.4.2"
|
||||
object-assign "4.1.0"
|
||||
|
||||
pg-promise@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-5.5.0.tgz#a89c7e25e8695c343a51f7821d4e16bb5f46d5cc"
|
||||
pg-promise@^5.6.4:
|
||||
version "5.6.4"
|
||||
resolved "https://registry.yarnpkg.com/pg-promise/-/pg-promise-5.6.4.tgz#80b18a2a1bdd9af7fb0087e01a1b87ada8559a71"
|
||||
dependencies:
|
||||
manakin "0.4"
|
||||
pg "5.1"
|
||||
pg-minify "0.4"
|
||||
spex "1.1"
|
||||
spex "1.2"
|
||||
|
||||
pg-types@1.*, pg-types@1.6.0:
|
||||
version "1.6.0"
|
||||
|
|
@ -4266,9 +4266,9 @@ spdx-license-ids@^1.0.2:
|
|||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
|
||||
|
||||
spex@1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/spex/-/spex-1.1.1.tgz#ceede36b128e7dcb26100b89e2b049a5f4477d50"
|
||||
spex@1.2:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/spex/-/spex-1.2.0.tgz#6264b3b8acbc444477f06dbb66d425c0ee1074c0"
|
||||
|
||||
split@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue