Merge branch 'release-8.1' into chore/update-ciphertrace-logs
This commit is contained in:
commit
5e1d706ca2
115 changed files with 1416 additions and 580 deletions
|
|
@ -57,3 +57,4 @@ HTTP=
|
||||||
DEV_MODE=
|
DEV_MODE=
|
||||||
|
|
||||||
## Uncategorized variables
|
## Uncategorized variables
|
||||||
|
WEBHOOK_URL=
|
||||||
|
|
|
||||||
43
bin/lamassu-clean-parsed-id
Normal file
43
bin/lamassu-clean-parsed-id
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
|
const argv = require('minimist')(process.argv.slice(2))
|
||||||
|
const _ = require('lodash')
|
||||||
|
const db = require('../lib/db')
|
||||||
|
|
||||||
|
const txId = argv.tx
|
||||||
|
const customerId = argv.customer
|
||||||
|
|
||||||
|
if ((!txId && !customerId) || (txId && customerId)) {
|
||||||
|
console.log('Usage: lamassu-clean-parsed-id [--tx <txId> | --customer <customerId>]')
|
||||||
|
console.log('The command can only be run with EITHER --tx OR --customer, NOT BOTH')
|
||||||
|
process.exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isNil(txId)) {
|
||||||
|
db.oneOrNone('SELECT * FROM (SELECT id, customer_id FROM cash_in_txs UNION SELECT id, customer_id FROM cash_out_txs) as txs WHERE txs.id = $1', [txId])
|
||||||
|
.then(res => {
|
||||||
|
return db.none('UPDATE customers SET id_card_data = null WHERE id = $1', [res.customer_id])
|
||||||
|
.then(() => {
|
||||||
|
console.log(`ID card data from customer ${res.customer_id} was cleared with success`)
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('A transaction with that ID was not found')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isNil(customerId)) {
|
||||||
|
db.none('UPDATE customers SET id_card_data = null WHERE id = $1', [customerId])
|
||||||
|
.then(() => {
|
||||||
|
console.log(`ID card data from customer ${customerId} was cleared with success`)
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log('A customer with that ID was not found')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const install = require('../lib/blockchain/install')
|
const install = require('../lib/blockchain/install')
|
||||||
|
|
||||||
install.run()
|
install.run()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const settingsLoader = require('../lib/new-settings-loader')
|
const settingsLoader = require('../lib/new-settings-loader')
|
||||||
const pp = require('../lib/pp')
|
const pp = require('../lib/pp')
|
||||||
|
|
||||||
|
|
|
||||||
75
bin/lamassu-eth-sweep-old-addresses.js
Normal file
75
bin/lamassu-eth-sweep-old-addresses.js
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
|
const hdkey = require('ethereumjs-wallet/hdkey')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const hkdf = require('futoin-hkdf')
|
||||||
|
const pify = require('pify')
|
||||||
|
const fs = pify(require('fs'))
|
||||||
|
const Web3 = require('web3')
|
||||||
|
const web3 = new Web3()
|
||||||
|
|
||||||
|
const db = require('../lib/db')
|
||||||
|
const configManager = require('../lib/new-config-manager')
|
||||||
|
const { loadLatest } = require('../lib/new-settings-loader')
|
||||||
|
const mnemonicHelpers = require('../lib/mnemonic-helpers')
|
||||||
|
const { sweep } = require('../lib/wallet')
|
||||||
|
const ph = require('../lib/plugin-helper')
|
||||||
|
|
||||||
|
const MNEMONIC_PATH = process.env.MNEMONIC_PATH
|
||||||
|
|
||||||
|
function fetchWallet (settings, cryptoCode) {
|
||||||
|
return fs.readFile(MNEMONIC_PATH, 'utf8')
|
||||||
|
.then(mnemonic => {
|
||||||
|
const masterSeed = mnemonicHelpers.toEntropyBuffer(mnemonic)
|
||||||
|
const plugin = configManager.getWalletSettings(cryptoCode, settings.config).wallet
|
||||||
|
const wallet = ph.load(ph.WALLET, plugin)
|
||||||
|
const rawAccount = settings.accounts[plugin]
|
||||||
|
const account = _.set('seed', computeSeed(masterSeed), rawAccount)
|
||||||
|
if (_.isFunction(wallet.run)) wallet.run(account)
|
||||||
|
return { wallet, account }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSeed (masterSeed) {
|
||||||
|
return hkdf(masterSeed, 32, { salt: 'lamassu-server-salt', info: 'wallet-seed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function paymentHdNode (account) {
|
||||||
|
const masterSeed = account.seed
|
||||||
|
if (!masterSeed) throw new Error('No master seed!')
|
||||||
|
const key = hdkey.fromMasterSeed(masterSeed)
|
||||||
|
return key.derivePath("m/44'/60'/0'/0'")
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHdIndices = db => {
|
||||||
|
const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs WHERE hd_index IS NOT NULL AND status IN ('confirmed', 'instant') AND crypto_code = 'ETH'`
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCashoutAddresses = (settings, indices) => {
|
||||||
|
return Promise.all(_.map(it => {
|
||||||
|
return fetchWallet(settings, it.crypto_code)
|
||||||
|
.then(({ wallet, account }) => Promise.all([wallet, paymentHdNode(account).deriveChild(it.hd_index).getWallet().getChecksumAddressString()]))
|
||||||
|
.then(([wallet, address]) => Promise.all([address, wallet._balance(false, address, 'ETH')]))
|
||||||
|
.then(([address, balance]) => ({ address, balance: balance.toNumber(), cryptoCode: it.crypto_code, index: it.hd_index, txId: it.id }))
|
||||||
|
}, indices))
|
||||||
|
}
|
||||||
|
|
||||||
|
Promise.all([getHdIndices(db), loadLatest()])
|
||||||
|
.then(([indices, settings]) => Promise.all([settings, getCashoutAddresses(settings, indices)]))
|
||||||
|
.then(([settings, addresses]) => {
|
||||||
|
console.log('Found these cash-out addresses for ETH:')
|
||||||
|
console.log(addresses)
|
||||||
|
|
||||||
|
return Promise.all(_.map(it => {
|
||||||
|
// If the address only has dust in it, don't bother sweeping
|
||||||
|
if (web3.utils.fromWei(it.balance.toString()) > 0.00001) {
|
||||||
|
console.log(`Address ${it.address} found to have ${web3.utils.fromWei(it.balance.toString())} ETH in it. Sweeping...`)
|
||||||
|
return sweep(settings, it.txId, it.cryptoCode, it.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Address ${it.address} contains no significant balance (${web3.utils.fromWei(it.balance.toString())}). Skipping the sweep process...`)
|
||||||
|
return Promise.resolve()
|
||||||
|
}, addresses))
|
||||||
|
})
|
||||||
|
.then(() => console.log('Process finished!'))
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const hdkey = require('ethereumjs-wallet/hdkey')
|
const hdkey = require('ethereumjs-wallet/hdkey')
|
||||||
const hkdf = require('futoin-hkdf')
|
const hkdf = require('futoin-hkdf')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
|
@ -263,7 +266,7 @@ settingsLoader.loadLatest()
|
||||||
}
|
}
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
chainId: 3,
|
chainId: 1,
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
includesFee: true
|
includesFee: true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const migrate = require('../lib/migrate-options')
|
const migrate = require('../lib/migrate-options')
|
||||||
|
|
||||||
migrate.run()
|
migrate.run()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const ofac = require('../lib/ofac/update')
|
const ofac = require('../lib/ofac/update')
|
||||||
|
|
||||||
console.log('Updating OFAC databases.')
|
console.log('Updating OFAC databases.')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const settingsLoader = require('../lib/new-settings-loader')
|
const settingsLoader = require('../lib/new-settings-loader')
|
||||||
const configManager = require('../lib/new-config-manager')
|
const configManager = require('../lib/new-config-manager')
|
||||||
const wallet = require('../lib/wallet')
|
const wallet = require('../lib/wallet')
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
|
|
||||||
const _ = require('lodash')
|
const _ = require('lodash')
|
||||||
const db = require('../lib/db')
|
const db = require('../lib/db')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
require('../lib/environment-helper')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const common = require('../lib/blockchain/common')
|
const common = require('../lib/blockchain/common')
|
||||||
const { utils: coinUtils } = require('@lamassu/coins')
|
const { utils: coinUtils } = require('@lamassu/coins')
|
||||||
|
|
|
||||||
36
build/Dockerfile
Normal file
36
build/Dockerfile
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
FROM ubuntu:20.04 as base
|
||||||
|
|
||||||
|
ARG VERSION
|
||||||
|
ENV SERVER_VERSION=$VERSION
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV TZ=Europe/Lisbon
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
RUN apt-get install -y -q curl \
|
||||||
|
sudo \
|
||||||
|
git \
|
||||||
|
python2-minimal \
|
||||||
|
build-essential \
|
||||||
|
libpq-dev \
|
||||||
|
net-tools \
|
||||||
|
tar
|
||||||
|
|
||||||
|
RUN curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
|
||||||
|
RUN apt-get install nodejs -y -q
|
||||||
|
|
||||||
|
FROM base as l-s-build
|
||||||
|
|
||||||
|
WORKDIR /lamassu
|
||||||
|
|
||||||
|
RUN git clone https://github.com/lamassu/lamassu-server -b ${SERVER_VERSION}
|
||||||
|
RUN rm -rf /lamassu/lamassu-server/public/*
|
||||||
|
RUN cd lamassu-server && npm install --production
|
||||||
|
|
||||||
|
RUN cd lamassu-server/new-lamassu-admin && npm install && npm run build
|
||||||
|
RUN cp -r /lamassu/lamassu-server/new-lamassu-admin/build/* /lamassu/lamassu-server/public
|
||||||
|
RUN rm -rf /lamassu/lamassu-server/new-lamassu-admin/node_modules
|
||||||
|
|
||||||
|
RUN tar -zcvf lamassu-server-$SERVER_VERSION.tar.gz lamassu-server/
|
||||||
|
|
||||||
|
ENTRYPOINT [ "/bin/bash" ]
|
||||||
13
build/build.sh
Executable file
13
build/build.sh
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Error: no arguments specified"
|
||||||
|
echo "Usage: ./build.sh <SERVER_VERSION_TAG>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker build --rm --build-arg VERSION=$1 --tag l-s-prepackage:$1 --file Dockerfile .
|
||||||
|
|
||||||
|
id=$(docker create l-s-prepackage:$1)
|
||||||
|
docker cp $id:/lamassu/lamassu-server-$1.tar.gz ./lamassu-server-$1.tar.gz
|
||||||
|
docker rm -v $id
|
||||||
|
|
@ -109,7 +109,7 @@ function makeChange(outCassettes, amount) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (available < amount) {
|
if (available < amount) {
|
||||||
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(cassettes)}`)
|
console.log(`Tried to dispense more than was available for amount ${amount.toNumber()} with cassettes ${JSON.stringify(outCassettes)}`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,22 +29,22 @@ const BINARIES = {
|
||||||
dir: 'bitcoin-23.0/bin'
|
dir: 'bitcoin-23.0/bin'
|
||||||
},
|
},
|
||||||
ETH: {
|
ETH: {
|
||||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.19-23bee162.tar.gz',
|
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.25-69568c55.tar.gz',
|
||||||
dir: 'geth-linux-amd64-1.10.19-23bee162'
|
dir: 'geth-linux-amd64-1.10.25-69568c55'
|
||||||
},
|
},
|
||||||
ZEC: {
|
ZEC: {
|
||||||
url: 'https://z.cash/downloads/zcash-5.0.0-linux64-debian-bullseye.tar.gz',
|
url: 'https://z.cash/downloads/zcash-5.3.0-linux64-debian-bullseye.tar.gz',
|
||||||
dir: 'zcash-5.0.0/bin'
|
dir: 'zcash-5.3.0/bin'
|
||||||
},
|
},
|
||||||
DASH: {
|
DASH: {
|
||||||
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'dashcore-0.17.0/bin'
|
dir: 'dashcore-18.1.0/bin'
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||||
defaultDir: 'litecoin-0.18.1/bin',
|
defaultDir: 'litecoin-0.18.1/bin',
|
||||||
url: 'https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz',
|
url: 'https://download.litecoin.org/litecoin-0.21.2.1/linux/litecoin-0.21.2.1-x86_64-linux-gnu.tar.gz',
|
||||||
dir: 'litecoin-0.21.2.1/bin'
|
dir: 'litecoin-0.21.2.1/bin'
|
||||||
},
|
},
|
||||||
BCH: {
|
BCH: {
|
||||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v24.1.0/bitcoin-cash-node-24.1.0-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v24.1.0/bitcoin-cash-node-24.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
|
|
@ -52,8 +52,8 @@ const BINARIES = {
|
||||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||||
},
|
},
|
||||||
XMR: {
|
XMR: {
|
||||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.3.2.tar.bz2',
|
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.2.tar.bz2',
|
||||||
dir: 'monero-x86_64-linux-gnu-v0.17.3.2',
|
dir: 'monero-x86_64-linux-gnu-v0.18.1.2',
|
||||||
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,19 +139,33 @@ function getBlockchainSyncStatus (cryptoList) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInstalled (crypto) {
|
||||||
|
return isInstalledSoftware(crypto) && isInstalledVolume(crypto)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDisabled (crypto) {
|
||||||
|
switch (crypto.cryptoCode) {
|
||||||
|
case 'ETH':
|
||||||
|
return 'Use admin\'s Infura plugin'
|
||||||
|
case 'ZEC':
|
||||||
|
return isInstalled(crypto) && 'Installed' || isInstalled(_.find(it => it.code === 'monero', cryptos)) && 'Insufficient resources. Contact support.'
|
||||||
|
case 'XMR':
|
||||||
|
return isInstalled(crypto) && 'Installed' || isInstalled(_.find(it => it.code === 'zcash', cryptos)) && 'Insufficient resources. Contact support.'
|
||||||
|
default:
|
||||||
|
return isInstalled(crypto) && 'Installed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function run () {
|
function run () {
|
||||||
const choices = _.flow([
|
const choices = _.flow([
|
||||||
_.filter(c => c.type !== 'erc-20'),
|
_.filter(c => c.type !== 'erc-20'),
|
||||||
_.map(c => {
|
_.map(c => {
|
||||||
const checked = isInstalledSoftware(c) && isInstalledVolume(c)
|
const name = c.code === 'ethereum' ? 'Ethereum and/or USDT' : c.display
|
||||||
const name = c.code === 'ethereum' ? 'Ethereum' : c.display
|
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
value: c.code,
|
value: c.code,
|
||||||
checked,
|
checked: isInstalled(c),
|
||||||
disabled: c.cryptoCode === 'ETH'
|
disabled: isDisabled(c)
|
||||||
? 'Use admin\'s Infura plugin'
|
|
||||||
: checked && 'Installed'
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
])(cryptos)
|
])(cryptos)
|
||||||
|
|
@ -160,6 +174,15 @@ function run () {
|
||||||
|
|
||||||
const validateAnswers = async (answers) => {
|
const validateAnswers = async (answers) => {
|
||||||
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
|
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
|
||||||
|
|
||||||
|
if (
|
||||||
|
_.isEmpty(_.difference(['monero', 'zcash'], answers)) ||
|
||||||
|
(_.includes('monero', answers) && isInstalled(_.find(it => it.code === 'zcash', cryptos))) ||
|
||||||
|
(_.includes('zcash', answers) && isInstalled(_.find(it => it.code === 'monero', cryptos)))
|
||||||
|
) {
|
||||||
|
return { message: `Zcash and Monero installations are temporarily mutually exclusive, given the space needed for their blockchains. Contact support for more information.`, isValid: false }
|
||||||
|
}
|
||||||
|
|
||||||
return getBlockchainSyncStatus(cryptos)
|
return getBlockchainSyncStatus(cryptos)
|
||||||
.then(blockchainStatuses => {
|
.then(blockchainStatuses => {
|
||||||
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
|
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
|
||||||
|
|
|
||||||
|
|
@ -167,23 +167,32 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
|
|
||||||
function doesTxReuseAddress (tx) {
|
function doesTxReuseAddress (tx) {
|
||||||
if (!tx.fiat || tx.fiat.isZero()) {
|
if (!tx.fiat || tx.fiat.isZero()) {
|
||||||
const sql = `SELECT EXISTS (SELECT DISTINCT to_address FROM cash_in_txs WHERE to_address = $1)`
|
const sql = `
|
||||||
return db.any(sql, [tx.toAddress])
|
SELECT EXISTS (
|
||||||
|
SELECT DISTINCT to_address FROM (
|
||||||
|
SELECT to_address FROM cash_in_txs WHERE id != $1
|
||||||
|
) AS x WHERE to_address = $2
|
||||||
|
)`
|
||||||
|
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
|
||||||
}
|
}
|
||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore (tx, pi) {
|
||||||
if (!tx.fiat || tx.fiat.isZero()) {
|
pi.isWalletScoringEnabled(tx)
|
||||||
return pi.rateWallet(tx.cryptoCode, tx.toAddress)
|
.then(isEnabled => {
|
||||||
}
|
if(!isEnabled) return null
|
||||||
// Passthrough the previous result
|
if (!tx.fiat || tx.fiat.isZero()) {
|
||||||
return pi.isValidWalletScore(tx.walletScore)
|
return pi.rateWallet(tx.cryptoCode, tx.toAddress)
|
||||||
.then(isValid => ({
|
}
|
||||||
address: tx.toAddress,
|
// Passthrough the previous result
|
||||||
score: tx.walletScore,
|
return pi.isValidWalletScore(tx.walletScore)
|
||||||
isValid
|
.then(isValid => ({
|
||||||
}))
|
address: tx.toAddress,
|
||||||
|
score: tx.walletScore,
|
||||||
|
isValid
|
||||||
|
}))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorPending (settings) {
|
function monitorPending (settings) {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ module.exports = {
|
||||||
|
|
||||||
const STALE_INCOMING_TX_AGE = T.day
|
const STALE_INCOMING_TX_AGE = T.day
|
||||||
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
|
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
|
||||||
const STALE_LIVE_INCOMING_TX_AGE_FILTER = 5 * T.minutes
|
|
||||||
const MAX_NOTIFY_AGE = T.day
|
const MAX_NOTIFY_AGE = T.day
|
||||||
const MIN_NOTIFY_AGE = 5 * T.minutes
|
const MIN_NOTIFY_AGE = 5 * T.minutes
|
||||||
const INSUFFICIENT_FUNDS_CODE = 570
|
const INSUFFICIENT_FUNDS_CODE = 570
|
||||||
|
|
@ -37,7 +36,8 @@ function selfPost (tx, pi) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function post (tx, pi, fromClient = true) {
|
function post (tx, pi, fromClient = true) {
|
||||||
logger.silly('Updating cashout tx:', tx)
|
logger.silly('Updating cashout -- tx:', JSON.stringify(tx))
|
||||||
|
logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient))
|
||||||
return cashOutAtomic.atomic(tx, pi, fromClient)
|
return cashOutAtomic.atomic(tx, pi, fromClient)
|
||||||
.then(txVector => {
|
.then(txVector => {
|
||||||
const [, newTx, justAuthorized] = txVector
|
const [, newTx, justAuthorized] = txVector
|
||||||
|
|
@ -64,7 +64,7 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
fiat: newTx.fiat
|
fiat: newTx.fiat
|
||||||
})
|
})
|
||||||
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
|
||||||
logger.silly('Bills to dispense:', bills)
|
logger.silly('Bills to dispense:', JSON.stringify(bills))
|
||||||
|
|
||||||
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
|
||||||
return bills
|
return bills
|
||||||
|
|
@ -91,21 +91,17 @@ function postProcess (txVector, justAuthorized, pi) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchOpenTxs (statuses, fromAge, toAge, applyFilter, coinFilter) {
|
function fetchOpenTxs (statuses, fromAge, toAge) {
|
||||||
const notClause = applyFilter ? '' : 'not'
|
|
||||||
const sql = `select *
|
const sql = `select *
|
||||||
from cash_out_txs
|
from cash_out_txs
|
||||||
where ((extract(epoch from (now() - created))) * 1000)>$1
|
where ((extract(epoch from (now() - created))) * 1000)>$1
|
||||||
and ((extract(epoch from (now() - created))) * 1000)<$2
|
and ((extract(epoch from (now() - created))) * 1000)<$2
|
||||||
${_.isEmpty(coinFilter)
|
and status in ($3^)
|
||||||
? ``
|
and error is distinct from 'Operator cancel'`
|
||||||
: `and crypto_code ${notClause} in ($3^)`}
|
|
||||||
and status in ($4^)`
|
|
||||||
|
|
||||||
const coinClause = _.map(pgp.as.text, coinFilter).join(',')
|
|
||||||
const statusClause = _.map(pgp.as.text, statuses).join(',')
|
const statusClause = _.map(pgp.as.text, statuses).join(',')
|
||||||
|
|
||||||
return db.any(sql, [fromAge, toAge, coinClause, statusClause])
|
return db.any(sql, [fromAge, toAge, statusClause])
|
||||||
.then(rows => rows.map(toObj))
|
.then(rows => rows.map(toObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,67 +115,55 @@ function processTxStatus (tx, settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWalletScore (tx, pi) {
|
function getWalletScore (tx, pi) {
|
||||||
const statuses = ['published', 'authorized', 'rejected', 'insufficientFunds']
|
const rejectEmpty = message => x => _.isNil(x) || _.isEmpty(x) ? Promise.reject({ message }) : x
|
||||||
|
const statuses = ['published', 'authorized', 'confirmed']
|
||||||
|
|
||||||
|
if (_.includes(tx.status, statuses) && _.isNil(tx.walletScore))
|
||||||
|
return tx
|
||||||
|
|
||||||
if (_.includes(tx.status, statuses) && _.isNil(tx.walletScore)) {
|
|
||||||
// Transaction shows up on the blockchain, we can request the sender address
|
// Transaction shows up on the blockchain, we can request the sender address
|
||||||
return pi.getTransactionHash(tx)
|
return pi.isWalletScoringEnabled(tx)
|
||||||
.then(txHashes => pi.getInputAddresses(tx, txHashes))
|
.then(isEnabled => {
|
||||||
.then(addresses => {
|
if (!isEnabled) return tx
|
||||||
const addressesPromise = []
|
return pi.getTransactionHash(tx)
|
||||||
_.forEach(it => addressesPromise.push(pi.rateWallet(tx.cryptoCode, it)), addresses)
|
.then(rejectEmpty("No transaction hashes"))
|
||||||
return Promise.all(addressesPromise)
|
.then(txHashes => pi.getInputAddresses(tx, txHashes))
|
||||||
})
|
.then(rejectEmpty("No input addresses"))
|
||||||
.then(scores => {
|
.then(addresses => Promise.all(_.map(it => pi.rateWallet(tx.cryptoCode, it), addresses)))
|
||||||
if (_.isNil(scores) || _.isEmpty(scores)) return tx
|
.then(rejectEmpty("No score ratings"))
|
||||||
const highestScore = _.maxBy(it => it.score, scores)
|
.then(_.maxBy(_.get(['score'])))
|
||||||
|
.then(highestScore =>
|
||||||
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
|
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
|
||||||
return highestScore.isValid
|
highestScore.isValid
|
||||||
? _.assign(tx, { walletScore: highestScore.score })
|
? _.assign(tx, { walletScore: highestScore.score })
|
||||||
: _.assign(tx, {
|
: _.assign(tx, {
|
||||||
walletScore: highestScore.score,
|
walletScore: highestScore.score,
|
||||||
error: 'Address score is above defined threshold',
|
error: 'Address score is above defined threshold',
|
||||||
errorCode: 'scoreThresholdReached',
|
errorCode: 'scoreThresholdReached',
|
||||||
dispense: true
|
dispense: true
|
||||||
})
|
})
|
||||||
})
|
)
|
||||||
.catch(error => _.assign(tx, {
|
.catch(error => _.assign(tx, {
|
||||||
walletScore: 10,
|
walletScore: 10,
|
||||||
error: `Failure getting address score: ${error.message}`,
|
error: `Failure getting address score: ${error.message}`,
|
||||||
errorCode: 'ciphertraceError',
|
errorCode: 'ciphertraceError',
|
||||||
dispense: true
|
dispense: true
|
||||||
}))
|
}))
|
||||||
}
|
})
|
||||||
|
|
||||||
if (_.includes(tx.status, statuses) && !_.isNil(tx.walletScore) && _.get('errorCode', tx) !== 'ciphertraceError') {
|
|
||||||
return pi.isValidWalletScore(tx.walletScore)
|
|
||||||
.then(isValid => isValid ? tx : _.assign(tx, {
|
|
||||||
error: 'Address score is above defined threshold',
|
|
||||||
errorCode: 'scoreThresholdReached',
|
|
||||||
dispense: true
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorLiveIncoming (settings, applyFilter, coinFilter) {
|
function monitorLiveIncoming (settings) {
|
||||||
const statuses = ['notSeen', 'published', 'insufficientFunds']
|
const statuses = ['notSeen', 'published', 'insufficientFunds']
|
||||||
const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE
|
return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
|
||||||
|
|
||||||
return monitorIncoming(settings, statuses, 0, toAge, applyFilter, coinFilter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorStaleIncoming (settings, applyFilter, coinFilter) {
|
function monitorStaleIncoming (settings) {
|
||||||
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
|
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
|
||||||
const fromAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE
|
return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
|
||||||
|
|
||||||
return monitorIncoming(settings, statuses, fromAge, STALE_INCOMING_TX_AGE, applyFilter, coinFilter)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function monitorIncoming (settings, statuses, fromAge, toAge, applyFilter, coinFilter) {
|
function monitorIncoming (settings, statuses, fromAge, toAge) {
|
||||||
return fetchOpenTxs(statuses, fromAge, toAge, applyFilter, coinFilter)
|
return fetchOpenTxs(statuses, fromAge, toAge)
|
||||||
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
|
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) {
|
if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) {
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@ const RECEIPT = 'sms_receipt'
|
||||||
|
|
||||||
const WALLET_SCORE_THRESHOLD = 9
|
const WALLET_SCORE_THRESHOLD = 9
|
||||||
|
|
||||||
|
const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
||||||
|
NORMAL: 1,
|
||||||
|
SLOW: 3
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
anonymousCustomer,
|
anonymousCustomer,
|
||||||
CASSETTE_MAX_CAPACITY,
|
CASSETTE_MAX_CAPACITY,
|
||||||
|
|
@ -48,5 +53,6 @@ module.exports = {
|
||||||
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
|
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
|
||||||
WALLET_SCORE_THRESHOLD,
|
WALLET_SCORE_THRESHOLD,
|
||||||
RECEIPT,
|
RECEIPT,
|
||||||
PSQL_URL
|
PSQL_URL,
|
||||||
|
BALANCE_FETCH_SPEED_MULTIPLIER
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,7 +262,7 @@ function deleteEditedData (id, data) {
|
||||||
*/
|
*/
|
||||||
async function updateEditedPhoto (id, photo, photoType) {
|
async function updateEditedPhoto (id, photo, photoType) {
|
||||||
const newPatch = {}
|
const newPatch = {}
|
||||||
const baseDir = photoType === 'frontCamera' ? frontCameraBaseDir : idPhotoCardBasedir
|
const baseDir = photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR
|
||||||
const { createReadStream, filename } = photo
|
const { createReadStream, filename } = photo
|
||||||
const stream = createReadStream()
|
const stream = createReadStream()
|
||||||
|
|
||||||
|
|
@ -339,7 +339,7 @@ function camelizeDeep (customer) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all available complianceTypes
|
* Get all available complianceTypes
|
||||||
* that can be overriden (excluding hard_limit)
|
* that can be overridden (excluding hard_limit)
|
||||||
*
|
*
|
||||||
* @name getComplianceTypes
|
* @name getComplianceTypes
|
||||||
* @function
|
* @function
|
||||||
|
|
@ -404,7 +404,7 @@ function enhanceAtFields (fields) {
|
||||||
*/
|
*/
|
||||||
function enhanceOverrideFields (fields, userToken) {
|
function enhanceOverrideFields (fields, userToken) {
|
||||||
if (!userToken) return fields
|
if (!userToken) return fields
|
||||||
// Populate with computedFields (user who overrode and overriden timestamps date)
|
// Populate with computedFields (user who overrode and overridden timestamps date)
|
||||||
return _.reduce(_.assign, fields, _.map((type) => {
|
return _.reduce(_.assign, fields, _.map((type) => {
|
||||||
return (fields[type + '_override'])
|
return (fields[type + '_override'])
|
||||||
? {
|
? {
|
||||||
|
|
@ -484,7 +484,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
||||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||||
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||||
sanctions_override, total_txs, total_spent, LEAST(created, last_transaction) AS last_active, fiat AS last_tx_fiat,
|
sanctions_override, total_txs, total_spent, GREATEST(created, last_transaction, last_data_provided) AS last_active, fiat AS last_tx_fiat,
|
||||||
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
|
fiat_code AS last_tx_fiat_code, tx_class AS last_tx_class, custom_fields, notes, is_test_customer
|
||||||
FROM (
|
FROM (
|
||||||
SELECT c.id, c.authorized_override,
|
SELECT c.id, c.authorized_override,
|
||||||
|
|
@ -493,6 +493,7 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
||||||
c.front_camera_path, c.front_camera_override,
|
c.front_camera_path, c.front_camera_override,
|
||||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
||||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||||
|
GREATEST(c.phone_at, c.id_card_data_at, c.front_camera_at, c.id_card_photo_at, c.us_ssn_at) AS last_data_provided,
|
||||||
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
c.sanctions_at, c.sanctions_override, c.is_test_customer, c.created, t.tx_class, t.fiat, t.fiat_code, t.created as last_transaction, cn.notes,
|
||||||
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
||||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
require('dotenv').config({ path: process.env.NODE_ENV === 'production' ? path.resolve('/etc', 'lamassu', '.env') : path.resolve(__dirname, '../.env') })
|
|
||||||
|
require('dotenv').config({ path: path.resolve(__dirname, '../.env') })
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,18 @@ const speedtestFiles = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const addSmthInfo = (dstField, srcFields) => smth =>
|
const addSmthInfo = (dstField, srcFields) => smth =>
|
||||||
smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
|
(smth && smth.active) ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
|
||||||
|
|
||||||
const addOperatorInfo = addSmthInfo(
|
const addOperatorInfo = addSmthInfo(
|
||||||
'operatorInfo',
|
'operatorInfo',
|
||||||
['name', 'phone', 'email', 'website', 'companyNumber']
|
['name', 'phone', 'email', 'website', 'companyNumber']
|
||||||
)
|
)
|
||||||
|
|
||||||
const addReceiptInfo = addSmthInfo(
|
const addReceiptInfo = receiptInfo => ret => {
|
||||||
'receiptInfo',
|
if (!receiptInfo) return ret
|
||||||
[
|
|
||||||
|
const fields = [
|
||||||
|
'paper',
|
||||||
'sms',
|
'sms',
|
||||||
'operatorWebsite',
|
'operatorWebsite',
|
||||||
'operatorEmail',
|
'operatorEmail',
|
||||||
|
|
@ -43,10 +45,22 @@ const addReceiptInfo = addSmthInfo(
|
||||||
'exchangeRate',
|
'exchangeRate',
|
||||||
'addressQRCode',
|
'addressQRCode',
|
||||||
]
|
]
|
||||||
)
|
const defaults = _.fromPairs(_.map(field => [field, false], fields))
|
||||||
|
|
||||||
|
receiptInfo = _.flow(
|
||||||
|
o => _.set('paper', o.active, o),
|
||||||
|
_.assign(defaults),
|
||||||
|
_.pick(fields),
|
||||||
|
)(receiptInfo)
|
||||||
|
|
||||||
|
return (receiptInfo.paper || receiptInfo.sms) ?
|
||||||
|
_.set('receiptInfo', receiptInfo, ret) :
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* TODO: Simplify this. */
|
/* TODO: Simplify this. */
|
||||||
const buildTriggers = (allTriggers) => {
|
const buildTriggers = allTriggers => {
|
||||||
const normalTriggers = []
|
const normalTriggers = []
|
||||||
const customTriggers = _.filter(o => {
|
const customTriggers = _.filter(o => {
|
||||||
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
|
if (_.isEmpty(o.customInfoRequestId) || _.isNil(o.customInfoRequestId)) normalTriggers.push(o)
|
||||||
|
|
@ -82,7 +96,6 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
|
|
||||||
const staticConf = _.flow(
|
const staticConf = _.flow(
|
||||||
_.pick([
|
_.pick([
|
||||||
'areThereAvailablePromoCodes',
|
|
||||||
'coins',
|
'coins',
|
||||||
'configVersion',
|
'configVersion',
|
||||||
'timezone'
|
'timezone'
|
||||||
|
|
@ -139,7 +152,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
const setZeroConfLimit = config => coin =>
|
const setZeroConfLimit = config => coin =>
|
||||||
_.set(
|
_.set(
|
||||||
'zeroConfLimit',
|
'zeroConfLimit',
|
||||||
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit,
|
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit ?? 0,
|
||||||
coin
|
coin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -157,7 +170,7 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||||
|
|
||||||
return _.flow(
|
return _.flow(
|
||||||
_.pick(['balances', 'cassettes', 'coins', 'rates']),
|
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']),
|
||||||
|
|
||||||
_.update('cassettes', massageCassettes),
|
_.update('cassettes', massageCassettes),
|
||||||
|
|
||||||
|
|
@ -172,7 +185,8 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
|
|
||||||
/* Group the separate objects by cryptoCode */
|
/* Group the separate objects by cryptoCode */
|
||||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||||
({ balances, cassettes, coins, rates }) => ({
|
({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({
|
||||||
|
areThereAvailablePromoCodes,
|
||||||
cassettes,
|
cassettes,
|
||||||
coins: _.flow(
|
coins: _.flow(
|
||||||
_.reduce(
|
_.reduce(
|
||||||
|
|
@ -184,7 +198,10 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
||||||
_.toPairs,
|
_.toPairs,
|
||||||
|
|
||||||
/* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */
|
/* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */
|
||||||
_.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj))
|
_.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)),
|
||||||
|
|
||||||
|
/* Only send coins which have all information needed by the machine. This prevents the machine going down if there's an issue with the coin node */
|
||||||
|
_.filter(coin => ['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(it => it in coin))
|
||||||
)(_.concat(balances, coins))
|
)(_.concat(balances, coins))
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -218,6 +235,7 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera
|
||||||
|
|
||||||
|
|
||||||
const massageTerms = terms => (terms.active && terms.text) ? ({
|
const massageTerms = terms => (terms.active && terms.text) ? ({
|
||||||
|
tcPhoto: Boolean(terms.tcPhoto),
|
||||||
delay: Boolean(terms.delay),
|
delay: Boolean(terms.delay),
|
||||||
title: terms.title,
|
title: terms.title,
|
||||||
text: nmd(terms.text),
|
text: nmd(terms.text),
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ type Coin {
|
||||||
cashInFee: String!
|
cashInFee: String!
|
||||||
cashInCommission: String!
|
cashInCommission: String!
|
||||||
cashOutCommission: String!
|
cashOutCommission: String!
|
||||||
cryptoNetwork: Boolean!
|
cryptoNetwork: String!
|
||||||
cryptoUnits: String!
|
cryptoUnits: String!
|
||||||
batchable: Boolean!
|
batchable: Boolean!
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ type MachineInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceiptInfo {
|
type ReceiptInfo {
|
||||||
|
paper: Boolean!
|
||||||
sms: Boolean!
|
sms: Boolean!
|
||||||
operatorWebsite: Boolean!
|
operatorWebsite: Boolean!
|
||||||
operatorEmail: Boolean!
|
operatorEmail: Boolean!
|
||||||
|
|
@ -57,6 +58,32 @@ type TriggersAutomation {
|
||||||
usSsn: Boolean!
|
usSsn: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomScreen {
|
||||||
|
text: String!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomInput {
|
||||||
|
type: String!
|
||||||
|
constraintType: String!
|
||||||
|
label1: String
|
||||||
|
label2: String
|
||||||
|
choiceList: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomRequest {
|
||||||
|
name: String!
|
||||||
|
input: CustomInput!
|
||||||
|
screen1: CustomScreen!
|
||||||
|
screen2: CustomScreen!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomInfoRequest {
|
||||||
|
id: String!
|
||||||
|
enabled: Boolean!
|
||||||
|
customRequest: CustomRequest!
|
||||||
|
}
|
||||||
|
|
||||||
type Trigger {
|
type Trigger {
|
||||||
id: String!
|
id: String!
|
||||||
customInfoRequestId: String!
|
customInfoRequestId: String!
|
||||||
|
|
@ -64,12 +91,14 @@ type Trigger {
|
||||||
requirement: String!
|
requirement: String!
|
||||||
triggerType: String!
|
triggerType: String!
|
||||||
|
|
||||||
suspensionDays: Int
|
suspensionDays: Float
|
||||||
threshold: Int
|
threshold: Int
|
||||||
thresholdDays: Int
|
thresholdDays: Int
|
||||||
|
customInfoRequest: CustomInfoRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
type TermsDetails {
|
type TermsDetails {
|
||||||
|
tcPhoto: Boolean!
|
||||||
delay: Boolean!
|
delay: Boolean!
|
||||||
title: String!
|
title: String!
|
||||||
accept: String!
|
accept: String!
|
||||||
|
|
@ -85,7 +114,6 @@ type Terms {
|
||||||
type StaticConfig {
|
type StaticConfig {
|
||||||
configVersion: Int!
|
configVersion: Int!
|
||||||
|
|
||||||
areThereAvailablePromoCodes: Boolean!
|
|
||||||
coins: [Coin!]!
|
coins: [Coin!]!
|
||||||
enablePaperWalletOnly: Boolean!
|
enablePaperWalletOnly: Boolean!
|
||||||
hasLightning: Boolean!
|
hasLightning: Boolean!
|
||||||
|
|
@ -136,6 +164,7 @@ type Cassettes {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicConfig {
|
type DynamicConfig {
|
||||||
|
areThereAvailablePromoCodes: Boolean!
|
||||||
cassettes: Cassettes
|
cassettes: Cassettes
|
||||||
coins: [DynamicCoinValues!]!
|
coins: [DynamicCoinValues!]!
|
||||||
reboot: Boolean!
|
reboot: Boolean!
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const { format } = require('date-fns/fp')
|
const { format, isValid } = require('date-fns/fp')
|
||||||
const { utcToZonedTime } = require('date-fns-tz/fp')
|
const { utcToZonedTime } = require('date-fns-tz/fp')
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
const logger = require('./logger')
|
||||||
const pgp = require('pg-promise')()
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
const getMachineName = require('./machine-loader').getMachineName
|
const getMachineName = require('./machine-loader').getMachineName
|
||||||
|
|
@ -118,6 +119,10 @@ function logDateFormat (timezone, logs, fields) {
|
||||||
field =>
|
field =>
|
||||||
{
|
{
|
||||||
if (_.isNil(log[field])) return null
|
if (_.isNil(log[field])) return null
|
||||||
|
if (!isValid(log[field])) {
|
||||||
|
logger.warn(`Tried to convert to ${timezone} timezone the value ${log[field]} and failed. Returning original value...`)
|
||||||
|
return log[field]
|
||||||
|
}
|
||||||
const date = utcToZonedTime(timezone, log[field])
|
const date = utcToZonedTime(timezone, log[field])
|
||||||
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
|
return `${format('yyyy-MM-dd', date)}T${format('HH:mm:ss.SSS', date)}`
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
const _ = require('lodash/fp')
|
|
||||||
|
|
||||||
const db = require('../db')
|
const db = require('../db')
|
||||||
const state = require('./state')
|
const state = require('./state')
|
||||||
const newSettingsLoader = require('../new-settings-loader')
|
const newSettingsLoader = require('../new-settings-loader')
|
||||||
const helpers = require('../route-helpers')
|
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
db.connect({ direct: true }).then(sco => {
|
db.connect({ direct: true }).then(sco => {
|
||||||
|
|
@ -58,31 +55,54 @@ const populateSettings = function (req, res, next) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const operatorSettings = settingsCache.get(operatorId)
|
// Priority of configs to retrieve
|
||||||
if (!versionId && (!operatorSettings || !!needsSettingsReload[operatorId])) {
|
// 1. Machine is in the middle of a transaction and has the config-version header set, fetch that config from cache or database, depending on whether it exists in cache
|
||||||
|
// 2. The operator settings changed, so we must update the cache
|
||||||
|
// 3. There's a cached config, send the cached value
|
||||||
|
// 4. There's no cached config, cache and send the latest config
|
||||||
|
|
||||||
|
if (versionId) {
|
||||||
|
const cachedVersionedSettings = settingsCache.get(`${operatorId}-v${versionId}`)
|
||||||
|
|
||||||
|
if (!cachedVersionedSettings) {
|
||||||
|
logger.debug('Fetching a specific config version cached value')
|
||||||
|
return newSettingsLoader.load(versionId)
|
||||||
|
.then(settings => {
|
||||||
|
settingsCache.set(`${operatorId}-v${versionId}`, settings)
|
||||||
|
req.settings = settings
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Fetching and caching a specific config version')
|
||||||
|
req.settings = cachedVersionedSettings
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const operatorSettings = settingsCache.get(`${operatorId}-latest`)
|
||||||
|
|
||||||
|
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
|
||||||
|
!!needsSettingsReload[operatorId]
|
||||||
|
? logger.debug('Fetching and caching a new latest config value, as a reload was requested')
|
||||||
|
: logger.debug('Fetching the latest config version because there\'s no cached value')
|
||||||
|
|
||||||
return newSettingsLoader.loadLatest()
|
return newSettingsLoader.loadLatest()
|
||||||
.then(settings => {
|
.then(settings => {
|
||||||
settingsCache.set(operatorId, settings)
|
settingsCache.set(`${operatorId}-latest`, settings)
|
||||||
delete needsSettingsReload[operatorId]
|
if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId]
|
||||||
req.settings = settings
|
req.settings = settings
|
||||||
})
|
})
|
||||||
.then(() => next())
|
.then(() => next())
|
||||||
.catch(next)
|
.catch(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!versionId && operatorSettings) {
|
logger.debug('Fetching the latest config value from cache')
|
||||||
req.settings = operatorSettings
|
req.settings = operatorSettings
|
||||||
return next()
|
return next()
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
newSettingsLoader.load(versionId)
|
|
||||||
.then(settings => { req.settings = settings })
|
|
||||||
.then(() => helpers.updateDeviceConfigVersion(versionId))
|
|
||||||
.then(() => next())
|
|
||||||
.catch(next)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = populateSettings
|
module.exports = populateSettings
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ function updateOptionBasepath (result, optionName) {
|
||||||
async function run () {
|
async function run () {
|
||||||
// load current opts
|
// load current opts
|
||||||
const options = load().opts
|
const options = load().opts
|
||||||
const shouldMigrate = !fs.existsSync(process.env.NODE_ENV === 'production' ? path.resolve('/etc', 'lamassu', '.env') : path.resolve(__dirname, '../.env'))
|
const shouldMigrate = !fs.existsSync(path.resolve(__dirname, '../.env'))
|
||||||
|
|
||||||
// write the resulting .env
|
// write the resulting .env
|
||||||
if (shouldMigrate) {
|
if (shouldMigrate) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ const _ = require('lodash/fp')
|
||||||
|
|
||||||
const { ALL } = require('../../plugins/common/ccxt')
|
const { ALL } = require('../../plugins/common/ccxt')
|
||||||
|
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, USDT, ZEC, XMR } = COINS
|
||||||
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, ftx, binance } = ALL
|
const { bitpay, coinbase, itbit, bitstamp, kraken, binanceus, cex, ftx, binance } = ALL
|
||||||
|
|
||||||
const TICKER = 'ticker'
|
const TICKER = 'ticker'
|
||||||
|
|
@ -29,8 +29,8 @@ const ALL_ACCOUNTS = [
|
||||||
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
||||||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||||
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH] },
|
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH, USDT] },
|
||||||
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH] },
|
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH, USDT] },
|
||||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ function transaction () {
|
||||||
SELECT 'address' AS type, to_address AS value FROM cash_in_txs UNION
|
SELECT 'address' AS type, to_address AS value FROM cash_in_txs UNION
|
||||||
SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION
|
SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION
|
||||||
SELECT 'status' AS type, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
|
SELECT 'status' AS type, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
|
||||||
SELECT 'status' AS type, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs
|
SELECT 'status' AS type, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs UNION
|
||||||
|
SELECT 'sweep status' AS type, CASE WHEN swept THEN 'Swept' WHEN NOT swept THEN 'Unswept' END AS value FROM cash_out_txs
|
||||||
) f`
|
) f`
|
||||||
|
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
|
|
|
||||||
|
|
@ -19,14 +19,14 @@ const resolvers = {
|
||||||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers }]) =>
|
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
|
||||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers),
|
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
|
||||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, excludeTestingCustomers, simplified }]) =>
|
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
|
||||||
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers, simplified)
|
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
|
||||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']))),
|
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
|
||||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||||
transactions.getTx(id, txClass).then(data =>
|
transactions.getTx(id, txClass).then(data =>
|
||||||
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime']))
|
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt']))
|
||||||
),
|
),
|
||||||
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
|
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
|
||||||
transactions.getTxAssociatedData(id, txClass).then(data =>
|
transactions.getTxAssociatedData(id, txClass).then(data =>
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ const typeDef = gql`
|
||||||
batchError: String
|
batchError: String
|
||||||
walletScore: Int
|
walletScore: Int
|
||||||
profit: String
|
profit: String
|
||||||
|
swept: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Filter {
|
type Filter {
|
||||||
|
|
@ -58,8 +59,8 @@ const typeDef = gql`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, excludeTestingCustomers: Boolean): [Transaction] @auth
|
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth
|
||||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
||||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||||
transactionFilters: [Filter] @auth
|
transactionFilters: [Filter] @auth
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ function batch (
|
||||||
cryptoCode = null,
|
cryptoCode = null,
|
||||||
toAddress = null,
|
toAddress = null,
|
||||||
status = null,
|
status = null,
|
||||||
|
swept = null,
|
||||||
excludeTestingCustomers = false,
|
excludeTestingCustomers = false,
|
||||||
simplified
|
simplified
|
||||||
) {
|
) {
|
||||||
|
|
@ -109,14 +110,33 @@ function batch (
|
||||||
AND ($11 is null or txs.crypto_code = $11)
|
AND ($11 is null or txs.crypto_code = $11)
|
||||||
AND ($12 is null or txs.to_address = $12)
|
AND ($12 is null or txs.to_address = $12)
|
||||||
AND ($13 is null or txs.txStatus = $13)
|
AND ($13 is null or txs.txStatus = $13)
|
||||||
|
AND ($14 is null or txs.swept = $14)
|
||||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||||
AND (fiat > 0)
|
AND (fiat > 0)
|
||||||
ORDER BY created DESC limit $4 offset $5`
|
ORDER BY created DESC limit $4 offset $5`
|
||||||
|
|
||||||
return Promise.all([
|
// The swept filter is cash-out only, so omit the cash-in query entirely
|
||||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
const hasCashInOnlyFilters = false
|
||||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
const hasCashOutOnlyFilters = !_.isNil(swept)
|
||||||
])
|
|
||||||
|
let promises
|
||||||
|
|
||||||
|
if (hasCashInOnlyFilters && hasCashOutOnlyFilters) {
|
||||||
|
throw new Error('Trying to filter transactions with mutually exclusive filters')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCashInOnlyFilters) {
|
||||||
|
promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])]
|
||||||
|
} else if (hasCashOutOnlyFilters) {
|
||||||
|
promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])]
|
||||||
|
} else {
|
||||||
|
promises = [
|
||||||
|
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
||||||
|
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
.then(packager)
|
.then(packager)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (simplified) return simplifiedBatch(res)
|
if (simplified) return simplifiedBatch(res)
|
||||||
|
|
@ -138,7 +158,7 @@ function advancedBatch (data) {
|
||||||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName',
|
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||||
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
|
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
|
||||||
|
|
||||||
const addAdvancedFields = _.map(it => ({
|
const addAdvancedFields = _.map(it => ({
|
||||||
|
|
@ -169,8 +189,8 @@ function simplifiedBatch (data) {
|
||||||
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode)
|
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode)
|
||||||
|
|
||||||
const getProfit = it => {
|
const getProfit = it => {
|
||||||
/* fiat - crypto*tickerPrice + fee */
|
/* fiat - crypto*tickerPrice */
|
||||||
const calcCashInProfit = (fiat, crypto, tickerPrice, fee) => fiat.minus(crypto.times(tickerPrice)).plus(fee)
|
const calcCashInProfit = (fiat, crypto, tickerPrice) => fiat.minus(crypto.times(tickerPrice))
|
||||||
/* crypto*tickerPrice - fiat */
|
/* crypto*tickerPrice - fiat */
|
||||||
const calcCashOutProfit = (fiat, crypto, tickerPrice) => crypto.times(tickerPrice).minus(fiat)
|
const calcCashOutProfit = (fiat, crypto, tickerPrice) => crypto.times(tickerPrice).minus(fiat)
|
||||||
|
|
||||||
|
|
@ -180,7 +200,7 @@ const getProfit = it => {
|
||||||
const isCashIn = it.txClass === 'cashIn'
|
const isCashIn = it.txClass === 'cashIn'
|
||||||
|
|
||||||
return isCashIn
|
return isCashIn
|
||||||
? calcCashInProfit(fiat, crypto, tickerPrice, BN(it.cashInFee))
|
? calcCashInProfit(fiat, crypto, tickerPrice)
|
||||||
: calcCashOutProfit(fiat, crypto, tickerPrice)
|
: calcCashOutProfit(fiat, crypto, tickerPrice)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,15 +145,13 @@ const getTriggersAutomation = (customInfoRequests, config) => {
|
||||||
|
|
||||||
const splitGetFirst = _.compose(_.head, _.split('_'))
|
const splitGetFirst = _.compose(_.head, _.split('_'))
|
||||||
|
|
||||||
const getCryptosFromWalletNamespace = config => {
|
const getCryptosFromWalletNamespace =
|
||||||
return _.uniq(_.map(splitGetFirst, _.keys(fromNamespace('wallets', config))))
|
_.compose(_.without(['advanced']), _.uniq, _.map(splitGetFirst), _.keys, fromNamespace('wallets'))
|
||||||
}
|
|
||||||
|
|
||||||
const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config)
|
const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config)
|
||||||
|
|
||||||
const getCryptoUnits = (crypto, config) => {
|
const getCryptoUnits = (crypto, config) =>
|
||||||
return getWalletSettings(crypto, config).cryptoUnits
|
getWalletSettings(crypto, config).cryptoUnits ?? 'full'
|
||||||
}
|
|
||||||
|
|
||||||
const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS)
|
const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,17 @@ function saveConfig (config) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeFromConfig (fields) {
|
||||||
|
return Promise.all([loadLatestConfigOrNone(), getOperatorId('middleware')])
|
||||||
|
.then(([currentConfig, operatorId]) => {
|
||||||
|
const newConfig = _.omit(fields, currentConfig)
|
||||||
|
return db.tx(t => {
|
||||||
|
return t.none(configSql, ['config', { config: newConfig }, true, NEW_SETTINGS_LOADER_SCHEMA_VERSION])
|
||||||
|
.then(() => t.none('NOTIFY $1:name, $2', ['reload', JSON.stringify({ schema: asyncLocalStorage.getStore().get('schema'), operatorId })]))
|
||||||
|
}).catch(console.error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function migrationSaveConfig (config) {
|
function migrationSaveConfig (config) {
|
||||||
return loadLatestConfigOrNone()
|
return loadLatestConfigOrNone()
|
||||||
.then(currentConfig => {
|
.then(currentConfig => {
|
||||||
|
|
@ -221,5 +232,6 @@ module.exports = {
|
||||||
loadLatestConfig,
|
loadLatestConfig,
|
||||||
loadLatestConfigOrNone,
|
loadLatestConfigOrNone,
|
||||||
load,
|
load,
|
||||||
migrate
|
migrate,
|
||||||
|
removeFromConfig
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const notificationCenter = require('./notificationCenter')
|
||||||
const utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const emailFuncs = require('./email')
|
const emailFuncs = require('./email')
|
||||||
const smsFuncs = require('./sms')
|
const smsFuncs = require('./sms')
|
||||||
|
const webhookFuncs = require('./webhook')
|
||||||
const { STALE, STALE_STATE } = require('./codes')
|
const { STALE, STALE_STATE } = require('./codes')
|
||||||
|
|
||||||
function buildMessage (alerts, notifications) {
|
function buildMessage (alerts, notifications) {
|
||||||
|
|
@ -185,6 +186,10 @@ function complianceNotify (customer, deviceId, action, period) {
|
||||||
email: {
|
email: {
|
||||||
subject: `Customer compliance`,
|
subject: `Customer compliance`,
|
||||||
body: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}`
|
body: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}`
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
topic: `Customer compliance`,
|
||||||
|
content: `Customer ${customer.phone} ${msgCore[action]} in machine ${machineName}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,8 +203,11 @@ function complianceNotify (customer, deviceId, action, period) {
|
||||||
notifications.sms.active &&
|
notifications.sms.active &&
|
||||||
notifications.sms.compliance
|
notifications.sms.compliance
|
||||||
|
|
||||||
|
const webhookActive = true
|
||||||
|
|
||||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||||
|
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||||
|
|
||||||
notifyIfActive('compliance', 'customerComplianceNotify', customer, deviceId, action, period)
|
notifyIfActive('compliance', 'customerComplianceNotify', customer, deviceId, action, period)
|
||||||
|
|
||||||
|
|
@ -220,6 +228,10 @@ function sendRedemptionMessage (txId, error) {
|
||||||
email: {
|
email: {
|
||||||
subject,
|
subject,
|
||||||
body
|
body
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
topic: `Transaction update`,
|
||||||
|
content: body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sendTransactionMessage(rec)
|
return sendTransactionMessage(rec)
|
||||||
|
|
@ -241,6 +253,11 @@ function sendTransactionMessage (rec, isHighValueTx) {
|
||||||
(notifications.sms.transactions || isHighValueTx)
|
(notifications.sms.transactions || isHighValueTx)
|
||||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||||
|
|
||||||
|
// TODO: Webhook transaction notifications are dependent on notification settings, due to how transactionNotify() is programmed
|
||||||
|
// As changing it would require structural change to that function and the current behavior is temporary (webhooks will eventually have settings tied to them), it's not worth those changes right now
|
||||||
|
const webhookActive = true
|
||||||
|
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -259,6 +276,10 @@ function cashboxNotify (deviceId) {
|
||||||
email: {
|
email: {
|
||||||
subject: `Cashbox removal`,
|
subject: `Cashbox removal`,
|
||||||
body: `Cashbox removed in machine ${machineName}`
|
body: `Cashbox removed in machine ${machineName}`
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
topic: `Cashbox removal`,
|
||||||
|
content: `Cashbox removed in machine ${machineName}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,9 +292,12 @@ function cashboxNotify (deviceId) {
|
||||||
const smsActive =
|
const smsActive =
|
||||||
notifications.sms.active &&
|
notifications.sms.active &&
|
||||||
notifications.sms.security
|
notifications.sms.security
|
||||||
|
|
||||||
|
const webhookActive = true
|
||||||
|
|
||||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||||
|
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||||
notifyIfActive('security', 'cashboxNotify', deviceId)
|
notifyIfActive('security', 'cashboxNotify', deviceId)
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,9 @@ const fiatBalancesNotify = (fiatWarnings) => {
|
||||||
const { cassette, deviceId } = o.detail
|
const { cassette, deviceId } = o.detail
|
||||||
return cassette === balance.cassette && deviceId === balance.deviceId
|
return cassette === balance.cassette && deviceId === balance.deviceId
|
||||||
}, notInvalidated)) return
|
}, notInvalidated)) return
|
||||||
const message = `Cash-out cassette ${balance.cassette} low or empty!`
|
const message = balance.code === 'LOW_CASH_OUT' ?
|
||||||
|
`Cash-out cassette ${balance.cassette} low or empty!` :
|
||||||
|
`Cash box full or almost full!`
|
||||||
const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette })
|
const detailB = utils.buildDetail({ deviceId: balance.deviceId, cassette: balance.cassette })
|
||||||
return queries.addNotification(FIAT_BALANCE, message, detailB)
|
return queries.addNotification(FIAT_BALANCE, message, detailB)
|
||||||
})
|
})
|
||||||
|
|
@ -111,11 +113,18 @@ const cryptoBalancesNotify = (cryptoWarnings) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const balancesNotify = (balances) => {
|
const balancesNotify = (balances) => {
|
||||||
const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE'
|
const isCryptoCode = c => _.includes(c, ['HIGH_CRYPTO_BALANCE', 'LOW_CRYPTO_BALANCE'])
|
||||||
const fiatFilter = o => o.code === 'LOW_CASH_OUT'
|
const isFiatCode = c => _.includes(c, ['LOW_CASH_OUT', 'CASH_BOX_FULL'])
|
||||||
const cryptoWarnings = _.filter(cryptoFilter, balances)
|
const by = o =>
|
||||||
const fiatWarnings = _.filter(fiatFilter, balances)
|
isCryptoCode(o) ? 'crypto' :
|
||||||
return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)])
|
isFiatCode(o) ? 'fiat' :
|
||||||
|
undefined
|
||||||
|
const warnings = _.flow(
|
||||||
|
_.groupBy(_.flow(_.get(['code']), by)),
|
||||||
|
_.update('crypto', _.defaultTo([])),
|
||||||
|
_.update('fiat', _.defaultTo([])),
|
||||||
|
)(balances)
|
||||||
|
return Promise.all([cryptoBalancesNotify(warnings.crypto), fiatBalancesNotify(warnings.fiat)])
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearOldErrorNotifications = alerts => {
|
const clearOldErrorNotifications = alerts => {
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,10 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
||||||
email: {
|
email: {
|
||||||
emailSubject,
|
emailSubject,
|
||||||
body
|
body
|
||||||
|
},
|
||||||
|
webhook: {
|
||||||
|
topic: `New transaction`,
|
||||||
|
content: body
|
||||||
}
|
}
|
||||||
}, highValueTx]
|
}, highValueTx]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
lib/notifier/webhook.js
Normal file
21
lib/notifier/webhook.js
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
const axios = require('axios')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
|
const WEBHOOK_URL = process.env.WEBHOOK_URL
|
||||||
|
|
||||||
|
const sendMessage = (settings, rec) => {
|
||||||
|
if (_.isEmpty(WEBHOOK_URL)) return Promise.resolve()
|
||||||
|
|
||||||
|
const body = _.merge(rec.webhook, { id: uuid.v4() })
|
||||||
|
|
||||||
|
return axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: WEBHOOK_URL,
|
||||||
|
data: body
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
sendMessage
|
||||||
|
}
|
||||||
|
|
@ -218,6 +218,7 @@ function plugins (settings, deviceId) {
|
||||||
return {
|
return {
|
||||||
cryptoCode,
|
cryptoCode,
|
||||||
display: cryptoRec.display,
|
display: cryptoRec.display,
|
||||||
|
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||||
minimumTx: BN.max(minimumTx, cashInFee),
|
minimumTx: BN.max(minimumTx, cashInFee),
|
||||||
cashInFee,
|
cashInFee,
|
||||||
cashInCommission,
|
cashInCommission,
|
||||||
|
|
@ -788,9 +789,10 @@ function plugins (settings, deviceId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweepHdRow (row) {
|
function sweepHdRow (row) {
|
||||||
|
const txId = row.id
|
||||||
const cryptoCode = row.crypto_code
|
const cryptoCode = row.crypto_code
|
||||||
|
|
||||||
return wallet.sweep(settings, cryptoCode, row.hd_index)
|
return wallet.sweep(settings, txId, cryptoCode, row.hd_index)
|
||||||
.then(txHash => {
|
.then(txHash => {
|
||||||
if (txHash) {
|
if (txHash) {
|
||||||
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
|
logger.debug('[%s] Swept address with tx: %s', cryptoCode, txHash)
|
||||||
|
|
@ -801,12 +803,12 @@ function plugins (settings, deviceId) {
|
||||||
return db.none(sql, row.id)
|
return db.none(sql, row.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => logger.error('[%s] Sweep error: %s', cryptoCode, err.message))
|
.catch(err => logger.error('[%s] [Session ID: %s] Sweep error: %s', cryptoCode, row.id, err.message))
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweepHd () {
|
function sweepHd () {
|
||||||
const sql = `select id, crypto_code, hd_index from cash_out_txs
|
const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs
|
||||||
where hd_index is not null and not swept and status in ('confirmed', 'instant')`
|
WHERE hd_index IS NOT NULL AND NOT swept AND status IN ('confirmed', 'instant') AND created > now() - interval '1 week'`
|
||||||
|
|
||||||
return db.any(sql)
|
return db.any(sql)
|
||||||
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
||||||
|
|
@ -848,6 +850,10 @@ function plugins (settings, deviceId) {
|
||||||
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
|
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWalletScoringEnabled (tx) {
|
||||||
|
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getRates,
|
getRates,
|
||||||
recordPing,
|
recordPing,
|
||||||
|
|
@ -880,7 +886,8 @@ function plugins (settings, deviceId) {
|
||||||
rateWallet,
|
rateWallet,
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses
|
getInputAddresses,
|
||||||
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const bitpay = require('../ticker/bitpay')
|
||||||
const binance = require('../exchange/binance')
|
const binance = require('../exchange/binance')
|
||||||
const logger = require('../../logger')
|
const logger = require('../../logger')
|
||||||
|
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
|
||||||
|
|
||||||
const ALL = {
|
const ALL = {
|
||||||
cex: cex,
|
cex: cex,
|
||||||
|
|
@ -22,7 +22,7 @@ const ALL = {
|
||||||
itbit: itbit,
|
itbit: itbit,
|
||||||
bitpay: bitpay,
|
bitpay: bitpay,
|
||||||
coinbase: {
|
coinbase: {
|
||||||
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH],
|
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT],
|
||||||
FIAT: 'ALL_CURRENCIES'
|
FIAT: 'ALL_CURRENCIES'
|
||||||
},
|
},
|
||||||
binance: binance
|
binance: binance
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, ETH, LTC, BCH } = COINS
|
const { BTC, ETH, LTC, BCH, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const AMOUNT_PRECISION = 8
|
const AMOUNT_PRECISION = 8
|
||||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, BCH]
|
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, ETH, LTC } = COINS
|
const { BTC, BCH, ETH, LTC, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
|
||||||
const { COINS } = require('@lamassu/coins')
|
const { COINS } = require('@lamassu/coins')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.LIMIT
|
const ORDER_TYPE = ORDER_TYPES.LIMIT
|
||||||
const { BTC, ETH } = COINS
|
const { BTC, ETH, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH]
|
const CRYPTO = [BTC, ETH, USDT]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
const AMOUNT_PRECISION = 4
|
const AMOUNT_PRECISION = 4
|
||||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']
|
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
|
||||||
const { COINS } = require('@lamassu/coins')
|
const { COINS } = require('@lamassu/coins')
|
||||||
|
|
||||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const AMOUNT_PRECISION = 6
|
const AMOUNT_PRECISION = 6
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||||
|
|
|
||||||
|
|
@ -52,44 +52,62 @@ function isValidWalletScore (account, score) {
|
||||||
return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
|
return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTransactionHash (account, cryptoCode, receivingAddress) {
|
function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wallet) {
|
||||||
const client = getClient(account)
|
|
||||||
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
|
||||||
|
|
||||||
const { apiVersion, authHeader } = client
|
const { apiVersion, authHeader } = client
|
||||||
|
|
||||||
logger.info(`** DEBUG ** getTransactionHash ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`)
|
logger.info(`** DEBUG ** getTransactionHash ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`)
|
||||||
return new Promise(resolve => {
|
|
||||||
setTimeout(resolve, 2000)
|
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`, {
|
||||||
|
headers: authHeader
|
||||||
})
|
})
|
||||||
.then(axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`, {
|
.then(_.flow(
|
||||||
headers: authHeader
|
_.get(['data', 'txHistory']),
|
||||||
}))
|
_.map(_.get(['txHash']))
|
||||||
.then(res => {
|
))
|
||||||
const data = res.data
|
|
||||||
if (_.size(data.txHistory) > 1) {
|
|
||||||
logger.warn('An address generated by this wallet was used in more than one transaction')
|
|
||||||
}
|
|
||||||
logger.info(`** DEBUG ** getTransactionHash RETURN: ${_.join(', ', _.map(it => it.txHash, data.txHistory))}`)
|
|
||||||
return _.join(', ', _.map(it => it.txHash, data.txHistory))
|
|
||||||
})
|
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
logger.error(`** DEBUG ** getTransactionHash ERROR: ${err}`)
|
logger.error(`** DEBUG ** getTransactionHash ERROR: ${err}`)
|
||||||
|
logger.error(`** DEBUG ** Fetching transactions hashes via wallet node...`)
|
||||||
|
return wallet.getTxHashesByAddress(cryptoCode, receivingAddress)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransactionHash (account, cryptoCode, receivingAddress, wallet) {
|
||||||
|
const client = getClient(account)
|
||||||
|
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
||||||
|
return getAddressTransactionsHashes(receivingAddress, cryptoCode, client, wallet)
|
||||||
|
.then(txHashes => {
|
||||||
|
if (_.size(txHashes) > 1) {
|
||||||
|
logger.warn('An address generated by this wallet was used in more than one transaction')
|
||||||
|
}
|
||||||
|
logger.info('** DEBUG ** getTransactionHash RETURN: ', _.join(', ', txHashes))
|
||||||
|
return txHashes
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
logger.error('** DEBUG ** getTransactionHash from wallet node ERROR: ', err)
|
||||||
throw err
|
throw err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInputAddresses (account, cryptoCode, txHashes) {
|
function getInputAddresses (account, cryptoCode, txHashes) {
|
||||||
const client = getClient(account)
|
const client = getClient(account)
|
||||||
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
if (_.isEmpty(txHashes) || !_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client))
|
||||||
|
return Promise.resolve([])
|
||||||
|
|
||||||
|
/* NOTE: The API accepts at most 10 hashes, and for us here more than 1 is already an exception, not the norm. */
|
||||||
|
if (_.size(txHashes) > 10)
|
||||||
|
return Promise.reject(new Error("Too many tx hashes -- shouldn't happen!"))
|
||||||
|
|
||||||
const { apiVersion, authHeader } = client
|
const { apiVersion, authHeader } = client
|
||||||
|
|
||||||
logger.info(`** DEBUG ** getInputAddresses ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}tx?txhashes=${txHashes}`)
|
cryptoCode = _.toLower(cryptoCode)
|
||||||
|
const lastPathComp = cryptoCode !== 'btc' ? cryptoCode + '_tx' : 'tx'
|
||||||
|
|
||||||
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}tx?txhashes=${txHashes}`, {
|
txHashes = _(txHashes).take(10).join(',')
|
||||||
headers: authHeader
|
|
||||||
})
|
const url = `https://rest.ciphertrace.com/api/${apiVersion}/${lastPathComp}?txhashes=${txHashes}`
|
||||||
|
console.log('** DEBUG ** getInputAddresses ENDPOINT: ', url)
|
||||||
|
|
||||||
|
return axios.get(url, { headers: authHeader })
|
||||||
.then(res => {
|
.then(res => {
|
||||||
const data = res.data
|
const data = res.data
|
||||||
if (_.size(data.transactions) > 1) {
|
if (_.size(data.transactions) > 1) {
|
||||||
|
|
@ -109,10 +127,19 @@ function getInputAddresses (account, cryptoCode, txHashes) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWalletScoringEnabled (account, cryptoCode) {
|
||||||
|
if (!SUPPORTED_COINS.includes(cryptoCode)) {
|
||||||
|
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(!_.isNil(account) && account.enabled)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NAME,
|
NAME,
|
||||||
rateWallet,
|
rateWallet,
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses
|
getInputAddresses,
|
||||||
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,20 @@ function getInputAddresses (account, cryptoCode, txHashes) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWalletScoringEnabled (account, cryptoCode) {
|
||||||
|
return new Promise((resolve, _) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
return resolve(true)
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
NAME,
|
NAME,
|
||||||
rateWallet,
|
rateWallet,
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses
|
getInputAddresses,
|
||||||
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,13 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => fetch('listreceivedbyaddress', [0, true, true, address]))
|
||||||
|
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||||
|
.then(_.map(({ hash }) => hash))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
|
|
@ -136,5 +143,6 @@ module.exports = {
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
cryptoNetwork,
|
cryptoNetwork,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,13 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => fetch('listreceivedbyaddress', [0, true, true, address]))
|
||||||
|
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||||
|
.then(_.map(({ hash }) => hash))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
|
|
@ -202,5 +209,6 @@ module.exports = {
|
||||||
estimateFee,
|
estimateFee,
|
||||||
sendCoinsBatch,
|
sendCoinsBatch,
|
||||||
checkBlockchainStatus,
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress,
|
||||||
SUPPORTS_BATCHING
|
SUPPORTS_BATCHING
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,19 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => fetch('listreceivedbyaddress', [0, true, true, true, address]))
|
||||||
|
.then(txsByAddress => Promise.all(_.map(id => fetch('getrawtransaction', [id]), _.flatMap(it => it.txids, txsByAddress))))
|
||||||
|
.then(_.map(({ hash }) => hash))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
newAddress,
|
newAddress,
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,15 @@ const web3 = new Web3()
|
||||||
const hdkey = require('ethereumjs-wallet/hdkey')
|
const hdkey = require('ethereumjs-wallet/hdkey')
|
||||||
const { FeeMarketEIP1559Transaction } = require('@ethereumjs/tx')
|
const { FeeMarketEIP1559Transaction } = require('@ethereumjs/tx')
|
||||||
const { default: Common, Chain, Hardfork } = require('@ethereumjs/common')
|
const { default: Common, Chain, Hardfork } = require('@ethereumjs/common')
|
||||||
|
const Tx = require('ethereumjs-tx')
|
||||||
|
const { default: PQueue } = require('p-queue')
|
||||||
const util = require('ethereumjs-util')
|
const util = require('ethereumjs-util')
|
||||||
const coins = require('@lamassu/coins')
|
const coins = require('@lamassu/coins')
|
||||||
const pify = require('pify')
|
|
||||||
|
const _pify = require('pify')
|
||||||
const BN = require('../../../bn')
|
const BN = require('../../../bn')
|
||||||
const ABI = require('../../tokens')
|
const ABI = require('../../tokens')
|
||||||
|
const logger = require('../../../logger')
|
||||||
|
|
||||||
exports.SUPPORTED_MODULES = ['wallet']
|
exports.SUPPORTED_MODULES = ['wallet']
|
||||||
|
|
||||||
|
|
@ -30,7 +34,27 @@ module.exports = {
|
||||||
privateKey,
|
privateKey,
|
||||||
isStrictAddress,
|
isStrictAddress,
|
||||||
connect,
|
connect,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress,
|
||||||
|
_balance
|
||||||
|
}
|
||||||
|
|
||||||
|
const SWEEP_QUEUE = new PQueue({
|
||||||
|
concurrency: 3,
|
||||||
|
interval: 250,
|
||||||
|
})
|
||||||
|
|
||||||
|
const infuraCalls = {}
|
||||||
|
|
||||||
|
const pify = _function => {
|
||||||
|
if (_.isString(_function.call)) logInfuraCall(_function.call)
|
||||||
|
return _pify(_function)
|
||||||
|
}
|
||||||
|
|
||||||
|
const logInfuraCall = call => {
|
||||||
|
if (!_.includes('infura', web3.currentProvider.host)) return
|
||||||
|
_.isNil(infuraCalls[call]) ? infuraCalls[call] = 1 : infuraCalls[call]++
|
||||||
|
logger.info(`Calling web3 method ${call} via Infura. Current count for this session: ${JSON.stringify(infuraCalls)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect (url) {
|
function connect (url) {
|
||||||
|
|
@ -44,12 +68,19 @@ function privateKey (account) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function isStrictAddress (cryptoCode, toAddress, settings, operatorId) {
|
function isStrictAddress (cryptoCode, toAddress, settings, operatorId) {
|
||||||
return cryptoCode === 'ETH' && util.isValidChecksumAddress(toAddress)
|
return checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => util.isValidChecksumAddress(toAddress))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
throw new Error(`Transactions hash retrieval is not implemented for this coin!`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
|
function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
|
||||||
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
const { toAddress, cryptoAtoms, cryptoCode } = tx
|
||||||
return generateTx(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode)
|
const isErc20Token = coins.utils.isErc20Token(cryptoCode)
|
||||||
|
|
||||||
|
return (isErc20Token ? generateErc20Tx : generateTx)(toAddress, defaultWallet(account), cryptoAtoms, false, cryptoCode)
|
||||||
.then(pify(web3.eth.sendSignedTransaction))
|
.then(pify(web3.eth.sendSignedTransaction))
|
||||||
.then(txid => {
|
.then(txid => {
|
||||||
return pify(web3.eth.getTransaction)(txid)
|
return pify(web3.eth.getTransaction)(txid)
|
||||||
|
|
@ -77,14 +108,16 @@ function balance (account, cryptoCode, settings, operatorId) {
|
||||||
|
|
||||||
const pendingBalance = (address, cryptoCode) => {
|
const pendingBalance = (address, cryptoCode) => {
|
||||||
const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)]
|
const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)]
|
||||||
return Promise.all(promises).then(([pending, confirmed]) => pending.minus(confirmed))
|
return Promise.all(promises).then(([pending, confirmed]) => BN(pending).minus(confirmed))
|
||||||
}
|
}
|
||||||
const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode)
|
const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode)
|
||||||
|
|
||||||
function _balance (includePending, address, cryptoCode) {
|
function _balance (includePending, address, cryptoCode) {
|
||||||
if (coins.utils.isErc20Token(cryptoCode)) {
|
if (coins.utils.isErc20Token(cryptoCode)) {
|
||||||
const contract = web3.eth.contract(ABI.ERC20).at(coins.utils.getErc20Token(cryptoCode).contractAddress)
|
const contract = new web3.eth.Contract(ABI.ERC20, coins.utils.getErc20Token(cryptoCode).contractAddress)
|
||||||
return contract.balanceOf(address.toLowerCase())
|
return contract.methods.balanceOf(address.toLowerCase()).call((_, balance) => {
|
||||||
|
return contract.methods.decimals().call((_, decimals) => BN(balance).div(10 ** decimals))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const block = includePending ? 'pending' : undefined
|
const block = includePending ? 'pending' : undefined
|
||||||
return pify(web3.eth.getBalance)(address.toLowerCase(), block)
|
return pify(web3.eth.getBalance)(address.toLowerCase(), block)
|
||||||
|
|
@ -92,27 +125,78 @@ function _balance (includePending, address, cryptoCode) {
|
||||||
.then(balance => balance ? BN(balance) : BN(0))
|
.then(balance => balance ? BN(balance) : BN(0))
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
function generateErc20Tx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
||||||
const fromAddress = '0x' + wallet.getAddress().toString('hex')
|
const fromAddress = '0x' + wallet.getAddress().toString('hex')
|
||||||
|
|
||||||
const isErc20Token = coins.utils.isErc20Token(cryptoCode)
|
const toAddress = coins.utils.getErc20Token(cryptoCode).contractAddress
|
||||||
const toAddress = isErc20Token ? coins.utils.getErc20Token(cryptoCode).contractAddress : _toAddress.toLowerCase()
|
|
||||||
|
|
||||||
let contract, contractData
|
const contract = new web3.eth.Contract(ABI.ERC20, toAddress)
|
||||||
if (isErc20Token) {
|
const contractData = contract.methods.transfer(_toAddress.toLowerCase(), hex(amount))
|
||||||
contract = web3.eth.contract(ABI.ERC20).at(toAddress)
|
|
||||||
contractData = isErc20Token && contract.transfer.getData(_toAddress.toLowerCase(), hex(toSend))
|
const txTemplate = {
|
||||||
|
from: fromAddress,
|
||||||
|
to: toAddress,
|
||||||
|
value: hex(BN(0)),
|
||||||
|
data: contractData.encodeABI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London })
|
||||||
|
|
||||||
|
const promises = [
|
||||||
|
pify(contractData.estimateGas)(txTemplate),
|
||||||
|
pify(web3.eth.getTransactionCount)(fromAddress),
|
||||||
|
pify(web3.eth.getBlock)('pending')
|
||||||
|
]
|
||||||
|
|
||||||
|
return Promise.all(promises)
|
||||||
|
.then(([gas, txCount, { baseFeePerGas }]) => [
|
||||||
|
BN(gas),
|
||||||
|
_.max([0, txCount, lastUsedNonces[fromAddress] + 1]),
|
||||||
|
BN(baseFeePerGas)
|
||||||
|
])
|
||||||
|
.then(([gas, txCount, baseFeePerGas]) => {
|
||||||
|
lastUsedNonces[fromAddress] = txCount
|
||||||
|
|
||||||
|
const maxPriorityFeePerGas = new BN(web3.utils.toWei('2.5', 'gwei')) // web3 default value
|
||||||
|
const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas)
|
||||||
|
|
||||||
|
if (includesFee && (toSend.isNegative() || toSend.isZero())) {
|
||||||
|
throw new Error(`Trying to send a nil or negative amount (Transaction ID: ${txId} | Value provided: ${toSend.toNumber()}). This is probably caused due to the estimated fee being higher than the address' balance.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawTx = {
|
||||||
|
chainId: 1,
|
||||||
|
nonce: txCount,
|
||||||
|
maxPriorityFeePerGas: web3.utils.toHex(maxPriorityFeePerGas),
|
||||||
|
maxFeePerGas: web3.utils.toHex(maxFeePerGas),
|
||||||
|
gasLimit: hex(gas),
|
||||||
|
to: toAddress,
|
||||||
|
from: fromAddress,
|
||||||
|
value: hex(BN(0)),
|
||||||
|
data: contractData.encodeABI()
|
||||||
|
}
|
||||||
|
|
||||||
|
const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common })
|
||||||
|
const privateKey = wallet.getPrivateKey()
|
||||||
|
|
||||||
|
const signedTx = tx.sign(privateKey)
|
||||||
|
|
||||||
|
return '0x' + signedTx.serialize().toString('hex')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode, txId) {
|
||||||
|
const fromAddress = '0x' + wallet.getAddress().toString('hex')
|
||||||
|
|
||||||
|
const toAddress = _toAddress.toLowerCase()
|
||||||
|
|
||||||
const txTemplate = {
|
const txTemplate = {
|
||||||
from: fromAddress,
|
from: fromAddress,
|
||||||
to: toAddress,
|
to: toAddress,
|
||||||
value: amount.toString()
|
value: amount.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isErc20Token) txTemplate.data = contractData
|
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London })
|
||||||
|
|
||||||
const common = new Common({ chain: Chain.Ropsten, hardfork: Hardfork.London })
|
|
||||||
|
|
||||||
const promises = [
|
const promises = [
|
||||||
pify(web3.eth.estimateGas)(txTemplate),
|
pify(web3.eth.estimateGas)(txTemplate),
|
||||||
|
|
@ -122,34 +206,33 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
||||||
]
|
]
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
.then(([gas, gasPrice, txCount]) => [
|
.then(([gas, gasPrice, txCount, { baseFeePerGas }]) => [
|
||||||
BN(gas),
|
BN(gas),
|
||||||
BN(gasPrice),
|
BN(gasPrice),
|
||||||
_.max([0, txCount, lastUsedNonces[fromAddress] + 1])
|
_.max([0, txCount, lastUsedNonces[fromAddress] + 1]),
|
||||||
|
BN(baseFeePerGas)
|
||||||
])
|
])
|
||||||
.then(([gas, gasPrice, txCount, baseFeePerGas]) => {
|
.then(([gas, gasPrice, txCount, baseFeePerGas]) => {
|
||||||
lastUsedNonces[fromAddress] = txCount
|
lastUsedNonces[fromAddress] = txCount
|
||||||
|
|
||||||
const toSend = includesFee
|
const maxPriorityFeePerGas = new BN(web3.utils.toWei('2.5', 'gwei')) // web3 default value
|
||||||
? amount.minus(gasPrice.times(gas))
|
const neededPriority = new BN(web3.utils.toWei('2.0', 'gwei'))
|
||||||
: amount
|
const maxFeePerGas = baseFeePerGas.plus(neededPriority)
|
||||||
|
const newGasPrice = BN.minimum(maxFeePerGas, baseFeePerGas.plus(maxPriorityFeePerGas))
|
||||||
|
|
||||||
const maxPriorityFeePerGas = new BN(2.5) // web3 default value
|
const toSend = includesFee
|
||||||
const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas)
|
? new BN(amount).minus(newGasPrice.times(gas))
|
||||||
|
: amount
|
||||||
|
|
||||||
const rawTx = {
|
const rawTx = {
|
||||||
chainId: 1,
|
chainId: 1,
|
||||||
nonce: txCount,
|
nonce: txCount,
|
||||||
maxPriorityFeePerGas: web3.utils.toHex(web3.utils.toWei(maxPriorityFeePerGas.toString(), 'gwei')),
|
maxPriorityFeePerGas: web3.utils.toHex(maxPriorityFeePerGas),
|
||||||
maxFeePerGas: web3.utils.toHex(web3.utils.toWei(maxFeePerGas.toString(), 'gwei')),
|
maxFeePerGas: web3.utils.toHex(maxFeePerGas),
|
||||||
gasLimit: hex(gas),
|
gasLimit: hex(gas),
|
||||||
to: toAddress,
|
to: toAddress,
|
||||||
from: fromAddress,
|
from: fromAddress,
|
||||||
value: isErc20Token ? hex(BN(0)) : hex(toSend)
|
value: hex(toSend)
|
||||||
}
|
|
||||||
|
|
||||||
if (isErc20Token) {
|
|
||||||
rawTx.data = contractData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common })
|
const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common })
|
||||||
|
|
@ -169,17 +252,18 @@ function defaultAddress (account) {
|
||||||
return defaultWallet(account).getChecksumAddressString()
|
return defaultWallet(account).getChecksumAddressString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweep (account, cryptoCode, hdIndex, settings, operatorId) {
|
function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) {
|
||||||
const wallet = paymentHdNode(account).deriveChild(hdIndex).getWallet()
|
const wallet = paymentHdNode(account).deriveChild(hdIndex).getWallet()
|
||||||
const fromAddress = wallet.getChecksumAddressString()
|
const fromAddress = wallet.getChecksumAddressString()
|
||||||
|
|
||||||
return confirmedBalance(fromAddress, cryptoCode)
|
return SWEEP_QUEUE.add(() => confirmedBalance(fromAddress, cryptoCode)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.eq(0)) return
|
if (r.eq(0)) return
|
||||||
|
|
||||||
return generateTx(defaultAddress(account), wallet, r, true, cryptoCode)
|
return generateTx(defaultAddress(account), wallet, r, true, cryptoCode, txId)
|
||||||
.then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx))
|
.then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx))
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function newAddress (account, info, tx, settings, operatorId) {
|
function newAddress (account, info, tx, settings, operatorId) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const NodeCache = require('node-cache')
|
||||||
const base = require('../geth/base')
|
const base = require('../geth/base')
|
||||||
|
const T = require('../../../time')
|
||||||
|
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('../../../constants')
|
||||||
|
|
||||||
|
const REGULAR_TX_POLLING = 5 * T.seconds
|
||||||
|
|
||||||
const NAME = 'infura'
|
const NAME = 'infura'
|
||||||
|
|
||||||
|
|
@ -12,4 +17,54 @@ function run (account) {
|
||||||
base.connect(endpoint)
|
base.connect(endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = _.merge(base, { NAME, run })
|
const txsCache = new NodeCache({
|
||||||
|
stdTTL: T.hour / 1000,
|
||||||
|
checkperiod: T.minute / 1000,
|
||||||
|
deleteOnExpire: true
|
||||||
|
})
|
||||||
|
|
||||||
|
function shouldGetStatus (tx) {
|
||||||
|
const timePassedSinceTx = Date.now() - new Date(tx.created)
|
||||||
|
const timePassedSinceReq = Date.now() - new Date(txsCache.get(tx.id).lastReqTime)
|
||||||
|
|
||||||
|
// Allow for infura to gradually lower the amount of requests based on the time passed since the transaction
|
||||||
|
// Until first 5 minutes - 1/2 regular polling speed
|
||||||
|
// Until first 10 minutes - 1/4 regular polling speed
|
||||||
|
// Until first hour - 1/8 polling speed
|
||||||
|
// Until first two hours - 1/12 polling speed
|
||||||
|
// Until first four hours - 1/16 polling speed
|
||||||
|
// Until first day - 1/24 polling speed
|
||||||
|
// After first day - 1/32 polling speed
|
||||||
|
if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * REGULAR_TX_POLLING
|
||||||
|
if (timePassedSinceTx < 10 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 4 * REGULAR_TX_POLLING
|
||||||
|
if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 8 * REGULAR_TX_POLLING
|
||||||
|
if (timePassedSinceTx < 2 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 12 * REGULAR_TX_POLLING
|
||||||
|
if (timePassedSinceTx < 4 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 16 * REGULAR_TX_POLLING
|
||||||
|
if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 24 * REGULAR_TX_POLLING
|
||||||
|
return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 32 * REGULAR_TX_POLLING
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override geth's getStatus function to allow for different polling timing
|
||||||
|
function getStatus (account, tx, requested, settings, operatorId) {
|
||||||
|
if (_.isNil(txsCache.get(tx.id))) {
|
||||||
|
txsCache.set(tx.id, { lastReqTime: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
// return last available response
|
||||||
|
if (!shouldGetStatus(tx)) {
|
||||||
|
return Promise.resolve(txsCache.get(tx.id).res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.getStatus(account, tx, requested, settings, operatorId)
|
||||||
|
.then(res => {
|
||||||
|
if (res.status === 'confirmed') {
|
||||||
|
txsCache.del(tx.id) // Transaction reached final status, can trim it from the caching obj
|
||||||
|
} else {
|
||||||
|
txsCache.set(tx.id, { lastReqTime: Date.now(), res })
|
||||||
|
txsCache.ttl(tx.id, T.hour / 1000)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = _.merge(base, { NAME, run, getStatus, fetchSpeed: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW })
|
||||||
|
|
|
||||||
|
|
@ -125,11 +125,16 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
throw new Error(`Transactions hash retrieval not implemented for this coin!`)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
newAddress,
|
newAddress,
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,14 @@ const SECONDS = 1000
|
||||||
const PUBLISH_TIME = 3 * SECONDS
|
const PUBLISH_TIME = 3 * SECONDS
|
||||||
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
||||||
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
||||||
|
const SUPPORTED_COINS = coinUtils.cryptoCurrencies()
|
||||||
|
|
||||||
let t0
|
let t0
|
||||||
|
|
||||||
|
const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS)
|
||||||
|
? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||||
|
: Promise.resolve()
|
||||||
|
|
||||||
function _balance (cryptoCode) {
|
function _balance (cryptoCode) {
|
||||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||||
const unitScale = cryptoRec.unitScale
|
const unitScale = cryptoRec.unitScale
|
||||||
|
|
@ -107,7 +112,15 @@ function getStatus (account, tx, requested, settings, operatorId) {
|
||||||
|
|
||||||
console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5))
|
console.log('[%s] DEBUG: Mock wallet has confirmed transaction [%s]', cryptoCode, toAddress.slice(0, 5))
|
||||||
|
|
||||||
return Promise.resolve({status: 'confirmed'})
|
return Promise.resolve({ status: 'confirmed' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
return resolve([]) // TODO: should return something other than empty list?
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkBlockchainStatus (cryptoCode) {
|
function checkBlockchainStatus (cryptoCode) {
|
||||||
|
|
@ -123,5 +136,6 @@ module.exports = {
|
||||||
newAddress,
|
newAddress,
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ function handleError (error, method) {
|
||||||
|
|
||||||
function openWallet () {
|
function openWallet () {
|
||||||
return fetch('open_wallet', { filename: 'Wallet' })
|
return fetch('open_wallet', { filename: 'Wallet' })
|
||||||
.catch(err => handleError(err, 'openWallet'))
|
.catch(() => openWalletWithPassword())
|
||||||
}
|
}
|
||||||
|
|
||||||
function openWalletWithPassword () {
|
function openWalletWithPassword () {
|
||||||
|
|
@ -164,7 +164,7 @@ function getStatus (account, tx, requested, settings, operatorId) {
|
||||||
return checkCryptoCode(cryptoCode)
|
return checkCryptoCode(cryptoCode)
|
||||||
.then(() => refreshWallet())
|
.then(() => refreshWallet())
|
||||||
.then(() => fetch('get_address_index', { address: toAddress }))
|
.then(() => fetch('get_address_index', { address: toAddress }))
|
||||||
.then(addressRes => fetch('get_transfers', { in: true, pool: true, account_index: addressRes.index.major, address_indices: [addressRes.index.minor] }))
|
.then(addressRes => fetch('get_transfers', { in: true, pool: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] }))
|
||||||
.then(transferRes => {
|
.then(transferRes => {
|
||||||
const confirmedToAddress = _.filter(it => it.address === toAddress, transferRes.in ?? [])
|
const confirmedToAddress = _.filter(it => it.address === toAddress, transferRes.in ?? [])
|
||||||
const pendingToAddress = _.filter(it => it.address === toAddress, transferRes.pool ?? [])
|
const pendingToAddress = _.filter(it => it.address === toAddress, transferRes.pool ?? [])
|
||||||
|
|
@ -235,6 +235,14 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => refreshWallet())
|
||||||
|
.then(() => fetch('get_address_index', { address: address }))
|
||||||
|
.then(addressRes => fetch('get_transfers', { in: true, pool: true, pending: true, account_index: addressRes.index.major, subaddr_indices: [addressRes.index.minor] }))
|
||||||
|
.then(_.map(({ txid }) => txid))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
|
|
@ -242,5 +250,6 @@ module.exports = {
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
cryptoNetwork,
|
cryptoNetwork,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ function newFunding (account, cryptoCode, settings, operatorId) {
|
||||||
throw new E.NotImplementedError()
|
throw new E.NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweep (account, cryptoCode, hdIndex, settings, operatorId) {
|
function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) {
|
||||||
throw new E.NotImplementedError()
|
throw new E.NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ function sendCoins (account, tx, settings, operatorId) {
|
||||||
const checker = opid => pRetry(() => checkSendStatus(opid), { retries: 20, minTimeout: 300, factor: 1.05 })
|
const checker = opid => pRetry(() => checkSendStatus(opid), { retries: 20, minTimeout: 300, factor: 1.05 })
|
||||||
|
|
||||||
return checkCryptoCode(cryptoCode)
|
return checkCryptoCode(cryptoCode)
|
||||||
.then(() => fetch('z_sendmany', ['ANY_TADDR', [{ address: toAddress, amount: coins }]]))
|
.then(() => fetch('z_sendmany', ['ANY_TADDR', [{ address: toAddress, amount: coins }], null, null, 'NoPrivacy']))
|
||||||
.then(checker)
|
.then(checker)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -151,11 +151,17 @@ function checkBlockchainStatus (cryptoCode) {
|
||||||
.then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing')
|
.then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTxHashesByAddress (cryptoCode, address) {
|
||||||
|
checkCryptoCode(cryptoCode)
|
||||||
|
.then(() => fetch('getaddresstxids', [address]))
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
balance,
|
balance,
|
||||||
sendCoins,
|
sendCoins,
|
||||||
newAddress,
|
newAddress,
|
||||||
getStatus,
|
getStatus,
|
||||||
newFunding,
|
newFunding,
|
||||||
checkBlockchainStatus
|
checkBlockchainStatus,
|
||||||
|
getTxHashesByAddress
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,8 @@ const processBatches = require('./tx-batching-processing')
|
||||||
|
|
||||||
const INCOMING_TX_INTERVAL = 30 * T.seconds
|
const INCOMING_TX_INTERVAL = 30 * T.seconds
|
||||||
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
|
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
|
||||||
const INCOMING_TX_INTERVAL_FILTER = 1 * T.minute
|
|
||||||
const LIVE_INCOMING_TX_INTERVAL_FILTER = 10 * T.seconds
|
|
||||||
const UNNOTIFIED_INTERVAL = 10 * T.seconds
|
const UNNOTIFIED_INTERVAL = 10 * T.seconds
|
||||||
const SWEEP_HD_INTERVAL = T.minute
|
const SWEEP_HD_INTERVAL = 5 * T.minute
|
||||||
const TRADE_INTERVAL = 60 * T.seconds
|
const TRADE_INTERVAL = 60 * T.seconds
|
||||||
const PONG_INTERVAL = 10 * T.seconds
|
const PONG_INTERVAL = 10 * T.seconds
|
||||||
const LOGS_CLEAR_INTERVAL = 1 * T.day
|
const LOGS_CLEAR_INTERVAL = 1 * T.day
|
||||||
|
|
@ -61,7 +59,6 @@ const QUEUE = {
|
||||||
SLOW: SLOW_QUEUE
|
SLOW: SLOW_QUEUE
|
||||||
}
|
}
|
||||||
|
|
||||||
const coinFilter = ['ETH']
|
|
||||||
const schemaCallbacks = new Map()
|
const schemaCallbacks = new Map()
|
||||||
|
|
||||||
const cachedVariables = new NodeCache({
|
const cachedVariables = new NodeCache({
|
||||||
|
|
@ -168,12 +165,8 @@ function doPolling (schema) {
|
||||||
pi().executeTrades()
|
pi().executeTrades()
|
||||||
pi().pong()
|
pi().pong()
|
||||||
pi().clearOldLogs()
|
pi().clearOldLogs()
|
||||||
cashOutTx.monitorLiveIncoming(settings(), false, coinFilter)
|
cashOutTx.monitorLiveIncoming(settings())
|
||||||
cashOutTx.monitorStaleIncoming(settings(), false, coinFilter)
|
cashOutTx.monitorStaleIncoming(settings())
|
||||||
if (!_.isEmpty(coinFilter)) {
|
|
||||||
cashOutTx.monitorLiveIncoming(settings(), true, coinFilter)
|
|
||||||
cashOutTx.monitorStaleIncoming(settings(), true, coinFilter)
|
|
||||||
}
|
|
||||||
cashOutTx.monitorUnnotified(settings())
|
cashOutTx.monitorUnnotified(settings())
|
||||||
pi().sweepHd()
|
pi().sweepHd()
|
||||||
notifier.checkNotification(pi())
|
notifier.checkNotification(pi())
|
||||||
|
|
@ -181,12 +174,8 @@ function doPolling (schema) {
|
||||||
|
|
||||||
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().getRawRates, TICKER_RATES_INTERVAL, schema, QUEUE.FAST)
|
||||||
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
|
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
|
||||||
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter)
|
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
||||||
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter)
|
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
||||||
if (!_.isEmpty(coinFilter)) {
|
|
||||||
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter)
|
|
||||||
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter)
|
|
||||||
}
|
|
||||||
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings)
|
||||||
addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings)
|
addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings)
|
||||||
addToQueue(processBatches, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE)
|
addToQueue(processBatches, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE)
|
||||||
|
|
|
||||||
|
|
@ -85,14 +85,9 @@ function fetchStatusTx (txId, status) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDeviceConfigVersion (versionId) {
|
|
||||||
return db.none('update devices set user_config_id=$1', [versionId])
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stateChange,
|
stateChange,
|
||||||
fetchPhoneTx,
|
fetchPhoneTx,
|
||||||
fetchStatusTx,
|
fetchStatusTx,
|
||||||
updateDeviceConfigVersion,
|
|
||||||
httpError
|
httpError
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,21 +6,36 @@ const notifier = require('../notifier')
|
||||||
const { getMachine, setMachine } = require('../machine-loader')
|
const { getMachine, setMachine } = require('../machine-loader')
|
||||||
const { loadLatestConfig } = require('../new-settings-loader')
|
const { loadLatestConfig } = require('../new-settings-loader')
|
||||||
const { getCashInSettings } = require('../new-config-manager')
|
const { getCashInSettings } = require('../new-config-manager')
|
||||||
const { AUTOMATIC } = require('../constants.js')
|
const { AUTOMATIC } = require('../constants')
|
||||||
|
const logger = require('../logger')
|
||||||
|
|
||||||
function notifyCashboxRemoval (req, res, next) {
|
function notifyCashboxRemoval (req, res, next) {
|
||||||
const operatorId = res.locals.operatorId
|
const operatorId = res.locals.operatorId
|
||||||
|
|
||||||
|
logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`)
|
||||||
|
|
||||||
return notifier.cashboxNotify(req.deviceId)
|
return notifier.cashboxNotify(req.deviceId)
|
||||||
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
||||||
.then(([machine, config]) => {
|
.then(([machine, config]) => {
|
||||||
|
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
|
||||||
const cashInSettings = getCashInSettings(config)
|
const cashInSettings = getCashInSettings(config)
|
||||||
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
||||||
|
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created')
|
||||||
|
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||||
return res.status(200).send({ status: 'OK' })
|
return res.status(200).send({ status: 'OK' })
|
||||||
}
|
}
|
||||||
|
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
||||||
|
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
||||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||||
.then(() => setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId))
|
.then(() => {
|
||||||
.then(() => res.status(200).send({ status: 'OK' }))
|
logger.info(`** DEBUG ** - Cashbox removal - Finished creating the new cashbox batch`)
|
||||||
|
logger.info(`** DEBUG ** - Cashbox removal - Resetting the cashbox counter on device ${req.deviceId}`)
|
||||||
|
return setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||||
|
return res.status(200).send({ status: 'OK' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.catch(next)
|
.catch(next)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const semver = require('semver')
|
const semver = require('semver')
|
||||||
const sms = require('../sms')
|
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const BN = require('../bn')
|
|
||||||
const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz/fp')
|
const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz/fp')
|
||||||
|
const { add, intervalToDuration } = require('date-fns/fp')
|
||||||
|
|
||||||
|
const sms = require('../sms')
|
||||||
|
const BN = require('../bn')
|
||||||
const compliance = require('../compliance')
|
const compliance = require('../compliance')
|
||||||
const complianceTriggers = require('../compliance-triggers')
|
const complianceTriggers = require('../compliance-triggers')
|
||||||
const configManager = require('../new-config-manager')
|
const configManager = require('../new-config-manager')
|
||||||
|
|
@ -18,6 +19,7 @@ const { getTx } = require('../new-admin/services/transactions.js')
|
||||||
const machineLoader = require('../machine-loader')
|
const machineLoader = require('../machine-loader')
|
||||||
const { loadLatestConfig } = require('../new-settings-loader')
|
const { loadLatestConfig } = require('../new-settings-loader')
|
||||||
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
||||||
|
const T = require('../time')
|
||||||
|
|
||||||
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
||||||
if (_.isNil(patch.data)) {
|
if (_.isNil(patch.data)) {
|
||||||
|
|
@ -112,9 +114,9 @@ function triggerSuspend (req, res, next) {
|
||||||
|
|
||||||
const days = triggerId === 'no-ff-camera' ? 1 : getSuspendDays(triggers)
|
const days = triggerId === 'no-ff-camera' ? 1 : getSuspendDays(triggers)
|
||||||
|
|
||||||
const date = new Date()
|
const suspensionDuration = intervalToDuration({ start: 0, end: T.day * days })
|
||||||
date.setDate(date.getDate() + days)
|
|
||||||
customers.update(id, { suspendedUntil: date })
|
customers.update(id, { suspendedUntil: add(suspensionDuration, new Date()) })
|
||||||
.then(customer => {
|
.then(customer => {
|
||||||
notifier.complianceNotify(customer, req.deviceId, 'SUSPENDED', days)
|
notifier.complianceNotify(customer, req.deviceId, 'SUSPENDED', days)
|
||||||
return respond(req, res, { customer })
|
return respond(req, res, { customer })
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
const mem = require('mem')
|
const mem = require('mem')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
@ -9,6 +10,8 @@ const bitpay = require('./plugins/ticker/bitpay')
|
||||||
|
|
||||||
const FETCH_INTERVAL = 58000
|
const FETCH_INTERVAL = 58000
|
||||||
|
|
||||||
|
const PEGGED_FIAT_CURRENCIES = { NAD: 'ZAR' }
|
||||||
|
|
||||||
function _getRates (settings, fiatCode, cryptoCode) {
|
function _getRates (settings, fiatCode, cryptoCode) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|
@ -33,9 +36,12 @@ function _getRates (settings, fiatCode, cryptoCode) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTicker (fiatCode, cryptoCode, tickerName) {
|
function buildTicker (fiatCode, cryptoCode, tickerName) {
|
||||||
if (tickerName === 'bitpay') return bitpay.ticker(fiatCode, cryptoCode)
|
const fiatPeggedEquivalent = _.includes(fiatCode, _.keys(PEGGED_FIAT_CURRENCIES))
|
||||||
if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatCode, cryptoCode)
|
? PEGGED_FIAT_CURRENCIES[fiatCode]
|
||||||
return ccxt.ticker(fiatCode, cryptoCode, tickerName)
|
: fiatCode
|
||||||
|
if (tickerName === 'bitpay') return bitpay.ticker(fiatPeggedEquivalent, cryptoCode)
|
||||||
|
if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatPeggedEquivalent, cryptoCode)
|
||||||
|
return ccxt.ticker(fiatPeggedEquivalent, cryptoCode, tickerName)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRates = mem(_getRates, {
|
const getRates = mem(_getRates, {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
const ph = require('./plugin-helper')
|
const ph = require('./plugin-helper')
|
||||||
const _ = require('lodash/fp')
|
|
||||||
const argv = require('minimist')(process.argv.slice(2))
|
const argv = require('minimist')(process.argv.slice(2))
|
||||||
|
const configManager = require('./new-config-manager')
|
||||||
|
|
||||||
function loadWalletScoring (settings) {
|
function loadWalletScoring (settings, cryptoCode) {
|
||||||
const pluginCode = argv.mockScoring ? 'mock-scoring' : 'ciphertrace'
|
const pluginCode = argv.mockScoring ? 'mock-scoring' : 'ciphertrace'
|
||||||
|
const wallet = cryptoCode ? ph.load(ph.WALLET, configManager.getWalletSettings(cryptoCode, settings.config).wallet) : null
|
||||||
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
|
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
|
||||||
const account = settings.accounts[pluginCode]
|
const account = settings.accounts[pluginCode]
|
||||||
|
|
||||||
return { plugin, account }
|
return { plugin, account, wallet }
|
||||||
}
|
}
|
||||||
|
|
||||||
function rateWallet (settings, cryptoCode, address) {
|
function rateWallet (settings, cryptoCode, address) {
|
||||||
|
|
@ -31,9 +32,9 @@ function isValidWalletScore (settings, score) {
|
||||||
function getTransactionHash (settings, cryptoCode, receivingAddress) {
|
function getTransactionHash (settings, cryptoCode, receivingAddress) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const { plugin, account } = loadWalletScoring(settings)
|
const { plugin, account, wallet } = loadWalletScoring(settings, cryptoCode)
|
||||||
|
|
||||||
return plugin.getTransactionHash(account, cryptoCode, receivingAddress)
|
return plugin.getTransactionHash(account, cryptoCode, receivingAddress, wallet)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,9 +47,19 @@ function getInputAddresses (settings, cryptoCode, txHashes) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isWalletScoringEnabled (settings, cryptoCode) {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
const { plugin, account } = loadWalletScoring(settings)
|
||||||
|
|
||||||
|
return plugin.isWalletScoringEnabled(account, cryptoCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
rateWallet,
|
rateWallet,
|
||||||
isValidWalletScore,
|
isValidWalletScore,
|
||||||
getTransactionHash,
|
getTransactionHash,
|
||||||
getInputAddresses
|
getInputAddresses,
|
||||||
|
isWalletScoringEnabled
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const httpError = require('./route-helpers').httpError
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const { getOpenBatchCryptoValue } = require('./tx-batching')
|
const { getOpenBatchCryptoValue } = require('./tx-batching')
|
||||||
const BN = require('./bn')
|
const BN = require('./bn')
|
||||||
|
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('./constants')
|
||||||
|
|
||||||
const FETCH_INTERVAL = 5000
|
const FETCH_INTERVAL = 5000
|
||||||
const INSUFFICIENT_FUNDS_CODE = 570
|
const INSUFFICIENT_FUNDS_CODE = 570
|
||||||
|
|
@ -169,6 +170,11 @@ function authorizeZeroConf (settings, tx, machineId) {
|
||||||
return Promise.reject(new Error('tx.fiat is undefined!'))
|
return Promise.reject(new Error('tx.fiat is undefined!'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: confirm if this treatment is needed for ERC-20 tokens, once their cash-out transactions are enabled
|
||||||
|
if (tx.cryptoCode === 'ETH') {
|
||||||
|
return Promise.resolve(false)
|
||||||
|
}
|
||||||
|
|
||||||
if (tx.fiat.gt(zeroConfLimit)) {
|
if (tx.fiat.gt(zeroConfLimit)) {
|
||||||
return Promise.resolve(false)
|
return Promise.resolve(false)
|
||||||
}
|
}
|
||||||
|
|
@ -205,9 +211,9 @@ function getStatus (settings, tx, machineId) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function sweep (settings, cryptoCode, hdIndex) {
|
function sweep (settings, txId, cryptoCode, hdIndex) {
|
||||||
return fetchWallet(settings, cryptoCode)
|
return fetchWallet(settings, cryptoCode)
|
||||||
.then(r => r.wallet.sweep(r.account, cryptoCode, hdIndex, settings, r.operatorId))
|
.then(r => r.wallet.sweep(r.account, txId, cryptoCode, hdIndex, settings, r.operatorId))
|
||||||
}
|
}
|
||||||
|
|
||||||
function isHd (settings, tx) {
|
function isHd (settings, tx) {
|
||||||
|
|
@ -255,20 +261,28 @@ function checkBlockchainStatus (settings, cryptoCode) {
|
||||||
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
|
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
|
||||||
}
|
}
|
||||||
|
|
||||||
const coinFilter = ['ETH']
|
|
||||||
|
|
||||||
const balance = (settings, cryptoCode) => {
|
const balance = (settings, cryptoCode) => {
|
||||||
if (_.includes(coinFilter, cryptoCode)) return balanceFiltered(settings, cryptoCode)
|
return fetchWallet(settings, cryptoCode)
|
||||||
return balanceUnfiltered(settings, cryptoCode)
|
.then(r => r.wallet.fetchSpeed ?? BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL)
|
||||||
|
.then(multiplier => {
|
||||||
|
switch (multiplier) {
|
||||||
|
case BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL:
|
||||||
|
return balanceNormal(settings, cryptoCode)
|
||||||
|
case BALANCE_FETCH_SPEED_MULTIPLIER.SLOW:
|
||||||
|
return balanceSlow(settings, cryptoCode)
|
||||||
|
default:
|
||||||
|
throw new Error()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const balanceUnfiltered = mem(_balance, {
|
const balanceNormal = mem(_balance, {
|
||||||
maxAge: FETCH_INTERVAL,
|
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL * FETCH_INTERVAL,
|
||||||
cacheKey: (settings, cryptoCode) => cryptoCode
|
cacheKey: (settings, cryptoCode) => cryptoCode
|
||||||
})
|
})
|
||||||
|
|
||||||
const balanceFiltered = mem(_balance, {
|
const balanceSlow = mem(_balance, {
|
||||||
maxAge: 3 * FETCH_INTERVAL,
|
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW * FETCH_INTERVAL,
|
||||||
cacheKey: (settings, cryptoCode) => cryptoCode
|
cacheKey: (settings, cryptoCode) => cryptoCode
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
20
migrations/1655807727853-default_timezone_fix.js
Normal file
20
migrations/1655807727853-default_timezone_fix.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
const newConfig = {}
|
||||||
|
return loadLatest()
|
||||||
|
.then(config => {
|
||||||
|
if (config.config.locale_timezone === "0:0") {
|
||||||
|
newConfig[`locale_timezone`] = 'GMT'
|
||||||
|
return saveConfig(newConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(next)
|
||||||
|
.catch(err => {
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
22
migrations/1658940716689-remove-coin-specific-cryptounits.js
Normal file
22
migrations/1658940716689-remove-coin-specific-cryptounits.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
const { removeFromConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||||
|
const { getCryptosFromWalletNamespace } = require('../lib/new-config-manager.js')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
loadLatest()
|
||||||
|
.then(settings => {
|
||||||
|
const configuredCryptos = getCryptosFromWalletNamespace(settings.config)
|
||||||
|
if (!configuredCryptos.length) return Promise.resolve()
|
||||||
|
|
||||||
|
return removeFromConfig(_.map(it => `wallets_${it}_cryptoUnits`, configuredCryptos))
|
||||||
|
})
|
||||||
|
.then(() => next())
|
||||||
|
.catch(err => {
|
||||||
|
console.log(err.message)
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
22
migrations/1661125970289-eth-zero-conf-value.js
Normal file
22
migrations/1661125970289-eth-zero-conf-value.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
|
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
const newConfig = {}
|
||||||
|
return loadLatest()
|
||||||
|
.then(config => {
|
||||||
|
if (!_.isNil(config.config.wallets_ETH_zeroConfLimit) && config.config.wallets_ETH_zeroConfLimit !== 0) {
|
||||||
|
newConfig[`wallets_ETH_zeroConfLimit`] = 0
|
||||||
|
return saveConfig(newConfig)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(next)
|
||||||
|
.catch(err => {
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import { P, Label3 } from 'src/components/typography'
|
||||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg'
|
import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg'
|
||||||
import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg'
|
import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg'
|
||||||
import { onlyFirstToUpper } from 'src/utils/string'
|
import { onlyFirstToUpper, singularOrPlural } from 'src/utils/string'
|
||||||
|
|
||||||
import { chipStyles, styles } from './SearchFilter.styles'
|
import { chipStyles, styles } from './SearchFilter.styles'
|
||||||
|
|
||||||
|
|
@ -18,7 +18,7 @@ const SearchFilter = ({
|
||||||
filters,
|
filters,
|
||||||
onFilterDelete,
|
onFilterDelete,
|
||||||
deleteAllFilters,
|
deleteAllFilters,
|
||||||
entries
|
entries = 0
|
||||||
}) => {
|
}) => {
|
||||||
const chipClasses = useChipStyles()
|
const chipClasses = useChipStyles()
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
@ -40,8 +40,11 @@ const SearchFilter = ({
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.deleteWrapper}>
|
<div className={classes.deleteWrapper}>
|
||||||
{
|
{
|
||||||
<Label3 className={classes.entries}>{`${entries ??
|
<Label3 className={classes.entries}>{`${entries} ${singularOrPlural(
|
||||||
0} entries`}</Label3>
|
entries,
|
||||||
|
`entry`,
|
||||||
|
`entries`
|
||||||
|
)}`}</Label3>
|
||||||
}
|
}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { compareAsc, differenceInDays, set } from 'date-fns/fp'
|
import { compareAsc, differenceInDays, set } from 'date-fns/fp'
|
||||||
|
import * as R from 'ramda'
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
|
|
||||||
import Calendar from './Calendar'
|
import Calendar from './Calendar'
|
||||||
|
|
@ -37,7 +38,12 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
||||||
set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day)
|
set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
setTo(from)
|
setTo(
|
||||||
|
set(
|
||||||
|
{ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 },
|
||||||
|
R.clone(from)
|
||||||
|
)
|
||||||
|
)
|
||||||
setFrom(day)
|
setFrom(day)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ const Header = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapElement = (
|
const mapElement = (
|
||||||
{ name, width = DEFAULT_COL_SIZE, header, textAlign },
|
{ name, display, width = DEFAULT_COL_SIZE, header, textAlign },
|
||||||
idx
|
idx
|
||||||
) => {
|
) => {
|
||||||
const orderClasses = classnames({
|
const orderClasses = classnames({
|
||||||
|
|
@ -99,7 +99,7 @@ const Header = () => {
|
||||||
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
||||||
) : (
|
) : (
|
||||||
<span className={orderClasses}>
|
<span className={orderClasses}>
|
||||||
{startCase(name)}{' '}
|
{!R.isNil(display) ? display : startCase(name)}{' '}
|
||||||
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,7 @@ const ERow = ({ editing, disabled, lastOfGroup, newRow }) => {
|
||||||
size={rowSize}
|
size={rowSize}
|
||||||
error={editing && hasErrors}
|
error={editing && hasErrors}
|
||||||
newRow={newRow && !hasErrors}
|
newRow={newRow && !hasErrors}
|
||||||
|
shouldShowError
|
||||||
errorMessage={errorMessage}>
|
errorMessage={errorMessage}>
|
||||||
{innerElements.map((it, idx) => {
|
{innerElements.map((it, idx) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -277,7 +277,7 @@ const Analytics = () => {
|
||||||
case 'topMachines':
|
case 'topMachines':
|
||||||
return (
|
return (
|
||||||
<TopMachinesWrapper
|
<TopMachinesWrapper
|
||||||
title="Transactions over time"
|
title="Top 5 Machines"
|
||||||
representing={representing}
|
representing={representing}
|
||||||
period={period}
|
period={period}
|
||||||
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
|
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ const Graph = ({
|
||||||
const GRAPH_MARGIN = useMemo(
|
const GRAPH_MARGIN = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
top: 25,
|
top: 25,
|
||||||
right: 0.5,
|
right: 3.5,
|
||||||
bottom: 27,
|
bottom: 27,
|
||||||
left: 36.5
|
left: 36.5
|
||||||
}),
|
}),
|
||||||
|
|
@ -158,6 +158,12 @@ const Graph = ({
|
||||||
.domain(periodDomains[period.code])
|
.domain(periodDomains[period.code])
|
||||||
.range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
|
.range([GRAPH_MARGIN.left, GRAPH_WIDTH - GRAPH_MARGIN.right])
|
||||||
|
|
||||||
|
// Create a second X axis for mouseover events to be placed correctly across the entire graph width and not limited by X's domain
|
||||||
|
const x2 = d3
|
||||||
|
.scaleUtc()
|
||||||
|
.domain(periodDomains[period.code])
|
||||||
|
.range([GRAPH_MARGIN.left, GRAPH_WIDTH])
|
||||||
|
|
||||||
const y = d3
|
const y = d3
|
||||||
.scaleLinear()
|
.scaleLinear()
|
||||||
.domain([
|
.domain([
|
||||||
|
|
@ -167,11 +173,11 @@ const Graph = ({
|
||||||
.nice()
|
.nice()
|
||||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||||
|
|
||||||
const getAreaInterval = (breakpoints, limits) => {
|
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||||
const fullBreakpoints = [
|
const fullBreakpoints = [
|
||||||
limits[1],
|
graphLimits[1],
|
||||||
...R.filter(it => it > limits[0] && it < limits[1], breakpoints),
|
...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints),
|
||||||
limits[0]
|
dataLimits[0]
|
||||||
]
|
]
|
||||||
|
|
||||||
const intervals = []
|
const intervals = []
|
||||||
|
|
@ -238,7 +244,7 @@ const Graph = ({
|
||||||
.selectAll('.tick line')
|
.selectAll('.tick line')
|
||||||
.filter(d => d === 0)
|
.filter(d => d === 0)
|
||||||
.clone()
|
.clone()
|
||||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right - GRAPH_MARGIN.left)
|
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.left)
|
||||||
.attr('stroke-width', 1)
|
.attr('stroke-width', 1)
|
||||||
.attr('stroke', primaryColor)
|
.attr('stroke', primaryColor)
|
||||||
),
|
),
|
||||||
|
|
@ -276,7 +282,7 @@ const Graph = ({
|
||||||
.attr('y1', d => 0.5 + y(d))
|
.attr('y1', d => 0.5 + y(d))
|
||||||
.attr('y2', d => 0.5 + y(d))
|
.attr('y2', d => 0.5 + y(d))
|
||||||
.attr('x1', GRAPH_MARGIN.left)
|
.attr('x1', GRAPH_MARGIN.left)
|
||||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
.attr('x2', GRAPH_WIDTH)
|
||||||
)
|
)
|
||||||
// Vertical transparent rectangles for events
|
// Vertical transparent rectangles for events
|
||||||
.call(g =>
|
.call(g =>
|
||||||
|
|
@ -291,7 +297,8 @@ const Graph = ({
|
||||||
const xValue = Math.round(x(d) * 100) / 100
|
const xValue = Math.round(x(d) * 100) / 100
|
||||||
const intervals = getAreaInterval(
|
const intervals = getAreaInterval(
|
||||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||||
x.range()
|
x.range(),
|
||||||
|
x2.range()
|
||||||
)
|
)
|
||||||
const interval = getAreaIntervalByX(intervals, xValue)
|
const interval = getAreaIntervalByX(intervals, xValue)
|
||||||
return Math.round((interval[0] - interval[1]) * 100) / 100
|
return Math.round((interval[0] - interval[1]) * 100) / 100
|
||||||
|
|
@ -307,10 +314,12 @@ const Graph = ({
|
||||||
const areas = buildAreas(x.domain())
|
const areas = buildAreas(x.domain())
|
||||||
const intervals = getAreaInterval(
|
const intervals = getAreaInterval(
|
||||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||||
x.range()
|
x.range(),
|
||||||
|
x2.range()
|
||||||
)
|
)
|
||||||
|
|
||||||
const dateInterval = getDateIntervalByX(areas, intervals, xValue)
|
const dateInterval = getDateIntervalByX(areas, intervals, xValue)
|
||||||
|
if (!dateInterval) return
|
||||||
const filteredData = data.filter(it => {
|
const filteredData = data.filter(it => {
|
||||||
const created = new Date(it.created)
|
const created = new Date(it.created)
|
||||||
const tzCreated = created.setTime(created.getTime() + offset)
|
const tzCreated = created.setTime(created.getTime() + offset)
|
||||||
|
|
@ -426,6 +435,7 @@ const Graph = ({
|
||||||
buildTicks,
|
buildTicks,
|
||||||
getPastAndCurrentDayLabels,
|
getPastAndCurrentDayLabels,
|
||||||
x,
|
x,
|
||||||
|
x2,
|
||||||
y,
|
y,
|
||||||
period,
|
period,
|
||||||
buildAreas,
|
buildAreas,
|
||||||
|
|
@ -482,7 +492,7 @@ const Graph = ({
|
||||||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
||||||
)
|
)
|
||||||
.attr('x1', GRAPH_MARGIN.left)
|
.attr('x1', GRAPH_MARGIN.left)
|
||||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
.attr('x2', GRAPH_WIDTH)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[GRAPH_MARGIN, y, data]
|
[GRAPH_MARGIN, y, data]
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ const BlackListModal = ({
|
||||||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
||||||
|
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
|
||||||
XMR:
|
XMR:
|
||||||
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,13 +74,13 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveOverridesFromList = it => (_, override) => {
|
const saveOverridesFromList = it => (_, override) => {
|
||||||
const cryptoOverriden = R.path(['cryptoCurrencies', 0], override)
|
const cryptoOverridden = R.path(['cryptoCurrencies', 0], override)
|
||||||
|
|
||||||
const sameMachine = R.eqProps('machine', override)
|
const sameMachine = R.eqProps('machine', override)
|
||||||
const notSameOverride = it => !R.eqProps('cryptoCurrencies', override, it)
|
const notSameOverride = it => !R.eqProps('cryptoCurrencies', override, it)
|
||||||
|
|
||||||
const filterMachine = R.filter(R.both(sameMachine, notSameOverride))
|
const filterMachine = R.filter(R.both(sameMachine, notSameOverride))
|
||||||
const removeCoin = removeCoinFromOverride(cryptoOverriden)
|
const removeCoin = removeCoinFromOverride(cryptoOverridden)
|
||||||
|
|
||||||
const machineOverrides = R.map(removeCoin)(filterMachine(it))
|
const machineOverrides = R.map(removeCoin)(filterMachine(it))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -318,7 +318,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
||||||
'deviceId'
|
'deviceId'
|
||||||
)(machine)
|
)(machine)
|
||||||
|
|
||||||
const message = `${codes} already overriden for machine: ${machineView}`
|
const message = `${codes} already overridden for machine: ${machineView}`
|
||||||
|
|
||||||
return this.createError({ message })
|
return this.createError({ message })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
import CustomersList from './CustomersList'
|
import CustomersList from './CustomersList'
|
||||||
import CreateCustomerModal from './components/CreateCustomerModal'
|
import CreateCustomerModal from './components/CreateCustomerModal'
|
||||||
|
import { getAuthorizedStatus } from './helper'
|
||||||
|
|
||||||
const GET_CUSTOMER_FILTERS = gql`
|
const GET_CUSTOMER_FILTERS = gql`
|
||||||
query filters {
|
query filters {
|
||||||
|
|
@ -130,9 +131,20 @@ const Customers = () => {
|
||||||
R.path(['customInfoRequests'], customersResponse) ?? []
|
R.path(['customInfoRequests'], customersResponse) ?? []
|
||||||
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
|
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
|
||||||
const triggers = configData && fromNamespace(namespaces.TRIGGERS, configData)
|
const triggers = configData && fromNamespace(namespaces.TRIGGERS, configData)
|
||||||
const customersData = R.sortWith([
|
|
||||||
R.descend(it => new Date(R.prop('lastActive', it) ?? '0'))
|
const setAuthorizedStatus = c =>
|
||||||
])(filteredCustomers ?? [])
|
R.assoc(
|
||||||
|
'authorizedStatus',
|
||||||
|
getAuthorizedStatus(c, triggers, customRequirementsData),
|
||||||
|
c
|
||||||
|
)
|
||||||
|
|
||||||
|
const byAuthorized = c => (c.authorizedStatus.label === 'Pending' ? 0 : 1)
|
||||||
|
const byLastActive = c => new Date(R.prop('lastActive', c) ?? '0')
|
||||||
|
const customersData = R.pipe(
|
||||||
|
R.map(setAuthorizedStatus),
|
||||||
|
R.sortWith([R.ascend(byAuthorized), R.descend(byLastActive)])
|
||||||
|
)(filteredCustomers ?? [])
|
||||||
|
|
||||||
const onFilterChange = filters => {
|
const onFilterChange = filters => {
|
||||||
const filtersObject = getFiltersObj(filters)
|
const filtersObject = getFiltersObj(filters)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.
|
||||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||||
|
|
||||||
import styles from './CustomersList.styles'
|
import styles from './CustomersList.styles'
|
||||||
import { getAuthorizedStatus, getFormattedPhone, getName } from './helper'
|
import { getFormattedPhone, getName } from './helper'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
|
@ -73,11 +73,7 @@ const CustomersList = ({
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
width: 191,
|
width: 191,
|
||||||
view: it => (
|
view: it => <MainStatus statuses={[it.authorizedStatus]} />
|
||||||
<MainStatus
|
|
||||||
statuses={[getAuthorizedStatus(it, triggers, customRequests)]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
||||||
const GRAPH_MARGIN = useMemo(
|
const GRAPH_MARGIN = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
top: 20,
|
top: 20,
|
||||||
right: 0.5,
|
right: 3.5,
|
||||||
bottom: 27,
|
bottom: 27,
|
||||||
left: 33.5
|
left: 33.5
|
||||||
}),
|
}),
|
||||||
|
|
@ -211,7 +211,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
||||||
.attr('y1', d => 0.5 + y(d))
|
.attr('y1', d => 0.5 + y(d))
|
||||||
.attr('y2', d => 0.5 + y(d))
|
.attr('y2', d => 0.5 + y(d))
|
||||||
.attr('x1', GRAPH_MARGIN.left)
|
.attr('x1', GRAPH_MARGIN.left)
|
||||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
.attr('x2', GRAPH_WIDTH)
|
||||||
)
|
)
|
||||||
// Thick vertical lines
|
// Thick vertical lines
|
||||||
.call(g =>
|
.call(g =>
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ const allFields = (getData, onChange, auxElements = []) => {
|
||||||
return R.compose(R.join(', '), R.map(getView(data, 'code')))(it)
|
return R.compose(R.join(', '), R.map(getView(data, 'code')))(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
const overridenMachines = R.map(override => override.machine, auxElements)
|
const overriddenMachines = R.map(override => override.machine, auxElements)
|
||||||
|
|
||||||
const suggestionFilter = it =>
|
const suggestionFilter = it =>
|
||||||
R.differenceWith((x, y) => x.deviceId === y, it, overridenMachines)
|
R.differenceWith((x, y) => x.deviceId === y, it, overriddenMachines)
|
||||||
|
|
||||||
const machineData = getData(['machines'])
|
const machineData = getData(['machines'])
|
||||||
const countryData = getData(['countries'])
|
const countryData = getData(['countries'])
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
customerId
|
customerId
|
||||||
isAnonymous
|
isAnonymous
|
||||||
rawTickerPrice
|
rawTickerPrice
|
||||||
|
profit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,11 @@ const MachineRoute = () => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
},
|
},
|
||||||
variables: {
|
variables: {
|
||||||
deviceId: id
|
|
||||||
},
|
|
||||||
billFilters: {
|
|
||||||
deviceId: id,
|
deviceId: id,
|
||||||
batch: 'none'
|
billFilters: {
|
||||||
|
deviceId: id,
|
||||||
|
batch: 'none'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import WizardSplash from './WizardSplash'
|
||||||
import WizardStep from './WizardStep'
|
import WizardStep from './WizardStep'
|
||||||
|
|
||||||
const MODAL_WIDTH = 554
|
const MODAL_WIDTH = 554
|
||||||
const MODAL_HEIGHT = 520
|
const MODAL_HEIGHT = 535
|
||||||
const CASHBOX_DEFAULT_CAPACITY = 500
|
const CASHBOX_DEFAULT_CAPACITY = 500
|
||||||
|
|
||||||
const CASSETTE_FIELDS = R.map(
|
const CASSETTE_FIELDS = R.map(
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Formik, Form, Field } from 'formik'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
import Stepper from 'src/components/Stepper'
|
import Stepper from 'src/components/Stepper'
|
||||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||||
import { Button } from 'src/components/buttons'
|
import { Button } from 'src/components/buttons'
|
||||||
|
|
@ -94,6 +95,10 @@ const styles = {
|
||||||
},
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
color: errorColor
|
color: errorColor
|
||||||
|
},
|
||||||
|
stepErrorMessage: {
|
||||||
|
maxWidth: 275,
|
||||||
|
marginTop: 25
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,6 +289,11 @@ const WizardStep = ({
|
||||||
= {numberToFiatAmount(cassetteTotal(values))}{' '}
|
= {numberToFiatAmount(cassetteTotal(values))}{' '}
|
||||||
{fiatCurrency}
|
{fiatCurrency}
|
||||||
</P>
|
</P>
|
||||||
|
{!R.isEmpty(errors) && (
|
||||||
|
<ErrorMessage className={classes.stepErrorMessage}>
|
||||||
|
{R.head(R.values(errors))}
|
||||||
|
</ErrorMessage>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,9 @@ const CryptoBalanceOverrides = ({ section }) => {
|
||||||
return save(newOverrides)
|
return save(newOverrides)
|
||||||
}
|
}
|
||||||
|
|
||||||
const overridenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(setupValues)
|
||||||
const suggestionFilter = R.filter(
|
const suggestionFilter = R.filter(
|
||||||
it => !R.contains(it.code, overridenCryptos)
|
it => !R.contains(it.code, overriddenCryptos)
|
||||||
)
|
)
|
||||||
const suggestions = suggestionFilter(cryptoCurrencies)
|
const suggestions = suggestionFilter(cryptoCurrencies)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ import styles from './FiatBalanceAlerts.styles.js'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const NAME = 'fiatBalanceAlerts'
|
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
||||||
|
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
||||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
const notesMin = 0
|
||||||
|
const notesMax = 9999999
|
||||||
|
|
||||||
const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -36,9 +39,13 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
DEFAULT_NUMBER_OF_CASSETTES
|
DEFAULT_NUMBER_OF_CASSETTES
|
||||||
)
|
)
|
||||||
|
|
||||||
const editing = isEditing(NAME)
|
|
||||||
|
|
||||||
const schema = Yup.object().shape({
|
const schema = Yup.object().shape({
|
||||||
|
cashInAlertThreshold: Yup.number()
|
||||||
|
.transform(transformNumber)
|
||||||
|
.integer()
|
||||||
|
.min(notesMin)
|
||||||
|
.max(notesMax)
|
||||||
|
.nullable(),
|
||||||
fillingPercentageCassette1: Yup.number()
|
fillingPercentageCassette1: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
.integer()
|
.integer()
|
||||||
|
|
@ -71,6 +78,7 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
validateOnChange={false}
|
validateOnChange={false}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
initialValues={{
|
initialValues={{
|
||||||
|
cashInAlertThreshold: data?.cashInAlertThreshold ?? '',
|
||||||
fillingPercentageCassette1: data?.fillingPercentageCassette1 ?? '',
|
fillingPercentageCassette1: data?.fillingPercentageCassette1 ?? '',
|
||||||
fillingPercentageCassette2: data?.fillingPercentageCassette2 ?? '',
|
fillingPercentageCassette2: data?.fillingPercentageCassette2 ?? '',
|
||||||
fillingPercentageCassette3: data?.fillingPercentageCassette3 ?? '',
|
fillingPercentageCassette3: data?.fillingPercentageCassette3 ?? '',
|
||||||
|
|
@ -79,52 +87,80 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||||
validationSchema={schema}
|
validationSchema={schema}
|
||||||
onSubmit={it => save(section, schema.cast(it))}
|
onSubmit={it => save(section, schema.cast(it))}
|
||||||
onReset={() => {
|
onReset={() => {
|
||||||
setEditing(NAME, false)
|
setEditing(CASH_IN_KEY, false)
|
||||||
|
setEditing(CASH_OUT_KEY, false)
|
||||||
}}>
|
}}>
|
||||||
{({ values }) => (
|
{({ values }) => (
|
||||||
<Form className={classes.form}>
|
<>
|
||||||
<PromptWhenDirty />
|
<Form className={classes.form}>
|
||||||
<Header
|
<PromptWhenDirty />
|
||||||
title="Cash out (Empty)"
|
<Header
|
||||||
editing={editing}
|
title="Cash box"
|
||||||
disabled={isDisabled(NAME)}
|
editing={isEditing(CASH_IN_KEY)}
|
||||||
setEditing={it => setEditing(NAME, it)}
|
disabled={isDisabled(CASH_IN_KEY)}
|
||||||
/>
|
setEditing={it => setEditing(CASH_IN_KEY, it)}
|
||||||
<div className={classes.wrapper}>
|
/>
|
||||||
{R.map(
|
<div className={classes.wrapper}>
|
||||||
it => (
|
<div className={classes.first}>
|
||||||
<>
|
<div className={classes.row}>
|
||||||
<div className={classes.row}>
|
<div className={classes.col2}>
|
||||||
<Cashbox
|
<EditableNumber
|
||||||
labelClassName={classes.cashboxLabel}
|
label="Alert me over"
|
||||||
emptyPartClassName={classes.cashboxEmptyPart}
|
name="cashInAlertThreshold"
|
||||||
percent={
|
editing={isEditing(CASH_IN_KEY)}
|
||||||
values[`fillingPercentageCassette${it + 1}`] ??
|
displayValue={x => (x === '' ? '-' : x)}
|
||||||
data[`cassette${it + 1}`]
|
decoration="notes"
|
||||||
}
|
width={fieldWidth}
|
||||||
applyColorVariant
|
|
||||||
applyFiatBalanceAlertsStyling
|
|
||||||
omitInnerPercentage
|
|
||||||
cashOut
|
|
||||||
/>
|
/>
|
||||||
<div className={classes.col2}>
|
|
||||||
<TL2 className={classes.title}>Cassette {it + 1}</TL2>
|
|
||||||
<EditableNumber
|
|
||||||
label="Alert me under"
|
|
||||||
name={`fillingPercentageCassette${it + 1}`}
|
|
||||||
editing={editing}
|
|
||||||
displayValue={x => (x === '' ? '-' : x)}
|
|
||||||
decoration="%"
|
|
||||||
width={fieldWidth}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
),
|
</div>
|
||||||
R.times(R.identity, maxNumberOfCassettes)
|
</div>
|
||||||
)}
|
</Form>
|
||||||
</div>
|
<Form className={classes.form}>
|
||||||
</Form>
|
<PromptWhenDirty />
|
||||||
|
<Header
|
||||||
|
title="Cash out (Empty)"
|
||||||
|
editing={isEditing(CASH_OUT_KEY)}
|
||||||
|
disabled={isDisabled(CASH_OUT_KEY)}
|
||||||
|
setEditing={it => setEditing(CASH_OUT_KEY, it)}
|
||||||
|
/>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
{R.map(
|
||||||
|
it => (
|
||||||
|
<>
|
||||||
|
<div className={classes.row}>
|
||||||
|
<Cashbox
|
||||||
|
labelClassName={classes.cashboxLabel}
|
||||||
|
emptyPartClassName={classes.cashboxEmptyPart}
|
||||||
|
percent={
|
||||||
|
values[`fillingPercentageCassette${it + 1}`] ??
|
||||||
|
data[`cassette${it + 1}`]
|
||||||
|
}
|
||||||
|
applyColorVariant
|
||||||
|
applyFiatBalanceAlertsStyling
|
||||||
|
omitInnerPercentage
|
||||||
|
cashOut
|
||||||
|
/>
|
||||||
|
<div className={classes.col2}>
|
||||||
|
<TL2 className={classes.title}>Cassette {it + 1}</TL2>
|
||||||
|
<EditableNumber
|
||||||
|
label="Alert me under"
|
||||||
|
name={`fillingPercentageCassette${it + 1}`}
|
||||||
|
editing={isEditing(CASH_OUT_KEY)}
|
||||||
|
displayValue={x => (x === '' ? '-' : x)}
|
||||||
|
decoration="%"
|
||||||
|
width={fieldWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
R.times(R.identity, maxNumberOfCassettes)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,14 @@ import { transformNumber } from 'src/utils/number'
|
||||||
|
|
||||||
import NotificationsCtx from '../NotificationsContext'
|
import NotificationsCtx from '../NotificationsContext'
|
||||||
|
|
||||||
|
const CASHBOX_KEY = 'cashbox'
|
||||||
const CASSETTE_1_KEY = 'fillingPercentageCassette1'
|
const CASSETTE_1_KEY = 'fillingPercentageCassette1'
|
||||||
const CASSETTE_2_KEY = 'fillingPercentageCassette2'
|
const CASSETTE_2_KEY = 'fillingPercentageCassette2'
|
||||||
const CASSETTE_3_KEY = 'fillingPercentageCassette3'
|
const CASSETTE_3_KEY = 'fillingPercentageCassette3'
|
||||||
const CASSETTE_4_KEY = 'fillingPercentageCassette4'
|
const CASSETTE_4_KEY = 'fillingPercentageCassette4'
|
||||||
const MACHINE_KEY = 'machine'
|
const MACHINE_KEY = 'machine'
|
||||||
const NAME = 'fiatBalanceOverrides'
|
const NAME = 'fiatBalanceOverrides'
|
||||||
|
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||||
|
|
||||||
const CASSETTE_LIST = [
|
const CASSETTE_LIST = [
|
||||||
CASSETTE_1_KEY,
|
CASSETTE_1_KEY,
|
||||||
|
|
@ -25,9 +27,9 @@ const CASSETTE_LIST = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const widthsByNumberOfCassettes = {
|
const widthsByNumberOfCassettes = {
|
||||||
2: { machine: 230, cassette: 250 },
|
2: { machine: 230, cashbox: 150, cassette: 250 },
|
||||||
3: { machine: 216, cassette: 270 },
|
3: { machine: 216, cashbox: 150, cassette: 270 },
|
||||||
4: { machine: 210, cassette: 204 }
|
4: { machine: 210, cashbox: 150, cassette: 204 }
|
||||||
}
|
}
|
||||||
|
|
||||||
const FiatBalanceOverrides = ({ config, section }) => {
|
const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
|
|
@ -42,33 +44,35 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
|
|
||||||
const setupValues = data?.fiatBalanceOverrides ?? []
|
const setupValues = data?.fiatBalanceOverrides ?? []
|
||||||
const innerSetEditing = it => setEditing(NAME, it)
|
const innerSetEditing = it => setEditing(NAME, it)
|
||||||
|
|
||||||
const cashoutConfig = it => fromNamespace(it)(config)
|
const cashoutConfig = it => fromNamespace(it)(config)
|
||||||
|
|
||||||
const overridenMachines = R.map(override => override.machine, setupValues)
|
const overriddenMachines = R.map(override => override.machine, setupValues)
|
||||||
const suggestionFilter = R.filter(
|
const suggestions = R.differenceWith(
|
||||||
it =>
|
(it, m) => it.deviceId === m,
|
||||||
!R.includes(it.deviceId, overridenMachines) &&
|
machines,
|
||||||
cashoutConfig(it.deviceId).active
|
overriddenMachines
|
||||||
)
|
)
|
||||||
const suggestions = suggestionFilter(machines)
|
|
||||||
|
|
||||||
const findSuggestion = it => {
|
const findSuggestion = it => {
|
||||||
const coin = R.compose(R.find(R.propEq('deviceId', it?.machine)))(machines)
|
const coin = R.find(R.propEq('deviceId', it?.machine), machines)
|
||||||
return coin ? [coin] : []
|
return coin ? [coin] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
[MACHINE_KEY]: null,
|
[MACHINE_KEY]: null,
|
||||||
|
[CASHBOX_KEY]: '',
|
||||||
[CASSETTE_1_KEY]: '',
|
[CASSETTE_1_KEY]: '',
|
||||||
[CASSETTE_2_KEY]: '',
|
[CASSETTE_2_KEY]: '',
|
||||||
[CASSETTE_3_KEY]: '',
|
[CASSETTE_3_KEY]: '',
|
||||||
[CASSETTE_4_KEY]: ''
|
[CASSETTE_4_KEY]: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const notesMin = 0
|
||||||
|
const notesMax = 9999999
|
||||||
|
|
||||||
const maxNumberOfCassettes = Math.max(
|
const maxNumberOfCassettes = Math.max(
|
||||||
...R.map(it => it.numberOfCassettes, machines),
|
...R.map(it => it.numberOfCassettes, machines),
|
||||||
2
|
DEFAULT_NUMBER_OF_CASSETTES
|
||||||
)
|
)
|
||||||
|
|
||||||
const percentMin = 0
|
const percentMin = 0
|
||||||
|
|
@ -77,8 +81,14 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
.shape({
|
.shape({
|
||||||
[MACHINE_KEY]: Yup.string()
|
[MACHINE_KEY]: Yup.string()
|
||||||
.label('Machine')
|
.label('Machine')
|
||||||
.nullable()
|
|
||||||
.required(),
|
.required(),
|
||||||
|
[CASHBOX_KEY]: Yup.number()
|
||||||
|
.label('Cash box')
|
||||||
|
.transform(transformNumber)
|
||||||
|
.integer()
|
||||||
|
.min(notesMin)
|
||||||
|
.max(notesMax)
|
||||||
|
.nullable(),
|
||||||
[CASSETTE_1_KEY]: Yup.number()
|
[CASSETTE_1_KEY]: Yup.number()
|
||||||
.label('Cassette 1')
|
.label('Cassette 1')
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
@ -108,39 +118,49 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
.max(percentMax)
|
.max(percentMax)
|
||||||
.nullable()
|
.nullable()
|
||||||
})
|
})
|
||||||
.test((values, context) => {
|
.test((values, context) =>
|
||||||
const picked = R.pick(CASSETTE_LIST, values)
|
R.any(key => !R.isNil(values[key]), R.prepend(CASHBOX_KEY, CASSETTE_LIST))
|
||||||
|
? undefined
|
||||||
if (CASSETTE_LIST.some(it => !R.isNil(picked[it]))) return
|
: context.createError({
|
||||||
|
path: CASHBOX_KEY,
|
||||||
return context.createError({
|
message:
|
||||||
path: CASSETTE_1_KEY,
|
'The cash box or at least one of the cassettes must have a value'
|
||||||
message: 'At least one of the cassettes must have a value'
|
})
|
||||||
})
|
)
|
||||||
})
|
|
||||||
|
|
||||||
const viewMachine = it =>
|
const viewMachine = it =>
|
||||||
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
|
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
|
||||||
|
|
||||||
const elements = [
|
const elements = R.concat(
|
||||||
{
|
[
|
||||||
name: MACHINE_KEY,
|
{
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine,
|
name: MACHINE_KEY,
|
||||||
size: 'sm',
|
display: 'Machine',
|
||||||
view: viewMachine,
|
width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine,
|
||||||
input: Autocomplete,
|
size: 'sm',
|
||||||
inputProps: {
|
view: viewMachine,
|
||||||
options: it => R.concat(suggestions, findSuggestion(it)),
|
input: Autocomplete,
|
||||||
valueProp: 'deviceId',
|
inputProps: {
|
||||||
labelProp: 'name'
|
options: it => R.concat(suggestions, findSuggestion(it)),
|
||||||
|
valueProp: 'deviceId',
|
||||||
|
labelProp: 'name'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: CASHBOX_KEY,
|
||||||
|
display: 'Cash box',
|
||||||
|
width: widthsByNumberOfCassettes[maxNumberOfCassettes].cashbox,
|
||||||
|
textAlign: 'right',
|
||||||
|
bold: true,
|
||||||
|
input: NumberInput,
|
||||||
|
suffix: 'notes',
|
||||||
|
inputProps: {
|
||||||
|
decimalPlaces: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
]
|
R.map(
|
||||||
|
it => ({
|
||||||
R.until(
|
|
||||||
R.gt(R.__, maxNumberOfCassettes),
|
|
||||||
it => {
|
|
||||||
elements.push({
|
|
||||||
name: `fillingPercentageCassette${it}`,
|
name: `fillingPercentageCassette${it}`,
|
||||||
display: `Cash cassette ${it}`,
|
display: `Cash cassette ${it}`,
|
||||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette,
|
width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette,
|
||||||
|
|
@ -152,15 +172,18 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
||||||
inputProps: {
|
inputProps: {
|
||||||
decimalPlaces: 0
|
decimalPlaces: 0
|
||||||
},
|
},
|
||||||
view: it => it?.toString() ?? '—',
|
view: el => el?.toString() ?? '—',
|
||||||
isHidden: value =>
|
isHidden: value =>
|
||||||
|
!cashoutConfig(value.machine).active ||
|
||||||
it >
|
it >
|
||||||
machines.find(({ deviceId }) => deviceId === value.machine)
|
R.defaultTo(
|
||||||
?.numberOfCassettes
|
0,
|
||||||
})
|
machines.find(({ deviceId }) => deviceId === value.machine)
|
||||||
return R.add(1, it)
|
?.numberOfCassettes
|
||||||
},
|
)
|
||||||
1
|
}),
|
||||||
|
R.range(1, maxNumberOfCassettes + 1)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -153,16 +153,18 @@ const TermsConditions = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
title: Yup.string()
|
title: Yup.string('The screen title must be a string')
|
||||||
.required()
|
.required('The screen title is required')
|
||||||
.max(50, 'Too long'),
|
.max(50, 'Too long'),
|
||||||
text: Yup.string().required(),
|
text: Yup.string('The text content must be a string').required(
|
||||||
acceptButtonText: Yup.string()
|
'The text content is required'
|
||||||
.required()
|
),
|
||||||
.max(50, 'Too long'),
|
acceptButtonText: Yup.string('The accept button text must be a string')
|
||||||
cancelButtonText: Yup.string()
|
.required('The accept button text is required')
|
||||||
.required()
|
.max(50, 'The accept button text is too long'),
|
||||||
.max(50, 'Too long')
|
cancelButtonText: Yup.string('The cancel button text must be a string')
|
||||||
|
.required('The cancel button text is required')
|
||||||
|
.max(50, 'The cancel button text is too long')
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -236,37 +238,42 @@ const TermsConditions = () => {
|
||||||
setEditing(false)
|
setEditing(false)
|
||||||
setError(null)
|
setError(null)
|
||||||
}}>
|
}}>
|
||||||
<Form>
|
{({ errors }) => (
|
||||||
<PromptWhenDirty />
|
<Form>
|
||||||
{fields.map((f, idx) => (
|
<PromptWhenDirty />
|
||||||
<div className={classes.row} key={idx}>
|
{fields.map((f, idx) => (
|
||||||
<Field
|
<div className={classes.row} key={idx}>
|
||||||
editing={editing}
|
<Field
|
||||||
name={f.name}
|
editing={editing}
|
||||||
width={f.width}
|
name={f.name}
|
||||||
placeholder={f.placeholder}
|
width={f.width}
|
||||||
label={f.label}
|
placeholder={f.placeholder}
|
||||||
value={f.value}
|
label={f.label}
|
||||||
multiline={f.multiline}
|
value={f.value}
|
||||||
rows={f.rows}
|
multiline={f.multiline}
|
||||||
onFocus={() => setError(null)}
|
rows={f.rows}
|
||||||
/>
|
onFocus={() => setError(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className={classnames(classes.row, classes.submit)}>
|
||||||
|
{editing && (
|
||||||
|
<>
|
||||||
|
<Link color="primary" type="submit">
|
||||||
|
Save
|
||||||
|
</Link>
|
||||||
|
<Link color="secondary" type="reset">
|
||||||
|
Cancel
|
||||||
|
</Link>
|
||||||
|
{!R.isEmpty(errors) && (
|
||||||
|
<ErrorMessage>{R.head(R.values(errors))}</ErrorMessage>
|
||||||
|
)}
|
||||||
|
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
</Form>
|
||||||
<div className={classnames(classes.row, classes.submit)}>
|
)}
|
||||||
{editing && (
|
|
||||||
<>
|
|
||||||
<Link color="primary" type="submit">
|
|
||||||
Save
|
|
||||||
</Link>
|
|
||||||
<Link color="secondary" type="reset">
|
|
||||||
Cancel
|
|
||||||
</Link>
|
|
||||||
{error && <ErrorMessage>Failed to save changes</ErrorMessage>}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
</Formik>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import CheckboxInput from 'src/components/inputs/formik/Checkbox'
|
import { Checkbox, TextInput, NumberInput } from 'src/components/inputs/formik'
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
code: 'blockcypher',
|
code: 'blockcypher',
|
||||||
|
|
@ -11,19 +10,19 @@ export default {
|
||||||
{
|
{
|
||||||
code: 'token',
|
code: 'token',
|
||||||
display: 'API Token',
|
display: 'API Token',
|
||||||
component: TextInputFormik,
|
component: TextInput,
|
||||||
face: true,
|
face: true,
|
||||||
long: true
|
long: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'confidenceFactor',
|
code: 'confidenceFactor',
|
||||||
display: 'Confidence Factor',
|
display: 'Confidence Factor',
|
||||||
component: TextInputFormik,
|
component: NumberInput,
|
||||||
face: true
|
face: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'rbf',
|
code: 'rbf',
|
||||||
component: CheckboxInput,
|
component: Checkbox,
|
||||||
settings: {
|
settings: {
|
||||||
field: 'wallets_BTC_wallet',
|
field: 'wallets_BTC_wallet',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -43,7 +42,8 @@ export default {
|
||||||
.required('The token is required'),
|
.required('The token is required'),
|
||||||
confidenceFactor: Yup.number('The confidence factor must be a number')
|
confidenceFactor: Yup.number('The confidence factor must be a number')
|
||||||
.integer('The confidence factor must be an integer')
|
.integer('The confidence factor must be an integer')
|
||||||
.positive('The confidence factor must be positive')
|
.min(0, 'The confidence factor must be between 0 and 100')
|
||||||
|
.max(100, 'The confidence factor must be between 0 and 100')
|
||||||
.required('The confidence factor is required')
|
.required('The confidence factor is required')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
offErrorColor
|
offErrorColor
|
||||||
} from 'src/styling/variables'
|
} from 'src/styling/variables'
|
||||||
import { URI } from 'src/utils/apollo'
|
import { URI } from 'src/utils/apollo'
|
||||||
|
import { SWEEPABLE_CRYPTOS } from 'src/utils/constants'
|
||||||
import * as Customer from 'src/utils/customer'
|
import * as Customer from 'src/utils/customer'
|
||||||
|
|
||||||
import CopyToClipboard from './CopyToClipboard'
|
import CopyToClipboard from './CopyToClipboard'
|
||||||
|
|
@ -88,24 +89,6 @@ const CANCEL_CASH_IN_TRANSACTION = gql`
|
||||||
const getCryptoAmount = tx =>
|
const getCryptoAmount = tx =>
|
||||||
coinUtils.toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toNumber()
|
coinUtils.toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toNumber()
|
||||||
|
|
||||||
/* Port of getProfit() from lib/new-admin/services/transactions.js */
|
|
||||||
const getCommission = tx => {
|
|
||||||
const calcCashInProfit = (fiat, crypto, tickerPrice, fee) =>
|
|
||||||
fiat - crypto * tickerPrice + fee
|
|
||||||
const calcCashOutProfit = (fiat, crypto, tickerPrice) =>
|
|
||||||
crypto * tickerPrice - fiat
|
|
||||||
|
|
||||||
const fiat = Number.parseFloat(tx.fiat)
|
|
||||||
const crypto = getCryptoAmount(tx)
|
|
||||||
const tickerPrice = Number.parseFloat(tx.rawTickerPrice)
|
|
||||||
const isCashIn = tx.txClass === 'cashIn'
|
|
||||||
const cashInFee = isCashIn ? Number.parseFloat(tx.cashInFee) : 0
|
|
||||||
|
|
||||||
return isCashIn
|
|
||||||
? calcCashInProfit(fiat, crypto, tickerPrice, cashInFee)
|
|
||||||
: calcCashOutProfit(fiat, crypto, tickerPrice)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatAddress = (cryptoCode = '', address = '') =>
|
const formatAddress = (cryptoCode = '', address = '') =>
|
||||||
coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
|
coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
|
||||||
|
|
||||||
|
|
@ -124,7 +107,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
const zip = new JSZip()
|
const zip = new JSZip()
|
||||||
|
|
||||||
const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
|
const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
|
||||||
onCompleted: data => createCsv(data)
|
onCompleted: data => createCsv(R.filter(it => !R.isEmpty(it), data))
|
||||||
})
|
})
|
||||||
|
|
||||||
const [cancelTransaction] = useMutation(
|
const [cancelTransaction] = useMutation(
|
||||||
|
|
@ -136,7 +119,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const commission = BigNumber(getCommission(tx))
|
const commission = BigNumber(tx.profit)
|
||||||
.abs()
|
.abs()
|
||||||
.toFixed(2, 1) // ROUND_DOWN
|
.toFixed(2, 1) // ROUND_DOWN
|
||||||
const commissionPercentage =
|
const commissionPercentage =
|
||||||
|
|
@ -407,6 +390,14 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{!R.isNil(tx.swept) && R.includes(tx.cryptoCode, SWEEPABLE_CRYPTOS) && (
|
||||||
|
<div className={classes.swept}>
|
||||||
|
<Label>Sweep status</Label>
|
||||||
|
<span className={classes.bold}>
|
||||||
|
{tx.swept ? `Swept` : `Unswept`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Label>Other actions</Label>
|
<Label>Other actions</Label>
|
||||||
<div className={classes.otherActionsGroup}>
|
<div className={classes.otherActionsGroup}>
|
||||||
|
|
|
||||||
|
|
@ -131,5 +131,8 @@ export default {
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
color: tomato
|
color: tomato
|
||||||
|
},
|
||||||
|
swept: {
|
||||||
|
width: 250
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
$cryptoCode: String
|
$cryptoCode: String
|
||||||
$toAddress: String
|
$toAddress: String
|
||||||
$status: String
|
$status: String
|
||||||
|
$swept: Boolean
|
||||||
) {
|
) {
|
||||||
transactions(
|
transactions(
|
||||||
limit: $limit
|
limit: $limit
|
||||||
|
|
@ -87,6 +88,7 @@ const GET_TRANSACTIONS = gql`
|
||||||
cryptoCode: $cryptoCode
|
cryptoCode: $cryptoCode
|
||||||
toAddress: $toAddress
|
toAddress: $toAddress
|
||||||
status: $status
|
status: $status
|
||||||
|
swept: $swept
|
||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
txClass
|
txClass
|
||||||
|
|
@ -121,6 +123,8 @@ const GET_TRANSACTIONS = gql`
|
||||||
rawTickerPrice
|
rawTickerPrice
|
||||||
batchError
|
batchError
|
||||||
walletScore
|
walletScore
|
||||||
|
profit
|
||||||
|
swept
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
@ -246,7 +250,8 @@ const Transactions = () => {
|
||||||
fiatCode: filtersObject.fiat,
|
fiatCode: filtersObject.fiat,
|
||||||
cryptoCode: filtersObject.crypto,
|
cryptoCode: filtersObject.crypto,
|
||||||
toAddress: filtersObject.address,
|
toAddress: filtersObject.address,
|
||||||
status: filtersObject.status
|
status: filtersObject.status,
|
||||||
|
swept: filtersObject.swept === 'Swept'
|
||||||
})
|
})
|
||||||
|
|
||||||
refetch && refetch()
|
refetch && refetch()
|
||||||
|
|
@ -269,7 +274,8 @@ const Transactions = () => {
|
||||||
fiatCode: filtersObject.fiat,
|
fiatCode: filtersObject.fiat,
|
||||||
cryptoCode: filtersObject.crypto,
|
cryptoCode: filtersObject.crypto,
|
||||||
toAddress: filtersObject.address,
|
toAddress: filtersObject.address,
|
||||||
status: filtersObject.status
|
status: filtersObject.status,
|
||||||
|
swept: filtersObject.swept === 'Swept'
|
||||||
})
|
})
|
||||||
|
|
||||||
refetch && refetch()
|
refetch && refetch()
|
||||||
|
|
@ -287,7 +293,8 @@ const Transactions = () => {
|
||||||
fiatCode: filtersObject.fiat,
|
fiatCode: filtersObject.fiat,
|
||||||
cryptoCode: filtersObject.crypto,
|
cryptoCode: filtersObject.crypto,
|
||||||
toAddress: filtersObject.address,
|
toAddress: filtersObject.address,
|
||||||
status: filtersObject.status
|
status: filtersObject.status,
|
||||||
|
swept: filtersObject.swept === 'Swept'
|
||||||
})
|
})
|
||||||
|
|
||||||
refetch && refetch()
|
refetch && refetch()
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,12 @@ const Wizard = ({
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const isEditing = !R.isNil(toBeEdited)
|
const isEditing = !R.isNil(toBeEdited)
|
||||||
const [step, setStep] = useState(isEditing ? 1 : 0)
|
const [step, setStep] = useState(isEditing ? 1 : 0)
|
||||||
const stepOptions = getStep(step, existingRequirements)
|
|
||||||
|
// If we're editing, filter out the requirement being edited so that validation schemas don't enter in circular conflicts
|
||||||
|
const _existingRequirements = isEditing
|
||||||
|
? R.filter(it => it.id !== toBeEdited.id, existingRequirements)
|
||||||
|
: existingRequirements
|
||||||
|
const stepOptions = getStep(step, _existingRequirements)
|
||||||
const isLastStep = step === LAST_STEP
|
const isLastStep = step === LAST_STEP
|
||||||
|
|
||||||
const onContinue = (values, actions) => {
|
const onContinue = (values, actions) => {
|
||||||
|
|
|
||||||
|
|
@ -47,13 +47,15 @@ const getOverridesSchema = (values, customInfoRequests) => {
|
||||||
.required()
|
.required()
|
||||||
.test({
|
.test({
|
||||||
test() {
|
test() {
|
||||||
const { requirement } = this.parent
|
const { id, requirement } = this.parent
|
||||||
if (R.find(R.propEq('requirement', requirement))(values)) {
|
// If we're editing, filter out the override being edited so that validation schemas don't enter in circular conflicts
|
||||||
|
const _values = R.filter(it => it.id !== id, values)
|
||||||
|
if (R.find(R.propEq('requirement', requirement))(_values)) {
|
||||||
return this.createError({
|
return this.createError({
|
||||||
message: `Requirement ${displayRequirement(
|
message: `Requirement '${displayRequirement(
|
||||||
requirement,
|
requirement,
|
||||||
customInfoRequests
|
customInfoRequests
|
||||||
)} already overriden`
|
)}' already overridden`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,9 @@ const Schema = Yup.object()
|
||||||
// TYPE
|
// TYPE
|
||||||
const typeSchema = Yup.object()
|
const typeSchema = Yup.object()
|
||||||
.shape({
|
.shape({
|
||||||
triggerType: Yup.string().required(),
|
triggerType: Yup.string('The trigger type must be a string').required(
|
||||||
|
'The trigger type is required'
|
||||||
|
),
|
||||||
threshold: Yup.object({
|
threshold: Yup.object({
|
||||||
threshold: Yup.number()
|
threshold: Yup.number()
|
||||||
.transform(transformNumber)
|
.transform(transformNumber)
|
||||||
|
|
@ -297,6 +299,8 @@ const typeSchema = Yup.object()
|
||||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!triggerType) return
|
||||||
|
|
||||||
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||||
|
|
||||||
return context.createError({
|
return context.createError({
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,11 @@ const AdvancedWallet = () => {
|
||||||
|
|
||||||
const AdvancedWalletSettingsOverrides = AdvancedWalletSettings.overrides ?? []
|
const AdvancedWalletSettingsOverrides = AdvancedWalletSettings.overrides ?? []
|
||||||
|
|
||||||
const overridenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(
|
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(
|
||||||
AdvancedWalletSettingsOverrides
|
AdvancedWalletSettingsOverrides
|
||||||
)
|
)
|
||||||
const suggestionFilter = R.filter(
|
const suggestionFilter = R.filter(
|
||||||
it => !R.contains(it.code, overridenCryptos)
|
it => !R.contains(it.code, overriddenCryptos)
|
||||||
)
|
)
|
||||||
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { utils as coinUtils } from '@lamassu/coins'
|
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
@ -104,14 +103,7 @@ const Wizard = ({
|
||||||
: accountsToSave
|
: accountsToSave
|
||||||
|
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
const defaultCryptoUnit = R.head(
|
return save(toNamespace(coin.code, newConfig), newAccounts)
|
||||||
R.keys(coinUtils.getCryptoCurrency(coin.code).units)
|
|
||||||
)
|
|
||||||
const configToSave = {
|
|
||||||
...newConfig,
|
|
||||||
cryptoUnits: defaultCryptoUnit
|
|
||||||
}
|
|
||||||
return save(toNamespace(coin.code, configToSave), newAccounts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from 'src/components/inputs/formik'
|
} from 'src/components/inputs/formik'
|
||||||
import { disabledColor } from 'src/styling/variables'
|
import { disabledColor } from 'src/styling/variables'
|
||||||
import { CURRENCY_MAX } from 'src/utils/constants'
|
import { CURRENCY_MAX } from 'src/utils/constants'
|
||||||
import { transformNumber } from 'src/utils/number'
|
import { defaultToZero } from 'src/utils/number'
|
||||||
|
|
||||||
const classes = {
|
const classes = {
|
||||||
editDisabled: {
|
editDisabled: {
|
||||||
|
|
@ -19,15 +19,21 @@ const filterClass = type => R.filter(it => it.class === type)
|
||||||
const filterCoins = ({ id }) => R.filter(it => R.contains(id)(it.cryptos))
|
const filterCoins = ({ id }) => R.filter(it => R.contains(id)(it.cryptos))
|
||||||
|
|
||||||
const WalletSchema = Yup.object().shape({
|
const WalletSchema = Yup.object().shape({
|
||||||
ticker: Yup.string().required(),
|
ticker: Yup.string('The ticker must be a string').required(
|
||||||
wallet: Yup.string().required(),
|
'The ticker is required'
|
||||||
exchange: Yup.string().required(),
|
),
|
||||||
zeroConf: Yup.string(),
|
wallet: Yup.string('The wallet must be a string').required(
|
||||||
zeroConfLimit: Yup.number()
|
'The wallet is required'
|
||||||
.integer()
|
),
|
||||||
.min(0)
|
exchange: Yup.string('The exchange must be a string').required(
|
||||||
|
'The exchange is required'
|
||||||
|
),
|
||||||
|
zeroConf: Yup.string('The confidence checking must be a string'),
|
||||||
|
zeroConfLimit: Yup.number('The 0-conf limit must be an integer')
|
||||||
|
.integer('The 0-conf limit must be an integer')
|
||||||
|
.min(0, 'The 0-conf limit must be a positive integer')
|
||||||
.max(CURRENCY_MAX)
|
.max(CURRENCY_MAX)
|
||||||
.transform(transformNumber)
|
.transform(defaultToZero)
|
||||||
})
|
})
|
||||||
|
|
||||||
const AdvancedWalletSchema = Yup.object().shape({
|
const AdvancedWalletSchema = Yup.object().shape({
|
||||||
|
|
@ -195,7 +201,7 @@ const getAdvancedWalletElementsOverrides = (
|
||||||
|
|
||||||
const has0Conf = R.complement(
|
const has0Conf = R.complement(
|
||||||
/* NOTE: List of coins without 0conf settings. */
|
/* NOTE: List of coins without 0conf settings. */
|
||||||
R.pipe(R.prop('id'), R.flip(R.includes)(['ETH']))
|
R.pipe(R.prop('id'), R.flip(R.includes)(['ETH', 'USDT']))
|
||||||
)
|
)
|
||||||
|
|
||||||
const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
import { utils as coinUtils } from '@lamassu/coins'
|
|
||||||
import { makeStyles } from '@material-ui/core'
|
import { makeStyles } from '@material-ui/core'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
|
|
@ -54,13 +53,9 @@ const AllSet = ({ data: currentData, doContinue }) => {
|
||||||
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
||||||
|
|
||||||
const save = () => {
|
const save = () => {
|
||||||
const defaultCryptoUnit = R.head(
|
|
||||||
R.keys(coinUtils.getCryptoCurrency(coin).units)
|
|
||||||
)
|
|
||||||
const adjustedData = {
|
const adjustedData = {
|
||||||
zeroConfLimit: 0,
|
zeroConfLimit: 0,
|
||||||
...currentData,
|
...currentData
|
||||||
cryptoUnits: defaultCryptoUnit
|
|
||||||
}
|
}
|
||||||
if (!WalletSchema.isValidSync(adjustedData)) return setError(true)
|
if (!WalletSchema.isValidSync(adjustedData)) return setError(true)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ const Blockcypher = ({ addData }) => {
|
||||||
value={accounts.blockcypher}
|
value={accounts.blockcypher}
|
||||||
save={save}
|
save={save}
|
||||||
elements={schema.blockcypher.elements}
|
elements={schema.blockcypher.elements}
|
||||||
validationSchema={schema.blockcypher.validationSchema}
|
validationSchema={schema.blockcypher.getValidationSchema}
|
||||||
buttonLabel={'Continue'}
|
buttonLabel={'Continue'}
|
||||||
buttonClass={classes.formButton}
|
buttonClass={classes.formButton}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue