merge lightning
This commit is contained in:
commit
6fa6ac1647
92 changed files with 2688 additions and 2651 deletions
86
INSTALL.md
Normal file
86
INSTALL.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ const schemas = ph.loadSchemas()
|
|||
function fetchAccounts () {
|
||||
return db.oneOrNone('select data from user_config where type=$1', ['accounts'])
|
||||
.then(row => {
|
||||
|
||||
// Hard code this for now
|
||||
const accounts = [{
|
||||
code: 'blockcypher',
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -51,17 +50,17 @@ 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()
|
||||
|
|
@ -72,7 +71,7 @@ function validateCurrentConfig () {
|
|||
.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,10 +79,10 @@ 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)
|
||||
|
|
@ -91,34 +90,42 @@ function fetchConfigGroup (code) {
|
|||
if (!groupSchema) throw new Error('No such group schema: ' + code)
|
||||
|
||||
const schemaFields = groupSchema.fields
|
||||
.map(R.curry(getField)(schema, groupSchema))
|
||||
.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')),
|
||||
schemaFields.map(_.get('requiredIf')),
|
||||
schemaFields.map(_.get('enabledIfAny')),
|
||||
schemaFields.map(_.get('enabledIfAll')),
|
||||
groupSchema.fields,
|
||||
'fiatCurrency'
|
||||
]
|
||||
const configFields = R.uniq(R.flatten(candidateFields)).filter(R.identity)
|
||||
|
||||
const smush = _.flow(_.flattenDeep, _.compact, _.uniq)
|
||||
const configFields = smush(candidateFields)
|
||||
|
||||
// Expand this to check against full schema
|
||||
const fieldValidator = field => !_.isNil(_.get('fieldLocator.fieldScope.crypto', field))
|
||||
|
||||
const reducer = (acc, configField) => {
|
||||
return acc.concat(config.filter(fieldLocatorCodeEq(configField)))
|
||||
}
|
||||
|
||||
const values = _.map(f => decorateEnabledIf(schema.fields, f), configFields.reduce(reducer, []))
|
||||
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: getCryptos(config, machineList),
|
||||
selectedCryptos,
|
||||
data
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
@ -162,14 +169,14 @@ function fetchData () {
|
|||
{crypto: 'LTC', display: 'Litecoin'},
|
||||
{crypto: 'DASH', display: 'Dash'},
|
||||
{crypto: 'ZEC', display: 'Zcash'},
|
||||
{crypto: 'BCH', display: 'BCH'}
|
||||
{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', 'LTC']},
|
||||
{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']},
|
||||
|
|
@ -180,7 +187,7 @@ function fetchData () {
|
|||
{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: '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},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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']]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,4 +114,3 @@ function run () {
|
|||
inquirer.prompt(questions)
|
||||
.then(answers => processCryptos(answers.crypto))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
83
lib/cash-in/cash-in-atomic.js
Normal file
83
lib/cash-in/cash-in-atomic.js
Normal 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
138
lib/cash-in/cash-in-low.js
Normal 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
126
lib/cash-in/cash-in-tx.js
Normal 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))
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
50
lib/cash-out/cash-out-actions.js
Normal file
50
lib/cash-out/cash-out-actions.js
Normal 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
|
||||
}
|
||||
}
|
||||
171
lib/cash-out/cash-out-atomic.js
Normal file
171
lib/cash-out/cash-out-atomic.js
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
64
lib/cash-out/cash-out-low.js
Normal file
64
lib/cash-out/cash-out-low.js
Normal 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
158
lib/cash-out/cash-out-tx.js
Normal 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))
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,9 +57,12 @@ 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)
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function getMachineNames (config) {
|
|||
.then(([machines, config]) => {
|
||||
const addName = r => {
|
||||
const machineScoped = configManager.machineScoped(r.deviceId, config)
|
||||
const name = machineScoped.machineName
|
||||
const name = _.defaultTo('', machineScoped.machineName)
|
||||
const cashOut = machineScoped.cashOutEnabled
|
||||
|
||||
return _.assign(r, {name, cashOut})
|
||||
|
|
|
|||
|
|
@ -16,4 +16,3 @@ function run () {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
@ -505,13 +505,23 @@ function plugins (settings, deviceId) {
|
|||
: null
|
||||
|
||||
const cassette1Alert = cashOutEnabled && device.cassette1 < config.cashOutCassette1AlertThreshold
|
||||
? {code: 'LOW_CASH_OUT', cassette: 1, machineName, deviceId: device.deviceId,
|
||||
notes: device.cassette1, denomination: denomination1, fiatCode}
|
||||
? {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}
|
||||
? {code: 'LOW_CASH_OUT',
|
||||
cassette: 2,
|
||||
machineName,
|
||||
deviceId: device.deviceId,
|
||||
notes: device.cassette2,
|
||||
denomination: denomination2,
|
||||
fiatCode}
|
||||
: null
|
||||
|
||||
return _.compact([cashInAlert, cassette1Alert, cassette2Alert])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ 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)}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -52,4 +52,3 @@ function ticker (account, fiatCode, cryptoCode) {
|
|||
module.exports = {
|
||||
ticker
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -37,29 +36,11 @@ 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]))
|
||||
.then(() => fetch('sendtoaddress', [address, coins]))
|
||||
.catch(err => {
|
||||
if (err.code === -6) throw new E.InsufficientFundsError()
|
||||
throw err
|
||||
|
|
@ -72,9 +53,7 @@ function newAddress (account, info) {
|
|||
}
|
||||
|
||||
function addressBalance (address, confs) {
|
||||
const btcAddress = bchToBtcAddress(address)
|
||||
|
||||
return fetch('getreceivedbyaddress', [btcAddress, confs])
|
||||
return fetch('getreceivedbyaddress', [address, confs])
|
||||
.then(r => BN(r).shift(unitScale).round())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ 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())
|
||||
|
|
|
|||
|
|
@ -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'})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,4 +35,3 @@ function authorize (account, toAddress, cryptoAtoms, cryptoCode) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const _ = require('lodash/fp')
|
||||
const R = require('ramda')
|
||||
|
||||
const db = require('./db')
|
||||
const dbm = require('./postgresql_interface')
|
||||
|
|
@ -57,11 +56,13 @@ function fetchPhoneTx (phone) {
|
|||
return db.any(sql, values)
|
||||
.then(_.map(toCashOutTx))
|
||||
.then(txs => {
|
||||
const confirmedTxs = txs.filter(tx => R.contains(tx.status, ['instant', 'confirmed']))
|
||||
const confirmedTxs = txs.filter(tx => _.includes(tx.status, ['instant', 'confirmed']))
|
||||
if (confirmedTxs.length > 0) {
|
||||
const maxTx = R.reduce((acc, val) => {
|
||||
const reducer = (acc, val) => {
|
||||
return !acc || val.cryptoAtoms.gt(acc.cryptoAtoms) ? val : acc
|
||||
}, null, confirmedTxs)
|
||||
}
|
||||
|
||||
const maxTx = _.reduce(reducer, null, confirmedTxs)
|
||||
|
||||
return maxTx
|
||||
}
|
||||
|
|
@ -110,4 +111,10 @@ function updateMachineDefaults (deviceId) {
|
|||
})
|
||||
}
|
||||
|
||||
module.exports = {stateChange, fetchPhoneTx, fetchStatusTx, updateDeviceConfigVersion, updateMachineDefaults}
|
||||
module.exports = {
|
||||
stateChange,
|
||||
fetchPhoneTx,
|
||||
fetchStatusTx,
|
||||
updateDeviceConfigVersion,
|
||||
updateMachineDefaults
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ function poll (req, res, next) {
|
|||
response.idVerificationLimit = config.idVerificationLimit
|
||||
}
|
||||
|
||||
console.log(response)
|
||||
return res.json(_.assign(response, results))
|
||||
})
|
||||
.catch(next)
|
||||
|
|
@ -181,12 +180,24 @@ function getCustomerWithPhoneCode (req, res, next) {
|
|||
})
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === 'BadNumberError') throw httpError('Bad number', 410)
|
||||
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))
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ function fetchWallet (settings, cryptoCode) {
|
|||
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()}))
|
||||
|
|
|
|||
406
package-lock.json
generated
406
package-lock.json
generated
|
|
@ -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=",
|
||||
"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"
|
||||
},
|
||||
"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=",
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-7.4.1.tgz",
|
||||
"integrity": "sha1-80Ecjd+faSMi/gXnAXoYiOR/ePE=",
|
||||
"requires": {
|
||||
"buffer-writer": "1.0.1",
|
||||
"js-string-escape": "1.0.1",
|
||||
"packet-reader": "0.3.1",
|
||||
"pg-connection-string": "0.1.3",
|
||||
"pg-pool": "1.8.0",
|
||||
"pg-types": "1.12.0",
|
||||
"pg-pool": "2.0.3",
|
||||
"pg-types": "1.12.1",
|
||||
"pgpass": "1.0.2",
|
||||
"semver": "4.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"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": {
|
||||
"postgres-array": "1.0.2",
|
||||
"postgres-bytea": "1.0.0",
|
||||
"postgres-date": "1.0.3",
|
||||
"postgres-interval": "1.1.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
|
|
@ -6485,18 +6326,89 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.2.tgz",
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue