merge lightning

This commit is contained in:
Josh Harvey 2018-03-28 22:40:28 +03:00
commit 6fa6ac1647
92 changed files with 2688 additions and 2651 deletions

86
INSTALL.md Normal file
View file

@ -0,0 +1,86 @@
# Installation on Ubuntu 16.04
Installation for other distros may be slightly different. This assumes nodejs 8 and npm are already installed. All of this is done in the lamassu-server directory.
## Packages
```
sudo apt-get update
sudo apt-get install postgresql postgresql-contrib postgresql-server-dev-9.5 libpq-dev git
```
## Set up PostgreSQL
```
sudo -u postgres createdb lamassu
sudo -u postgres psql postgres
```
In ``psql``, run the following and set password to ``postgres123``:
```
\password postgres
ctrl-d
```
## Install node modules
Ignore any warnings.
```
npm install
```
## Generate certificates
```
bash bin/cert-gen.sh
```
Note: This will create a ``.lamassu`` directory in your home directory.
## Set up database
Important: lamassu-migrate currently gripes about a QueryResultError. Ignore this, it works anyway. Also, ignore Debug lines from lamassu-apply-defaults.
```
node bin/lamassu-migrate
node bin/lamassu-apply-defaults
```
## Register admin user
You'll use this generated URL in the brower in moment.
```
node bin/lamassu-register admin
```
## Run lamassu-admin-server
In first terminal window:
```
node bin/lamassu-admin-server --dev
```
## Complete configuration
Paste the URL from lamassu-register exactly as output, into a browser (chrome or firefox).
**Important**: the host must be localhost. Tell your browser to trust the certificate even though it's not signed by a CA.
Go to all the required, unconfigured red fields and choose some values. Choose mock services whenever available.
## Run lamassu-server
In second terminal window:
```
node bin/lamassu-server --mockSms
```
## Add a lamassu-machine
Click on ``+ Add Machine`` in the sidebar. Type in a name for your machine and click **Pair**. Open up development tools to show the JavaScript console and copy the totem. You will use this to run lamassu-machine. This pairing totem expires after an hour.
Now continue with lamassu-machine instructions from the ``INSTALL.md`` file in lamassu-machine.

View file

@ -4,11 +4,13 @@ set -e
DOMAIN=localhost
CONFIG_DIR=$HOME/.lamassu
LOG_FILE=/tmp/cert-gen.log
CERT_DIR=$PWD/certs
KEY_DIR=$PWD/certs
CONFIG_DIR=$HOME/.lamassu
LAMASSU_CA_PATH=$PWD/Lamassu_CA.pem
MIGRATE_STATE_PATH=$CONFIG_DIR/.migrate
POSTGRES_PASS=postgres123
mkdir -p $CERT_DIR
mkdir -p $CONFIG_DIR >> $LOG_FILE 2>&1
@ -49,11 +51,7 @@ openssl genrsa \
openssl req -new \
-key $SERVER_KEY_PATH \
-out /tmp/Lamassu_OP.csr.pem \
-subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator/CN=$IP" \
-reqexts SAN \
-sha256 \
-config <(cat /etc/ssl/openssl.cnf \
<(printf "[SAN]\nsubjectAltName=IP.1:$IP")) \
-subj "/C=IS/ST=/L=Reykjavik/O=Lamassu Operator/CN=$DOMAIN" \
>> $LOG_FILE 2>&1
openssl x509 \
@ -62,22 +60,22 @@ openssl x509 \
-CAkey $CA_KEY_PATH \
-CAcreateserial \
-out $SERVER_CERT_PATH \
-extfile <(cat /etc/ssl/openssl.cnf \
<(printf "[SAN]\nsubjectAltName=IP.1:$IP")) \
-extensions SAN \
-days 3650 >> $LOG_FILE 2>&1
rm /tmp/Lamassu_OP.csr.pem
cat <<EOF > $CONFIG_DIR/lamassu.json
{
"postgresql": "psql://lamassu:lamassu@localhost/lamassu",
"postgresql": "psql://postgres:$POSTGRES_PASS@localhost/lamassu",
"seedPath": "$SEED_FILE",
"caPath": "$CA_PATH",
"certPath": "$SERVER_CERT_PATH",
"keyPath": "$SERVER_KEY_PATH",
"hostname": "$DOMAIN",
"logLevel": "debug"
"logLevel": "debug",
"lamassuCaPath": "$LAMASSU_CA_PATH",
"lamassuServerPath": "$PWD",
"migrateStatePath": "$MIGRATE_STATE_PATH"
}
EOF

View file

@ -7,44 +7,44 @@ var db = pgp(psqlUrl)
db.manyOrNone(`select * from transactions where incoming=false
and stage='final_request' and authority='machine'`)
.then(rs =>
db.tx(t =>
t.batch(rs.map(r => db.none(`insert into cash_in_txs (session_id,
.then(rs =>
db.tx(t =>
t.batch(rs.map(r => db.none(`insert into cash_in_txs (session_id,
device_fingerprint, to_address, crypto_atoms, crypto_code, fiat,
currency_code, fee, tx_hash, error, created) values ($1, $2, $3, $4, $5,
$6, $7, $8, $9, $10, $11)`, [r.session_id, r.device_fingerprint,
r.to_address, r.satoshis, r.crypto_code, r.fiat, r.currency_code, r.fee,
r.tx_hash, r.error, r.created]))
r.to_address, r.satoshis, r.crypto_code, r.fiat, r.currency_code, r.fee,
r.tx_hash, r.error, r.created]))
)
)
)
)
.then(() => db.manyOrNone(`select * from transactions where incoming=true
.then(() => db.manyOrNone(`select * from transactions where incoming=true
and stage='initial_request' and authority='pending'`))
.then(rs =>
db.tx(t =>
t.batch(rs.map(r => db.none(`insert into cash_out_txs (session_id,
.then(rs =>
db.tx(t =>
t.batch(rs.map(r => db.none(`insert into cash_out_txs (session_id,
device_fingerprint, to_address, crypto_atoms, crypto_code, fiat,
currency_code, tx_hash, phone, error, created) values ($1, $2, $3, $4, $5,
$6, $7, $8, $9, $10, $11)`, [r.session_id, r.device_fingerprint,
r.to_address, r.satoshis, r.crypto_code, r.fiat, r.currency_code,
r.tx_hash, r.phone, r.error, r.created]))
r.to_address, r.satoshis, r.crypto_code, r.fiat, r.currency_code,
r.tx_hash, r.phone, r.error, r.created]))
)
)
)
)
.then(() => db.manyOrNone(`select * from transactions where incoming=true
.then(() => db.manyOrNone(`select * from transactions where incoming=true
and stage='dispense' and authority='authorized'`))
.then(rs =>
db.tx(t =>
t.batch(rs.map(r =>
db.none(`update cash_out_txs set dispensed=true where session_id=$1`, [r.session_id])
.then(() => db.none(`insert into cash_out_actions (session_id, action,
.then(rs =>
db.tx(t =>
t.batch(rs.map(r =>
db.none(`update cash_out_txs set dispensed=true where session_id=$1`, [r.session_id])
.then(() => db.none(`insert into cash_out_actions (session_id, action,
created) values ($1, $2, $3)`, [r.session_id, 'dispensed', r.created]))
))
))
)
)
)
.then(() => pgp.end())
.then(() => console.log('Success.'))
.catch(e => {
console.log(e)
pgp.end()
})
.then(() => pgp.end())
.then(() => console.log('Success.'))
.catch(e => {
console.log(e)
pgp.end()
})

View file

@ -2,7 +2,7 @@
const uuid = require('@fczbkk/uuid4')
const tx = require('../lib/cash-out-tx')
const tx = require('../lib/cash-out/cash-out-tx.js')
const argv = process.argv.slice(2)

View file

@ -2,11 +2,11 @@ const settingsLoader = require('../lib/settings-loader')
const pp = require('../lib/pp')
settingsLoader.loadLatest()
.then(r => {
pp('config')(r)
process.exit(0)
})
.catch(e => {
console.log(e.stack)
process.exit(1)
})
.then(r => {
pp('config')(r)
process.exit(0)
})
.catch(e => {
console.log(e.stack)
process.exit(1)
})

View file

@ -3,7 +3,7 @@ set -e
supervisorctl stop lamassu-server
supervisorctl stop lamassu-admin-server
npm -g install lamassu/lamassu-server#v5
npm -g install lamassu/lamassu-server#v5 --unsafe-perm
lamassu-migrate
supervisorctl start lamassu-server
supervisorctl start lamassu-admin-server

View file

@ -8,38 +8,38 @@ const psqlUrl = require('../lib/options').postgresql
const db = pgp(psqlUrl)
db.many('select data from user_config', 'exchanges')
.then(rows => {
const config = rows.filter(r => r.type === 'exchanges')[0].data
const brain = rows.filter(r => r.type === 'unit')[0].data
const settings = config.exchanges.settings
const compliance = settings.compliance
const newConfig = {
global: {
cashInTransactionLimit: compliance.maximum.limit,
cashOutTransactionLimit: settings.fiatTxLimit,
cashInCommission: settings.commission,
cashOutCommission: settings.fiatCommission || settings.commission,
idVerificationEnabled: compliance.idVerificationEnabled,
idVerificationLimit: compliance.idVerificationLimit,
lowBalanceMargin: settings.lowBalanceMargin,
zeroConfLimit: settings.zeroConfLimit,
fiatCurrency: settings.currency,
topCashOutDenomination: settings.cartridges[0],
bottomCashOutDenomination: settings.cartridges[1],
virtualCashOutDenomination: settings.virtualCartridges[0],
machineLanguages: brain.locale.localeInfo.primaryLocales,
coins: settings.coins
},
accounts: settings.plugins.settings
}
.then(rows => {
const config = rows.filter(r => r.type === 'exchanges')[0].data
const brain = rows.filter(r => r.type === 'unit')[0].data
const settings = config.exchanges.settings
const compliance = settings.compliance
const newConfig = {
global: {
cashInTransactionLimit: compliance.maximum.limit,
cashOutTransactionLimit: settings.fiatTxLimit,
cashInCommission: settings.commission,
cashOutCommission: settings.fiatCommission || settings.commission,
idVerificationEnabled: compliance.idVerificationEnabled,
idVerificationLimit: compliance.idVerificationLimit,
lowBalanceMargin: settings.lowBalanceMargin,
zeroConfLimit: settings.zeroConfLimit,
fiatCurrency: settings.currency,
topCashOutDenomination: settings.cartridges[0],
bottomCashOutDenomination: settings.cartridges[1],
virtualCashOutDenomination: settings.virtualCartridges[0],
machineLanguages: brain.locale.localeInfo.primaryLocales,
coins: settings.coins
},
accounts: settings.plugins.settings
}
db.none('insert into user_config (type, data) values ($1, $2)', ['global', newConfig])
.then(() => {
console.log('Success.')
process.exit(0)
db.none('insert into user_config (type, data) values ($1, $2)', ['global', newConfig])
.then(() => {
console.log('Success.')
process.exit(0)
})
.catch(err => {
console.error('Error: %s', err)
process.exit(1)
})
})
.catch(err => {
console.error('Error: %s', err)
process.exit(1)
})
})

View file

@ -13,7 +13,7 @@ const headers = {
const body = JSON.stringify({tx: tx})
got('http://localhost:3000/dispense', {body: body, json: true, headers: headers})
.then(res => {
console.log(res.body)
})
.catch(console.log)
.then(res => {
console.log(res.body)
})
.catch(console.log)

View file

@ -15,17 +15,17 @@ db.init(psqlUrl)
notifier.init(db, getBalances, {lowBalanceThreshold: 10})
console.log('DEBUG0')
notifier.checkStatus()
.then(function (alertRec) {
console.log('DEBUG1')
console.log('%j', alertRec)
var subject = notifier.alertSubject(alertRec)
console.log(subject)
var body = notifier.printEmailAlerts(alertRec)
console.log(body)
console.log(notifier.alertFingerprint(alertRec))
process.exit(0)
})
.catch(function (err) {
console.log(err.stack)
process.exit(1)
})
.then(function (alertRec) {
console.log('DEBUG1')
console.log('%j', alertRec)
var subject = notifier.alertSubject(alertRec)
console.log(subject)
var body = notifier.printEmailAlerts(alertRec)
console.log(body)
console.log(notifier.alertFingerprint(alertRec))
process.exit(0)
})
.catch(function (err) {
console.log(err.stack)
process.exit(1)
})

View file

@ -17,13 +17,13 @@ var rec = {
var db = config.connection
config.loadConfig(db)
.then(function (config) {
plugins.configure(config)
plugins.sendMessage(rec)
.then(function () {
console.log('Success.')
.then(function (config) {
plugins.configure(config)
plugins.sendMessage(rec)
.then(function () {
console.log('Success.')
})
.catch(function (err) {
console.log(err.stack)
})
})
.catch(function (err) {
console.log(err.stack)
})
})

View file

@ -8,22 +8,21 @@ const schemas = ph.loadSchemas()
function fetchAccounts () {
return db.oneOrNone('select data from user_config where type=$1', ['accounts'])
.then(row => {
.then(row => {
// Hard code this for now
const accounts = [{
code: 'blockcypher',
display: 'Blockcypher',
fields: [
{ code: 'confidenceFactor', display: 'Confidence Factor', fieldType: 'integer', required: true, value: 90 }
]
}]
const accounts = [{
code: 'blockcypher',
display: 'Blockcypher',
fields: [
{ code: 'confidenceFactor', display: 'Confidence Factor', fieldType: 'integer', required: true, value: 90 }
]
}]
return row
? Promise.resolve(row.data.accounts)
: db.none('insert into user_config (type, data, valid) values ($1, $2, $3)', ['accounts', {accounts}, true])
.then(fetchAccounts)
})
return row
? Promise.resolve(row.data.accounts)
: db.none('insert into user_config (type, data, valid) values ($1, $2, $3)', ['accounts', {accounts}, true])
.then(fetchAccounts)
})
}
function selectedAccounts () {

View file

@ -53,7 +53,7 @@ module.exports = {run}
function dbNotify () {
return got.post('http://localhost:3030/dbChange')
.catch(e => console.error('Error: lamassu-server not responding'))
.catch(e => console.error('Error: lamassu-server not responding'))
}
const skip = (req, res) => req.path === '/api/status/' && res.statusCode === 200
@ -81,23 +81,23 @@ app.get('/api/totem', (req, res) => {
if (!name) return res.status(400).send('Name is required')
return pairing.totem(hostname, name)
.then(totem => res.send(totem))
.then(totem => res.send(totem))
})
app.get('/api/accounts', (req, res) => {
accounts.selectedAccounts()
.then(accounts => res.json({accounts: accounts}))
.then(accounts => res.json({accounts: accounts}))
})
app.get('/api/account/:account', (req, res) => {
accounts.getAccount(req.params.account)
.then(account => res.json(account))
.then(account => res.json(account))
})
app.post('/api/account', (req, res) => {
return accounts.updateAccount(req.body)
.then(account => res.json(account))
.then(() => dbNotify())
.then(account => res.json(account))
.then(() => dbNotify())
})
app.get('/api/config/:config', (req, res) =>
@ -105,133 +105,133 @@ app.get('/api/config/:config', (req, res) =>
app.post('/api/config', (req, res, next) => {
config.saveConfigGroup(req.body)
.then(c => res.json(c))
.then(() => dbNotify())
.catch(next)
.then(c => res.json(c))
.then(() => dbNotify())
.catch(next)
})
app.get('/api/accounts/account/:account', (req, res) => {
accounts.getAccount(req.params.account)
.then(r => res.send(r))
.then(r => res.send(r))
})
app.get('/api/machines', (req, res) => {
machineLoader.getMachineNames()
.then(r => res.send({machines: r}))
.then(r => res.send({machines: r}))
})
app.post('/api/machines', (req, res) => {
machineLoader.setMachine(req.body)
.then(() => machineLoader.getMachineNames())
.then(r => res.send({machines: r}))
.then(() => dbNotify())
.then(() => machineLoader.getMachineNames())
.then(r => res.send({machines: r}))
.then(() => dbNotify())
})
app.get('/api/funding', (req, res) => {
return funding.getFunding()
.then(r => res.json(r))
.then(r => res.json(r))
})
app.get('/api/funding/:cryptoCode', (req, res) => {
const cryptoCode = req.params.cryptoCode
return funding.getFunding(cryptoCode)
.then(r => res.json(r))
.then(r => res.json(r))
})
app.get('/api/status', (req, res, next) => {
return Promise.all([server.status(), config.validateCurrentConfig()])
.then(([serverStatus, invalidConfigGroups]) => res.send({
server: serverStatus,
invalidConfigGroups
}))
.catch(next)
.then(([serverStatus, invalidConfigGroups]) => res.send({
server: serverStatus,
invalidConfigGroups
}))
.catch(next)
})
app.get('/api/transactions', (req, res, next) => {
return transactions.batch()
.then(r => res.send({transactions: r}))
.catch(next)
.then(r => res.send({transactions: r}))
.catch(next)
})
app.get('/api/transaction/:id', (req, res, next) => {
return transactions.single(req.params.id)
.then(r => {
if (!r) return res.status(404).send({Error: 'Not found'})
return res.send(r)
})
.then(r => {
if (!r) return res.status(404).send({Error: 'Not found'})
return res.send(r)
})
})
app.patch('/api/transaction/:id', (req, res, next) => {
if (!req.query.cancel) return res.status(400).send({Error: 'Requires cancel'})
return transactions.cancel(req.params.id)
.then(r => {
return res.send(r)
})
.catch(() => res.status(404).send({Error: 'Not found'}))
.then(r => {
return res.send(r)
})
.catch(() => res.status(404).send({Error: 'Not found'}))
})
app.get('/api/customers', (req, res, next) => {
return customers.batch()
.then(r => res.send({customers: r}))
.catch(next)
.then(r => res.send({customers: r}))
.catch(next)
})
app.get('/api/customer/:id', (req, res, next) => {
return customers.getById(req.params.id)
.then(r => {
if (!r) return res.status(404).send({Error: 'Not found'})
return res.send(r)
})
.then(r => {
if (!r) return res.status(404).send({Error: 'Not found'})
return res.send(r)
})
})
app.get('/api/logs/:deviceId', (req, res, next) => {
return logs.getMachineLogs(req.params.deviceId)
.then(r => res.send(r))
.catch(next)
.then(r => res.send(r))
.catch(next)
})
app.get('/api/logs', (req, res, next) => {
return machineLoader.getMachines()
.then(machines => {
const firstMachine = _.first(machines)
if (!firstMachine) return res.status(404).send({Error: 'No machines'})
return logs.getMachineLogs(firstMachine.deviceId)
.then(r => res.send(r))
})
.catch(next)
.then(machines => {
const firstMachine = _.first(machines)
if (!firstMachine) return res.status(404).send({Error: 'No machines'})
return logs.getMachineLogs(firstMachine.deviceId)
.then(r => res.send(r))
})
.catch(next)
})
app.get('/api/support_logs', (req, res, next) => {
return supportLogs.batch()
.then(supportLogs => res.send({ supportLogs }))
.catch(next)
.then(supportLogs => res.send({ supportLogs }))
.catch(next)
})
app.get('/api/support_logs/logs', (req, res, next) => {
return supportLogs.get(req.query.supportLogId)
.then(log => (!_.isNil(log) && !_.isEmpty(log)) ? log : supportLogs.batch().then(_.first))
.then(result => {
const log = result || {}
return logs.getMachineLogs(log.deviceId, log.timestamp)
})
.then(r => res.send(r))
.catch(next)
.then(log => (!_.isNil(log) && !_.isEmpty(log)) ? log : supportLogs.batch().then(_.first))
.then(result => {
const log = result || {}
return logs.getMachineLogs(log.deviceId, log.timestamp)
})
.then(r => res.send(r))
.catch(next)
})
app.post('/api/support_logs', (req, res, next) => {
return supportLogs.insert(req.query.deviceId)
.then(r => res.send(r))
.catch(next)
.then(r => res.send(r))
.catch(next)
})
app.patch('/api/customer/:id', (req, res, next) => {
if (!req.params.id) return res.status(400).send({Error: 'Requires id'})
const token = req.token || req.cookies.token
return customers.update(req.params.id, req.query, token)
.then(r => res.send(r))
.catch(() => res.status(404).send({Error: 'Not found'}))
.then(r => res.send(r))
.catch(() => res.status(404).send({Error: 'Not found'}))
})
app.use((err, req, res, next) => {
@ -253,35 +253,35 @@ function register (req, res, next) {
if (!otp) return next()
return login.register(otp)
.then(r => {
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
.then(r => {
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
// Maybe user is using old registration key, attempt to authenticate
if (!r.success) return next()
// Maybe user is using old registration key, attempt to authenticate
if (!r.success) return next()
const cookieOpts = {
httpOnly: true,
secure: true,
domain: hostname,
sameSite: true,
expires: NEVER
}
const cookieOpts = {
httpOnly: true,
secure: true,
domain: hostname,
sameSite: true,
expires: NEVER
}
const token = r.token
req.token = token
res.cookie('token', token, cookieOpts)
next()
})
const token = r.token
req.token = token
res.cookie('token', token, cookieOpts)
next()
})
}
function authenticate (req, res, next) {
const token = req.token || req.cookies.token
return login.authenticate(token)
.then(success => {
if (!success) return res.status(401).send('Authentication failed')
next()
})
.then(success => {
if (!success) return res.status(401).send('Authentication failed')
next()
})
}
process.on('unhandledRejection', err => {
@ -303,27 +303,27 @@ const wss = new WebSocket.Server({server: webServer})
function establishSocket (ws, token) {
return login.authenticate(token)
.then(success => {
if (!success) return ws.close(1008, 'Authentication error')
.then(success => {
if (!success) return ws.close(1008, 'Authentication error')
const listener = data => {
ws.send(JSON.stringify(data))
}
const listener = data => {
ws.send(JSON.stringify(data))
}
// Reauthenticate every once in a while, in case token expired
setInterval(() => {
return login.authenticate(token)
.then(success => {
if (!success) {
socketEmitter.removeListener('message', listener)
ws.close()
}
})
}, REAUTHENTICATE_INTERVAL)
// Reauthenticate every once in a while, in case token expired
setInterval(() => {
return login.authenticate(token)
.then(success => {
if (!success) {
socketEmitter.removeListener('message', listener)
ws.close()
}
})
}, REAUTHENTICATE_INTERVAL)
socketEmitter.on('message', listener)
ws.send('Testing123')
})
socketEmitter.on('message', listener)
ws.send('Testing123')
})
}
wss.on('connection', ws => {

View file

@ -32,25 +32,25 @@ const certOptions = {
app.get('/api/support_logs', (req, res, next) => {
return supportLogs.batch()
.then(supportLogs => res.send({ supportLogs }))
.catch(next)
.then(supportLogs => res.send({ supportLogs }))
.catch(next)
})
app.get('/api/support_logs/logs', (req, res, next) => {
return supportLogs.get(req.query.supportLogId)
.then(log => (!_.isNil(log) && !_.isEmpty(log)) ? log : supportLogs.batch().then(_.first))
.then(result => {
const log = result || {}
return logs.getUnlimitedMachineLogs(log.deviceId, log.timestamp)
})
.then(r => res.send(r))
.catch(next)
.then(log => (!_.isNil(log) && !_.isEmpty(log)) ? log : supportLogs.batch().then(_.first))
.then(result => {
const log = result || {}
return logs.getUnlimitedMachineLogs(log.deviceId, log.timestamp)
})
.then(r => res.send(r))
.catch(next)
})
app.post('/api/support_logs', (req, res, next) => {
return supportLogs.insert(req.query.deviceId)
.then(r => res.send(r))
.catch(next)
.then(r => res.send(r))
.catch(next)
})
function run (port) {

View file

@ -1,7 +1,6 @@
const pify = require('pify')
const fs = pify(require('fs'))
const path = require('path')
const R = require('ramda')
const _ = require('lodash/fp')
const currencies = require('../../currencies.json')
@ -19,7 +18,7 @@ function fetchSchema () {
const schemaPath = path.resolve(options.lamassuServerPath, 'lamassu-schema.json')
return fs.readFile(schemaPath)
.then(JSON.parse)
.then(JSON.parse)
}
function fetchConfig () {
@ -27,7 +26,7 @@ function fetchConfig () {
order by id desc limit 1`
return db.oneOrNone(sql, ['config'])
.then(row => row ? row.data.config : [])
.then(row => row ? row.data.config : [])
}
function allScopes (cryptoScopes, machineScopes) {
@ -51,28 +50,28 @@ function allMachineScopes (machineList, machineScope) {
function getCryptos (config, machineList) {
const scopes = allScopes(['global'], allMachineScopes(machineList, 'both'))
const scoped = scope => configManager.scopedValue(scope[0], scope[1], 'cryptoCurrencies', config)
return scopes.reduce((acc, scope) => R.union(acc, scoped(scope)), [])
return scopes.reduce((acc, scope) => _.union(acc, scoped(scope)), [])
}
function getGroup (schema, fieldCode) {
return schema.groups.find(group => group.fields.find(R.equals(fieldCode)))
return schema.groups.find(group => group.fields.find(_.isEqual(fieldCode)))
}
function getField (schema, group, fieldCode) {
if (!group) group = getGroup(schema, fieldCode)
const field = schema.fields.find(r => r.code === fieldCode)
return R.merge(R.pick(['cryptoScope', 'machineScope'], group), field)
return _.merge(_.pick(['cryptoScope', 'machineScope'], group), field)
}
const fetchMachines = () => machineLoader.getMachines()
.then(machineList => machineList.map(r => r.deviceId))
.then(machineList => machineList.map(r => r.deviceId))
function validateCurrentConfig () {
return fetchConfig()
.then(configValidate.validateRequires)
.then(configValidate.validateRequires)
}
function decorateEnabledIf (schemaFields, schemaField) {
const decorateEnabledIf = _.curry((schemaFields, schemaField) => {
const code = schemaField.fieldLocator.code
const field = _.find(f => f.code === code, schemaFields)
@ -80,48 +79,56 @@ function decorateEnabledIf (schemaFields, schemaField) {
fieldEnabledIfAny: field.enabledIfAny || [],
fieldEnabledIfAll: field.enabledIfAll || []
})
}
})
function fetchConfigGroup (code) {
const fieldLocatorCodeEq = R.pathEq(['fieldLocator', 'code'])
const fieldLocatorCodeEq = _.matchesProperty(['fieldLocator', 'code'])
return Promise.all([fetchSchema(), fetchData(), fetchConfig(), fetchMachines()])
.then(([schema, data, config, machineList]) => {
const groupSchema = schema.groups.find(r => r.code === code)
.then(([schema, data, config, machineList]) => {
const groupSchema = schema.groups.find(r => r.code === code)
if (!groupSchema) throw new Error('No such group schema: ' + code)
if (!groupSchema) throw new Error('No such group schema: ' + code)
const schemaFields = groupSchema.fields
.map(R.curry(getField)(schema, groupSchema))
.map(f => _.assign(f, {
fieldEnabledIfAny: f.enabledIfAny || [],
fieldEnabledIfAll: f.enabledIfAll || []
}))
const schemaFields = groupSchema.fields
.map(_.curry(getField)(schema, groupSchema))
.map(f => _.assign(f, {
fieldEnabledIfAny: f.enabledIfAny || [],
fieldEnabledIfAll: f.enabledIfAll || []
}))
const candidateFields = [
schemaFields.map(R.prop('requiredIf')),
schemaFields.map(R.prop('enabledIfAny')),
schemaFields.map(R.prop('enabledIfAll')),
groupSchema.fields,
'fiatCurrency'
]
const configFields = R.uniq(R.flatten(candidateFields)).filter(R.identity)
const candidateFields = [
schemaFields.map(_.get('requiredIf')),
schemaFields.map(_.get('enabledIfAny')),
schemaFields.map(_.get('enabledIfAll')),
groupSchema.fields,
'fiatCurrency'
]
const reducer = (acc, configField) => {
return acc.concat(config.filter(fieldLocatorCodeEq(configField)))
}
const smush = _.flow(_.flattenDeep, _.compact, _.uniq)
const configFields = smush(candidateFields)
const values = _.map(f => decorateEnabledIf(schema.fields, f), configFields.reduce(reducer, []))
// Expand this to check against full schema
const fieldValidator = field => !_.isNil(_.get('fieldLocator.fieldScope.crypto', field))
groupSchema.fields = undefined
groupSchema.entries = schemaFields
const reducer = (acc, configField) => {
return acc.concat(config.filter(fieldLocatorCodeEq(configField)))
}
return {
schema: groupSchema,
values,
selectedCryptos: getCryptos(config, machineList),
data
}
})
const reducedFields = _.filter(fieldValidator, configFields.reduce(reducer, []))
const values = _.map(decorateEnabledIf(schema.fields), reducedFields)
groupSchema.fields = undefined
groupSchema.entries = schemaFields
const selectedCryptos = _.defaultTo([], getCryptos(config, machineList))
return {
schema: groupSchema,
values,
selectedCryptos,
data
}
})
}
function massageCurrencies (currencies) {
@ -130,10 +137,10 @@ function massageCurrencies (currencies) {
display: r['Currency']
})
const top5Codes = ['USD', 'EUR', 'GBP', 'CAD', 'AUD']
const mapped = R.map(convert, currencies)
const codeToRec = code => R.find(R.propEq('code', code), mapped)
const top5 = R.map(codeToRec, top5Codes)
const raw = R.uniqBy(R.prop('code'), R.concat(top5, mapped))
const mapped = _.map(convert, currencies)
const codeToRec = code => _.find(_.matchesProperty('code', code), mapped)
const top5 = _.map(codeToRec, top5Codes)
const raw = _.uniqBy(_.get('code'), _.concat(top5, mapped))
return raw.filter(r => r.code[0] !== 'X' && r.display.indexOf('(') === -1)
}
@ -154,55 +161,55 @@ const ALL_CRYPTOS = ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']
function fetchData () {
return machineLoader.getMachineNames()
.then(machineList => ({
currencies: massageCurrencies(currencies),
cryptoCurrencies: [
{crypto: 'BTC', display: 'Bitcoin'},
{crypto: 'ETH', display: 'Ethereum'},
{crypto: 'LTC', display: 'Litecoin'},
{crypto: 'DASH', display: 'Dash'},
{crypto: 'ZEC', display: 'Zcash'},
{crypto: 'BCH', display: 'BCH'}
],
languages: languages,
countries,
accounts: [
{code: 'bitpay', display: 'Bitpay', class: 'ticker', cryptos: ['BTC', 'BCH']},
{code: 'kraken', display: 'Kraken', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']},
{code: 'bitstamp', display: 'Bitstamp', class: 'ticker', cryptos: ['BTC', 'LTC']},
{code: 'coinbase', display: 'Coinbase', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']},
{code: 'mock-ticker', display: 'Mock ticker', class: 'ticker', cryptos: ALL_CRYPTOS},
{code: 'bitcoind', display: 'bitcoind', class: 'wallet', cryptos: ['BTC']},
{code: 'lnd', display: 'Lightning Network', class: 'wallet', cryptos: ['BTC']},
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']},
{code: 'zcashd', display: 'zcashd', class: 'wallet', cryptos: ['ZEC']},
{code: 'litecoind', display: 'litecoind', class: 'wallet', cryptos: ['LTC']},
{code: 'dashd', display: 'dashd', class: 'wallet', cryptos: ['DASH']},
{code: 'bitcoincashd', display: 'bitcoincashd', class: 'wallet', cryptos: ['BCH']},
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
{code: 'bitstamp', display: 'Bitstamp', class: 'exchange', cryptos: ['BTC', 'LTC']},
{code: 'kraken', display: 'Kraken', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']},
{code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ALL_CRYPTOS},
{code: 'no-exchange', display: 'No exchange', class: 'exchange', cryptos: ALL_CRYPTOS},
{code: 'mock-exchange', display: 'Mock exchange', class: 'exchange', cryptos: ALL_CRYPTOS},
{code: 'mock-sms', display: 'Mock SMS', class: 'sms'},
{code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
{code: 'twilio', display: 'Twilio', class: 'sms'},
{code: 'mailjet', display: 'Mailjet', class: 'email'},
{code: 'all-zero-conf', display: 'Always 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']},
{code: 'no-zero-conf', display: 'Always 1-conf', class: 'zeroConf', cryptos: ALL_CRYPTOS},
{code: 'blockcypher', display: 'Blockcypher', class: 'zeroConf', cryptos: ['BTC']},
{code: 'mock-zero-conf', display: 'Mock 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']}
],
machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name}))
}))
.then(machineList => ({
currencies: massageCurrencies(currencies),
cryptoCurrencies: [
{crypto: 'BTC', display: 'Bitcoin'},
{crypto: 'ETH', display: 'Ethereum'},
{crypto: 'LTC', display: 'Litecoin'},
{crypto: 'DASH', display: 'Dash'},
{crypto: 'ZEC', display: 'Zcash'},
{crypto: 'BCH', display: 'Bitcoin Cash'}
],
languages: languages,
countries,
accounts: [
{code: 'bitpay', display: 'Bitpay', class: 'ticker', cryptos: ['BTC', 'BCH']},
{code: 'kraken', display: 'Kraken', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']},
{code: 'bitstamp', display: 'Bitstamp', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']},
{code: 'coinbase', display: 'Coinbase', class: 'ticker', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']},
{code: 'mock-ticker', display: 'Mock ticker', class: 'ticker', cryptos: ALL_CRYPTOS},
{code: 'bitcoind', display: 'bitcoind', class: 'wallet', cryptos: ['BTC']},
{code: 'lnd', display: 'Lightning Network', class: 'wallet', cryptos: ['BTC']},
{code: 'geth', display: 'geth', class: 'wallet', cryptos: ['ETH']},
{code: 'zcashd', display: 'zcashd', class: 'wallet', cryptos: ['ZEC']},
{code: 'litecoind', display: 'litecoind', class: 'wallet', cryptos: ['LTC']},
{code: 'dashd', display: 'dashd', class: 'wallet', cryptos: ['DASH']},
{code: 'bitcoincashd', display: 'bitcoincashd', class: 'wallet', cryptos: ['BCH']},
{code: 'bitgo', display: 'BitGo', class: 'wallet', cryptos: ['BTC']},
{code: 'bitstamp', display: 'Bitstamp', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'BCH']},
{code: 'kraken', display: 'Kraken', class: 'exchange', cryptos: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH']},
{code: 'mock-wallet', display: 'Mock wallet', class: 'wallet', cryptos: ALL_CRYPTOS},
{code: 'no-exchange', display: 'No exchange', class: 'exchange', cryptos: ALL_CRYPTOS},
{code: 'mock-exchange', display: 'Mock exchange', class: 'exchange', cryptos: ALL_CRYPTOS},
{code: 'mock-sms', display: 'Mock SMS', class: 'sms'},
{code: 'mock-id-verify', display: 'Mock ID verifier', class: 'idVerifier'},
{code: 'twilio', display: 'Twilio', class: 'sms'},
{code: 'mailjet', display: 'Mailjet', class: 'email'},
{code: 'all-zero-conf', display: 'Always 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']},
{code: 'no-zero-conf', display: 'Always 1-conf', class: 'zeroConf', cryptos: ALL_CRYPTOS},
{code: 'blockcypher', display: 'Blockcypher', class: 'zeroConf', cryptos: ['BTC']},
{code: 'mock-zero-conf', display: 'Mock 0-conf', class: 'zeroConf', cryptos: ['BTC', 'ZEC', 'LTC', 'DASH', 'BCH']}
],
machines: machineList.map(machine => ({machine: machine.deviceId, display: machine.name}))
}))
}
function saveConfigGroup (results) {
if (results.values.length === 0) return fetchConfigGroup(results.groupCode)
return settingsLoader.modifyConfig(results.values)
.then(() => fetchConfigGroup(results.groupCode))
.then(() => fetchConfigGroup(results.groupCode))
}
module.exports = {

View file

@ -36,7 +36,7 @@ function getCryptos (config, machineList) {
function fetchMachines () {
return machineLoader.getMachines()
.then(machineList => machineList.map(r => r.deviceId))
.then(machineList => machineList.map(r => r.deviceId))
}
function computeCrypto (cryptoCode, _balance) {
@ -55,45 +55,45 @@ function computeFiat (rate, cryptoCode, _balance) {
function getFunding (_cryptoCode) {
return Promise.all([settingsLoader.loadLatest(), fetchMachines()])
.then(([settings, machineList]) => {
const config = configManager.unscoped(settings.config)
const cryptoCodes = getCryptos(settings.config, machineList)
const cryptoCode = _cryptoCode || cryptoCodes[0]
const fiatCode = config.fiatCurrency
const pareCoins = c => _.includes(c.cryptoCode, cryptoCodes)
const cryptoCurrencies = coinUtils.cryptoCurrencies()
const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies)
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
.then(([settings, machineList]) => {
const config = configManager.unscoped(settings.config)
const cryptoCodes = getCryptos(settings.config, machineList)
const cryptoCode = _cryptoCode || cryptoCodes[0]
const fiatCode = config.fiatCurrency
const pareCoins = c => _.includes(c.cryptoCode, cryptoCodes)
const cryptoCurrencies = coinUtils.cryptoCurrencies()
const cryptoDisplays = _.filter(pareCoins, cryptoCurrencies)
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
if (!cryptoRec) throw new Error(`Unsupported coin: ${cryptoCode}`)
if (!cryptoRec) throw new Error(`Unsupported coin: ${cryptoCode}`)
const promises = [
wallet.newFunding(settings, cryptoCode),
ticker.getRates(settings, fiatCode, cryptoCode)
]
const promises = [
wallet.newFunding(settings, cryptoCode),
ticker.getRates(settings, fiatCode, cryptoCode)
]
return Promise.all(promises)
.then(([fundingRec, ratesRec]) => {
const rates = ratesRec.rates
const rate = (rates.ask.add(rates.bid)).div(2)
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
const pending = fundingRec.fundingPendingBalance.sub(fundingConfirmedBalance)
const fiatPending = computeFiat(rate, cryptoCode, pending)
const fundingAddress = fundingRec.fundingAddress
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)
return Promise.all(promises)
.then(([fundingRec, ratesRec]) => {
const rates = ratesRec.rates
const rate = (rates.ask.add(rates.bid)).div(2)
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
const pending = fundingRec.fundingPendingBalance.sub(fundingConfirmedBalance)
const fiatPending = computeFiat(rate, cryptoCode, pending)
const fundingAddress = fundingRec.fundingAddress
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)
return {
cryptoCode,
cryptoDisplays,
fundingAddress,
fundingAddressUrl,
confirmedBalance: computeCrypto(cryptoCode, fundingConfirmedBalance).toFormat(5),
pending: computeCrypto(cryptoCode, pending).toFormat(5),
fiatConfirmedBalance: fiatConfirmedBalance.toFormat(2),
fiatPending: fiatPending.toFormat(2),
fiatCode
}
return {
cryptoCode,
cryptoDisplays,
fundingAddress,
fundingAddressUrl,
confirmedBalance: computeCrypto(cryptoCode, fundingConfirmedBalance).toFormat(5),
pending: computeCrypto(cryptoCode, pending).toFormat(5),
fiatConfirmedBalance: fiatConfirmedBalance.toFormat(2),
fiatPending: fiatPending.toFormat(2),
fiatCode
}
})
})
})
}

View file

@ -8,7 +8,7 @@ function generateOTP (name) {
const sql = 'insert into one_time_passes (token, name) values ($1, $2)'
return db.none(sql, [otp, name])
.then(() => otp)
.then(() => otp)
}
function validateOTP (otp) {
@ -17,22 +17,22 @@ function validateOTP (otp) {
returning name, created < now() - interval '1 hour' as expired`
return db.one(sql, [otp])
.then(r => ({success: !r.expired, expired: r.expired, name: r.name}))
.catch(() => ({success: false, expired: false}))
.then(r => ({success: !r.expired, expired: r.expired, name: r.name}))
.catch(() => ({success: false, expired: false}))
}
function register (otp) {
return validateOTP(otp)
.then(r => {
if (!r.success) return r
.then(r => {
if (!r.success) return r
const token = crypto.randomBytes(32).toString('hex')
const sql = 'insert into user_tokens (token, name) values ($1, $2)'
const token = crypto.randomBytes(32).toString('hex')
const sql = 'insert into user_tokens (token, name) values ($1, $2)'
return db.none(sql, [token, r.name])
.then(() => ({success: true, token: token}))
})
.catch(() => ({success: false, expired: false}))
return db.none(sql, [token, r.name])
.then(() => ({success: true, token: token}))
})
.catch(() => ({success: false, expired: false}))
}
function authenticate (token) {

View file

@ -17,17 +17,17 @@ function totem (hostname, name) {
const caPath = options.caPath
return readFile(caPath)
.then(data => {
const caHash = crypto.createHash('sha256').update(data).digest()
const token = crypto.randomBytes(32)
const hexToken = token.toString('hex')
const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex')
const buf = Buffer.concat([caHash, token, Buffer.from(hostname)])
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
.then(data => {
const caHash = crypto.createHash('sha256').update(data).digest()
const token = crypto.randomBytes(32)
const hexToken = token.toString('hex')
const caHexToken = crypto.createHash('sha256').update(hexToken).digest('hex')
const buf = Buffer.concat([caHash, token, Buffer.from(hostname)])
const sql = 'insert into pairing_tokens (token, name) values ($1, $3), ($2, $3)'
return db.none(sql, [hexToken, caHexToken, name])
.then(() => bsAlpha.encode(buf))
})
return db.none(sql, [hexToken, caHexToken, name])
.then(() => bsAlpha.encode(buf))
})
}
module.exports = {totem, unpair}

View file

@ -11,8 +11,8 @@ const CONSIDERED_UP_SECS = 30
function checkWasConfigured () {
return settingsLoader.loadLatest()
.then(() => true)
.catch(() => false)
.then(() => true)
.catch(() => false)
}
function machinesLastPing () {
@ -21,28 +21,28 @@ function machinesLastPing () {
group by device_id`
return Promise.all([machineLoader.getMachineNames(), db.any(sql)])
.then(([machines, events]) => {
if (machines.length === 0) return 'No paired machines'
.then(([machines, events]) => {
if (machines.length === 0) return 'No paired machines'
const addName = event => {
const machine = _.find(['deviceId', event.deviceId], machines)
if (!machine) return null
return _.set('name', machine.name, event)
}
const addName = event => {
const machine = _.find(['deviceId', event.deviceId], machines)
if (!machine) return null
return _.set('name', machine.name, event)
}
const mapper = _.flow(_.filter(row => row.age > CONSIDERED_UP_SECS), _.map(addName), _.compact)
const downRows = mapper(events)
const mapper = _.flow(_.filter(row => row.age > CONSIDERED_UP_SECS), _.map(addName), _.compact)
const downRows = mapper(events)
if (downRows.length === 0) return 'All machines are up'
if (downRows.length === 0) return 'All machines are up'
if (downRows.length === 1) {
const row = downRows[0]
const age = moment.duration(row.age, 'seconds')
return `${row.name} down for ${age.humanize()}`
}
if (downRows.length === 1) {
const row = downRows[0]
const age = moment.duration(row.age, 'seconds')
return `${row.name} down for ${age.humanize()}`
}
return 'Multiple machines down'
})
return 'Multiple machines down'
})
}
function status () {
@ -53,32 +53,32 @@ function status () {
limit 1`
return Promise.all([checkWasConfigured(), db.oneOrNone(sql, ['ping']), machinesLastPing()])
.then(([wasConfigured, statusRow, machineStatus]) => {
const age = statusRow && moment.duration(statusRow.age, 'seconds')
const up = statusRow ? statusRow.age < CONSIDERED_UP_SECS : false
const lastPing = statusRow && age.humanize()
.then(([wasConfigured, statusRow, machineStatus]) => {
const age = statusRow && moment.duration(statusRow.age, 'seconds')
const up = statusRow ? statusRow.age < CONSIDERED_UP_SECS : false
const lastPing = statusRow && age.humanize()
return settingsLoader.loadLatest()
.catch(() => null)
.then(settings => {
return getRates(settings)
.then(rates => ({wasConfigured, up, lastPing, rates, machineStatus}))
return settingsLoader.loadLatest()
.catch(() => null)
.then(settings => {
return getRates(settings)
.then(rates => ({wasConfigured, up, lastPing, rates, machineStatus}))
})
})
})
}
function getRates (settings) {
if (!settings) return Promise.resolve([])
return ticker.getRates(settings, 'USD', 'BTC')
.then(ratesRec => {
return [{
crypto: 'BTC',
bid: parseFloat(ratesRec.rates.bid),
ask: parseFloat(ratesRec.rates.ask)
}]
})
.catch(() => [])
.then(ratesRec => {
return [{
crypto: 'BTC',
bid: parseFloat(ratesRec.rates.bid),
ask: parseFloat(ratesRec.rates.ask)
}]
})
.catch(() => [])
}
module.exports = {status}

View file

@ -3,21 +3,21 @@ const _ = require('lodash/fp')
const db = require('../db')
const machineLoader = require('../machine-loader')
const tx = require('../tx')
const cashInTx = require('../cash-in-tx')
const cashInTx = require('../cash-in/cash-in-tx')
const NUM_RESULTS = 20
function addNames (txs) {
return machineLoader.getMachineNames()
.then(machines => {
const addName = tx => {
const machine = _.find(['deviceId', tx.deviceId], machines)
const name = machine ? machine.name : 'Unpaired'
return _.set('machineName', name, tx)
}
.then(machines => {
const addName = tx => {
const machine = _.find(['deviceId', tx.deviceId], machines)
const name = machine ? machine.name : 'Unpaired'
return _.set('machineName', name, tx)
}
return _.map(addName, txs)
})
return _.map(addName, txs)
})
}
const camelize = _.mapKeys(_.camelCase)
@ -36,7 +36,7 @@ function batch () {
order by created desc limit $1`
return Promise.all([db.any(cashInSql, [cashInTx.PENDING_INTERVAL, NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS])])
.then(packager)
.then(packager)
}
function single (txId) {
@ -56,13 +56,13 @@ function single (txId) {
db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId]),
db.oneOrNone(cashOutSql, [txId])
])
.then(packager)
.then(_.head)
.then(packager)
.then(_.head)
}
function cancel (txId) {
return tx.cancel(txId)
.then(() => single(txId))
.then(() => single(txId))
}
module.exports = {batch, single, cancel}

View file

@ -24,8 +24,8 @@ function run () {
}
const runner = () => runOnce()
.then(() => clearInterval(handler))
.catch(errorHandler)
.then(() => clearInterval(handler))
.catch(errorHandler)
const handler = setInterval(runner, 10000)
return runner()
@ -33,35 +33,35 @@ function run () {
function runOnce () {
return settingsLoader.loadLatest()
.then(settings => {
poller.start(settings)
.then(settings => {
poller.start(settings)
const httpsServerOptions = {
key: fs.readFileSync(options.keyPath),
cert: fs.readFileSync(options.certPath),
requestCert: true,
rejectUnauthorized: false
}
const httpsServerOptions = {
key: fs.readFileSync(options.keyPath),
cert: fs.readFileSync(options.certPath),
requestCert: true,
rejectUnauthorized: false
}
const server = devMode
? http.createServer(routes.app)
: https.createServer(httpsServerOptions, routes.app)
const server = devMode
? http.createServer(routes.app)
: https.createServer(httpsServerOptions, routes.app)
const port = argv.port || 3000
const localPort = 3030
const localServer = http.createServer(routes.localApp)
const port = argv.port || 3000
const localPort = 3030
const localServer = http.createServer(routes.localApp)
if (options.devMode) logger.info('In dev mode')
if (options.devMode) logger.info('In dev mode')
server.listen(port, () => {
console.log('lamassu-server listening on port ' +
server.listen(port, () => {
console.log('lamassu-server listening on port ' +
port + ' ' + (devMode ? '(http)' : '(https)'))
})
})
localServer.listen(localPort, 'localhost', () => {
console.log('lamassu-server listening on local port ' + localPort)
localServer.listen(localPort, 'localhost', () => {
console.log('lamassu-server listening on local port ' + localPort)
})
})
})
}
module.exports = {run}

View file

@ -13,36 +13,36 @@ module.exports = {run}
function run () {
return Promise.resolve()
.then(() => {
schema.groups.forEach(group => {
return group.fields.forEach(fieldCode => {
const field = schema.fields.find(r => r.code === fieldCode)
if (!field) throw new Error('No such field: ' + fieldCode)
if (_.isNil(field.default)) return
if (group.machineScope === 'specific') return
.then(() => {
schema.groups.forEach(group => {
return group.fields.forEach(fieldCode => {
const field = schema.fields.find(r => r.code === fieldCode)
if (!field) throw new Error('No such field: ' + fieldCode)
if (_.isNil(field.default)) return
if (group.machineScope === 'specific') return
const crypto = group.cryptoScope === 'specific'
? DEFAULT_CRYPTO
: 'global'
const crypto = group.cryptoScope === 'specific'
? DEFAULT_CRYPTO
: 'global'
return newFields.push({
fieldLocator: {
fieldScope: {
crypto,
machine: 'global'
return newFields.push({
fieldLocator: {
fieldScope: {
crypto,
machine: 'global'
},
code: fieldCode,
fieldType: field.fieldType,
fieldClass: field.fieldClass
},
code: fieldCode,
fieldType: field.fieldType,
fieldClass: field.fieldClass
},
fieldValue: {
fieldType: field.fieldType,
value: field.default
}
fieldValue: {
fieldType: field.fieldType,
value: field.default
}
})
})
})
})
return settingsLoader.save(newFields)
})
return settingsLoader.save(newFields)
})
}

View file

@ -21,28 +21,28 @@ module.exports = {
const BINARIES = {
BTC: {
url: 'https://bitcoin.org/bin/bitcoin-core-0.15.1/bitcoin-0.15.1-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-0.15.1/bin'
url: 'https://bitcoin.org/bin/bitcoin-core-0.16.0/bitcoin-0.16.0-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-0.16.0/bin'
},
ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.7.2-1db4ecdc.tar.gz',
dir: 'geth-linux-amd64-1.7.2-1db4ecdc'
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.8.2-b8b9f7f4.tar.gz',
dir: 'geth-linux-amd64-1.8.2-b8b9f7f4'
},
ZEC: {
url: 'https://z.cash/downloads/zcash-1.0.13-linux64.tar.gz',
dir: 'zcash-1.0.13/bin'
url: 'https://z.cash/downloads/zcash-1.0.15-linux64.tar.gz',
dir: 'zcash-1.0.15/bin'
},
DASH: {
url: 'https://github.com/dashpay/dash/releases/download/v0.12.2.1/dashcore-0.12.2.1-linux64.tar.gz',
url: 'https://github.com/dashpay/dash/releases/download/v0.12.2.3/dashcore-0.12.2.3-linux64.tar.gz',
dir: 'dashcore-0.12.2/bin'
},
LTC: {
url: 'https://download.litecoin.org/litecoin-0.14.2/linux/litecoin-0.14.2-x86_64-linux-gnu.tar.gz',
dir: 'litecoin-0.14.2/bin'
url: 'https://download.litecoin.org/litecoin-0.15.1/linux/litecoin-0.15.1-x86_64-linux-gnu.tar.gz',
dir: 'litecoin-0.15.1/bin'
},
BCH: {
url: 'https://download.bitcoinabc.org/0.16.1/linux/bitcoin-abc-0.16.1-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-abc-0.16.1/bin',
url: 'https://download.bitcoinabc.org/0.16.2/linux/bitcoin-abc-0.16.2-x86_64-linux-gnu.tar.gz',
dir: 'bitcoin-abc-0.16.2/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
}
}

View file

@ -7,6 +7,6 @@ module.exports = {setup}
function setup (dataDir) {
const coinRec = coinUtils.getCryptoCurrency('ETH')
common.firewall([coinRec.defaultPort])
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="fast" --cache 500 --rpc`
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="fast" --cache 2048 --maxpeers 40 --rpc`
common.writeSupervisorConfig(coinRec, cmd)
}

View file

@ -112,6 +112,5 @@ function run () {
})
inquirer.prompt(questions)
.then(answers => processCryptos(answers.crypto))
.then(answers => processCryptos(answers.crypto))
}

View file

@ -20,7 +20,7 @@ function setup (dataDir) {
logger.info('Finished fetching proofs.')
const config = buildConfig()
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
const cmd = `/usr/local/bin/${coinRec.daemon} -datadir=${dataDir} -disabledeprecation=1.0.13`
const cmd = `/usr/local/bin/${coinRec.daemon} -datadir=${dataDir} -disabledeprecation=1.0.15`
common.writeSupervisorConfig(coinRec, cmd)
}

View file

@ -1,327 +0,0 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('./db')
const BN = require('./bn')
const plugins = require('./plugins')
const logger = require('./logger')
const T = require('./time')
const E = require('./error')
const PENDING_INTERVAL = '60 minutes'
const PENDING_INTERVAL_MS = 60 * T.minutes
const MAX_PENDING = 10
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL}
function atomic (machineTx, pi) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select * from cash_in_txs where id=$1'
const sql2 = 'select * from bills where cash_in_txs_id=$1'
return t.oneOrNone(sql, [machineTx.id])
.then(row => {
if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError('Stale tx')
return t.any(sql2, [machineTx.id])
.then(billRows => {
const dbTx = toObj(row)
return preProcess(dbTx, machineTx, pi)
.then(preProcessedTx => upsert(dbTx, preProcessedTx))
.then(r => {
return insertNewBills(billRows, machineTx)
.then(newBills => _.set('newBills', newBills, r))
})
})
})
}
transaction.txMode = tmSRD
return transaction
}
function post (machineTx, pi) {
return db.tx(atomic(machineTx, pi))
.then(r => {
const updatedTx = r.tx
return postProcess(r, pi)
.then(changes => update(updatedTx, changes))
.then(tx => _.set('bills', machineTx.bills, tx))
})
}
function nilEqual (a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined
}
function isMonotonic (oldField, newField, fieldKey) {
if (_.isNil(newField)) return false
if (_.isBoolean(oldField)) return oldField === newField || !oldField
if (oldField.isBigNumber) return oldField.lte(newField)
if (_.isNumber(oldField)) return oldField <= newField
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
}
function ensureRatchet (oldField, newField, fieldKey) {
const monotonic = ['cryptoAtoms', 'fiat', 'cashInFeeCrypto', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion']
const free = ['sendPending', 'error', 'errorCode', 'customerId']
if (_.isNil(oldField)) return true
if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey)
if (_.includes(fieldKey, free)) {
if (_.isNil(newField)) return false
return true
}
if (_.isNil(newField)) return false
if (oldField.isBigNumber && newField.isBigNumber) return BN(oldField).eq(newField)
if (oldField.toString() === newField.toString()) return true
return false
}
function diff (oldTx, newTx) {
let updatedTx = {}
if (!oldTx) throw new Error('oldTx must not be null')
if (!newTx) throw new Error('newTx must not be null')
_.forEach(fieldKey => {
const oldField = oldTx[fieldKey]
const newField = newTx[fieldKey]
if (fieldKey === 'bills') return
if (_.isEqualWith(nilEqual, oldField, newField)) return
if (!ensureRatchet(oldField, newField, fieldKey)) {
logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey)
logger.warn('Old tx: %j', oldTx)
logger.warn('New tx: %j', newTx)
throw new E.RatchetError('Value from lamassu-machine would violate ratchet')
}
updatedTx[fieldKey] = newField
}, _.keys(newTx))
return updatedTx
}
function toObj (row) {
if (!row) return null
const keys = _.keys(row)
let newObj = {}
keys.forEach(key => {
const objKey = _.camelCase(key)
if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'cash_in_fee_crypto'])) {
newObj[objKey] = BN(row[key])
return
}
newObj[objKey] = row[key]
})
newObj.direction = 'cashIn'
return newObj
}
function convertBigNumFields (obj) {
const convert = value => value && value.isBigNumber
? value.toString()
: value
return _.mapValues(convert, obj)
}
function pullNewBills (billRows, machineTx) {
if (_.isEmpty(machineTx.bills)) return []
const toBill = _.mapKeys(_.camelCase)
const bills = _.map(toBill, billRows)
return _.differenceBy(_.get('id'), machineTx.bills, bills)
}
const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills']), convertBigNumFields, _.mapKeys(_.snakeCase))
function insertNewBills (billRows, machineTx) {
const bills = pullNewBills(billRows, machineTx)
if (_.isEmpty(bills)) return Promise.resolve([])
const dbBills = _.map(massage, bills)
const columns = _.keys(dbBills[0])
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
return db.none(sql)
.then(() => bills)
}
function upsert (dbTx, preProcessedTx) {
if (!dbTx) {
return insert(preProcessedTx)
.then(tx => ({dbTx, tx}))
}
return update(dbTx, diff(dbTx, preProcessedTx))
.then(tx => ({dbTx, tx}))
}
function insert (tx) {
const dbTx = massage(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
return db.one(sql)
.then(toObj)
}
function update (tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = massage(changes)
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 registerTrades (pi, newBills) {
_.forEach(bill => pi.buy(bill), newBills)
}
function logAction (rec, tx) {
const action = {
tx_id: tx.id,
action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
error: rec.error,
error_code: rec.errorCode,
tx_hash: rec.txHash
}
const sql = pgp.helpers.insert(action, null, 'cash_in_actions')
return db.none(sql)
.then(_.constant(rec))
}
function logActionById (action, _rec, txId) {
const rec = _.assign(_rec, {action, tx_id: txId})
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
return db.none(sql)
}
function isClearToSend (oldTx, newTx) {
const now = Date.now()
return newTx.send &&
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
(newTx.created > now - PENDING_INTERVAL_MS)
}
function postProcess (r, pi) {
registerTrades(pi, r.newBills)
if (!isClearToSend(r.dbTx, r.tx)) return Promise.resolve({})
return pi.sendCoins(r.tx)
.then(txHash => ({
txHash,
sendConfirmed: true,
sendTime: 'now()^',
sendPending: false,
error: null,
errorCode: null
}))
.catch(err => {
// Important: We don't know what kind of error this is
// so not safe to assume that funds weren't sent.
// Therefore, don't set sendPending to false except for
// errors (like InsufficientFundsError) that are guaranteed
// not to send.
const sendPending = err.name !== 'InsufficientFundsError'
return {
sendTime: 'now()^',
error: err.message,
errorCode: err.name,
sendPending
}
})
.then(sendRec => logAction(sendRec, r.tx))
}
function preProcess (dbTx, machineTx, pi) {
// Note: The way this works is if we're clear to send,
// we mark the transaction as sendPending.
//
// If another process is trying to also mark this as sendPending
// that means that it saw the tx as sendPending=false.
// But if that's true, then it must be serialized before this
// (otherwise it would see sendPending=true), and therefore we can't
// be seeing sendPending=false (a pre-condition of clearToSend()).
// Therefore, one of the conflicting transactions will error,
// which is what we want.
return new Promise(resolve => {
if (!dbTx) return resolve(machineTx)
if (isClearToSend(dbTx, machineTx)) {
return resolve(_.set('sendPending', true, machineTx))
}
return resolve(machineTx)
})
}
function monitorPending (settings) {
const sql = `select * from cash_in_txs
where created > now() - interval $1
and send
and not send_confirmed
and not send_pending
and not operator_completed
order by created
limit $2`
const processPending = row => {
const tx = toObj(row)
const pi = plugins(settings, tx.deviceId)
return post(tx, pi)
.catch(logger.error)
}
return db.any(sql, [PENDING_INTERVAL, MAX_PENDING])
.then(rows => Promise.all(_.map(processPending, rows)))
.catch(logger.error)
}
function cancel (txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
operator_completed: true
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [txId])
})
.then(sql => db.result(sql, false))
.then(res => {
if (res.rowCount !== 1) throw new Error('No such tx-id')
})
.then(() => logActionById('operatorCompleted', {}, txId))
}

View file

@ -0,0 +1,83 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const E = require('../error')
const cashInLow = require('./cash-in-low')
module.exports = {atomic}
function atomic (machineTx, pi) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select * from cash_in_txs where id=$1'
const sql2 = 'select * from bills where cash_in_txs_id=$1'
return t.oneOrNone(sql, [machineTx.id])
.then(row => {
if (row && row.tx_version >= machineTx.txVersion) throw new E.StaleTxError('Stale tx')
return t.any(sql2, [machineTx.id])
.then(billRows => {
const dbTx = cashInLow.toObj(row)
return preProcess(dbTx, machineTx, pi)
.then(preProcessedTx => cashInLow.upsert(t, dbTx, preProcessedTx))
.then(r => {
return insertNewBills(t, billRows, machineTx)
.then(newBills => _.set('newBills', newBills, r))
})
})
})
}
transaction.txMode = tmSRD
return transaction
}
function insertNewBills (t, billRows, machineTx) {
const bills = pullNewBills(billRows, machineTx)
if (_.isEmpty(bills)) return Promise.resolve([])
const dbBills = _.map(cashInLow.massage, bills)
const columns = _.keys(dbBills[0])
const sql = pgp.helpers.insert(dbBills, columns, 'bills')
return t.none(sql)
.then(() => bills)
}
function pullNewBills (billRows, machineTx) {
if (_.isEmpty(machineTx.bills)) return []
const toBill = _.mapKeys(_.camelCase)
const bills = _.map(toBill, billRows)
return _.differenceBy(_.get('id'), machineTx.bills, bills)
}
function preProcess (dbTx, machineTx, pi) {
// Note: The way this works is if we're clear to send,
// we mark the transaction as sendPending.
//
// If another process is trying to also mark this as sendPending
// that means that it saw the tx as sendPending=false.
// But if that's true, then it must be serialized before this
// (otherwise it would see sendPending=true), and therefore we can't
// be seeing sendPending=false (a pre-condition of clearToSend()).
// Therefore, one of the conflicting transactions will error,
// which is what we want.
return new Promise(resolve => {
if (!dbTx) return resolve(machineTx)
if (cashInLow.isClearToSend(dbTx, machineTx)) {
return resolve(_.set('sendPending', true, machineTx))
}
return resolve(machineTx)
})
}

138
lib/cash-in/cash-in-low.js Normal file
View file

@ -0,0 +1,138 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const BN = require('../bn')
const T = require('../time')
const logger = require('../logger')
const E = require('../error')
const PENDING_INTERVAL_MS = 60 * T.minutes
const massage = _.flow(_.omit(['direction', 'cryptoNetwork', 'bills']),
convertBigNumFields, _.mapKeys(_.snakeCase))
module.exports = {toObj, upsert, insert, update, massage, isClearToSend}
function convertBigNumFields (obj) {
const convert = value => value && value.isBigNumber
? value.toString()
: value
return _.mapValues(convert, obj)
}
function toObj (row) {
if (!row) return null
const keys = _.keys(row)
let newObj = {}
keys.forEach(key => {
const objKey = _.camelCase(key)
if (_.includes(key, ['crypto_atoms', 'fiat', 'cash_in_fee', 'cash_in_fee_crypto'])) {
newObj[objKey] = BN(row[key])
return
}
newObj[objKey] = row[key]
})
newObj.direction = 'cashIn'
return newObj
}
function upsert (t, dbTx, preProcessedTx) {
if (!dbTx) {
return insert(t, preProcessedTx)
.then(tx => ({dbTx, tx}))
}
return update(t, dbTx, diff(dbTx, preProcessedTx))
.then(tx => ({dbTx, tx}))
}
function insert (t, tx) {
const dbTx = massage(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_in_txs') + ' returning *'
return t.one(sql)
.then(toObj)
}
function update (t, tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = massage(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [tx.id]) + ' returning *'
return t.one(sql)
.then(toObj)
}
function diff (oldTx, newTx) {
let updatedTx = {}
if (!oldTx) throw new Error('oldTx must not be null')
if (!newTx) throw new Error('newTx must not be null')
_.forEach(fieldKey => {
const oldField = oldTx[fieldKey]
const newField = newTx[fieldKey]
if (fieldKey === 'bills') return
if (_.isEqualWith(nilEqual, oldField, newField)) return
if (!ensureRatchet(oldField, newField, fieldKey)) {
logger.warn('Value from lamassu-machine would violate ratchet [%s]', fieldKey)
logger.warn('Old tx: %j', oldTx)
logger.warn('New tx: %j', newTx)
throw new E.RatchetError('Value from lamassu-machine would violate ratchet')
}
updatedTx[fieldKey] = newField
}, _.keys(newTx))
return updatedTx
}
function ensureRatchet (oldField, newField, fieldKey) {
const monotonic = ['cryptoAtoms', 'fiat', 'cashInFeeCrypto', 'send', 'sendConfirmed', 'operatorCompleted', 'timedout', 'txVersion']
const free = ['sendPending', 'error', 'errorCode', 'customerId']
if (_.isNil(oldField)) return true
if (_.includes(fieldKey, monotonic)) return isMonotonic(oldField, newField, fieldKey)
if (_.includes(fieldKey, free)) {
if (_.isNil(newField)) return false
return true
}
if (_.isNil(newField)) return false
if (oldField.isBigNumber && newField.isBigNumber) return BN(oldField).eq(newField)
if (oldField.toString() === newField.toString()) return true
return false
}
function isMonotonic (oldField, newField, fieldKey) {
if (_.isNil(newField)) return false
if (_.isBoolean(oldField)) return oldField === newField || !oldField
if (oldField.isBigNumber) return oldField.lte(newField)
if (_.isNumber(oldField)) return oldField <= newField
throw new Error(`Unexpected value [${fieldKey}]: ${oldField}, ${newField}`)
}
function nilEqual (a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined
}
function isClearToSend (oldTx, newTx) {
const now = Date.now()
return newTx.send &&
(!oldTx || (!oldTx.sendPending && !oldTx.sendConfirmed)) &&
(newTx.created > now - PENDING_INTERVAL_MS)
}

126
lib/cash-in/cash-in-tx.js Normal file
View file

@ -0,0 +1,126 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const pEachSeries = require('p-each-series')
const db = require('../db')
const plugins = require('../plugins')
const logger = require('../logger')
const cashInAtomic = require('./cash-in-atomic')
const cashInLow = require('./cash-in-low')
const PENDING_INTERVAL = '60 minutes'
const MAX_PENDING = 10
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL}
function post (machineTx, pi) {
return db.tx(cashInAtomic.atomic(machineTx, pi))
.then(r => {
const updatedTx = r.tx
return postProcess(r, pi)
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(tx => _.set('bills', machineTx.bills, tx))
})
}
function registerTrades (pi, newBills) {
_.forEach(bill => pi.buy(bill), newBills)
}
function logAction (rec, tx) {
const action = {
tx_id: tx.id,
action: rec.action || (rec.sendConfirmed ? 'sendCoins' : 'sendCoinsError'),
error: rec.error,
error_code: rec.errorCode,
tx_hash: rec.txHash
}
const sql = pgp.helpers.insert(action, null, 'cash_in_actions')
return db.none(sql)
.then(_.constant(rec))
}
function logActionById (action, _rec, txId) {
const rec = _.assign(_rec, {action, tx_id: txId})
const sql = pgp.helpers.insert(rec, null, 'cash_in_actions')
return db.none(sql)
}
function postProcess (r, pi) {
registerTrades(pi, r.newBills)
if (!cashInLow.isClearToSend(r.dbTx, r.tx)) return Promise.resolve({})
return pi.sendCoins(r.tx)
.then(txHash => ({
txHash,
sendConfirmed: true,
sendTime: 'now()^',
sendPending: false,
error: null,
errorCode: null
}))
.catch(err => {
// Important: We don't know what kind of error this is
// so not safe to assume that funds weren't sent.
// Therefore, don't set sendPending to false except for
// errors (like InsufficientFundsError) that are guaranteed
// not to send.
const sendPending = err.name !== 'InsufficientFundsError'
return {
sendTime: 'now()^',
error: err.message,
errorCode: err.name,
sendPending
}
})
.then(sendRec => logAction(sendRec, r.tx))
}
function monitorPending (settings) {
const sql = `select * from cash_in_txs
where created > now() - interval $1
and send
and not send_confirmed
and not send_pending
and not operator_completed
order by created
limit $2`
const processPending = row => {
const tx = cashInLow.toObj(row)
const pi = plugins(settings, tx.deviceId)
return post(tx, pi)
.catch(logger.error)
}
return db.any(sql, [PENDING_INTERVAL, MAX_PENDING])
.then(rows => pEachSeries(rows, row => processPending(row)))
.catch(logger.error)
}
function cancel (txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
operator_completed: true
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_in_txs') +
pgp.as.format(' where id=$1', [txId])
})
.then(sql => db.result(sql, false))
.then(res => {
if (res.rowCount !== 1) throw new Error('No such tx-id')
})
.then(() => logActionById('operatorCompleted', {}, txId))
}

View file

@ -1,410 +0,0 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const db = require('./db')
const billMath = require('./bill-math')
const T = require('./time')
const logger = require('./logger')
const plugins = require('./plugins')
const helper = require('./cash-out-helper')
const socket = require('./socket-client')
const E = require('./error')
module.exports = {
post,
monitorLiveIncoming,
monitorStaleIncoming,
monitorUnnotified,
cancel
}
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt']
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 INSUFFICIENT_FUNDS_CODE = 570
const toObj = helper.toObj
const toDb = helper.toDb
function httpError (msg, code) {
const err = new Error(msg)
err.name = 'HTTPError'
err.code = code || 500
return err
}
function selfPost (tx, pi) {
return post(tx, pi, false)
}
function post (tx, pi, fromClient = true) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select * from cash_out_txs where id=$1'
return t.oneOrNone(sql, [tx.id])
.then(toObj)
.then(oldTx => {
const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion)
if (isStale) throw new E.StaleTxError('Stale tx')
return preProcess(oldTx, tx, pi)
.then(preProcessedTx => upsert(oldTx, preProcessedTx))
})
}
transaction.txMode = tmSRD
return db.tx(transaction)
.then(txVector => {
const [, newTx] = txVector
return postProcess(txVector, pi)
.then(changes => update(newTx, changes))
})
}
function logError (action, err, tx) {
return logAction(action, {
error: err.message,
error_code: err.name
}, tx)
}
function mapDispense (tx) {
const bills = tx.bills
if (_.isEmpty(bills)) return {}
return {
provisioned_1: bills[0].provisioned,
provisioned_2: bills[1].provisioned,
dispensed_1: bills[0].dispensed,
dispensed_2: bills[1].dispensed,
rejected_1: bills[0].rejected,
rejected_2: bills[1].rejected,
denomination_1: bills[0].denomination,
denomination_2: bills[1].denomination
}
}
function logDispense (tx) {
const baseRec = {error: tx.error, error_code: tx.errorCode}
const rec = _.merge(mapDispense(tx), baseRec)
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
return logAction(action, rec, tx)
}
function logActionById (action, _rec, txId) {
const rec = _.assign(_rec, {action, tx_id: txId, redeem: false})
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return db.none(sql)
}
function logAction (action, _rec, tx) {
const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem})
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return db.none(sql)
.then(_.constant(tx))
}
function nilEqual (a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined
}
function diff (oldTx, newTx) {
let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => {
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return
// We never null out an existing field
if (oldTx && _.isNil(newTx[fieldKey])) return
updatedTx[fieldKey] = newTx[fieldKey]
})
return updatedTx
}
function upsert (oldTx, tx) {
if (!oldTx) {
return insert(tx)
.then(newTx => [oldTx, newTx])
}
return update(tx, diff(oldTx, tx))
.then(newTx => [oldTx, newTx])
}
function insert (tx) {
const dbTx = toDb(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *'
return db.one(sql)
.then(toObj)
}
function update (tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = toDb(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [tx.id])
const newTx = _.merge(tx, changes)
return db.none(sql)
.then(() => newTx)
}
function nextHd (isHd, tx) {
if (!isHd) return Promise.resolve(tx)
return db.one("select nextval('hd_indices_seq') as hd_index")
.then(row => _.set('hdIndex', row.hd_index, tx))
}
function dispenseOccurred (bills) {
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
}
function updateCassettes (tx) {
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
const sql = `update devices set
cassette1 = cassette1 - $1,
cassette2 = cassette2 - $2
where device_id = $3
returning cassette1, cassette2`
const values = [
tx.bills[0].dispensed + tx.bills[0].rejected,
tx.bills[1].dispensed + tx.bills[1].rejected,
tx.deviceId
]
return db.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published']) &&
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized']) &&
_.includes(newTx.status, ['instant', 'confirmed'])
return isZeroConf ? isAuthorized() : isConfirmed()
}
function preProcess (oldTx, newTx, pi) {
if (!oldTx) {
return pi.isHd(newTx)
.then(isHd => nextHd(isHd, newTx))
.then(newTxHd => {
return pi.newAddress(newTxHd)
.then(_.set('toAddress', _, newTxHd))
.then(_.unset('isLightning'))
})
.then(addressedTx => {
const rec = {to_address: addressedTx.toAddress}
return logAction('provisionAddress', rec, addressedTx)
})
.catch(err => {
return logError('provisionAddress', err, newTx)
.then(() => { throw err })
})
}
return Promise.resolve(updateStatus(oldTx, newTx))
.then(updatedTx => {
if (updatedTx.status !== oldTx.status) {
const isZeroConf = pi.isZeroConf(updatedTx)
if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx)
const rec = {
to_address: updatedTx.toAddress,
tx_hash: updatedTx.txHash
}
return logAction(updatedTx.status, rec, updatedTx)
}
const hasError = !oldTx.error && newTx.error
const hasDispenseOccurred = !dispenseOccurred(oldTx.bills) && dispenseOccurred(newTx.bills)
if (hasError || hasDispenseOccurred) {
return logDispense(updatedTx)
.then(updateCassettes(updatedTx))
}
if (!oldTx.phone && newTx.phone) {
return logAction('addPhone', {}, updatedTx)
}
if (!oldTx.redeem && newTx.redeem) {
return logAction('redeemLater', {}, updatedTx)
}
return updatedTx
})
}
function postProcess (txVector, pi) {
const [oldTx, newTx] = txVector
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
return pi.buildAvailableCassettes(newTx.id)
.then(cassettes => {
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
return bills
})
.then(bills => {
const provisioned1 = bills[0].provisioned
const provisioned2 = bills[1].provisioned
const denomination1 = bills[0].denomination
const denomination2 = bills[1].denomination
const rec = {
provisioned_1: provisioned1,
provisioned_2: provisioned2,
denomination_1: denomination1,
denomination_2: denomination2
}
return logAction('provisionNotes', rec, newTx)
.then(_.constant({bills}))
})
.catch(err => {
return logError('provisionNotesError', err, newTx)
.then(() => { throw err })
})
}
return Promise.resolve({})
}
function isPublished (status) {
return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed'])
}
function isConfirmed (status) {
return status === 'confirmed'
}
function updateStatus (oldTx, newTx) {
const oldStatus = oldTx.status
const newStatus = ratchetStatus(oldStatus, newTx.status)
const publishedAt = !oldTx.publishedAt && isPublished(newStatus)
? 'now()^'
: undefined
const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus)
? 'now()^'
: undefined
const updateRec = {
publishedAt,
confirmedAt,
status: newStatus
}
return _.merge(newTx, updateRec)
}
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 => _.assign(tx, {status: res.status}))
.then(_tx => selfPost(_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 dispense=$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)
}
function cancel (txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
dispense: true
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [txId])
})
.then(sql => db.result(sql, false))
.then(res => {
if (res.rowCount !== 1) throw new Error('No such tx-id')
})
.then(() => logActionById('operatorCompleted', {}, txId))
}

View file

@ -0,0 +1,50 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
module.exports = {logDispense, logActionById, logAction, logError}
function logDispense (t, tx) {
const baseRec = {error: tx.error, error_code: tx.errorCode}
const rec = _.merge(mapDispense(tx), baseRec)
const action = _.isEmpty(tx.error) ? 'dispense' : 'dispenseError'
return logAction(t, action, rec, tx)
}
function logActionById (t, action, _rec, txId) {
const rec = _.assign(_rec, {action, tx_id: txId, redeem: false})
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return t.none(sql)
}
function logAction (t, action, _rec, tx) {
const rec = _.assign(_rec, {action, tx_id: tx.id, redeem: !!tx.redeem})
const sql = pgp.helpers.insert(rec, null, 'cash_out_actions')
return t.none(sql)
.then(_.constant(tx))
}
function logError (t, action, err, tx) {
return logAction(t, action, {
error: err.message,
error_code: err.name
}, tx)
}
function mapDispense (tx) {
const bills = tx.bills
if (_.isEmpty(bills)) return {}
return {
provisioned_1: bills[0].provisioned,
provisioned_2: bills[1].provisioned,
dispensed_1: bills[0].dispensed,
dispensed_2: bills[1].dispensed,
rejected_1: bills[0].rejected,
rejected_2: bills[1].rejected,
denomination_1: bills[0].denomination,
denomination_2: bills[1].denomination
}
}

View file

@ -0,0 +1,171 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const E = require('../error')
const socket = require('../socket-client')
const helper = require('./cash-out-helper')
const cashOutActions = require('./cash-out-actions')
const cashOutLow = require('./cash-out-low')
const toObj = helper.toObj
module.exports = {atomic}
function atomic (tx, pi, fromClient) {
const TransactionMode = pgp.txMode.TransactionMode
const isolationLevel = pgp.txMode.isolationLevel
const tmSRD = new TransactionMode({tiLevel: isolationLevel.serializable})
function transaction (t) {
const sql = 'select * from cash_out_txs where id=$1'
return t.oneOrNone(sql, [tx.id])
.then(toObj)
.then(oldTx => {
const isStale = fromClient && oldTx && (oldTx.txVersion >= tx.txVersion)
if (isStale) throw new E.StaleTxError('Stale tx')
return preProcess(t, oldTx, tx, pi)
.then(preProcessedTx => cashOutLow.upsert(t, oldTx, preProcessedTx))
})
}
transaction.txMode = tmSRD
return transaction
}
function preProcess (t, oldTx, newTx, pi) {
if (!oldTx) {
return pi.isHd(newTx)
.then(isHd => nextHd(t, isHd, newTx))
.then(newTxHd => {
return pi.newAddress(newTxHd)
.then(_.set('toAddress', _, newTxHd))
.then(_.unset('isLightning'))
})
.then(addressedTx => {
const rec = {to_address: addressedTx.toAddress}
return cashOutActions.logAction(t, 'provisionAddress', rec, addressedTx)
})
.catch(err => {
return cashOutActions.logError(t, 'provisionAddress', err, newTx)
.then(() => { throw err })
})
}
return Promise.resolve(updateStatus(oldTx, newTx))
.then(updatedTx => {
if (updatedTx.status !== oldTx.status) {
const isZeroConf = pi.isZeroConf(updatedTx)
if (wasJustAuthorized(oldTx, updatedTx, isZeroConf)) pi.sell(updatedTx)
const rec = {
to_address: updatedTx.toAddress,
tx_hash: updatedTx.txHash
}
return cashOutActions.logAction(t, updatedTx.status, rec, updatedTx)
}
const hasError = !oldTx.error && newTx.error
const hasDispenseOccurred = !dispenseOccurred(oldTx.bills) && dispenseOccurred(newTx.bills)
if (hasError || hasDispenseOccurred) {
return cashOutActions.logDispense(t, updatedTx)
.then(updateCassettes(t, updatedTx))
}
if (!oldTx.phone && newTx.phone) {
return cashOutActions.logAction(t, 'addPhone', {}, updatedTx)
}
if (!oldTx.redeem && newTx.redeem) {
return cashOutActions.logAction(t, 'redeemLater', {}, updatedTx)
}
return updatedTx
})
}
function nextHd (t, isHd, tx) {
if (!isHd) return Promise.resolve(tx)
return t.one("select nextval('hd_indices_seq') as hd_index")
.then(row => _.set('hdIndex', row.hd_index, tx))
}
function updateCassettes (t, tx) {
if (!dispenseOccurred(tx.bills)) return Promise.resolve()
const sql = `update devices set
cassette1 = cassette1 - $1,
cassette2 = cassette2 - $2
where device_id = $3
returning cassette1, cassette2`
const values = [
tx.bills[0].dispensed + tx.bills[0].rejected,
tx.bills[1].dispensed + tx.bills[1].rejected,
tx.deviceId
]
return t.one(sql, values)
.then(r => socket.emit(_.assign(r, {op: 'cassetteUpdate', deviceId: tx.deviceId})))
}
function wasJustAuthorized (oldTx, newTx, isZeroConf) {
const isAuthorized = () => _.includes(oldTx.status, ['notSeen', 'published']) &&
_.includes(newTx.status, ['authorized', 'instant', 'confirmed'])
const isConfirmed = () => _.includes(oldTx.status, ['notSeen', 'published', 'authorized']) &&
_.includes(newTx.status, ['instant', 'confirmed'])
return isZeroConf ? isAuthorized() : isConfirmed()
}
function isPublished (status) {
return _.includes(status, ['published', 'rejected', 'authorized', 'instant', 'confirmed'])
}
function isConfirmed (status) {
return status === 'confirmed'
}
function updateStatus (oldTx, newTx) {
const oldStatus = oldTx.status
const newStatus = ratchetStatus(oldStatus, newTx.status)
const publishedAt = !oldTx.publishedAt && isPublished(newStatus)
? 'now()^'
: undefined
const confirmedAt = !oldTx.confirmedAt && isConfirmed(newStatus)
? 'now()^'
: undefined
const updateRec = {
publishedAt,
confirmedAt,
status: newStatus
}
return _.merge(newTx, updateRec)
}
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 dispenseOccurred (bills) {
if (_.isEmpty(bills)) return false
return _.every(_.overEvery([_.has('dispensed'), _.has('rejected')]), bills)
}

View file

@ -1,8 +1,8 @@
const _ = require('lodash/fp')
const db = require('./db')
const T = require('./time')
const BN = require('./bn')
const db = require('../db')
const T = require('../time')
const BN = require('../bn')
const REDEEMABLE_AGE = T.day
@ -12,12 +12,12 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
function convertBigNumFields (obj) {
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
? value.toString()
: value
? value.toString()
: value
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
? key + '#'
: key
? key + '#'
: key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
@ -91,5 +91,5 @@ function redeemableTxs (deviceId) {
and (extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) < $4`
return db.any(sql, [deviceId, true, false, REDEEMABLE_AGE])
.then(_.map(toObj))
.then(_.map(toObj))
}

View file

@ -0,0 +1,64 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const helper = require('./cash-out-helper')
const toDb = helper.toDb
const toObj = helper.toObj
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt']
module.exports = {upsert, update, insert}
function upsert (t, oldTx, tx) {
if (!oldTx) {
return insert(t, tx)
.then(newTx => [oldTx, newTx])
}
return update(t, tx, diff(oldTx, tx))
.then(newTx => [oldTx, newTx])
}
function insert (t, tx) {
const dbTx = toDb(tx)
const sql = pgp.helpers.insert(dbTx, null, 'cash_out_txs') + ' returning *'
return t.one(sql)
.then(toObj)
}
function update (t, tx, changes) {
if (_.isEmpty(changes)) return Promise.resolve(tx)
const dbChanges = toDb(changes)
const sql = pgp.helpers.update(dbChanges, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [tx.id])
const newTx = _.merge(tx, changes)
return t.none(sql)
.then(() => newTx)
}
function diff (oldTx, newTx) {
let updatedTx = {}
UPDATEABLE_FIELDS.forEach(fieldKey => {
if (oldTx && _.isEqualWith(nilEqual, oldTx[fieldKey], newTx[fieldKey])) return
// We never null out an existing field
if (oldTx && _.isNil(newTx[fieldKey])) return
updatedTx[fieldKey] = newTx[fieldKey]
})
return updatedTx
}
function nilEqual (a, b) {
if (_.isNil(a) && _.isNil(b)) return true
return undefined
}

158
lib/cash-out/cash-out-tx.js Normal file
View file

@ -0,0 +1,158 @@
const _ = require('lodash/fp')
const pgp = require('pg-promise')()
const pEachSeries = require('p-each-series')
const db = require('../db')
const billMath = require('../bill-math')
const T = require('../time')
const logger = require('../logger')
const plugins = require('../plugins')
const helper = require('./cash-out-helper')
const cashOutAtomic = require('./cash-out-atomic')
const cashOutActions = require('./cash-out-actions')
const cashOutLow = require('./cash-out-low')
module.exports = {
post,
monitorLiveIncoming,
monitorStaleIncoming,
monitorUnnotified,
cancel
}
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 INSUFFICIENT_FUNDS_CODE = 570
const toObj = helper.toObj
function httpError (msg, code) {
const err = new Error(msg)
err.name = 'HTTPError'
err.code = code || 500
return err
}
function selfPost (tx, pi) {
return post(tx, pi, false)
}
function post (tx, pi, fromClient = true) {
return db.tx(cashOutAtomic.atomic(tx, pi, fromClient))
.then(txVector => {
const [, newTx] = txVector
return postProcess(txVector, pi)
.then(changes => cashOutLow.update(db, newTx, changes))
})
}
function postProcess (txVector, pi) {
const [oldTx, newTx] = txVector
if ((newTx.dispense && !oldTx.dispense) || (newTx.redeem && !oldTx.redeem)) {
return pi.buildAvailableCassettes(newTx.id)
.then(cassettes => {
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
return bills
})
.then(bills => {
const provisioned1 = bills[0].provisioned
const provisioned2 = bills[1].provisioned
const denomination1 = bills[0].denomination
const denomination2 = bills[1].denomination
const rec = {
provisioned_1: provisioned1,
provisioned_2: provisioned2,
denomination_1: denomination1,
denomination_2: denomination2
}
return cashOutActions.logAction(db, 'provisionNotes', rec, newTx)
.then(_.constant({bills}))
})
.catch(err => {
return cashOutActions.logError(db, 'provisionNotesError', err, newTx)
.then(() => { throw err })
})
}
return Promise.resolve({})
}
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 => _.assign(tx, {status: res.status}))
.then(_tx => selfPost(_tx, pi))
}
function monitorLiveIncoming (settings) {
const statuses = ['notSeen', 'published', 'insufficientFunds']
return fetchOpenTxs(statuses, STALE_LIVE_INCOMING_TX_AGE)
.then(txs => pEachSeries(txs, 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 => pEachSeries(txs, 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 dispense=$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)
}
function cancel (txId) {
const updateRec = {
error: 'Operator cancel',
error_code: 'operatorCancel',
dispense: true
}
return Promise.resolve()
.then(() => {
return pgp.helpers.update(updateRec, null, 'cash_out_txs') +
pgp.as.format(' where id=$1', [txId])
})
.then(sql => db.result(sql, false))
.then(res => {
if (res.rowCount !== 1) throw new Error('No such tx-id')
})
.then(() => cashOutActions.logActionById(db, 'operatorCompleted', {}, txId))
}

View file

@ -52,7 +52,7 @@ const CRYPTO_CURRENCIES = [
},
{
cryptoCode: 'BCH',
display: 'BCH',
display: 'Bitcoin Cash',
code: 'bitcoincash',
configFile: 'bitcoincash.conf',
daemon: 'bitcoincashd',
@ -80,7 +80,7 @@ function buildUrl (cryptoCode, address) {
case 'ZEC': return `zcash:${address}`
case 'LTC': return `litecoin:${address}`
case 'DASH': return `dash:${address}`
case 'BCH': return `bitcoincash:${address}`
case 'BCH': return `${address}`
default: throw new Error(`Unsupported crypto: ${cryptoCode}`)
}
}

View file

@ -55,10 +55,6 @@ function satisfiesRequire (config, cryptos, machineList, field, anyFields, allFi
const isValid = isRequired ? !isBlank : true
if (!isValid) {
pp('DEBUG103')({fieldCode, isBlank, isRequired})
}
return isValid
})
}
@ -66,12 +62,12 @@ function satisfiesRequire (config, cryptos, machineList, field, anyFields, allFi
function isScopeEnabled (config, cryptos, machineList, refField, scope) {
const [cryptoScope, machineScope] = scope
const candidateCryptoScopes = cryptoScope === 'global'
? allCryptoScopes(cryptos, refField.cryptoScope)
: [cryptoScope]
? allCryptoScopes(cryptos, refField.cryptoScope)
: [cryptoScope]
const candidateMachineScopes = machineScope === 'global'
? allMachineScopes(machineList, refField.machineScope)
: [ machineScope ]
? allMachineScopes(machineList, refField.machineScope)
: [ machineScope ]
const allRefCandidateScopes = allScopes(candidateCryptoScopes, candidateMachineScopes)
const getFallbackValue = scope => configManager.scopedValue(scope[0], scope[1], refField.code, config)
@ -108,13 +104,13 @@ function getMachines () {
function fetchMachines () {
return getMachines()
.then(machineList => machineList.map(r => r.device_id))
.then(machineList => machineList.map(r => r.device_id))
}
function validateFieldParameter (value, validator) {
switch (validator.code) {
case 'required':
return true // We don't validate this here
return true // We don't validate this here
case 'min':
return value >= validator.min
case 'max':
@ -128,58 +124,58 @@ function ensureConstraints (config) {
const pickField = fieldCode => schema.fields.find(r => r.code === fieldCode)
return Promise.resolve()
.then(() => {
config.every(fieldInstance => {
const fieldCode = fieldInstance.fieldLocator.code
const field = pickField(fieldCode)
if (!field) {
logger.warn('No such field: %s, %j', fieldCode, fieldInstance.fieldLocator.fieldScope)
return
}
.then(() => {
config.every(fieldInstance => {
const fieldCode = fieldInstance.fieldLocator.code
const field = pickField(fieldCode)
if (!field) {
logger.warn('No such field: %s, %j', fieldCode, fieldInstance.fieldLocator.fieldScope)
return
}
const fieldValue = fieldInstance.fieldValue
const fieldValue = fieldInstance.fieldValue
const isValid = field.fieldValidation
.every(validator => validateFieldParameter(fieldValue.value, validator))
const isValid = field.fieldValidation
.every(validator => validateFieldParameter(fieldValue.value, validator))
if (isValid) return true
throw new Error('Invalid config value')
if (isValid) return true
throw new Error('Invalid config value')
})
})
})
}
const pp = require('./pp')
function validateRequires (config) {
return fetchMachines()
.then(machineList => {
const cryptos = getCryptos(config, machineList)
.then(machineList => {
const cryptos = getCryptos(config, machineList)
return schema.groups.filter(group => {
return group.fields.some(fieldCode => {
const field = getGroupField(group, fieldCode)
return schema.groups.filter(group => {
return group.fields.some(fieldCode => {
const field = getGroupField(group, fieldCode)
if (!field.fieldValidation.find(r => r.code === 'required')) return false
if (!field.fieldValidation.find(r => r.code === 'required')) return false
const refFieldsAny = _.map(_.partial(getField, group), field.enabledIfAny)
const refFieldsAll = _.map(_.partial(getField, group), field.enabledIfAll)
const isInvalid = !satisfiesRequire(config, cryptos, machineList, field, refFieldsAny, refFieldsAll)
const refFieldsAny = _.map(_.partial(getField, group), field.enabledIfAny)
const refFieldsAll = _.map(_.partial(getField, group), field.enabledIfAll)
const isInvalid = !satisfiesRequire(config, cryptos, machineList, field, refFieldsAny, refFieldsAll)
return isInvalid
return isInvalid
})
})
})
})
.then(arr => arr.map(r => r.code))
.then(arr => arr.map(r => r.code))
}
function validate (config) {
return Promise.resolve()
.then(() => ensureConstraints(config))
.then(() => validateRequires(config))
.then(arr => {
if (arr.length === 0) return config
throw new Error('Invalid configuration:' + arr)
})
.then(() => ensureConstraints(config))
.then(() => validateRequires(config))
.then(arr => {
if (arr.length === 0) return config
throw new Error('Invalid configuration:' + arr)
})
}
module.exports = {validate, ensureConstraints, validateRequires}

View file

@ -39,8 +39,8 @@ function add (customer) {
function get (phone) {
const sql = 'select * from customers where phone=$1'
return db.oneOrNone(sql, [phone])
.then(populateDailyVolume)
.then(camelize)
.then(populateDailyVolume)
.then(camelize)
}
/**
@ -57,15 +57,18 @@ function get (phone) {
*/
function update (id, data, userToken) {
const formattedData = _.omit(['id'], _.mapKeys(_.snakeCase, data))
const updateData = enhanceOverrideFields(formattedData, userToken)
const updateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
' where id=$1 returning *'
return db.one(sql, [id])
.then(addComplianceOverrides(id, updateData, userToken))
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
.then(addComplianceOverrides(id, updateData, userToken))
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
}
/**
@ -82,10 +85,10 @@ function update (id, data, userToken) {
function getById (id, userToken) {
const sql = 'select * from customers where id=$1'
return db.oneOrNone(sql, [id])
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
.then(populateOverrideUsernames)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
}
/**
@ -100,11 +103,11 @@ function getById (id, userToken) {
*/
function getDailyVolume (id) {
return Promise.all([
db.one(`select coalesce(sum(fiat), 0) as total from cash_in_txs
where customer_id=$1
db.one(`select coalesce(sum(fiat), 0) as total from cash_in_txs
where customer_id=$1
and created > now() - interval '1 day'`, [id]),
db.one(`select coalesce(sum(fiat), 0) as total from cash_out_txs
where customer_id=$1
db.one(`select coalesce(sum(fiat), 0) as total from cash_out_txs
where customer_id=$1
and created > now() - interval '1 day'`, [id])
]).then(([cashInTotal, cashOutTotal]) => {
return BN(cashInTotal.total).add(cashOutTotal.total)
@ -161,6 +164,21 @@ function getComplianceTypes () {
'authorized' ]
}
function enhanceAtFields (fields) {
const updateableFields = [
'id_card_data',
'id_card_photo',
'front_camera',
'sanctions',
'authorized'
]
const updatedFields = _.intersection(updateableFields, _.keys(fields))
const atFields = _.fromPairs(_.map(f => [`${f}_at`, 'now()^'], updatedFields))
return _.merge(fields, atFields)
}
/**
* Add *override_by and *override_at fields with acting user's token
* and date of override respectively before saving to db.
@ -174,7 +192,7 @@ function getComplianceTypes () {
*/
function enhanceOverrideFields (fields, userToken) {
if (!userToken) return fields
// Populate with computedFields (user who overrode and overriden timestamps date)
// Populate with computedFields (user who overrode and overriden timestamps date)
return _.reduce(_.assign, fields, _.map((type) => {
return (fields[type + '_override'])
? {
@ -214,7 +232,7 @@ function addComplianceOverrides (id, customer, userToken) {
// Save all the updated override fields
return Promise.all(_.map(complianceOverrides.add, _.compact(overrides)))
.then(() => customer)
.then(() => customer)
}
/**
@ -277,15 +295,15 @@ function populateOverrideUsernames (customer) {
const queryTokens = _.map('token', fieldsToUpdate)
return users.getByIds(queryTokens)
.then(usersList => {
return _.map(userField => {
const user = _.find({token: userField.token}, usersList)
return {
[userField.field]: user ? user.name : null
}
}, fieldsToUpdate)
})
.then(_.reduce(_.assign, customer))
.then(usersList => {
return _.map(userField => {
const user = _.find({token: userField.token}, usersList)
return {
[userField.field]: user ? user.name : null
}
}, fieldsToUpdate)
})
.then(_.reduce(_.assign, customer))
}
/**
@ -298,16 +316,16 @@ function populateOverrideUsernames (customer) {
* @returns {array} Array of customers populated with status field
*/
function batch () {
const sql = `select * from customers
const sql = `select * from customers
where id != $1
order by created desc limit $2`
return db.any(sql, [ anonymous.uuid, NUM_RESULTS ])
.then(customers => Promise.all(_.map(customer => {
return populateOverrideUsernames(customer)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
}, customers)))
.then(customers => Promise.all(_.map(customer => {
return populateOverrideUsernames(customer)
.then(computeStatus)
.then(populateDailyVolume)
.then(camelize)
}, customers)))
}
module.exports = { add, get, batch, getById, update }

View file

@ -3,13 +3,13 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = configManager.unscoped(settings.config).email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
.then(() => {
const pluginCode = configManager.unscoped(settings.config).email
const plugin = ph.load(ph.EMAIL, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
return plugin.sendMessage(account, rec)
})
}
module.exports = {sendMessage}

View file

@ -9,24 +9,24 @@ function lookupExchange (settings, cryptoCode) {
function fetchExchange (settings, cryptoCode) {
return Promise.resolve()
.then(() => {
const plugin = lookupExchange(settings, cryptoCode)
if (!plugin) throw new Error('No exchange set')
const exchange = ph.load(ph.EXCHANGE, plugin)
const account = settings.accounts[plugin]
.then(() => {
const plugin = lookupExchange(settings, cryptoCode)
if (!plugin) throw new Error('No exchange set')
const exchange = ph.load(ph.EXCHANGE, plugin)
const account = settings.accounts[plugin]
return {exchange, account}
})
return {exchange, account}
})
}
function buy (settings, cryptoAtoms, fiatCode, cryptoCode) {
return fetchExchange(settings, cryptoCode)
.then(r => r.exchange.buy(r.account, cryptoAtoms, fiatCode, cryptoCode))
.then(r => r.exchange.buy(r.account, cryptoAtoms, fiatCode, cryptoCode))
}
function sell (settings, cryptoAtoms, fiatCode, cryptoCode) {
return fetchExchange(settings, cryptoCode)
.then(r => r.exchange.sell(r.account, cryptoAtoms, fiatCode, cryptoCode))
.then(r => r.exchange.sell(r.account, cryptoAtoms, fiatCode, cryptoCode))
}
function active (settings, cryptoCode) {

View file

@ -22,7 +22,7 @@ function getLastSeen (deviceId) {
where device_id=$1
order by timestamp desc, serial desc limit 1`
return db.oneOrNone(sql, [deviceId])
.then(log => log ? {timestamp: log.timestamp, serial: log.serial, id: log.id} : null)
.then(log => log ? {timestamp: log.timestamp, serial: log.serial, id: log.id} : null)
}
/**
@ -40,7 +40,7 @@ function getLastSeen (deviceId) {
function update (deviceId, logLines) {
const cs = new pgp.helpers.ColumnSet([
'id', 'device_id', 'log_level', 'timestamp', 'serial', 'message'],
{table: 'logs'})
{table: 'logs'})
const logs = _.map(log => {
const formatted = {
@ -65,10 +65,10 @@ function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
order by timestamp asc, serial asc`
return Promise.all([db.any(sql, [ deviceId, until ]), getMachineName(deviceId)])
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
}
function getMachineLogs (deviceId, until = new Date().toISOString()) {
@ -79,10 +79,10 @@ function getMachineLogs (deviceId, until = new Date().toISOString()) {
limit $2`
return Promise.all([db.any(sql, [ deviceId, NUM_RESULTS, until ]), getMachineName(deviceId)])
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
.then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName}
}))
}
module.exports = { getUnlimitedMachineLogs, getMachineLogs, update, getLastSeen }

View file

@ -10,13 +10,13 @@ module.exports = {getMachineName, getMachines, getMachineNames, setMachine}
function getMachines () {
return db.any('select * from devices where display=TRUE order by created')
.then(rr => rr.map(r => ({
deviceId: r.device_id,
cashbox: r.cashbox,
cassette1: r.cassette1,
cassette2: r.cassette2,
paired: r.paired
})))
.then(rr => rr.map(r => ({
deviceId: r.device_id,
cashbox: r.cashbox,
cassette1: r.cassette1,
cassette2: r.cassette2,
paired: r.paired
})))
}
function getConfig (defaultConfig) {
@ -27,17 +27,17 @@ function getConfig (defaultConfig) {
function getMachineNames (config) {
return Promise.all([getMachines(), getConfig(config)])
.then(([machines, config]) => {
const addName = r => {
const machineScoped = configManager.machineScoped(r.deviceId, config)
const name = machineScoped.machineName
const cashOut = machineScoped.cashOutEnabled
.then(([machines, config]) => {
const addName = r => {
const machineScoped = configManager.machineScoped(r.deviceId, config)
const name = _.defaultTo('', machineScoped.machineName)
const cashOut = machineScoped.cashOutEnabled
return _.assign(r, {name, cashOut})
}
return _.assign(r, {name, cashOut})
}
return _.map(addName, machines)
})
return _.map(addName, machines)
})
}
/**
@ -52,10 +52,10 @@ function getMachineNames (config) {
*/
function getMachineName (machineId) {
return settingsLoader.loadRecentConfig()
.then(config => {
const machineScoped = configManager.machineScoped(machineId, config)
return machineScoped.machineName
})
.then(config => {
const machineScoped = configManager.machineScoped(machineId, config)
return machineScoped.machineName
})
}
function resetCashOutBills (rec) {

View file

@ -16,4 +16,3 @@ function run () {
})
})
}

View file

@ -60,46 +60,46 @@ function checkNotification (plugins) {
if (!plugins.notificationsEnabled()) return Promise.resolve()
return checkStatus(plugins)
.then(alertRec => {
const currentAlertFingerprint = buildAlertFingerprint(alertRec)
if (!currentAlertFingerprint) {
const inAlert = !!alertFingerprint
alertFingerprint = null
lastAlertTime = null
if (inAlert) return sendNoAlerts(plugins)
}
const alertChanged = currentAlertFingerprint === alertFingerprint &&
lastAlertTime - Date.now() < ALERT_SEND_INTERVAL
if (alertChanged) return
const subject = alertSubject(alertRec)
const rec = {
sms: {
body: subject
},
email: {
subject,
body: printEmailAlerts(alertRec)
.then(alertRec => {
const currentAlertFingerprint = buildAlertFingerprint(alertRec)
if (!currentAlertFingerprint) {
const inAlert = !!alertFingerprint
alertFingerprint = null
lastAlertTime = null
if (inAlert) return sendNoAlerts(plugins)
}
}
alertFingerprint = currentAlertFingerprint
lastAlertTime = Date.now()
return plugins.sendMessage(rec)
})
.then(results => {
if (results && results.length > 0) logger.debug('Successfully sent alerts')
})
.catch(logger.error)
const alertChanged = currentAlertFingerprint === alertFingerprint &&
lastAlertTime - Date.now() < ALERT_SEND_INTERVAL
if (alertChanged) return
const subject = alertSubject(alertRec)
const rec = {
sms: {
body: subject
},
email: {
subject,
body: printEmailAlerts(alertRec)
}
}
alertFingerprint = currentAlertFingerprint
lastAlertTime = Date.now()
return plugins.sendMessage(rec)
})
.then(results => {
if (results && results.length > 0) logger.debug('Successfully sent alerts')
})
.catch(logger.error)
}
const getDeviceTime = _.flow(_.get('device_time'), Date.parse)
function dropRepeatsWith (comparator, arr) {
const iteratee = (acc, val) => val === acc.last
? acc
: {arr: _.concat(acc.arr, val), last: val}
? acc
: {arr: _.concat(acc.arr, val), last: val}
return _.reduce(iteratee, {arr: []}, arr).arr
}
@ -135,11 +135,11 @@ function checkPing (deviceId) {
limit 1`
return db.oneOrNone(sql, [deviceId])
.then(row => {
if (!row) return [{code: PING}]
if (row.age > NETWORK_DOWN_TIME) return [{code: PING, age: row.age}]
return []
})
.then(row => {
if (!row) return [{code: PING}]
if (row.age > NETWORK_DOWN_TIME) return [{code: PING, age: row.age}]
return []
})
}
function checkPings (devices) {
@ -147,37 +147,37 @@ function checkPings (devices) {
const promises = _.map(checkPing, deviceIds)
return Promise.all(promises)
.then(_.zipObject(deviceIds))
.then(_.zipObject(deviceIds))
}
function checkStatus (plugins) {
const alerts = {devices: {}, deviceNames: {}}
return Promise.all([plugins.checkBalances(), dbm.machineEvents(), plugins.getMachineNames()])
.then(([balances, events, devices]) => {
return checkPings(devices)
.then(pings => {
alerts.general = _.filter(r => !r.deviceId, balances)
devices.forEach(function (device) {
const deviceId = device.deviceId
const deviceName = device.name
const deviceEvents = events.filter(function (eventRow) {
return eventRow.device_id === deviceId
.then(([balances, events, devices]) => {
return checkPings(devices)
.then(pings => {
alerts.general = _.filter(r => !r.deviceId, balances)
devices.forEach(function (device) {
const deviceId = device.deviceId
const deviceName = device.name
const deviceEvents = events.filter(function (eventRow) {
return eventRow.device_id === deviceId
})
const balanceAlerts = _.filter(['deviceId', deviceId], balances)
const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents)
const deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.devices[deviceId] = _.concat(deviceAlerts, balanceAlerts)
alerts.deviceNames[deviceId] = deviceName
})
return alerts
})
const balanceAlerts = _.filter(['deviceId', deviceId], balances)
const ping = pings[deviceId] || []
const stuckScreen = checkStuckScreen(deviceEvents)
const deviceAlerts = _.isEmpty(ping) ? stuckScreen : ping
alerts.devices[deviceId] = _.concat(deviceAlerts, balanceAlerts)
alerts.deviceNames[deviceId] = deviceName
})
return alerts
})
})
}
function formatCurrency (num, code) {

View file

@ -38,43 +38,43 @@ function removeDeviceConfig (deviceId) {
function unpair (deviceId) {
const sql = 'delete from devices where device_id=$1'
return db.none(sql, [deviceId])
.then(() => removeDeviceConfig(deviceId))
.then(() => removeDeviceConfig(deviceId))
}
function pair (token, deviceId, machineModel) {
return pullToken(token)
.then(r => {
if (r.expired) return false
.then(r => {
if (r.expired) return false
const insertSql = `insert into devices (device_id, name) values ($1, $2)
const insertSql = `insert into devices (device_id, name) values ($1, $2)
on conflict (device_id)
do update set paired=TRUE, display=TRUE`
return configureNewDevice(deviceId, r.name, machineModel)
.then(() => db.none(insertSql, [deviceId, r.name]))
.then(() => true)
})
.catch(err => {
logger.debug(err)
return false
})
return configureNewDevice(deviceId, r.name, machineModel)
.then(() => db.none(insertSql, [deviceId, r.name]))
.then(() => true)
})
.catch(err => {
logger.debug(err)
return false
})
}
function authorizeCaDownload (caToken) {
return pullToken(caToken)
.then(r => {
if (r.expired) throw new Error('Expired')
.then(r => {
if (r.expired) throw new Error('Expired')
const caPath = options.caPath
return readFile(caPath, {encoding: 'utf8'})
})
const caPath = options.caPath
return readFile(caPath, {encoding: 'utf8'})
})
}
function isPaired (deviceId) {
const sql = 'select device_id from devices where device_id=$1 and paired=TRUE'
return db.oneOrNone(sql, [deviceId])
.then(row => row && row.device_id === deviceId)
.then(row => row && row.device_id === deviceId)
}
module.exports = {pair, unpair, authorizeCaDownload, isPaired}

View file

@ -16,7 +16,7 @@ const wallet = require('./wallet')
const exchange = require('./exchange')
const sms = require('./sms')
const email = require('./email')
const cashOutHelper = require('./cash-out-helper')
const cashOutHelper = require('./cash-out/cash-out-helper')
const machineLoader = require('./machine-loader')
const coinUtils = require('./coin-utils')
@ -44,8 +44,8 @@ function plugins (settings, deviceId) {
const cashInCommission = BN(1).add(BN(cryptoConfig.cashInCommission).div(100))
const cashOutCommission = _.isNil(cryptoConfig.cashOutCommission)
? undefined
: BN(1).add(BN(cryptoConfig.cashOutCommission).div(100))
? undefined
: 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
@ -131,34 +131,34 @@ function plugins (settings, deviceId) {
const virtualCassettes = [config.virtualCashOutDenomination]
return Promise.all([dbm.cassetteCounts(deviceId), cashOutHelper.redeemableTxs(deviceId, excludeTxId)])
.then(([rec, _redeemableTxs]) => {
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
.then(([rec, _redeemableTxs]) => {
const redeemableTxs = _.reject(_.matchesProperty('id', excludeTxId), _redeemableTxs)
const counts = argv.cassettes
? argv.cassettes.split(',')
: rec.counts
const counts = argv.cassettes
? argv.cassettes.split(',')
: rec.counts
const cassettes = [
{
denomination: parseInt(denominations[0], 10),
count: parseInt(counts[0], 10)
},
{
denomination: parseInt(denominations[1], 10),
count: parseInt(counts[1], 10)
const cassettes = [
{
denomination: parseInt(denominations[0], 10),
count: parseInt(counts[0], 10)
},
{
denomination: parseInt(denominations[1], 10),
count: parseInt(counts[1], 10)
}
]
try {
return {
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
virtualCassettes
}
} catch (err) {
logger.error(err)
return {cassettes, virtualCassettes}
}
]
try {
return {
cassettes: computeAvailableCassettes(cassettes, redeemableTxs),
virtualCassettes
}
} catch (err) {
logger.error(err)
return {cassettes, virtualCassettes}
}
})
})
}
function fetchCurrentConfigVersion () {
@ -169,7 +169,7 @@ function plugins (settings, deviceId) {
limit 1`
return db.one(sql, ['config'])
.then(row => row.id)
.then(row => row.id)
}
function mapCoinSettings (coinParams) {
@ -207,23 +207,23 @@ function plugins (settings, deviceId) {
].concat(tickerPromises, balancePromises, testnetPromises)
return Promise.all(promises)
.then(arr => {
const cassettes = arr[0]
const configVersion = arr[2]
const cryptoCodesCount = cryptoCodes.length
const tickers = arr.slice(3, cryptoCodesCount + 3)
const balances = arr.slice(cryptoCodesCount + 3, 2 * cryptoCodesCount + 3)
const testNets = arr.slice(2 * cryptoCodesCount + 3)
const coinParams = _.zip(cryptoCodes, testNets)
.then(arr => {
const cassettes = arr[0]
const configVersion = arr[2]
const cryptoCodesCount = cryptoCodes.length
const tickers = arr.slice(3, cryptoCodesCount + 3)
const balances = arr.slice(cryptoCodesCount + 3, 2 * cryptoCodesCount + 3)
const testNets = arr.slice(2 * cryptoCodesCount + 3)
const coinParams = _.zip(cryptoCodes, testNets)
return {
cassettes,
rates: buildRates(tickers),
balances: buildBalances(balances),
coins: _.map(mapCoinSettings, coinParams),
configVersion
}
})
return {
cassettes,
rates: buildRates(tickers),
balances: buildBalances(balances),
coins: _.map(mapCoinSettings, coinParams),
configVersion
}
})
}
function sendCoins (tx) {
@ -275,26 +275,26 @@ function plugins (settings, deviceId) {
ticker.getRates(settings, fiatCode, cryptoCode),
wallet.balance(settings, cryptoCode)
])
.then(([rates, balanceRec]) => {
if (!rates || !balanceRec) return null
.then(([rates, balanceRec]) => {
if (!rates || !balanceRec) return null
const rawRate = rates.rates.ask
const cashInCommission = BN(1).minus(BN(config.cashInCommission).div(100))
const balance = balanceRec.balance
const rawRate = rates.rates.ask
const cashInCommission = BN(1).minus(BN(config.cashInCommission).div(100))
const balance = balanceRec.balance
if (!rawRate || !balance) return null
if (!rawRate || !balance) return null
const rate = rawRate.div(cashInCommission)
const rate = rawRate.div(cashInCommission)
const lowBalanceMargin = BN(1)
const lowBalanceMargin = BN(1)
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
const unitScale = cryptoRec.unitScale
const shiftedRate = rate.shift(-unitScale)
const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
const unitScale = cryptoRec.unitScale
const shiftedRate = rate.shift(-unitScale)
const fiatTransferBalance = balance.mul(shiftedRate).div(lowBalanceMargin)
return {timestamp: balanceRec.timestamp, balance: fiatTransferBalance.truncated().toString()}
})
return {timestamp: balanceRec.timestamp, balance: fiatTransferBalance.truncated().toString()}
})
}
function notifyConfirmation (tx) {
@ -310,17 +310,17 @@ function plugins (settings, deviceId) {
}
return sms.sendMessage(settings, rec)
.then(() => {
const sql = 'update cash_out_txs set notified=$1 where id=$2'
const values = [true, tx.id]
.then(() => {
const sql = 'update cash_out_txs set notified=$1 where id=$2'
const values = [true, tx.id]
return db.none(sql, values)
})
return db.none(sql, values)
})
}
function pong () {
db.none('insert into server_events (event_type) values ($1)', ['ping'])
.catch(logger.error)
.catch(logger.error)
}
function pongClear () {
@ -329,7 +329,7 @@ function plugins (settings, deviceId) {
and created < now() - interval $2`
db.none(sql, ['ping', PONG_TTL])
.catch(logger.error)
.catch(logger.error)
}
/*
@ -375,9 +375,9 @@ function plugins (settings, deviceId) {
const t1 = Date.now()
const filtered = marketTradesQueues
.filter(tradeEntry => {
return t1 - tradeEntry.timestamp < TRADE_TTL
})
.filter(tradeEntry => {
return t1 - tradeEntry.timestamp < TRADE_TTL
})
const filteredCount = marketTradesQueues.length - filtered.length
@ -389,7 +389,7 @@ function plugins (settings, deviceId) {
if (filtered.length === 0) return null
const cryptoAtoms = filtered
.reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0))
.reduce((prev, current) => prev.plus(current.cryptoAtoms), BN(0))
const timestamp = filtered.map(r => r.timestamp).reduce((acc, r) => Math.max(acc, r), 0)
@ -408,22 +408,22 @@ function plugins (settings, deviceId) {
function executeTrades () {
return machineLoader.getMachines()
.then(devices => {
const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => {
const config = configManager.machineScoped(deviceId, settings.config)
const fiatCode = config.fiatCurrency
const cryptoCodes = config.cryptoCurrencies
.then(devices => {
const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => {
const config = configManager.machineScoped(deviceId, settings.config)
const fiatCode = config.fiatCurrency
const cryptoCodes = config.cryptoCurrencies
return cryptoCodes.map(cryptoCode => ({fiatCode, cryptoCode}))
return cryptoCodes.map(cryptoCode => ({fiatCode, cryptoCode}))
})
const tradesPromises = _.uniq(_.flatten(lists))
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
return Promise.all(tradesPromises)
})
const tradesPromises = _.uniq(_.flatten(lists))
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
return Promise.all(tradesPromises)
})
.catch(logger.error)
.catch(logger.error)
}
function executeTradesForMarket (settings, fiatCode, cryptoCode) {
@ -435,11 +435,11 @@ function plugins (settings, deviceId) {
if (tradeEntry === null || tradeEntry.cryptoAtoms.eq(0)) return
return executeTradeForType(tradeEntry)
.catch(err => {
tradesQueues[market].push(tradeEntry)
if (err.name === 'orderTooSmall') return logger.debug(err.message)
logger.error(err)
})
.catch(err => {
tradesQueues[market].push(tradeEntry)
if (err.name === 'orderTooSmall') return logger.debug(err.message)
logger.error(err)
})
}
function executeTradeForType (_tradeEntry) {
@ -452,17 +452,17 @@ function plugins (settings, deviceId) {
const execute = tradeEntry.type === 'buy' ? exchange.buy : exchange.sell
return execute(settings, tradeEntry.cryptoAtoms, tradeEntry.fiatCode, tradeEntry.cryptoCode)
.then(() => recordTrade(tradeEntry))
.then(() => recordTrade(tradeEntry))
}
function convertBigNumFields (obj) {
const convert = (value, key) => _.includes(key, ['cryptoAtoms', 'fiat'])
? value.toString()
: value
? value.toString()
: value
const convertKey = key => _.includes(key, ['cryptoAtoms', 'fiat'])
? key + '#'
: key
? key + '#'
: key
return _.mapKeys(convertKey, mapValuesWithKey(convert, obj))
}
@ -501,18 +501,28 @@ function plugins (settings, deviceId) {
const cashOutEnabled = config.cashOutEnabled
const cashInAlert = device.cashbox > config.cashInAlertThreshold
? {code: 'CASH_BOX_FULL', machineName, deviceId: device.deviceId, notes: device.cashbox}
: null
? {code: 'CASH_BOX_FULL', machineName, deviceId: device.deviceId, notes: device.cashbox}
: null
const cassette1Alert = cashOutEnabled && device.cassette1 < config.cashOutCassette1AlertThreshold
? {code: 'LOW_CASH_OUT', cassette: 1, machineName, deviceId: device.deviceId,
notes: device.cassette1, denomination: denomination1, fiatCode}
: null
? {code: 'LOW_CASH_OUT',
cassette: 1,
machineName,
deviceId: device.deviceId,
notes: device.cassette1,
denomination: denomination1,
fiatCode}
: null
const cassette2Alert = cashOutEnabled && device.cassette2 < config.cashOutCassette2AlertThreshold
? {code: 'LOW_CASH_OUT', cassette: 2, machineName, deviceId: device.deviceId,
notes: device.cassette2, denomination: denomination2, fiatCode}
: null
? {code: 'LOW_CASH_OUT',
cassette: 2,
machineName,
deviceId: device.deviceId,
notes: device.cassette2,
denomination: denomination2,
fiatCode}
: null
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
}
@ -530,7 +540,7 @@ function plugins (settings, deviceId) {
const checkCryptoBalanceWithFiat = _.partial(checkCryptoBalance, [fiatCode])
return Promise.all(fiatBalancePromises(cryptoCodes))
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
.then(balances => _.map(checkCryptoBalanceWithFiat, _.zip(cryptoCodes, balances)))
}
function checkCryptoBalance (fiatCode, rec) {
@ -542,8 +552,8 @@ function plugins (settings, deviceId) {
const cryptoAlertThreshold = config.cryptoAlertThreshold
return BN(fiatBalance.balance).lt(cryptoAlertThreshold)
? {code: 'LOW_CRYPTO_BALANCE', cryptoCode, fiatBalance, fiatCode}
: null
? {code: 'LOW_CRYPTO_BALANCE', cryptoCode, fiatBalance, fiatCode}
: null
}
function checkBalances () {
@ -551,13 +561,13 @@ function plugins (settings, deviceId) {
const fiatCode = globalConfig.fiatCurrency
return machineLoader.getMachines()
.then(devices => {
return Promise.all([
checkCryptoBalances(fiatCode, devices),
checkDevicesCashBalances(fiatCode, devices)
])
.then(_.flow(_.flattenDeep, _.compact))
})
.then(devices => {
return Promise.all([
checkCryptoBalances(fiatCode, devices),
checkDevicesCashBalances(fiatCode, devices)
])
.then(_.flow(_.flattenDeep, _.compact))
})
}
function randomCode () {
@ -566,8 +576,8 @@ function plugins (settings, deviceId) {
function getPhoneCode (phone) {
const code = argv.mockSms
? '123'
: randomCode()
? '123'
: randomCode()
const rec = {
sms: {
@ -577,24 +587,24 @@ function plugins (settings, deviceId) {
}
return sms.sendMessage(settings, rec)
.then(() => code)
.then(() => code)
}
function sweepHdRow (row) {
const cryptoCode = row.crypto_code
return wallet.sweep(settings, cryptoCode, row.hd_index)
.then(txHash => {
if (txHash) {
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
.then(txHash => {
if (txHash) {
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
const sql = `update cash_out_txs set swept='t'
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))
return db.none(sql, row.id)
}
})
.catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
}
function sweepHd () {
@ -602,8 +612,8 @@ function plugins (settings, deviceId) {
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
return db.any(sql)
.then(rows => Promise.all(rows.map(sweepHdRow)))
.catch(err => logger.error(err))
.then(rows => Promise.all(rows.map(sweepHdRow)))
.catch(err => logger.error(err))
}
function getMachineNames () {

View file

@ -37,10 +37,10 @@ function authRequest (config, path, data) {
const msg = [nonce, config.clientId, config.key].join('')
const signature = crypto
.createHmac('sha256', Buffer.from(config.secret))
.update(msg)
.digest('hex')
.toUpperCase()
.createHmac('sha256', Buffer.from(config.secret))
.update(msg)
.digest('hex')
.toUpperCase()
const signedData = _.merge(data, {
key: config.key,
@ -52,7 +52,7 @@ function authRequest (config, path, data) {
}
function buildMarket (fiatCode, cryptoCode) {
if (!_.includes(cryptoCode, ['BTC', 'LTC'])) {
if (!_.includes(cryptoCode, ['BTC', 'ETH', 'LTC', 'BCH'])) {
throw new Error('Unsupported crypto: ' + cryptoCode)
}
@ -76,7 +76,7 @@ function request (path, method, data) {
if (data) options.data = querystring.stringify(data)
return axios(options)
.then(r => r.data)
.then(r => r.data)
}
module.exports = {

View file

@ -18,17 +18,17 @@ function fetch (account, method, params) {
url: `http://localhost:${account.port}`,
data
})
.then(r => {
if (r.error) throw r.error
return r.data.result
})
.catch(err => {
console.log(err.message)
try {
console.log(err.response.data.error)
} catch (__) {}
throw err
})
.then(r => {
if (r.error) throw r.error
return r.data.result
})
.catch(err => {
console.log(err.message)
try {
console.log(err.response.data.error)
} catch (__) {}
throw err
})
}
function split (str) {

View file

@ -22,17 +22,19 @@ function handleErrors (data) {
throw err
}
function trade (type, account, cryptoAtoms, fiatCode, cryptoCode) {
function trade (type, account, cryptoAtoms, _fiatCode, cryptoCode) {
const fiatCode = _fiatCode === 'USD' ? 'USD' : 'EUR'
try {
const market = common.buildMarket(fiatCode, cryptoCode)
const options = {amount: cryptoAtoms.shift(-SATOSHI_SHIFT).toFixed(8)}
return common.authRequest(account, '/' + type + '/market/' + market, options)
.catch(e => {
if (e.response) handleErrors(e.response.data)
throw e
})
.then(handleErrors)
.catch(e => {
if (e.response) handleErrors(e.response.data)
throw e
})
.then(handleErrors)
} catch (e) {
return Promise.reject(e)
}

View file

@ -1,5 +1,6 @@
// Note: Using DeX3/npm-kraken-api to adjust timeout time
const Kraken = require('kraken-api')
const _ = require('lodash/fp')
const common = require('../../common/kraken')
@ -19,11 +20,14 @@ function trade (account, type, cryptoAtoms, fiatCode, cryptoCode) {
const kraken = new Kraken(account.apiKey, account.privateKey, {timeout: 30000})
const amount = common.toUnit(cryptoAtoms, cryptoCode)
const amountStr = amount.toFixed(6)
const pair = PAIRS[cryptoCode][fiatCode]
const pair = _.includes(fiatCode, ['USD', 'EUR'])
? PAIRS[cryptoCode][fiatCode]
: PAIRS[cryptoCode]['EUR']
var orderInfo = {
pair: pair,
type: type,
pair,
type,
ordertype: 'market',
volume: amountStr,
expiretm: '+60'

View file

@ -18,15 +18,15 @@ function sendMessage (account, rec) {
}
return client.messages.create(opts)
.catch(err => {
if (_.includes(err.code, BAD_NUMBER_CODES)) {
const badNumberError = new Error(err.message)
badNumberError.name = 'BadNumberError'
throw badNumberError
}
.catch(err => {
if (_.includes(err.code, BAD_NUMBER_CODES)) {
const badNumberError = new Error(err.message)
badNumberError.name = 'BadNumberError'
throw badNumberError
}
throw new Error(err.message)
})
throw new Error(err.message)
})
}
module.exports = {

View file

@ -2,18 +2,17 @@ const axios = require('axios')
const BN = require('../../../bn')
function ticker (account, fiatCode, cryptoCode) {
return axios.get('https://bitpay.com/api/rates/' + cryptoCode + '/' + fiatCode)
.then(r => {
const data = r.data
const price = BN(data.rate)
return {
rates: {
ask: price,
bid: price
.then(r => {
const data = r.data
const price = BN(data.rate)
return {
rates: {
ask: price,
bid: price
}
}
}
})
})
}
module.exports = {

View file

@ -3,16 +3,16 @@ const common = require('../../common/bitstamp')
function ticker (account, fiatCode, cryptoCode) {
return Promise.resolve()
.then(() => {
const market = common.buildMarket(fiatCode, cryptoCode)
return common.request('/ticker/' + market, 'GET')
})
.then(r => ({
rates: {
ask: BN(r.ask),
bid: BN(r.bid)
}
}))
.then(() => {
const market = common.buildMarket(fiatCode, cryptoCode)
return common.request('/ticker/' + market, 'GET')
})
.then(r => ({
rates: {
ask: BN(r.ask),
bid: BN(r.bid)
}
}))
}
module.exports = {

View file

@ -11,7 +11,7 @@ function getBuyPrice (obj) {
url: `https://api.coinbase.com/v2/prices/${currencyPair}/buy`,
headers: {'CB-Version': '2017-07-10'}
})
.then(r => r.data)
.then(r => r.data)
}
function getSellPrice (obj) {
@ -22,34 +22,33 @@ function getSellPrice (obj) {
url: `https://api.coinbase.com/v2/prices/${currencyPair}/sell`,
headers: {'CB-Version': '2017-07-10'}
})
.then(r => r.data)
.then(r => r.data)
}
function ticker (account, fiatCode, cryptoCode) {
return Promise.resolve()
.then(() => {
if (!_.includes(cryptoCode, ['BTC', 'ETH', 'LTC', 'BCH'])) {
throw new Error('Unsupported crypto: ' + cryptoCode)
}
})
.then(() => {
const currencyPair = `${cryptoCode}-${fiatCode}`
const promises = [
getBuyPrice({currencyPair}),
getSellPrice({currencyPair})
]
.then(() => {
if (!_.includes(cryptoCode, ['BTC', 'ETH', 'LTC', 'BCH'])) {
throw new Error('Unsupported crypto: ' + cryptoCode)
}
})
.then(() => {
const currencyPair = `${cryptoCode}-${fiatCode}`
const promises = [
getBuyPrice({currencyPair}),
getSellPrice({currencyPair})
]
return Promise.all(promises)
})
.then(([buyPrice, sellPrice]) => ({
rates: {
ask: BN(buyPrice.data.amount),
bid: BN(sellPrice.data.amount)
}
}))
return Promise.all(promises)
})
.then(([buyPrice, sellPrice]) => ({
rates: {
ask: BN(buyPrice.data.amount),
bid: BN(sellPrice.data.amount)
}
}))
}
module.exports = {
ticker
}

View file

@ -21,32 +21,32 @@ exports.ticker = function ticker (account, fiatCode, cryptoCode) {
}
return axios.get('https://bitpay.com/api/rates')
.then(response => {
const fxRates = response.data
const usdRate = findCurrency(fxRates, 'USD')
const fxRate = findCurrency(fxRates, fiatCode).div(usdRate)
.then(response => {
const fxRates = response.data
const usdRate = findCurrency(fxRates, 'USD')
const fxRate = findCurrency(fxRates, fiatCode).div(usdRate)
return getCurrencyRates('USD', cryptoCode)
.then(res => ({
rates: {
ask: res.rates.ask.times(fxRate),
bid: res.rates.bid.times(fxRate)
}
}))
})
return getCurrencyRates('USD', cryptoCode)
.then(res => ({
rates: {
ask: res.rates.ask.times(fxRate),
bid: res.rates.bid.times(fxRate)
}
}))
})
}
function getCurrencyRates (fiatCode, cryptoCode) {
const pair = PAIRS[cryptoCode][fiatCode]
return axios.get('https://api.kraken.com/0/public/Ticker?pair=' + pair)
.then(function (response) {
const rates = response.data.result[pair]
return {
rates: {
ask: BN(rates.a[0]),
bid: BN(rates.b[0])
.then(function (response) {
const rates = response.data.result[pair]
return {
rates: {
ask: BN(rates.a[0]),
bid: BN(rates.b[0])
}
}
}
})
})
}

View file

@ -1,6 +1,5 @@
const jsonRpc = require('../../common/json-rpc')
const bs58check = require('bs58check')
const BN = require('../../../bn')
const E = require('../../../error')
const coinUtils = require('../../../coin-utils')
@ -27,8 +26,8 @@ function checkCryptoCode (cryptoCode) {
function accountBalance (account, cryptoCode, confirmations) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
}
// We want a balance that includes all spends (0 conf) but only deposits that
@ -37,93 +36,73 @@ function balance (account, cryptoCode) {
return accountBalance(account, cryptoCode, 1)
}
function bchToBtcVersion (version) {
if (version === 0x1c) return 0x00
if (version === 0x28) return 0x05
return version
}
// Bitcoin-ABC only accepts BTC style addresses at this point,
// so we need to convert
function bchToBtcAddress (address) {
const buf = bs58check.decode(address)
const version = buf[0]
buf[0] = bchToBtcVersion(version)
return bs58check.encode(buf)
}
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const coins = cryptoAtoms.shift(-unitScale).toFixed(8)
const btcAddress = bchToBtcAddress(address)
return checkCryptoCode(cryptoCode)
.then(() => fetch('sendtoaddress', [btcAddress, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
.then(() => fetch('getnewaddress'))
}
function addressBalance (address, confs) {
const btcAddress = bchToBtcAddress(address)
return fetch('getreceivedbyaddress', [btcAddress, confs])
.then(r => BN(r).shift(unitScale).round())
return fetch('getreceivedbyaddress', [address, confs])
.then(r => BN(r).shift(unitScale).round())
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 1))
.then(() => addressBalance(address, 1))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 0))
.then(() => addressBalance(address, 0))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
function cryptoNetwork (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
}
module.exports = {

View file

@ -24,10 +24,10 @@ function checkCryptoCode (cryptoCode) {
return Promise.resolve()
}
function accountBalance (acount, cryptoCode, confirmations) {
function accountBalance (account, cryptoCode, confirmations) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
}
// We want a balance that includes all spends (0 conf) but only deposits that
@ -40,69 +40,69 @@ function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const coins = cryptoAtoms.shift(-unitScale).toFixed(8)
return checkCryptoCode(cryptoCode)
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
.then(() => fetch('getnewaddress'))
}
function addressBalance (address, confs) {
return fetch('getreceivedbyaddress', [address, confs])
.then(r => BN(r).shift(unitScale).round())
.then(r => BN(r).shift(unitScale).round())
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 1))
.then(() => addressBalance(address, 1))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 0))
.then(() => addressBalance(address, 0))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
function cryptoNetwork (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
}
module.exports = {

View file

@ -29,83 +29,83 @@ function checkCryptoCode (cryptoCode) {
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => {
const params = {
address: address,
amount: cryptoAtoms.toNumber(),
walletPassphrase: account.walletPassphrase
}
return wallet.sendCoins(params)
})
.then(result => {
return result.hash
})
.catch(err => {
if (err.message === 'Insufficient funds') throw new E.InsufficientFundsError()
throw err
})
.then(() => getWallet(account))
.then(wallet => {
const params = {
address: address,
amount: cryptoAtoms.toNumber(),
walletPassphrase: account.walletPassphrase
}
return wallet.sendCoins(params)
})
.then(result => {
return result.hash
})
.catch(err => {
if (err.message === 'Insufficient funds') throw new E.InsufficientFundsError()
throw err
})
}
function balance (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => getWallet(account))
.then(wallet => BN(wallet.wallet.spendableConfirmedBalance))
.then(() => getWallet(account))
.then(wallet => BN(wallet.wallet.spendableConfirmedBalance))
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => getWallet(account))
.then(wallet => {
return wallet.createAddress()
.then(result => {
const address = result.address
.then(() => getWallet(account))
.then(wallet => {
return wallet.createAddress()
.then(result => {
const address = result.address
// If a label was provided, set the label
if (info.label) {
return wallet.setLabel({ address: address, label: info.label })
.then(() => address)
}
// If a label was provided, set the label
if (info.label) {
return wallet.setLabel({ address: address, label: info.label })
.then(() => address)
}
return address
return address
})
})
})
}
function getStatus (account, toAddress, requested, cryptoCode) {
const bitgo = buildBitgo(account)
return checkCryptoCode(cryptoCode)
.then(() => bitgo.blockchain().getAddress({address: toAddress}))
.then(rec => {
if (rec.balance === 0) return {status: 'notSeen'}
if (requested.gt(rec.balance)) return {status: 'insufficientFunds'}
if (requested.gt(rec.confirmedBalance)) return {status: 'authorized'}
return {status: 'confirmed'}
})
.then(() => bitgo.blockchain().getAddress({address: toAddress}))
.then(rec => {
if (rec.balance === 0) return {status: 'notSeen'}
if (requested.gt(rec.balance)) return {status: 'insufficientFunds'}
if (requested.gt(rec.confirmedBalance)) return {status: 'authorized'}
return {status: 'confirmed'}
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
return getWallet(account)
.then(wallet => {
return wallet.createAddress()
.then(result => {
const fundingAddress = result.address
return wallet.setLabel({address: fundingAddress, label: 'Funding Address'})
.then(() => ({
fundingPendingBalance: BN(wallet.wallet.balance),
fundingConfirmedBalance: BN(wallet.wallet.confirmedBalance),
fundingAddress
}))
})
.then(() => {
return getWallet(account)
.then(wallet => {
return wallet.createAddress()
.then(result => {
const fundingAddress = result.address
return wallet.setLabel({address: fundingAddress, label: 'Funding Address'})
.then(() => ({
fundingPendingBalance: BN(wallet.wallet.balance),
fundingConfirmedBalance: BN(wallet.wallet.confirmedBalance),
fundingAddress
}))
})
})
})
})
}
function cryptoNetwork (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => account.environment === 'test' ? 'test' : 'main')
.then(() => account.environment === 'test' ? 'test' : 'main')
}
module.exports = {

View file

@ -27,8 +27,8 @@ function checkCryptoCode (cryptoCode) {
function accountBalance (acount, cryptoCode, confirmations) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
}
// We want a balance that includes all spends (0 conf) but only deposits that
@ -41,64 +41,64 @@ function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const coins = cryptoAtoms.shift(-unitScale).toFixed(8)
return checkCryptoCode(cryptoCode)
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
.then(() => fetch('getnewaddress'))
}
function addressBalance (address, confs) {
return fetch('getreceivedbyaddress', [address, confs])
.then(r => BN(r).shift(unitScale).round())
.then(r => BN(r).shift(unitScale).round())
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 1))
.then(() => addressBalance(address, 1))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 0))
.then(() => addressBalance(address, 0))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
module.exports = {

View file

@ -42,7 +42,7 @@ function privateKey (account) {
function sendCoins (account, toAddress, cryptoAtoms, cryptoCode) {
return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false)
.then(pify(web3.eth.sendRawTransaction))
.then(pify(web3.eth.sendRawTransaction))
}
function checkCryptoCode (cryptoCode) {
@ -52,7 +52,7 @@ function checkCryptoCode (cryptoCode) {
function balance (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => pendingBalance(defaultAddress(account)))
.then(() => pendingBalance(defaultAddress(account)))
}
const pendingBalance = address => _balance(true, address)
@ -81,31 +81,31 @@ function generateTx (_toAddress, wallet, amount, includesFee) {
]
return Promise.all(promises)
.then(arr => {
const gas = arr[0]
const gasPrice = arr[1]
const txCount = arr[2]
.then(arr => {
const gas = arr[0]
const gasPrice = arr[1]
const txCount = arr[2]
const toSend = includesFee
? amount.minus(gasPrice.times(gas))
: amount
const toSend = includesFee
? amount.minus(gasPrice.times(gas))
: amount
const rawTx = {
nonce: txCount,
gasPrice: hex(gasPrice),
gasLimit: gas,
to: toAddress,
from: fromAddress,
value: hex(toSend)
}
const rawTx = {
nonce: txCount,
gasPrice: hex(gasPrice),
gasLimit: gas,
to: toAddress,
from: fromAddress,
value: hex(toSend)
}
const tx = new Tx(rawTx)
const privateKey = wallet.getPrivateKey()
const tx = new Tx(rawTx)
const privateKey = wallet.getPrivateKey()
tx.sign(privateKey)
tx.sign(privateKey)
return '0x' + tx.serialize().toString('hex')
})
return '0x' + tx.serialize().toString('hex')
})
}
function defaultWallet (account) {
@ -121,12 +121,12 @@ function sweep (account, cryptoCode, hdIndex) {
const fromAddress = wallet.getChecksumAddressString()
return confirmedBalance(fromAddress)
.then(r => {
if (r.eq(0)) return
.then(r => {
if (r.eq(0)) return
return generateTx(defaultAddress(account), wallet, r, true)
.then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx))
})
return generateTx(defaultAddress(account), wallet, r, true)
.then(signedTx => pify(web3.eth.sendRawTransaction)(signedTx))
})
}
function newAddress (account, info) {
@ -136,17 +136,17 @@ function newAddress (account, info) {
function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress))
.then(confirmed => {
if (confirmed.gte(cryptoAtoms)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress))
.then(confirmed => {
if (confirmed.gte(cryptoAtoms)) return {status: 'confirmed'}
return pendingBalance(toAddress)
.then(pending => {
if (pending.gte(cryptoAtoms)) return {status: 'published'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress)
.then(pending => {
if (pending.gte(cryptoAtoms)) return {status: 'published'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function paymentHdNode (account) {
@ -165,19 +165,19 @@ function defaultHdNode (account) {
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const fundingAddress = defaultAddress(account)
.then(() => {
const fundingAddress = defaultAddress(account)
const promises = [
pendingBalance(fundingAddress),
confirmedBalance(fundingAddress)
]
const promises = [
pendingBalance(fundingAddress),
confirmedBalance(fundingAddress)
]
return Promise.all(promises)
.then(([fundingPendingBalance, fundingConfirmedBalance]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
})
return Promise.all(promises)
.then(([fundingPendingBalance, fundingConfirmedBalance]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
})
}

View file

@ -27,8 +27,8 @@ function checkCryptoCode (cryptoCode) {
function accountBalance (acount, cryptoCode, confirmations) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
}
// We want a balance that includes all spends (0 conf) but only deposits that
@ -41,64 +41,64 @@ function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const coins = cryptoAtoms.shift(-unitScale).toFixed(8)
return checkCryptoCode(cryptoCode)
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
.then(() => fetch('getnewaddress'))
}
function addressBalance (address, confs) {
return fetch('getreceivedbyaddress', [address, confs])
.then(r => BN(r).shift(unitScale).round())
.then(r => BN(r).shift(unitScale).round())
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 1))
.then(() => addressBalance(address, 1))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 0))
.then(() => addressBalance(address, 0))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
module.exports = {

View file

@ -34,11 +34,11 @@ function checkCryptoCode (cryptoCode) {
function balance (acount, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(connect)
.then(c => c.channelBalance({}))
.then(_.get('balance'))
.then(BN)
.then(r => r.shift(unitScale).round())
.then(connect)
.then(c => c.channelBalance({}))
.then(_.get('balance'))
.then(BN)
.then(r => r.shift(unitScale).round())
}
function sendCoins (account, address, cryptoAtoms, cryptoCode) {
@ -53,37 +53,37 @@ function newFunding (account, cryptoCode) {
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(connect)
.then(c => {
if (info.isLightning) {
return c.addInvoice({memo: 'Lamassu cryptomat deposit', value: info.cryptoAtoms.toNumber()})
.then(r => `${r.r_hash.toString('hex')}:${r.payment_request}`)
}
.then(connect)
.then(c => {
if (info.isLightning) {
return c.addInvoice({memo: 'Lamassu cryptomat deposit', value: info.cryptoAtoms.toNumber()})
.then(r => `${r.r_hash.toString('hex')}:${r.payment_request}`)
}
return c.newAddress({type: 2})
.then(_.get('address'))
})
return c.newAddress({type: 2})
.then(_.get('address'))
})
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const parts = _.split(':', toAddress)
const isLightning = _.size(parts) === 2
const rHashStr = isLightning && _.head(parts)
.then(() => {
const parts = _.split(':', toAddress)
const isLightning = _.size(parts) === 2
const rHashStr = isLightning && _.head(parts)
return connect()
.then(c => {
if (isLightning) {
return c.lookupInvoice({r_hash_str: rHashStr})
.then(r => {
if (r.settled) return {status: 'confirmed'}
return connect()
.then(c => {
if (isLightning) {
return c.lookupInvoice({r_hash_str: rHashStr})
.then(r => {
if (r.settled) return {status: 'confirmed'}
return {status: 'notSeen'}
})
}
// Note: this must be handled outside of lnd
return {status: 'notSeen'}
})
}
// Note: this must be handled outside of lnd
return {status: 'notSeen'}
})
})
}

View file

@ -19,12 +19,12 @@ function _balance (cryptoCode) {
function balance (account, cryptoCode) {
return Promise.resolve()
.then(() => _balance(cryptoCode))
.then(() => _balance(cryptoCode))
}
function pendingBalance (account, cryptoCode) {
return balance(account, cryptoCode)
.then(b => b.mul(1.1))
.then(b => b.mul(1.1))
}
function confirmedBalance (account, cryptoCode) {
@ -69,11 +69,11 @@ function newFunding (account, cryptoCode) {
]
return Promise.all(promises)
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
@ -83,7 +83,8 @@ function getStatus (account, toAddress, cryptoAtoms, cryptoCode) {
if (elapsed < AUTHORIZE_TIME) return Promise.resolve({status: 'published'})
if (elapsed < CONFIRM_TIME) return Promise.resolve({status: 'authorized'})
console.log('[%s] DEBUG: Mock wallet has confirmed transaction: %s', cryptoCode, toAddress)
console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5))
return Promise.resolve({status: 'confirmed'})
}

View file

@ -27,8 +27,8 @@ function checkCryptoCode (cryptoCode) {
function accountBalance (acount, cryptoCode, confirmations) {
return checkCryptoCode(cryptoCode)
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
.then(() => fetch('getbalance', ['', confirmations]))
.then(r => BN(r).shift(unitScale).round())
}
// We want a balance that includes all spends (0 conf) but only deposits that
@ -41,64 +41,64 @@ function sendCoins (account, address, cryptoAtoms, cryptoCode) {
const coins = cryptoAtoms.shift(-unitScale).toFixed(8)
return checkCryptoCode(cryptoCode)
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
.then(() => fetch('sendtoaddress', [address, coins]))
.catch(err => {
if (err.code === -6) throw new E.InsufficientFundsError()
throw err
})
}
function newAddress (account, info) {
return checkCryptoCode(info.cryptoCode)
.then(() => fetch('getnewaddress'))
.then(() => fetch('getnewaddress'))
}
function addressBalance (address, confs) {
return fetch('getreceivedbyaddress', [address, confs])
.then(r => BN(r).shift(unitScale).round())
.then(r => BN(r).shift(unitScale).round())
}
function confirmedBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 1))
.then(() => addressBalance(address, 1))
}
function pendingBalance (address, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => addressBalance(address, 0))
.then(() => addressBalance(address, 0))
}
function getStatus (account, toAddress, requested, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
.then(() => confirmedBalance(toAddress, cryptoCode))
.then(confirmed => {
if (confirmed.gte(requested)) return {status: 'confirmed'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
return pendingBalance(toAddress, cryptoCode)
.then(pending => {
if (pending.gte(requested)) return {status: 'authorized'}
if (pending.gt(0)) return {status: 'insufficientFunds'}
return {status: 'notSeen'}
})
})
})
}
function newFunding (account, cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
.then(() => {
const promises = [
accountBalance(account, cryptoCode, 0),
accountBalance(account, cryptoCode, 1),
newAddress(account, {cryptoCode})
]
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
return Promise.all(promises)
})
.then(([fundingPendingBalance, fundingConfirmedBalance, fundingAddress]) => ({
fundingPendingBalance,
fundingConfirmedBalance,
fundingAddress
}))
}
module.exports = {

View file

@ -12,27 +12,26 @@ function highConfidence (confidence, txref) {
function authorize (account, toAddress, cryptoAtoms, cryptoCode) {
return Promise.resolve()
.then(() => {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
.then(() => {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
const query = qs.stringify({
token: account.token,
includeConfidence: true
const query = qs.stringify({
token: account.token,
includeConfidence: true
})
const confidence = account.confidenceFactor
const url = `https://api.blockcypher.com/v1/btc/main/addrs/${toAddress}?${query}`
return axios.get(url)
.then(r => {
const data = r.data
const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref), txrefs)
const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs)
return cryptoAtoms.lte(authorizedValue)
})
})
const confidence = account.confidenceFactor
const url = `https://api.blockcypher.com/v1/btc/main/addrs/${toAddress}?${query}`
return axios.get(url)
.then(r => {
const data = r.data
const sumTxRefs = txrefs => _.sumBy(txref => highConfidence(confidence, txref), txrefs)
const authorizedValue = sumTxRefs(data.txrefs) + sumTxRefs(data.unconfirmed_txrefs)
return cryptoAtoms.lte(authorizedValue)
})
})
}

View file

@ -2,10 +2,10 @@ module.exports = {authorize}
function authorize (account, toAddress, cryptoAtoms, cryptoCode) {
return Promise.resolve()
.then(() => {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
.then(() => {
if (cryptoCode !== 'BTC') throw new Error('Unsupported crypto: ' + cryptoCode)
const isAuthorized = false
return isAuthorized
})
const isAuthorized = false
return isAuthorized
})
}

View file

@ -2,8 +2,8 @@ const plugins = require('./plugins')
const notifier = require('./notifier')
const T = require('./time')
const logger = require('./logger')
const cashOutTx = require('./cash-out-tx')
const cashInTx = require('./cash-in-tx')
const cashOutTx = require('./cash-out/cash-out-tx')
const cashInTx = require('./cash-in/cash-in-tx')
const INCOMING_TX_INTERVAL = 30 * T.seconds
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds

View file

@ -28,10 +28,10 @@ exports.cassetteCounts = function cassetteCounts (deviceId) {
'WHERE device_id=$1'
return db.one(sql, [deviceId])
.then(row => {
const counts = [row.cassette1, row.cassette2]
return {counts}
})
.then(row => {
const counts = [row.cassette1, row.cassette2]
return {counts}
})
}
// Note: since we only prune on insert, we'll always have
@ -47,7 +47,7 @@ exports.machineEvent = function machineEvent (rec) {
and created < now() - interval '2 days'`
return db.none(sql, values)
.then(() => db.none(deleteSql, [rec.deviceId, rec.eventType]))
.then(() => db.none(deleteSql, [rec.deviceId, rec.eventType]))
}
exports.machineEvents = function machineEvents () {

View file

@ -1,5 +1,4 @@
const _ = require('lodash/fp')
const R = require('ramda')
const db = require('./db')
const dbm = require('./postgresql_interface')
@ -55,32 +54,34 @@ function fetchPhoneTx (phone) {
const values = [phone, false, TRANSACTION_EXPIRATION]
return db.any(sql, values)
.then(_.map(toCashOutTx))
.then(txs => {
const confirmedTxs = txs.filter(tx => R.contains(tx.status, ['instant', 'confirmed']))
if (confirmedTxs.length > 0) {
const maxTx = R.reduce((acc, val) => {
return !acc || val.cryptoAtoms.gt(acc.cryptoAtoms) ? val : acc
}, null, confirmedTxs)
.then(_.map(toCashOutTx))
.then(txs => {
const confirmedTxs = txs.filter(tx => _.includes(tx.status, ['instant', 'confirmed']))
if (confirmedTxs.length > 0) {
const reducer = (acc, val) => {
return !acc || val.cryptoAtoms.gt(acc.cryptoAtoms) ? val : acc
}
return maxTx
}
const maxTx = _.reduce(reducer, null, confirmedTxs)
if (txs.length > 0) throw httpError('Pending transactions', 412)
throw httpError('No transactions', 404)
})
return maxTx
}
if (txs.length > 0) throw httpError('Pending transactions', 412)
throw httpError('No transactions', 404)
})
}
function fetchStatusTx (txId, status) {
const sql = 'select * from cash_out_txs where id=$1'
return db.oneOrNone(sql, [txId])
.then(toCashOutTx)
.then(tx => {
if (!tx) throw httpError('No transaction', 404)
if (tx.status === status) throw httpError('Not Modified', 304)
return tx
})
.then(toCashOutTx)
.then(tx => {
if (!tx) throw httpError('No transaction', 404)
if (tx.status === status) throw httpError('Not Modified', 304)
return tx
})
}
function updateDeviceConfigVersion (versionId) {
@ -105,9 +106,15 @@ function updateMachineDefaults (deviceId) {
}]
return settingsLoader.loadLatest()
.then(settings => {
return settingsLoader.save(settingsLoader.mergeValues(settings.config, newFields))
})
.then(settings => {
return settingsLoader.save(settingsLoader.mergeValues(settings.config, newFields))
})
}
module.exports = {stateChange, fetchPhoneTx, fetchStatusTx, updateDeviceConfigVersion, updateMachineDefaults}
module.exports = {
stateChange,
fetchPhoneTx,
fetchStatusTx,
updateDeviceConfigVersion,
updateMachineDefaults
}

View file

@ -49,62 +49,61 @@ function poll (req, res, next) {
pids[deviceId] = {pid, ts: Date.now()}
return pi.pollQueries(serialNumber, deviceTime, req.query)
.then(results => {
const cassettes = results.cassettes
.then(results => {
const cassettes = results.cassettes
const reboot = pid && reboots[deviceId] && reboots[deviceId] === pid
const langs = config.machineLanguages
const reboot = pid && reboots[deviceId] && reboots[deviceId] === pid
const langs = config.machineLanguages
const locale = {
fiatCode: config.fiatCurrency,
localeInfo: {
primaryLocale: langs[0],
primaryLocales: langs,
country: config.country
const locale = {
fiatCode: config.fiatCurrency,
localeInfo: {
primaryLocale: langs[0],
primaryLocales: langs,
country: config.country
}
}
}
const response = {
error: null,
locale,
txLimit: config.cashInTransactionLimit,
idVerificationEnabled: config.idVerificationEnabled,
smsVerificationActive: config.smsVerificationActive,
smsVerificationThreshold: config.smsVerificationThreshold,
hardLimitVerificationActive: config.hardLimitVerificationActive,
hardLimitVerificationThreshold: config.hardLimitVerificationThreshold,
idCardDataVerificationActive: config.idCardDataVerificationActive,
idCardDataVerificationThreshold: config.idCardDataVerificationThreshold,
idCardPhotoVerificationActive: config.idCardPhotoVerificationActive,
idCardPhotoVerificationThreshold: config.idCardPhotoVerificationThreshold,
sanctionsVerificationActive: config.sanctionsVerificationActive,
sanctionsVerificationThreshold: config.sanctionsVerificationThreshold,
crossRefVerificationActive: config.crossRefVerificationActive,
crossRefVerificationThreshold: config.crossRefVerificationThreshold,
frontCameraVerificationActive: config.frontCameraVerificationActive,
frontCameraVerificationThreshold: config.frontCameraVerificationThreshold,
cassettes,
twoWayMode: config.cashOutEnabled,
zeroConfLimit: config.zeroConfLimit,
reboot,
hasLightning
}
const response = {
error: null,
locale,
txLimit: config.cashInTransactionLimit,
idVerificationEnabled: config.idVerificationEnabled,
smsVerificationActive: config.smsVerificationActive,
smsVerificationThreshold: config.smsVerificationThreshold,
hardLimitVerificationActive: config.hardLimitVerificationActive,
hardLimitVerificationThreshold: config.hardLimitVerificationThreshold,
idCardDataVerificationActive: config.idCardDataVerificationActive,
idCardDataVerificationThreshold: config.idCardDataVerificationThreshold,
idCardPhotoVerificationActive: config.idCardPhotoVerificationActive,
idCardPhotoVerificationThreshold: config.idCardPhotoVerificationThreshold,
sanctionsVerificationActive: config.sanctionsVerificationActive,
sanctionsVerificationThreshold: config.sanctionsVerificationThreshold,
crossRefVerificationActive: config.crossRefVerificationActive,
crossRefVerificationThreshold: config.crossRefVerificationThreshold,
frontCameraVerificationActive: config.frontCameraVerificationActive,
frontCameraVerificationThreshold: config.frontCameraVerificationThreshold,
cassettes,
twoWayMode: config.cashOutEnabled,
zeroConfLimit: config.zeroConfLimit,
reboot,
hasLightning
}
if (response.idVerificationEnabled) {
response.idVerificationLimit = config.idVerificationLimit
}
if (response.idVerificationEnabled) {
response.idVerificationLimit = config.idVerificationLimit
}
console.log(response)
return res.json(_.assign(response, results))
})
.catch(next)
return res.json(_.assign(response, results))
})
.catch(next)
}
function getTx (req, res, next) {
if (req.query.status) {
return helpers.fetchStatusTx(req.params.id, req.query.status)
.then(r => res.json(r))
.catch(next)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
@ -113,8 +112,8 @@ function getTx (req, res, next) {
function getPhoneTx (req, res, next) {
if (req.query.phone) {
return helpers.fetchPhoneTx(req.query.phone)
.then(r => res.json(r))
.catch(next)
.then(r => res.json(r))
.catch(next)
}
return next(httpError('Not Found', 404))
@ -124,87 +123,99 @@ function postTx (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
return Tx.post(_.set('deviceId', req.deviceId, req.body), pi)
.then(tx => {
if (tx.errorCode) {
logger.error(tx.error)
throw httpError(tx.error, 500)
}
.then(tx => {
if (tx.errorCode) {
logger.error(tx.error)
throw httpError(tx.error, 500)
}
return res.json(tx)
})
.catch(err => {
if (err instanceof E.StaleTxError) return res.status(409).json({})
if (err instanceof E.RatchetError) return res.status(409).json({})
return res.json(tx)
})
.catch(err => {
if (err instanceof E.StaleTxError) return res.status(409).json({})
if (err instanceof E.RatchetError) return res.status(409).json({})
throw err
})
.catch(next)
throw err
})
.catch(next)
}
function stateChange (req, res, next) {
helpers.stateChange(req.deviceId, req.deviceTime, req.body)
.then(() => respond(req, res))
.catch(next)
.then(() => respond(req, res))
.catch(next)
}
function deviceEvent (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.logEvent(req.body)
.then(() => respond(req, res))
.catch(next)
.then(() => respond(req, res))
.catch(next)
}
function verifyUser (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.verifyUser(req.body)
.then(idResult => respond(req, res, idResult))
.catch(next)
.then(idResult => respond(req, res, idResult))
.catch(next)
}
function verifyTx (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
pi.verifyTransaction(req.body)
.then(idResult => respond(req, res, idResult))
.catch(next)
.then(idResult => respond(req, res, idResult))
.catch(next)
}
function getCustomerWithPhoneCode (req, res, next) {
const pi = plugins(req.settings, req.deviceId)
const phone = req.body.phone
return pi.getPhoneCode(phone)
.then(code => {
return customers.get(phone)
.then(customer => {
if (customer) return respond(req, res, {code, customer})
return customers.add(req.body)
.then(customer => respond(req, res, {code, customer}))
.then(code => {
return customers.get(phone)
.then(customer => {
if (customer) return respond(req, res, {code, customer})
return customers.add(req.body)
.then(customer => respond(req, res, {code, customer}))
})
})
})
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 410)
throw err
})
.catch(next)
.catch(err => {
if (err.name === 'BadNumberError') throw httpError('Bad number', 401)
throw err
})
.catch(next)
}
function updateCustomer (req, res, next) {
const id = req.params.id
const patch = req.body
customers.getById(id)
.then(customer => {
if (!customer) { throw httpError('Not Found', 404) }
return customers.update(id, patch)
})
.then(customer => respond(req, res, {customer}))
.catch(next)
}
function getLastSeen (req, res, next) {
return logs.getLastSeen(req.deviceId)
.then(r => res.json(r))
.catch(next)
.then(r => res.json(r))
.catch(next)
}
function updateLogs (req, res, next) {
return logs.update(req.deviceId, req.body.logs)
.then(status => res.json({success: status}))
.catch(next)
.then(status => res.json({success: status}))
.catch(next)
}
function ca (req, res) {
const token = req.query.token
return pairing.authorizeCaDownload(token)
.then(ca => res.json({ca}))
.catch(() => res.status(403).json({error: 'forbidden'}))
.then(ca => res.json({ca}))
.catch(() => res.status(403).json({error: 'forbidden'}))
}
function pair (req, res, next) {
@ -213,21 +224,21 @@ function pair (req, res, next) {
const model = req.query.model
return pairing.pair(token, deviceId, model)
.then(valid => {
if (valid) {
return helpers.updateMachineDefaults(deviceId)
.then(() => res.json({status: 'paired'}))
}
.then(valid => {
if (valid) {
return helpers.updateMachineDefaults(deviceId)
.then(() => res.json({status: 'paired'}))
}
throw httpError('Pairing failed')
})
.catch(next)
throw httpError('Pairing failed')
})
.catch(next)
}
function errorHandler (err, req, res, next) {
const statusCode = err.name === 'HTTPError'
? err.code || 500
: 500
? err.code || 500
: 500
const json = {error: err.message}
@ -267,15 +278,15 @@ function authorize (req, res, next) {
const deviceId = req.deviceId
return pairing.isPaired(deviceId)
.then(r => {
if (r) {
req.deviceId = deviceId
return next()
}
.then(r => {
if (r) {
req.deviceId = deviceId
return next()
}
return res.status(403).json({error: 'Forbidden'})
})
.catch(next)
return res.status(403).json({error: 'Forbidden'})
})
.catch(next)
}
const skip = (req, res) => _.includes(req.path, ['/poll', '/state', '/logs']) &&
@ -314,6 +325,8 @@ app.post('/verify_user', verifyUser)
app.post('/verify_transaction', verifyTx)
app.post('/phone_code', getCustomerWithPhoneCode)
app.patch('/customer/:id', updateCustomer)
app.post('/tx', postTx)
app.get('/tx/:id', getTx)
app.get('/tx', getPhoneTx)
@ -345,12 +358,12 @@ localApp.post('/reboot', (req, res) => {
localApp.post('/dbChange', (req, res, next) => {
return settingsLoader.loadLatest()
.then(poller.reload)
.then(() => logger.info('Config reloaded'))
.catch(err => {
logger.error(err)
res.sendStatus(500)
})
.then(poller.reload)
.then(() => logger.info('Config reloaded'))
.catch(err => {
logger.error(err)
res.sendStatus(500)
})
})
function sha256 (buf) {
@ -363,8 +376,8 @@ function sha256 (buf) {
function populateDeviceId (req, res, next) {
const deviceId = _.isFunction(req.connection.getPeerCertificate)
? sha256(req.connection.getPeerCertificate().raw)
: null
? sha256(req.connection.getPeerCertificate().raw)
: null
req.deviceId = deviceId
req.deviceTime = req.get('date')
@ -382,16 +395,16 @@ function populateSettings (req, res, next) {
if (!versionId) {
return settingsLoader.loadLatest()
.then(settings => { req.settings = settings })
.then(() => next())
.catch(next)
.then(settings => { req.settings = settings })
.then(() => next())
.catch(next)
}
settingsLoader.load(versionId)
.then(settings => { req.settings = settings })
.then(() => helpers.updateDeviceConfigVersion(versionId))
.then(() => next())
.catch(next)
.then(settings => { req.settings = settings })
.then(() => helpers.updateDeviceConfigVersion(versionId))
.then(() => next())
.catch(next)
}
module.exports = {app, localApp}

View file

@ -21,15 +21,15 @@ function loadFixture () {
const fixturePath = fixture => path.resolve(__dirname, '..', 'test', 'fixtures', fixture + '.json')
const promise = fixture
? pify(fs.readFile)(fixturePath(fixture)).then(JSON.parse)
: Promise.resolve([])
? pify(fs.readFile)(fixturePath(fixture)).then(JSON.parse)
: Promise.resolve([])
return promise
.then(values => _.map(v => {
return (v.fieldLocator.fieldScope.machine === 'machine')
? _.set('fieldLocator.fieldScope.machine', machine, v)
: v
}, values))
.then(values => _.map(v => {
return (v.fieldLocator.fieldScope.machine === 'machine')
? _.set('fieldLocator.fieldScope.machine', machine, v)
: v
}, values))
}
function isEquivalentField (a, b) {
@ -48,18 +48,18 @@ function load (versionId) {
if (!versionId) throw new Error('versionId is required')
return Promise.all([loadConfig(versionId), loadAccounts()])
.then(([config, accounts]) => ({
config,
accounts
}))
.then(([config, accounts]) => ({
config,
accounts
}))
}
function loadLatest () {
return Promise.all([loadLatestConfig(), loadAccounts()])
.then(([config, accounts]) => ({
config,
accounts
}))
.then(([config, accounts]) => ({
config,
accounts
}))
}
function loadConfig (versionId) {
@ -71,15 +71,15 @@ function loadConfig (versionId) {
and valid`
return db.one(sql, [versionId, 'config'])
.then(row => row.data.config)
.then(configValidate.validate)
.catch(err => {
if (err.name === 'QueryResultError') {
throw new Error('No such config version: ' + versionId)
}
.then(row => row.data.config)
.then(configValidate.validate)
.catch(err => {
if (err.name === 'QueryResultError') {
throw new Error('No such config version: ' + versionId)
}
throw err
})
throw err
})
}
function loadLatestConfig () {
@ -93,15 +93,15 @@ function loadLatestConfig () {
limit 1`
return db.one(sql, ['config'])
.then(row => row.data.config)
.then(configValidate.validate)
.catch(err => {
if (err.name === 'QueryResultError') {
throw new Error('lamassu-server is not configured')
}
.then(row => row.data.config)
.then(configValidate.validate)
.catch(err => {
if (err.name === 'QueryResultError') {
throw new Error('lamassu-server is not configured')
}
throw err
})
throw err
})
}
function loadRecentConfig () {
@ -114,7 +114,7 @@ function loadRecentConfig () {
limit 1`
return db.one(sql, ['config'])
.then(row => row.data.config)
.then(row => row.data.config)
}
function loadAccounts () {
@ -122,10 +122,10 @@ function loadAccounts () {
const toPairs = r => [r.code, toFields(r.fields)]
return db.oneOrNone('select data from user_config where type=$1', 'accounts')
.then(function (data) {
if (!data) return {}
return _.fromPairs(_.map(toPairs, data.data.accounts))
})
.then(function (data) {
if (!data) return {}
return _.fromPairs(_.map(toPairs, data.data.accounts))
})
}
function settings () {
@ -136,8 +136,8 @@ function save (config) {
const sql = 'insert into user_config (type, data, valid) values ($1, $2, $3)'
return configValidate.validate(config)
.then(() => db.none(sql, ['config', {config}, true]))
.catch(() => db.none(sql, ['config', {config}, false]))
.then(() => db.none(sql, ['config', {config}, true]))
.catch(() => db.none(sql, ['config', {config}, false]))
}
function configAddField (scope, fieldCode, fieldType, fieldClass, value) {
@ -197,6 +197,8 @@ function cryptoCodeDefaults (schema, cryptoCode) {
const hasCryptoSpecificDefault = r => r.cryptoScope === 'specific' && !_.isNil(r.default)
const cryptoSpecificFields = _.filter(hasCryptoSpecificDefault, schemaEntries)
pp('DEBUG202')({scope, cryptoCode})
return _.map(r => {
const defaultValue = cryptoDefaultOverride(cryptoCode, r.code, r.default)
@ -204,14 +206,19 @@ function cryptoCodeDefaults (schema, cryptoCode) {
}, cryptoSpecificFields)
}
const uniqCompact = _.flow(_.compact, _.uniq)
const pp = require('./pp')
function addCryptoDefaults (oldConfig, newFields) {
const cryptoCodeEntries = _.filter(v => v.fieldLocator.code === 'cryptoCurrencies', newFields)
const cryptoCodes = _.map(_.get('fieldValue.value'), cryptoCodeEntries)
const uniqueCryptoCodes = _.uniq(_.flatten(cryptoCodes))
const cryptoCodes = _.flatMap(_.get('fieldValue.value'), cryptoCodeEntries)
const uniqueCryptoCodes = uniqCompact(cryptoCodes)
const mapDefaults = cryptoCode => cryptoCodeDefaults(schema, cryptoCode)
const defaults = _.flatten(_.map(mapDefaults, uniqueCryptoCodes))
const defaults = _.flatMap(mapDefaults, uniqueCryptoCodes)
pp('DEBUG201')({cryptoCodeEntries, cryptoCodes, uniqueCryptoCodes})
pp('DEBUG200')(defaults)
return mergeValues(defaults, oldConfig)
}
@ -222,11 +229,11 @@ function modifyConfig (newFields) {
function transaction (t) {
return loadRecentConfig()
.then(oldConfig => {
const oldConfigWithDefaults = addCryptoDefaults(oldConfig, newFields)
const doSave = _.flow(mergeValues, save)
return doSave(oldConfigWithDefaults, newFields)
})
.then(oldConfig => {
const oldConfigWithDefaults = addCryptoDefaults(oldConfig, newFields)
const doSave = _.flow(mergeValues, save)
return doSave(oldConfigWithDefaults, newFields)
})
}
transaction.txMode = tmSRD

View file

@ -3,13 +3,13 @@ const ph = require('./plugin-helper')
function sendMessage (settings, rec) {
return Promise.resolve()
.then(() => {
const pluginCode = configManager.unscoped(settings.config).sms
const plugin = ph.load(ph.SMS, pluginCode)
const account = settings.accounts[pluginCode]
.then(() => {
const pluginCode = configManager.unscoped(settings.config).sms
const plugin = ph.load(ph.SMS, pluginCode)
const account = settings.accounts[pluginCode]
return plugin.sendMessage(account, rec)
})
return plugin.sendMessage(account, rec)
})
}
module.exports = {sendMessage}

View file

@ -17,7 +17,7 @@ function get (id) {
if (!id || _.isEmpty(id)) return Promise.resolve()
const sql = 'select * from support_logs where id=$1'
return db.oneOrNone(sql, [id])
.then(_.mapKeys(_.camelCase))
.then(_.mapKeys(_.camelCase))
}
/**
* Insert a single support_logs row in db
@ -34,7 +34,7 @@ function insert (deviceId) {
const sql = `insert into support_logs
(id, device_id) values ($1, $2) returning *`
return db.one(sql, [uuid.v4(), deviceId])
.then(_.mapKeys(_.camelCase))
.then(_.mapKeys(_.camelCase))
}
/**
@ -52,7 +52,7 @@ function batch () {
where timestamp > (now() - interval '1 week')
order by s.timestamp desc`
return db.any(sql)
.then(_.map(_.mapKeys(_.camelCase)))
.then(_.map(_.mapKeys(_.camelCase)))
}
module.exports = { get, insert, batch }

View file

@ -9,29 +9,29 @@ const FETCH_INTERVAL = 10000
function _getRates (settings, fiatCode, cryptoCode) {
return Promise.resolve()
.then(() => {
const config = settings.config
const plugin = configManager.cryptoScoped(cryptoCode, config).ticker
.then(() => {
const config = settings.config
const plugin = configManager.cryptoScoped(cryptoCode, config).ticker
const account = settings.accounts[plugin]
const ticker = ph.load(ph.TICKER, plugin)
const account = settings.accounts[plugin]
const ticker = ph.load(ph.TICKER, plugin)
const market = [cryptoCode, fiatCode].join('-')
const market = [cryptoCode, fiatCode].join('-')
return ticker.ticker(account, fiatCode, cryptoCode)
.then(r => ({
rates: r.rates,
timestamp: Date.now()
}))
.then(r => {
lastRate[market] = r
return r
return ticker.ticker(account, fiatCode, cryptoCode)
.then(r => ({
rates: r.rates,
timestamp: Date.now()
}))
.then(r => {
lastRate[market] = r
return r
})
.catch(err => {
logger.error(err)
return lastRate[market]
})
})
.catch(err => {
logger.error(err)
return lastRate[market]
})
})
}
const getRates = mem(_getRates, {

View file

@ -1,7 +1,7 @@
const _ = require('lodash/fp')
const BN = require('./bn')
const CashInTx = require('./cash-in-tx')
const CashOutTx = require('./cash-out-tx')
const CashInTx = require('./cash-in/cash-in-tx')
const CashOutTx = require('./cash-out/cash-out-tx')
function process (tx, pi) {
const mtx = massage(tx)
@ -13,7 +13,7 @@ function process (tx, pi) {
function post (tx, pi) {
return process(tx, pi)
.then(_.set('dirty', false))
.then(_.set('dirty', false))
}
function massage (tx) {
@ -24,17 +24,17 @@ function massage (tx) {
const mapBN = r => {
const update = r.direction === 'cashIn'
? {
cryptoAtoms: BN(r.cryptoAtoms),
fiat: BN(r.fiat),
cashInFee: BN(r.cashInFee),
cashInFeeCrypto: BN(r.cashInFeeCrypto),
minimumTx: BN(r.minimumTx)
}
: {
cryptoAtoms: BN(r.cryptoAtoms),
fiat: BN(r.fiat)
}
? {
cryptoAtoms: BN(r.cryptoAtoms),
fiat: BN(r.fiat),
cashInFee: BN(r.cashInFee),
cashInFeeCrypto: BN(r.cashInFeeCrypto),
minimumTx: BN(r.minimumTx)
}
: {
cryptoAtoms: BN(r.cryptoAtoms),
fiat: BN(r.fiat)
}
return _.assign(r, update)
}
@ -51,10 +51,10 @@ function cancel (txId) {
]
return Promise.all(promises)
.then(r => {
if (_.some(r)) return
throw new Error('No such transaction')
})
.then(r => {
if (_.some(r)) return
throw new Error('No such transaction')
})
}
module.exports = {post, cancel}

View file

@ -30,69 +30,68 @@ function computeSeed (masterSeed) {
function fetchWallet (settings, cryptoCode) {
return fs.readFile(options.seedPath, 'utf8')
.then(hex => {
const masterSeed = Buffer.from(hex.trim(), 'hex')
const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet
const wallet = ph.load(ph.WALLET, plugin)
const account = settings.accounts[plugin]
.then(hex => {
const masterSeed = Buffer.from(hex.trim(), 'hex')
const plugin = configManager.cryptoScoped(cryptoCode, settings.config).wallet
const wallet = ph.load(ph.WALLET, plugin)
const account = settings.accounts[plugin]
return {wallet, account: _.set('seed', computeSeed(masterSeed), account)}
})
return {wallet, account: _.set('seed', computeSeed(masterSeed), account)}
})
}
const lastBalance = {}
function _balance (settings, cryptoCode) {
logger.debug('Polled wallet balance')
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.balance(r.account, cryptoCode))
.then(balance => ({balance, timestamp: Date.now()}))
.then(r => {
lastBalance[cryptoCode] = r
return r
})
.catch(err => {
console.error(err)
return lastBalance[cryptoCode]
})
.then(r => r.wallet.balance(r.account, cryptoCode))
.then(balance => ({balance, timestamp: Date.now()}))
.then(r => {
lastBalance[cryptoCode] = r
return r
})
.catch(err => {
console.error(err)
return lastBalance[cryptoCode]
})
}
function sendCoins (settings, toAddress, cryptoAtoms, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.then(r => {
return r.wallet.sendCoins(r.account, toAddress, cryptoAtoms, cryptoCode)
.then(res => {
mem.clear(module.exports.balance)
return res
.then(r => {
return r.wallet.sendCoins(r.account, toAddress, cryptoAtoms, cryptoCode)
.then(res => {
mem.clear(module.exports.balance)
return res
})
})
})
.catch(err => {
if (err.name === INSUFFICIENT_FUNDS_NAME) {
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
}
.catch(err => {
if (err.name === INSUFFICIENT_FUNDS_NAME) {
throw httpError(INSUFFICIENT_FUNDS_NAME, INSUFFICIENT_FUNDS_CODE)
}
throw err
})
throw err
})
}
function newAddress (settings, info) {
return fetchWallet(settings, info.cryptoCode)
.then(r => r.wallet.newAddress(r.account, info))
.then(r => r.wallet.newAddress(r.account, info))
}
function newFunding (settings, cryptoCode, address) {
return fetchWallet(settings, cryptoCode)
.then(r => {
const wallet = r.wallet
const account = r.account
.then(r => {
const wallet = r.wallet
const account = r.account
return wallet.newFunding(account, cryptoCode)
})
return wallet.newFunding(account, cryptoCode)
})
}
function getWalletStatus (settings, tx) {
return fetchWallet(settings, tx.cryptoCode)
.then(r => r.wallet.getStatus(r.account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode))
.then(r => r.wallet.getStatus(r.account, tx.toAddress, tx.cryptoAtoms, tx.cryptoCode))
}
function authorizeZeroConf (settings, tx, machineId) {
@ -115,34 +114,34 @@ function authorizeZeroConf (settings, tx, machineId) {
function getStatus (settings, tx, machineId) {
return getWalletStatus(settings, tx)
.then((statusRec) => {
if (statusRec.status === 'authorized') {
return authorizeZeroConf(settings, tx, machineId)
.then(isAuthorized => {
const publishAge = Date.now() - tx.publishedAt
.then((statusRec) => {
if (statusRec.status === 'authorized') {
return authorizeZeroConf(settings, tx, machineId)
.then(isAuthorized => {
const publishAge = Date.now() - tx.publishedAt
const unauthorizedStatus = publishAge < ZERO_CONF_EXPIRATION
? 'published'
: 'rejected'
const unauthorizedStatus = publishAge < ZERO_CONF_EXPIRATION
? 'published'
: 'rejected'
const status = isAuthorized ? 'authorized' : unauthorizedStatus
const status = isAuthorized ? 'authorized' : unauthorizedStatus
return {status}
})
}
return {status}
})
}
return statusRec
})
return statusRec
})
}
function sweep (settings, cryptoCode, hdIndex) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.sweep(r.account, cryptoCode, hdIndex))
.then(r => r.wallet.sweep(r.account, cryptoCode, hdIndex))
}
function isHd (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.supportsHd)
.then(r => r.wallet.supportsHd)
}
function cryptoNetwork (settings, cryptoCode) {

View file

@ -5,7 +5,7 @@ function singleQuotify (item) { return '\'' + item + '\'' }
exports.up = function (next) {
var statuses = ['notSeen', 'published', 'authorized', 'instant',
'confirmed', 'rejected', 'insufficientFunds']
.map(singleQuotify).join(',')
.map(singleQuotify).join(',')
var sql = [
'create type status_stage AS enum (' + statuses + ')',

View file

@ -4,9 +4,9 @@ function singleQuotify (item) { return '\'' + item + '\'' }
exports.up = function (next) {
var actions = ['published', 'authorized', 'instant', 'confirmed', 'rejected',
'insufficientFunds', 'dispenseRequested', 'dispensed', 'notified',
'addedPhone', 'redeem']
.map(singleQuotify).join(',')
'insufficientFunds', 'dispenseRequested', 'dispensed', 'notified',
'addedPhone', 'redeem']
.map(singleQuotify).join(',')
var sql = [
`create table cash_in_txs (

View file

@ -23,7 +23,7 @@ exports.up = function (next) {
`insert into customers (id, name) VALUES ( '${anonymous.uuid}','${anonymous.name}' )`,
`alter table cash_in_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`,
`alter table cash_out_txs add column customer_id uuid references customers (id) DEFAULT '${anonymous.uuid}'`
]
]
db.multi(sql, next)
}

View file

@ -3,7 +3,7 @@ var db = require('./db')
exports.up = function (next) {
const sql =
[ "create type compliance_types as enum ('manual', 'sanctions', 'sanctions_override')",
`create table compliance_authorizations (
`create table compliance_authorizations (
id uuid PRIMARY KEY,
customer_id uuid REFERENCES customers (id),
compliance_type compliance_types NOT NULL,

View file

@ -6,7 +6,7 @@ exports.up = function (next) {
id uuid PRIMARY KEY,
device_id text,
timestamp timestamptz not null default now() )`,
'alter table logs add column server_timestamp timestamptz not null default now() '
'alter table logs add column server_timestamp timestamptz not null default now() '
]
db.multi(sql, next)

View file

@ -3,23 +3,23 @@ const migrateTools = require('./migrate-tools')
exports.up = function (next) {
return migrateTools.migrateNames()
.then(updateSql => {
const sql = [
'alter table devices add column name text',
updateSql,
'alter table devices alter column name set not null'
]
.then(updateSql => {
const sql = [
'alter table devices add column name text',
updateSql,
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
.catch(() => {
const sql = [
'alter table devices add column name text',
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
.catch(() => {
const sql = [
'alter table devices add column name text',
'alter table devices alter column name set not null'
]
return db.multi(sql, next)
})
return db.multi(sql, next)
})
}
exports.down = function (next) {

View file

@ -7,17 +7,17 @@ function multi (sqls, cb) {
const doQuery = s => {
return () => {
return db.none(s)
.catch(err => {
console.log(err.stack)
throw err
})
.catch(err => {
console.log(err.stack)
throw err
})
}
}
return sequential(sqls.map(doQuery))
.then(() => cb())
.catch(err => {
console.log(err.stack)
cb(err)
})
.then(() => cb())
.catch(err => {
console.log(err.stack)
cb(err)
})
}

View file

@ -10,7 +10,7 @@ function migrateNames () {
const cs = new pgp.helpers.ColumnSet(['device_id', 'name'], {table: 'devices'})
return settingsLoader.loadLatest()
.then(r => machineLoader.getMachineNames(r.config))
.then(_.map(r => ({device_id: r.deviceId, name: r.name})))
.then(data => pgp.helpers.update(data, cs))
.then(r => machineLoader.getMachineNames(r.config))
.then(_.map(r => ({device_id: r.deviceId, name: r.name})))
.then(data => pgp.helpers.update(data, cs))
}

420
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "lamassu-server",
"version": "5.7.6",
"version": "5.8.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -199,11 +199,6 @@
"micromatch": "2.3.11"
}
},
"ap": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ap/-/ap-0.2.0.tgz",
"integrity": "sha1-rglCYAspkS8NKxTsYMRejzMLYRA="
},
"aproba": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz",
@ -1159,7 +1154,7 @@
"bitcore-lib": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-0.15.0.tgz",
"integrity": "sha512-AeXLWhiivF6CDFzrABZHT4jJrflyylDWTi32o30rF92HW9msfuKpjzrHtFKYGa9w0kNVv5HABQjCB3OEav4PhQ==",
"integrity": "sha1-+SS+E4afKqt+BK7sVkKtM1m2zsI=",
"requires": {
"bn.js": "4.11.8",
"bs58": "4.0.1",
@ -1189,38 +1184,6 @@
}
}
},
"bitcore-lib-cash": {
"version": "git+https://github.com/bitpay/bitcore-lib.git#2b494651c95a31a2a936b28673fb8fc4a76f8099",
"requires": {
"bn.js": "4.11.8",
"bs58": "4.0.1",
"buffer-compare": "1.1.1",
"elliptic": "6.4.0",
"inherits": "2.0.1",
"lodash": "4.17.4",
"phantomjs-prebuilt": "2.1.16"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
"requires": {
"base-x": "3.0.2"
}
},
"inherits": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
"integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
}
}
},
"bitgo": {
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/bitgo/-/bitgo-3.4.11.tgz",
@ -1971,16 +1934,6 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
"integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
"requires": {
"inherits": "2.0.3",
"readable-stream": "2.3.3",
"typedarray": "0.0.6"
}
},
"configstore": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.0.tgz",
@ -2624,11 +2577,6 @@
"integrity": "sha1-7sXHJurO9Rt/a3PCDbbhsTsGnJg=",
"dev": true
},
"es6-promise": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
"integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ=="
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@ -2955,40 +2903,6 @@
"is-extglob": "1.0.0"
}
},
"extract-zip": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.6.tgz",
"integrity": "sha1-EpDt6NINCHK0Kf0/NRyhKOxe+Fw=",
"requires": {
"concat-stream": "1.6.0",
"debug": "2.6.9",
"mkdirp": "0.5.0",
"yauzl": "2.4.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
"integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
"requires": {
"minimist": "0.0.8"
}
}
}
},
"extsprintf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz",
@ -3004,14 +2918,6 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
},
"fd-slicer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
"integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
"requires": {
"pend": "1.2.0"
}
},
"figures": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
@ -3152,16 +3058,6 @@
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.0.tgz",
"integrity": "sha1-9HTKXmqSRtb9jglTz6m5yAWvp44="
},
"fs-extra": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz",
"integrity": "sha1-zTzl9+fLYUWIP8rjGR6Yd/hYeVA=",
"requires": {
"graceful-fs": "4.1.11",
"jsonfile": "2.4.0",
"klaw": "1.3.1"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3671,11 +3567,6 @@
"is-property": "1.0.2"
}
},
"generic-pool": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.3.tgz",
"integrity": "sha1-eAw29p360FpaBF3Te+etyhGk9v8="
},
"get-port": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/get-port/-/get-port-3.1.0.tgz",
@ -4625,15 +4516,6 @@
"minimalistic-assert": "1.0.0"
}
},
"hasha": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz",
"integrity": "sha1-eNfL/B5tZjA/55g3NlmEUXsvbuE=",
"requires": {
"is-stream": "1.1.0",
"pinkie-promise": "2.0.1"
}
},
"hawk": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
@ -5169,7 +5051,8 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
},
"isobject": {
"version": "2.1.0",
@ -5286,6 +5169,11 @@
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.3.1.tgz",
"integrity": "sha1-hhIoAhQvCChQKg0d7h2V4lO7AkM="
},
"js-string-escape": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
"integrity": "sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
@ -5361,14 +5249,6 @@
"integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=",
"dev": true
},
"jsonfile": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
"requires": {
"graceful-fs": "4.1.11"
}
},
"jsonify": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
@ -5448,11 +5328,6 @@
"sha3": "1.2.0"
}
},
"kew": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz",
"integrity": "sha1-edk9LTM2PW/dKXCzNdkUGtWR15s="
},
"kind-of": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
@ -5462,21 +5337,6 @@
"is-buffer": "1.1.5"
}
},
"klaw": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz",
"integrity": "sha1-QIhDO0azsbolnXh4XY6W9zugJDk=",
"requires": {
"graceful-fs": "4.1.11"
}
},
"kraken-api": {
"version": "github:DeX3/npm-kraken-api#ca939e6dd6c6cd9d3ca9a6ee52dc2170a8d25978",
"requires": {
"querystring": "0.2.0",
"request": "2.81.0"
}
},
"last-line-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/last-line-stream/-/last-line-stream-1.0.0.tgz",
@ -5635,6 +5495,14 @@
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s="
},
"longjohn": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/longjohn/-/longjohn-0.2.12.tgz",
"integrity": "sha1-fKdEawg2VcN351EiE9x1TVKmSn4=",
"requires": {
"source-map-support": "0.4.15"
}
},
"loose-envify": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
@ -5678,11 +5546,6 @@
}
}
},
"manakin": {
"version": "0.4.7",
"resolved": "https://registry.npmjs.org/manakin/-/manakin-0.4.7.tgz",
"integrity": "sha1-QcpEm1W+qcTE/s7Dk7nE1jgY/D8="
},
"map-obj": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
@ -6182,6 +6045,14 @@
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz",
"integrity": "sha1-ueEjgAvOu3rBOkeb4ZW1B7mNMPo="
},
"p-each-series": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz",
"integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=",
"requires": {
"p-reduce": "1.0.0"
}
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
@ -6202,6 +6073,11 @@
"p-limit": "1.1.0"
}
},
"p-reduce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz",
"integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo="
},
"p-timeout": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.0.tgz",
@ -6407,75 +6283,40 @@
"sha.js": "2.4.8"
}
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
"integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
},
"pg-connection-string": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz",
"integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc="
},
"pg-minify": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-0.5.2.tgz",
"integrity": "sha512-NxolpovLgwjQG/daDYSCiHxJfIlo10DtGPUTVyF6ay5KxKhiJPGN8bqOxHje1MYEoefmtqdTLWKnDxSyvv0ykw=="
},
"pg-native": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pg-native/-/pg-native-2.0.1.tgz",
"integrity": "sha1-9xOTYOAB9SL0QZLcLBn7alXizxg=",
"pg": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-7.4.1.tgz",
"integrity": "sha1-80Ecjd+faSMi/gXnAXoYiOR/ePE=",
"requires": {
"libpq": "1.8.7",
"pg-types": "1.12.0",
"readable-stream": "2.3.3"
}
},
"pg-pool": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-1.8.0.tgz",
"integrity": "sha1-9+xzgkw3oD8Hb1G/33DjQBR8Tzc=",
"requires": {
"generic-pool": "2.4.3",
"object-assign": "4.1.0"
"buffer-writer": "1.0.1",
"js-string-escape": "1.0.1",
"packet-reader": "0.3.1",
"pg-connection-string": "0.1.3",
"pg-pool": "2.0.3",
"pg-types": "1.12.1",
"pgpass": "1.0.2",
"semver": "4.3.2"
},
"dependencies": {
"object-assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz",
"integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A="
}
}
},
"pg-promise": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-6.3.7.tgz",
"integrity": "sha512-qUGvluqVeJXIg8viJG2Oqd2iHJ0CNKuTvmRKdOMFjcbotglsj/MFMCMzluPUtb8Kb5XsBux4Gc1NnV7pi1q60w==",
"requires": {
"manakin": "0.4.7",
"pg": "6.4.1",
"pg-minify": "0.5.2",
"spex": "1.2.0"
},
"dependencies": {
"pg": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/pg/-/pg-6.4.1.tgz",
"integrity": "sha1-PqvYygVoFEN8dp8X/3oMNqxwI8U=",
"pg-pool": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-2.0.3.tgz",
"integrity": "sha1-wCIDLIlJ8xKk+R+2QJzgQHa+Mlc="
},
"pg-types": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.12.1.tgz",
"integrity": "sha1-1kCH45A7WP+q0nnnWVxSIIoUw9I=",
"requires": {
"buffer-writer": "1.0.1",
"packet-reader": "0.3.1",
"pg-connection-string": "0.1.3",
"pg-pool": "1.8.0",
"pg-types": "1.12.0",
"pgpass": "1.0.2",
"semver": "4.3.2"
"postgres-array": "1.0.2",
"postgres-bytea": "1.0.0",
"postgres-date": "1.0.3",
"postgres-interval": "1.1.0"
}
},
"semver": {
@ -6485,16 +6326,87 @@
}
}
},
"pg-types": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.12.0.tgz",
"integrity": "sha1-itO3uJfj/UY+Yt4kGtX8ZAtKZvA=",
"pg-connection-string": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-0.1.3.tgz",
"integrity": "sha1-2hhHsglA5C7hSSvq9l1J2RskXfc="
},
"pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha1-lDvUY79bcbQXARX4D478mgwOt4w="
},
"pg-native": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-native/-/pg-native-2.2.0.tgz",
"integrity": "sha1-vIR1b07L9gD/fLSOpYVBQYMM8eQ=",
"requires": {
"ap": "0.2.0",
"postgres-array": "1.0.2",
"postgres-bytea": "1.0.0",
"postgres-date": "1.0.3",
"postgres-interval": "1.1.0"
"libpq": "1.8.7",
"pg-types": "1.13.0",
"readable-stream": "1.0.31"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"pg-types": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-1.13.0.tgz",
"integrity": "sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ==",
"requires": {
"pg-int8": "1.0.1",
"postgres-array": "1.0.2",
"postgres-bytea": "1.0.0",
"postgres-date": "1.0.3",
"postgres-interval": "1.1.0"
}
},
"readable-stream": {
"version": "1.0.31",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.31.tgz",
"integrity": "sha1-jyUC4LyeOw2huUUgqrtOJgPsr64=",
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "0.0.1",
"string_decoder": "0.10.31"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"pg-promise": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-7.4.1.tgz",
"integrity": "sha1-uyDjnS0ObUXZLwu6szidHmEXlxo=",
"requires": {
"manakin": "0.5.1",
"pg": "7.4.1",
"pg-minify": "0.5.4",
"spex": "2.0.2"
},
"dependencies": {
"manakin": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/manakin/-/manakin-0.5.1.tgz",
"integrity": "sha1-xKcRb2sA3z1fGjetPKUV0iBlplg="
},
"pg-minify": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-0.5.4.tgz",
"integrity": "sha512-GHB2v4OiMHDgwiHH86ZWNfvgEPVijrnfuWLQocseX6Zlf30k+x0imA65zBy4skIpEwfBBEplIEEKP4n3q9KkVA=="
},
"spex": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/spex/-/spex-2.0.2.tgz",
"integrity": "sha512-LU6TS3qTEpRth+FnNs/fIWEmridYN7JmaN2k1Jk31XVC4ex7+wYxiHMnKguRxS7oKjbOFl4H6seeWNDFFgkVRg=="
}
}
},
"pgpass": {
@ -6505,22 +6417,6 @@
"split": "1.0.0"
}
},
"phantomjs-prebuilt": {
"version": "2.1.16",
"resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz",
"integrity": "sha1-79ISpKOWbTZHaE6ouniFSb4q7+8=",
"requires": {
"es6-promise": "4.2.4",
"extract-zip": "1.6.6",
"fs-extra": "1.0.0",
"hasha": "2.2.0",
"kew": "0.7.0",
"progress": "1.1.8",
"request": "2.81.0",
"request-progress": "2.0.1",
"which": "1.2.14"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
@ -6704,11 +6600,6 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
"integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
},
"progress": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz",
"integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74="
},
"promise-sequential": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/promise-sequential/-/promise-sequential-1.1.1.tgz",
@ -6862,11 +6753,6 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
"integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"ramda": {
"version": "0.22.1",
"resolved": "https://registry.npmjs.org/ramda/-/ramda-0.22.1.tgz",
@ -7161,14 +7047,6 @@
}
}
},
"request-progress": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz",
"integrity": "sha1-XTa7V5YcZzqlt4jbyBQf3yO0Tgg=",
"requires": {
"throttleit": "1.0.0"
}
},
"require-precompiled": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/require-precompiled/-/require-precompiled-0.1.0.tgz",
@ -7721,7 +7599,6 @@
"version": "0.4.15",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz",
"integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=",
"dev": true,
"requires": {
"source-map": "0.5.6"
},
@ -7729,8 +7606,7 @@
"source-map": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz",
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=",
"dev": true
"integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI="
}
}
},
@ -7752,11 +7628,6 @@
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz",
"integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc="
},
"spex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/spex/-/spex-1.2.0.tgz",
"integrity": "sha1-YmSzuKy8RER38G27ZtQlwO4QdMA="
},
"split": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz",
@ -8008,11 +7879,6 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
"throttleit": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz",
"integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw="
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
@ -8172,11 +8038,6 @@
"mime-types": "2.1.15"
}
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typeforce": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.11.1.tgz",
@ -8369,6 +8230,7 @@
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/which/-/which-1.2.14.tgz",
"integrity": "sha1-mofEN48D6CfOyvGs31bHNsAcFOU=",
"dev": true,
"requires": {
"isexe": "2.0.0"
}
@ -8605,14 +8467,6 @@
"y18n": "3.2.1"
}
},
"yauzl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
"integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
"requires": {
"fd-slicer": "1.0.1"
}
},
"yeast": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",

View file

@ -2,7 +2,7 @@
"name": "lamassu-server",
"description": "bitcoin atm client server protocol module",
"keywords": [],
"version": "5.7.6",
"version": "5.8.3",
"license": "Unlicense",
"author": "Lamassu (https://lamassu.is)",
"dependencies": {
@ -31,6 +31,7 @@
"kraken-api": "github:DeX3/npm-kraken-api",
"lnd-async": "^1.0.1",
"lodash": "^4.17.2",
"longjohn": "^0.2.12",
"make-dir": "^1.0.0",
"mem": "^1.1.0",
"migrate": "^0.2.2",
@ -40,12 +41,12 @@
"node-hkdf-sync": "^1.0.0",
"node-mailjet": "^3.2.1",
"numeral": "^2.0.3",
"pg-native": "^2.0.1",
"pg-promise": "^6.3.7",
"p-each-series": "^1.0.0",
"pg-native": "^2.2.0",
"pg-promise": "^7.4.1",
"pify": "^3.0.0",
"pretty-ms": "^2.1.0",
"promise-sequential": "^1.1.1",
"ramda": "^0.22.1",
"serve-static": "^1.12.4",
"socket.io": "^2.0.3",
"socket.io-client": "^2.0.3",
@ -70,7 +71,8 @@
"lamassu-mnemonic": "./bin/lamassu-mnemonic",
"lamassu-cancel": "./bin/lamassu-cancel",
"lamassu-nuke-db": "./bin/lamassu-nuke-db",
"lamassu-coins": "./bin/lamassu-coins"
"lamassu-coins": "./bin/lamassu-coins",
"lamassu-update": "./bin/lamassu-update"
},
"scripts": {
"start": "node bin/lamassu-server",

View file

@ -28021,6 +28021,9 @@ var _user$project$Common_Customer_Types$authorizedToString = function (model) {
return 'automatic';
}
};
var _user$project$Common_Customer_Types$IdCardData = function (a) {
return {uid: a};
};
var _user$project$Common_Customer_Types$Customer = function (a) {
return function (b) {
return function (c) {
@ -28094,6 +28097,11 @@ var _user$project$Common_Customer_Types$Verified = {ctor: 'Verified'};
var _user$project$Common_Customer_Types$Blocked = {ctor: 'Blocked'};
var _user$project$Common_Customer_Types$Automatic = {ctor: 'Automatic'};
var _user$project$Common_Customer_Decoder$idCardDataDecoder = A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'uid',
_elm_lang$core$Json_Decode$string,
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$decode(_user$project$Common_Customer_Types$IdCardData));
var _user$project$Common_Customer_Decoder$mapAuthorizedTypes = function (s) {
var _p0 = s;
switch (_p0) {
@ -28192,7 +28200,7 @@ var _user$project$Common_Customer_Decoder$customerDecoder = A3(
A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'idCardData',
_elm_lang$core$Json_Decode$nullable(_elm_lang$core$Json_Decode$string),
_elm_lang$core$Json_Decode$nullable(_user$project$Common_Customer_Decoder$idCardDataDecoder),
A3(
_NoRedInk$elm_decode_pipeline$Json_Decode_Pipeline$required,
'authorizedAt',
@ -33803,7 +33811,7 @@ var _user$project$Transactions$rowView = function (tx) {
});
} else {
var _p3 = _p1._0;
var status = _elm_community$maybe_extra$Maybe_Extra$isJust(_p3.error) ? 'Error' : 'Success';
var status = _elm_community$maybe_extra$Maybe_Extra$isJust(_p3.error) ? 'Error' : (_p3.dispense ? 'Success' : 'Pending');
return A2(
_elm_lang$html$Html$tr,
{

View file

@ -40,10 +40,10 @@ test('bigger merge', t => {
)
const expected = [
{fieldLocator: fieldLocator1, fieldValue: fieldValue4},
{fieldLocator: fieldLocator4, fieldValue: fieldValue5},
{fieldLocator: fieldLocator2, fieldValue: fieldValue2},
{fieldLocator: fieldLocator3, fieldValue: fieldValue3}
{fieldLocator: fieldLocator1, fieldValue: fieldValue4},
{fieldLocator: fieldLocator4, fieldValue: fieldValue5},
{fieldLocator: fieldLocator2, fieldValue: fieldValue2},
{fieldLocator: fieldLocator3, fieldValue: fieldValue3}
]
t.deepEqual(merged, expected)

View file

@ -1,7 +1,6 @@
// Pull latest from: http://www.currency-iso.org/en/home/tables/table-a1.html
// Convert to JSON at: http://www.csvjson.com/csv2json
const R = require('ramda')
const currencies = require('../currencies.json')
function goodCurrency (currency) {

View file

@ -6,7 +6,7 @@ const rawCountries = require('../raw-countries.json')
const topCodes = ['US', 'GB', 'CA', 'AU']
const countries = rawCountries
.map(r => ({code: r.cca2, display: r.name.common}))
.map(r => ({code: r.cca2, display: r.name.common}))
const topCountries = topCodes.map(c => countries.find(_.matchesProperty('code', c)))
const final = _.uniqBy(_.get('code'), _.concat(topCountries, countries))

View file

@ -1,11 +1,11 @@
const settingsLoader = require('../lib/settings-loader')
const fields = [
settingsLoader.configDeleteField({crypto: 'ETH', machine: 'global'}, 'exchange')
settingsLoader.configDeleteField({crypto: 'BTC', machine: 'global'}, 'wallet')
]
settingsLoader.modifyConfig(fields)
.then(() => {
console.log('success.')
process.exit(0)
})
.then(() => {
console.log('success.')
process.exit(0)
})

View file

@ -11,15 +11,15 @@ function dbFetchConfig () {
'select data from user_config where type=$1 order by id desc limit 1',
['config']
)
.then(row => row && row.data)
.then(row => row && row.data)
}
dbFetchConfig()
.then(config => {
pp(config)
process.exit(0)
})
.catch(e => {
console.log(e)
process.exit(1)
})
.then(config => {
pp(config)
process.exit(0)
})
.catch(e => {
console.log(e)
process.exit(1)
})