This commit is contained in:
Josh Harvey 2017-03-06 14:22:33 +02:00
parent 0bf54fa1d8
commit 30071151ff
6 changed files with 107 additions and 242 deletions

View file

@ -3,17 +3,19 @@ const pgp = require('pg-promise')()
const db = require('./db') const db = require('./db')
const BN = require('./bn') const BN = require('./bn')
module.exports = {postCashIn} module.exports = {post}
const UPDATEABLE_FIELDS = ['fee', 'txHash', 'phone', 'error'] const UPDATEABLE_FIELDS = ['fee', 'txHash', 'phone', 'error', 'send']
function postCashIn (tx, pi) { function post (tx, pi) {
const TransactionMode = pgp.txMode.TransactionMode const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable}) const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) { function transaction (t) {
const sql = 'select * from cash_in_txs where id=$1' const sql = 'select * from cash_in_txs where id=$1'
console.log('DEBUG888: %j', tx)
return t.oneOrNone(sql, [tx.id]) return t.oneOrNone(sql, [tx.id])
.then(row => upsert(row, tx)) .then(row => upsert(row, tx))
} }
@ -21,15 +23,18 @@ function postCashIn (tx, pi) {
transaction.txMode = tmSRD transaction.txMode = tmSRD
return db.tx(transaction) return db.tx(transaction)
.then(txVector => postProcess(txVector, pi)) .then(txVector => {
.then(changes => update(tx.id, changes)) const [, newTx] = txVector
postProcess(txVector, pi)
.then(changes => update(newTx, changes))
})
} }
function diff (oldTx, newTx) { function diff (oldTx, newTx) {
let updatedTx = {} let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => { UPDATEABLE_FIELDS.forEach(fieldKey => {
if (_.isEqual(oldTx[fieldKey], newTx[fieldKey])) return if (oldTx && _.isEqual(oldTx[fieldKey], newTx[fieldKey])) return
updatedTx[fieldKey] = newTx[fieldKey] updatedTx[fieldKey] = newTx[fieldKey]
}) })
@ -37,6 +42,8 @@ function diff (oldTx, newTx) {
} }
function toObj (row) { function toObj (row) {
if (!row) return null
const keys = _.keys(row) const keys = _.keys(row)
let newObj = {} let newObj = {}
@ -56,31 +63,48 @@ function toObj (row) {
function upsert (row, tx) { function upsert (row, tx) {
const oldTx = toObj(row) const oldTx = toObj(row)
if (oldTx) return insert(tx) // insert bills
return update(tx.id, diff(oldTx, tx))
if (!oldTx) {
return insert(tx)
.then(newTx => [oldTx, newTx])
}
return update(tx, diff(oldTx, tx))
.then(newTx => [oldTx, newTx])
} }
function insert (tx) { function insert (tx) {
const dbTx = _.mapKeys(_.snakeCase, tx) const dbTx = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], tx))
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
return db.none(sql) return db.one(sql)
.then(toObj)
} }
function update (txId, changes) { function update (tx, changes) {
const dbChanges = _.mapKeys(_.snakeCase, changes) if (_.isEmpty(changes)) return Promise.resolve(tx)
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [txId])
return db.none(sql) const dbChanges = _.mapKeys(_.snakeCase, _.omit(['direction', 'bills'], changes))
console.log('DEBUG893: %j', dbChanges)
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
return db.one(sql)
.then(toObj)
} }
function postProcess (txVector, pi) { function postProcess (txVector, pi) {
const [oldTx, newTx] = txVector const [oldTx, newTx] = txVector
if (newTx.sent && !oldTx.sent) { if (newTx.send && !oldTx.send) {
return pi.sendCoins(newTx) return pi.sendCoins(newTx)
.then(txHash => ({txHash})) .then(txHash => ({txHash}))
.catch(error => ({error})) .catch(error => {
console.log('DEBUG895: %j', error)
return {error}
})
} }
return Promise.resolve({})
} }

View file

@ -30,8 +30,8 @@ const coins = {
ETH: {unitScale: 18} ETH: {unitScale: 18}
} }
function plugins (settings) { function plugins (settings, deviceId) {
function buildRates (deviceId, tickers) { function buildRates (tickers) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const cryptoCodes = config.cryptoCurrencies const cryptoCodes = config.cryptoCurrencies
const cashOut = config.cashOutEnabled const cashOut = config.cashOutEnabled
@ -41,6 +41,7 @@ function plugins (settings) {
const rates = {} const rates = {}
console.log('DEBUG444: %j', tickers)
cryptoCodes.forEach((cryptoCode, i) => { cryptoCodes.forEach((cryptoCode, i) => {
const rateRec = tickers[i] const rateRec = tickers[i]
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)
@ -54,7 +55,7 @@ function plugins (settings) {
return rates return rates
} }
function buildBalances (deviceId, balanceRecs) { function buildBalances (balanceRecs) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const cryptoCodes = config.cryptoCurrencies const cryptoCodes = config.cryptoCurrencies
@ -97,7 +98,7 @@ function plugins (settings) {
.then(row => row.id) .then(row => row.id)
} }
function pollQueries (deviceTime, deviceId, deviceRec) { function pollQueries (deviceTime, deviceRec) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const fiatCode = config.fiatCurrency const fiatCode = config.fiatCurrency
const cryptoCodes = config.cryptoCurrencies const cryptoCodes = config.cryptoCurrencies
@ -106,8 +107,8 @@ function plugins (settings) {
const virtualCartridges = [config.virtualCashOutDenomination] const virtualCartridges = [config.virtualCashOutDenomination]
const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c)) const tickerPromises = cryptoCodes.map(c => ticker.getRates(settings, fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c, deviceId)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
const pingPromise = recordPing(deviceId, deviceTime, deviceRec) const pingPromise = recordPing(deviceTime, deviceRec)
const currentConfigVersionPromise = fetchCurrentConfigVersion() const currentConfigVersionPromise = fetchCurrentConfigVersion()
const promises = [ const promises = [
@ -125,8 +126,8 @@ function plugins (settings) {
return { return {
cartridges: buildCartridges(cartridges, virtualCartridges, cartridgeCounts), cartridges: buildCartridges(cartridges, virtualCartridges, cartridgeCounts),
rates: buildRates(deviceId, tickers), rates: buildRates(tickers),
balances: buildBalances(deviceId, balances), balances: buildBalances(balances),
currentConfigVersion currentConfigVersion
} }
}) })
@ -134,23 +135,12 @@ function plugins (settings) {
// NOTE: This will fail if we have already sent coins because there will be // NOTE: This will fail if we have already sent coins because there will be
// a unique dbm record in the table already. // a unique dbm record in the table already.
function sendCoins (deviceId, tx) { function sendCoins (tx) {
return dbm.addOutgoingTx(deviceId, tx) console.log('DEBUG50: %j', settings)
.then(() => wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)) return wallet.sendCoins(settings, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode)
.then(txHash => {
const fee = null // Need to fill this out in plugins
const toSend = {cryptoAtoms: tx.cryptoAtoms, fiat: tx.fiat}
return dbm.sentCoins(tx, toSend, fee, null, txHash)
.then(() => ({
statusCode: 201, // Created
txHash,
txId: tx.id
}))
})
} }
function trade (deviceId, rawTrade) { function trade (rawTrade) {
// TODO: move this to dbm, too // TODO: move this to dbm, too
// add bill to trader queue (if trader is enabled) // add bill to trader queue (if trader is enabled)
const cryptoCode = rawTrade.cryptoCode const cryptoCode = rawTrade.cryptoCode
@ -174,18 +164,18 @@ function plugins (settings) {
}) })
} }
function recordPing (deviceId, deviceTime, rec) { function recordPing (deviceTime, rec) {
const event = { const event = {
id: uuid.v4(), id: uuid.v4(),
deviceId: deviceId, deviceId,
eventType: 'ping', eventType: 'ping',
note: JSON.stringify({state: rec.state, isIdle: rec.idle === 'true', txId: rec.txId}), note: JSON.stringify({state: rec.state, isIdle: rec.idle === 'true', txId: rec.txId}),
deviceTime: deviceTime deviceTime
} }
return dbm.machineEvent(event) return dbm.machineEvent(event)
} }
function cashOut (deviceId, tx) { function cashOut (tx) {
const cryptoCode = tx.cryptoCode const cryptoCode = tx.cryptoCode
const serialPromise = wallet.supportsHD const serialPromise = wallet.supportsHD
@ -210,7 +200,7 @@ function plugins (settings) {
}) })
} }
function dispenseAck (deviceId, tx) { function dispenseAck (tx) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const cartridges = [ config.topCashOutDenomination, const cartridges = [ config.topCashOutDenomination,
config.bottomCashOutDenomination ] config.bottomCashOutDenomination ]
@ -218,7 +208,7 @@ function plugins (settings) {
return dbm.addDispense(deviceId, tx, cartridges) return dbm.addDispense(deviceId, tx, cartridges)
} }
function fiatBalance (fiatCode, cryptoCode, deviceId) { function fiatBalance (fiatCode, cryptoCode) {
const config = configManager.scoped(cryptoCode, deviceId, settings.config) const config = configManager.scoped(cryptoCode, deviceId, settings.config)
return Promise.all([ return Promise.all([
@ -404,11 +394,11 @@ function plugins (settings) {
return Promise.all(promises) return Promise.all(promises)
} }
function checkDeviceBalances (deviceId) { function checkDeviceBalances (_deviceId) {
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(_deviceId, settings.config)
const cryptoCodes = config.cryptoCurrencies const cryptoCodes = config.cryptoCurrencies
const fiatCode = config.fiatCurrency const fiatCode = config.fiatCurrency
const fiatBalancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c, deviceId)) const fiatBalancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
return Promise.all(fiatBalancePromises) return Promise.all(fiatBalancePromises)
.then(arr => { .then(arr => {
@ -416,7 +406,7 @@ function plugins (settings) {
fiatBalance: balance, fiatBalance: balance,
cryptoCode: cryptoCodes[i], cryptoCode: cryptoCodes[i],
fiatCode, fiatCode,
deviceId _deviceId
})) }))
}) })
} }

View file

@ -3,15 +3,12 @@
const morgan = require('morgan') const morgan = require('morgan')
const helmet = require('helmet') const helmet = require('helmet')
const bodyParser = require('body-parser') const bodyParser = require('body-parser')
const BigNumber = require('bignumber.js')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const express = require('express') const express = require('express')
const options = require('./options') const options = require('./options')
const logger = require('./logger') const logger = require('./logger')
const configManager = require('./config-manager') const configManager = require('./config-manager')
const db = require('./db')
const dbm = require('./postgresql_interface')
const pairing = require('./pairing') const pairing = require('./pairing')
const settingsLoader = require('./settings-loader') const settingsLoader = require('./settings-loader')
const plugins = require('./plugins') const plugins = require('./plugins')
@ -35,11 +32,11 @@ function poll (req, res, next) {
const pid = req.query.pid const pid = req.query.pid
const settings = req.settings const settings = req.settings
const config = configManager.machineScoped(deviceId, settings.config) const config = configManager.machineScoped(deviceId, settings.config)
const pi = plugins(settings) const pi = plugins(settings, deviceId)
pids[deviceId] = {pid, ts: Date.now()} pids[deviceId] = {pid, ts: Date.now()}
pi.pollQueries(deviceTime, deviceId, req.query) pi.pollQueries(deviceTime, req.query)
.then(results => { .then(results => {
const cartridges = results.cartridges const cartridges = results.cartridges
@ -82,77 +79,38 @@ function poll (req, res, next) {
} }
function postTx (req, res, next) { function postTx (req, res, next) {
return Tx.post(req.body) console.log('DEBUG60: %j', req.settings)
const pi = plugins(req.settings, req.deviceId)
return Tx.post(_.set('deviceId', req.deviceId, req.body), pi)
.then(tx => res.json(tx)) .then(tx => res.json(tx))
.catch(next) .catch(next)
} }
function trade (req, res, next) {
const tx = req.body
const pi = plugins(req.settings)
tx.cryptoAtoms = new BigNumber(tx.cryptoAtoms)
pi.trade(req.deviceId, tx)
.then(() => cacheAndRespond(req, res))
.catch(next)
}
function stateChange (req, res, next) { function stateChange (req, res, next) {
helpers.stateChange(req.deviceId, req.deviceTime, req.body) helpers.stateChange(req.deviceId, req.deviceTime, req.body)
.then(() => cacheAndRespond(req, res)) .then(() => respond(req, res))
.catch(next)
}
function send (req, res, next) {
const pi = plugins(req.settings)
const tx = req.body
tx.cryptoAtoms = new BigNumber(tx.cryptoAtoms)
return pi.sendCoins(req.deviceId, tx)
.then(status => {
const body = {txId: status && status.txId}
return cacheAndRespond(req, res, body)
})
.catch(next)
}
function cashOut (req, res, next) {
const pi = plugins(req.settings)
logger.info({tx: req.body, cmd: 'cashOut'})
const tx = req.body
tx.cryptoAtoms = new BigNumber(tx.cryptoAtoms)
return pi.cashOut(req.deviceId, tx)
.then(cryptoAddress => cacheAndRespond(req, res, {toAddress: cryptoAddress}))
.catch(next)
}
function dispenseAck (req, res, next) {
const pi = plugins(req.settings)
pi.dispenseAck(req.deviceId, req.body.tx)
.then(() => cacheAndRespond(req, res))
.catch(next) .catch(next)
} }
function deviceEvent (req, res, next) { function deviceEvent (req, res, next) {
const pi = plugins(req.settings) const pi = plugins(req.settings, req.deviceId)
pi.logEvent(req.deviceId, req.body) pi.logEvent(req.body)
.then(() => cacheAndRespond(req, res)) .then(() => respond(req, res))
.catch(next) .catch(next)
} }
function verifyUser (req, res, next) { function verifyUser (req, res, next) {
const pi = plugins(req.settings) const pi = plugins(req.settings, req.deviceId)
pi.verifyUser(req.body) pi.verifyUser(req.body)
.then(idResult => cacheAndRespond(req, res, idResult)) .then(idResult => respond(req, res, idResult))
.catch(next) .catch(next)
} }
function verifyTx (req, res, next) { function verifyTx (req, res, next) {
const pi = plugins(req.settings) const pi = plugins(req.settings, req.deviceId)
pi.verifyTransaction(req.body) pi.verifyTransaction(req.body)
.then(idResult => cacheAndRespond(req, res, idResult)) .then(idResult => respond(req, res, idResult))
.catch(next) .catch(next)
} }
@ -177,11 +135,11 @@ function pair (req, res, next) {
} }
function phoneCode (req, res, next) { function phoneCode (req, res, next) {
const pi = plugins(req.settings) const pi = plugins(req.settings, req.deviceId)
const phone = req.body.phone const phone = req.body.phone
return pi.getPhoneCode(phone) return pi.getPhoneCode(phone)
.then(code => cacheAndRespond(req, res, {code})) .then(code => respond(req, res, {code}))
.catch(err => { .catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 410) if (err.name === 'BadNumberError') throw httpError('Bad number', 410)
throw err throw err
@ -189,88 +147,6 @@ function phoneCode (req, res, next) {
.catch(next) .catch(next)
} }
function updatePhone (req, res, next) {
const notified = req.query.notified === 'true'
const tx = req.body
return dbm.updatePhone(tx, notified)
.then(r => cacheAndRespond(req, res, r))
.catch(next)
}
function fetchPhoneTx (req, res, next) {
return helpers.fetchPhoneTx(req.query.phone)
.then(r => res.json(r))
.catch(next)
}
function registerRedeem (req, res, next) {
const txId = req.params.txId
return dbm.registerRedeem(txId)
.then(() => cacheAndRespond(req, res))
.catch(next)
}
function waitForDispense (req, res, next) {
logger.debug('waitForDispense')
return dbm.fetchTx(req.params.txId)
.then(tx => {
logger.debug('tx fetched')
logger.debug(tx)
if (!tx) return res.sendStatus(404)
if (tx.status === req.query.status) return res.sendStatus(304)
res.json({tx})
})
.catch(next)
}
function dispense (req, res, next) {
const tx = req.body.tx
return dbm.addDispenseRequest(tx)
.then(dispenseRec => cacheAndRespond(req, res, dispenseRec))
.catch(next)
}
function isUniqueViolation (err) {
return err.code === '23505'
}
function cacheAction (req, res, next) {
const requestId = req.headers['request-id']
if (!requestId) return next()
const sql = `insert into idempotents (request_id, device_id, body, status, pending)
values ($1, $2, $3, $4, $5)`
const deviceId = req.deviceId
db.none(sql, [requestId, deviceId, {}, 204, true])
.then(() => next())
.catch(err => {
if (!isUniqueViolation(err)) throw err
const sql2 = 'select body, status, pending from idempotents where request_id=$1'
return db.one(sql2, [requestId])
.then(row => {
if (row.pending) return res.status(204).end()
return res.status(row.status).json(row.body)
})
})
}
function updateCachedAction (req, body, status) {
const requestId = req.headers['request-id']
if (!requestId) return Promise.resolve()
const sql = `update idempotents set body=$1, status=$2, pending=$3
where request_id=$4 and device_id=$5 and pending=$6`
const deviceId = req.deviceId
return db.none(sql, [body, status, false, requestId, deviceId, true])
}
function errorHandler (err, req, res, next) { function errorHandler (err, req, res, next) {
const statusCode = err.name === 'HttpError' const statusCode = err.name === 'HttpError'
? err.code || 500 ? err.code || 500
@ -280,22 +156,14 @@ function errorHandler (err, req, res, next) {
logger.error(err) logger.error(err)
return updateCachedAction(req, json, statusCode) return res.status(statusCode).json(json)
.then(() => res.status(statusCode).json(json))
} }
function cacheAndRespond (req, res, _body, _status) { function respond (req, res, _body, _status) {
const status = _status || 200 const status = _status || 200
const body = _body || {} const body = _body || {}
return updateCachedAction(req, body, status) return res.status(status).json(body)
.then(() => res.status(status).json(body))
}
function pruneIdempotents () {
const sql = "delete from idempotents where created < now() - interval '24 hours'"
return db.none(sql)
} }
function httpError (msg, code) { function httpError (msg, code) {
@ -339,14 +207,11 @@ const skip = options.logLevel === 'debug'
const configRequiredRoutes = [ const configRequiredRoutes = [
'/poll', '/poll',
'/trade',
'/send',
'/cash_out',
'/dispense_ack',
'/event', '/event',
'/verify_user', '/verify_user',
'/verify_transaction', '/verify_transaction',
'/phone_code' '/phone_code',
'/tx'
] ]
const app = express() const app = express()
@ -364,7 +229,6 @@ app.use(populateDeviceId)
if (!devMode) app.use(authorize) if (!devMode) app.use(authorize)
app.use(configRequiredRoutes, populateSettings) app.use(configRequiredRoutes, populateSettings)
app.use(filterOldRequests) app.use(filterOldRequests)
app.post('*', cacheAction)
app.get('/poll', poll) app.get('/poll', poll)
app.post('/state', stateChange) app.post('/state', stateChange)
@ -435,6 +299,4 @@ function populateSettings (req, res, next) {
.catch(next) .catch(next)
} }
setInterval(pruneIdempotents, 60000)
module.exports = {app, localApp} module.exports = {app, localApp}

View file

@ -1,35 +1,8 @@
const db = require('./db') const CashInTx = require('./cash-in-tx')
const pgp = require('pg-promise')()
function postCashIn (tx) { function post (tx, pi) {
const TransactionMode = pgp.txMode.TransactionMode if (tx.direction === 'cashIn') return CashInTx.post(tx, pi)
const isolationLevel = pgp.txMode.isolationLevel if (tx.direction === 'cashOut') throw new Error('not implemented')
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select * from cash_in_txs where id=$1'
return t.oneOrNone(sql, [tx.id])
.then(row => {
const newTx = executeCashInTxChange(tx, row)
if (row) return updateCashInTx(newTx)
insertCashInTx(newTx)
})
}
transaction.txMode = tmSRD
return db.tx(transaction)
// retry failed
}
function postCashOut (tx) {
throw new Error('not implemented')
}
function post (tx) {
if (tx.direction === 'cashIn') return postCashIn(tx)
if (tx.direction === 'cashOut') return postCashOut(tx)
return Promise.reject(new Error('No such tx direction: %s', tx.direction)) return Promise.reject(new Error('No such tx direction: %s', tx.direction))
} }

View file

@ -6,10 +6,21 @@ const FETCH_INTERVAL = 5000
function fetchWallet (settings, cryptoCode) { function fetchWallet (settings, cryptoCode) {
return Promise.resolve() return Promise.resolve()
.then(() => { .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)
}
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} return {wallet, account}
}) })
} }
@ -21,11 +32,15 @@ function balance (settings, cryptoCode) {
} }
function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) { function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) {
console.log('DEBUG40')
return fetchWallet(settings, cryptoCode) return fetchWallet(settings, cryptoCode)
.then(r => { .then(r => {
console.log('DEBUG41')
return r.wallet.sendCoins(r.account, toAddress, cryptoAtoms, cryptoCode) return r.wallet.sendCoins(r.account, toAddress, cryptoAtoms, cryptoCode)
.then(res => { .then(res => {
console.log('DEBUG42')
mem.clear(module.exports.balance) mem.clear(module.exports.balance)
console.log('DEBUG43: %j', res)
return res return res
}) })
}) })

View file

@ -2,7 +2,8 @@ var db = require('./db')
exports.up = function (next) { exports.up = function (next) {
var sql = [ var sql = [
'alter table cash_in_txs add column sent boolean not null default false' 'alter table cash_in_txs add column send boolean not null default false',
'alter table cash_in_txs rename currency_code to fiat_code'
] ]
db.multi(sql, next) db.multi(sql, next)
} }