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=
|
||||
|
||||
## 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
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const install = require('../lib/blockchain/install')
|
||||
|
||||
install.run()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
require('../lib/environment-helper')
|
||||
|
||||
const settingsLoader = require('../lib/new-settings-loader')
|
||||
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
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const hdkey = require('ethereumjs-wallet/hdkey')
|
||||
const hkdf = require('futoin-hkdf')
|
||||
const crypto = require('crypto')
|
||||
|
|
@ -263,7 +266,7 @@ settingsLoader.loadLatest()
|
|||
}
|
||||
|
||||
const opts = {
|
||||
chainId: 3,
|
||||
chainId: 1,
|
||||
nonce: 0,
|
||||
includesFee: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const migrate = require('../lib/migrate-options')
|
||||
|
||||
migrate.run()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const ofac = require('../lib/ofac/update')
|
||||
|
||||
console.log('Updating OFAC databases.')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const settingsLoader = require('../lib/new-settings-loader')
|
||||
const configManager = require('../lib/new-config-manager')
|
||||
const wallet = require('../lib/wallet')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
|
||||
const _ = require('lodash')
|
||||
const db = require('../lib/db')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
require('../lib/environment-helper')
|
||||
const _ = require('lodash/fp')
|
||||
const common = require('../lib/blockchain/common')
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,22 +29,22 @@ const BINARIES = {
|
|||
dir: 'bitcoin-23.0/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.19-23bee162.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.19-23bee162'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.25-69568c55.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.25-69568c55'
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://z.cash/downloads/zcash-5.0.0-linux64-debian-bullseye.tar.gz',
|
||||
dir: 'zcash-5.0.0/bin'
|
||||
url: 'https://z.cash/downloads/zcash-5.3.0-linux64-debian-bullseye.tar.gz',
|
||||
dir: 'zcash-5.3.0/bin'
|
||||
},
|
||||
DASH: {
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.17.0/bin'
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-18.1.0/bin'
|
||||
},
|
||||
LTC: {
|
||||
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',
|
||||
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: {
|
||||
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']]
|
||||
},
|
||||
XMR: {
|
||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.17.3.2.tar.bz2',
|
||||
dir: 'monero-x86_64-linux-gnu-v0.17.3.2',
|
||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.1.2.tar.bz2',
|
||||
dir: 'monero-x86_64-linux-gnu-v0.18.1.2',
|
||||
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 () {
|
||||
const choices = _.flow([
|
||||
_.filter(c => c.type !== 'erc-20'),
|
||||
_.map(c => {
|
||||
const checked = isInstalledSoftware(c) && isInstalledVolume(c)
|
||||
const name = c.code === 'ethereum' ? 'Ethereum' : c.display
|
||||
const name = c.code === 'ethereum' ? 'Ethereum and/or USDT' : c.display
|
||||
return {
|
||||
name,
|
||||
value: c.code,
|
||||
checked,
|
||||
disabled: c.cryptoCode === 'ETH'
|
||||
? 'Use admin\'s Infura plugin'
|
||||
: checked && 'Installed'
|
||||
checked: isInstalled(c),
|
||||
disabled: isDisabled(c)
|
||||
}
|
||||
}),
|
||||
])(cryptos)
|
||||
|
|
@ -160,6 +174,15 @@ function run () {
|
|||
|
||||
const validateAnswers = async (answers) => {
|
||||
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)
|
||||
.then(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) {
|
||||
if (!tx.fiat || tx.fiat.isZero()) {
|
||||
const sql = `SELECT EXISTS (SELECT DISTINCT to_address FROM cash_in_txs WHERE to_address = $1)`
|
||||
return db.any(sql, [tx.toAddress])
|
||||
const sql = `
|
||||
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)
|
||||
}
|
||||
|
||||
function getWalletScore (tx, pi) {
|
||||
if (!tx.fiat || tx.fiat.isZero()) {
|
||||
return pi.rateWallet(tx.cryptoCode, tx.toAddress)
|
||||
}
|
||||
// Passthrough the previous result
|
||||
return pi.isValidWalletScore(tx.walletScore)
|
||||
.then(isValid => ({
|
||||
address: tx.toAddress,
|
||||
score: tx.walletScore,
|
||||
isValid
|
||||
}))
|
||||
pi.isWalletScoringEnabled(tx)
|
||||
.then(isEnabled => {
|
||||
if(!isEnabled) return null
|
||||
if (!tx.fiat || tx.fiat.isZero()) {
|
||||
return pi.rateWallet(tx.cryptoCode, tx.toAddress)
|
||||
}
|
||||
// Passthrough the previous result
|
||||
return pi.isValidWalletScore(tx.walletScore)
|
||||
.then(isValid => ({
|
||||
address: tx.toAddress,
|
||||
score: tx.walletScore,
|
||||
isValid
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function monitorPending (settings) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ module.exports = {
|
|||
|
||||
const STALE_INCOMING_TX_AGE = T.day
|
||||
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 MIN_NOTIFY_AGE = 5 * T.minutes
|
||||
const INSUFFICIENT_FUNDS_CODE = 570
|
||||
|
|
@ -37,7 +36,8 @@ function selfPost (tx, pi) {
|
|||
}
|
||||
|
||||
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)
|
||||
.then(txVector => {
|
||||
const [, newTx, justAuthorized] = txVector
|
||||
|
|
@ -64,7 +64,7 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
fiat: 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)
|
||||
return bills
|
||||
|
|
@ -91,21 +91,17 @@ function postProcess (txVector, justAuthorized, pi) {
|
|||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
function fetchOpenTxs (statuses, fromAge, toAge, applyFilter, coinFilter) {
|
||||
const notClause = applyFilter ? '' : 'not'
|
||||
function fetchOpenTxs (statuses, fromAge, toAge) {
|
||||
const sql = `select *
|
||||
from cash_out_txs
|
||||
where ((extract(epoch from (now() - created))) * 1000)>$1
|
||||
and ((extract(epoch from (now() - created))) * 1000)<$2
|
||||
${_.isEmpty(coinFilter)
|
||||
? ``
|
||||
: `and crypto_code ${notClause} in ($3^)`}
|
||||
and status in ($4^)`
|
||||
and status in ($3^)
|
||||
and error is distinct from 'Operator cancel'`
|
||||
|
||||
const coinClause = _.map(pgp.as.text, coinFilter).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))
|
||||
}
|
||||
|
||||
|
|
@ -119,67 +115,55 @@ function processTxStatus (tx, settings) {
|
|||
}
|
||||
|
||||
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
|
||||
return pi.getTransactionHash(tx)
|
||||
.then(txHashes => pi.getInputAddresses(tx, txHashes))
|
||||
.then(addresses => {
|
||||
const addressesPromise = []
|
||||
_.forEach(it => addressesPromise.push(pi.rateWallet(tx.cryptoCode, it)), addresses)
|
||||
return Promise.all(addressesPromise)
|
||||
})
|
||||
.then(scores => {
|
||||
if (_.isNil(scores) || _.isEmpty(scores)) return tx
|
||||
const highestScore = _.maxBy(it => it.score, scores)
|
||||
|
||||
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
|
||||
return highestScore.isValid
|
||||
? _.assign(tx, { walletScore: highestScore.score })
|
||||
: _.assign(tx, {
|
||||
walletScore: highestScore.score,
|
||||
error: 'Address score is above defined threshold',
|
||||
errorCode: 'scoreThresholdReached',
|
||||
dispense: true
|
||||
})
|
||||
})
|
||||
.catch(error => _.assign(tx, {
|
||||
walletScore: 10,
|
||||
error: `Failure getting address score: ${error.message}`,
|
||||
errorCode: 'ciphertraceError',
|
||||
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
|
||||
return pi.isWalletScoringEnabled(tx)
|
||||
.then(isEnabled => {
|
||||
if (!isEnabled) return tx
|
||||
return pi.getTransactionHash(tx)
|
||||
.then(rejectEmpty("No transaction hashes"))
|
||||
.then(txHashes => pi.getInputAddresses(tx, txHashes))
|
||||
.then(rejectEmpty("No input addresses"))
|
||||
.then(addresses => Promise.all(_.map(it => pi.rateWallet(tx.cryptoCode, it), addresses)))
|
||||
.then(rejectEmpty("No score ratings"))
|
||||
.then(_.maxBy(_.get(['score'])))
|
||||
.then(highestScore =>
|
||||
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
|
||||
highestScore.isValid
|
||||
? _.assign(tx, { walletScore: highestScore.score })
|
||||
: _.assign(tx, {
|
||||
walletScore: highestScore.score,
|
||||
error: 'Address score is above defined threshold',
|
||||
errorCode: 'scoreThresholdReached',
|
||||
dispense: true
|
||||
})
|
||||
)
|
||||
.catch(error => _.assign(tx, {
|
||||
walletScore: 10,
|
||||
error: `Failure getting address score: ${error.message}`,
|
||||
errorCode: 'ciphertraceError',
|
||||
dispense: true
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
function monitorLiveIncoming (settings, applyFilter, coinFilter) {
|
||||
function monitorLiveIncoming (settings) {
|
||||
const statuses = ['notSeen', 'published', 'insufficientFunds']
|
||||
const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE
|
||||
|
||||
return monitorIncoming(settings, statuses, 0, toAge, applyFilter, coinFilter)
|
||||
return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
|
||||
}
|
||||
|
||||
function monitorStaleIncoming (settings, applyFilter, coinFilter) {
|
||||
function monitorStaleIncoming (settings) {
|
||||
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, fromAge, STALE_INCOMING_TX_AGE, applyFilter, coinFilter)
|
||||
return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
|
||||
}
|
||||
|
||||
function monitorIncoming (settings, statuses, fromAge, toAge, applyFilter, coinFilter) {
|
||||
return fetchOpenTxs(statuses, fromAge, toAge, applyFilter, coinFilter)
|
||||
function monitorIncoming (settings, statuses, fromAge, toAge) {
|
||||
return fetchOpenTxs(statuses, fromAge, toAge)
|
||||
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
|
||||
.catch(err => {
|
||||
if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) {
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ const RECEIPT = 'sms_receipt'
|
|||
|
||||
const WALLET_SCORE_THRESHOLD = 9
|
||||
|
||||
const BALANCE_FETCH_SPEED_MULTIPLIER = {
|
||||
NORMAL: 1,
|
||||
SLOW: 3
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
anonymousCustomer,
|
||||
CASSETTE_MAX_CAPACITY,
|
||||
|
|
@ -48,5 +53,6 @@ module.exports = {
|
|||
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
|
||||
WALLET_SCORE_THRESHOLD,
|
||||
RECEIPT,
|
||||
PSQL_URL
|
||||
PSQL_URL,
|
||||
BALANCE_FETCH_SPEED_MULTIPLIER
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ function deleteEditedData (id, data) {
|
|||
*/
|
||||
async function updateEditedPhoto (id, photo, photoType) {
|
||||
const newPatch = {}
|
||||
const baseDir = photoType === 'frontCamera' ? frontCameraBaseDir : idPhotoCardBasedir
|
||||
const baseDir = photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR
|
||||
const { createReadStream, filename } = photo
|
||||
const stream = createReadStream()
|
||||
|
||||
|
|
@ -339,7 +339,7 @@ function camelizeDeep (customer) {
|
|||
|
||||
/**
|
||||
* Get all available complianceTypes
|
||||
* that can be overriden (excluding hard_limit)
|
||||
* that can be overridden (excluding hard_limit)
|
||||
*
|
||||
* @name getComplianceTypes
|
||||
* @function
|
||||
|
|
@ -404,7 +404,7 @@ function enhanceAtFields (fields) {
|
|||
*/
|
||||
function enhanceOverrideFields (fields, userToken) {
|
||||
if (!userToken) return fields
|
||||
// Populate with computedFields (user who overrode and overriden timestamps date)
|
||||
// Populate with computedFields (user who overrode and overridden timestamps date)
|
||||
return _.reduce(_.assign, fields, _.map((type) => {
|
||||
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,
|
||||
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,
|
||||
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
|
||||
FROM (
|
||||
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.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,
|
||||
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,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
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 =>
|
||||
smth && smth.active ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
|
||||
(smth && smth.active) ? _.set(dstField, _.pick(srcFields, smth)) : _.identity
|
||||
|
||||
const addOperatorInfo = addSmthInfo(
|
||||
'operatorInfo',
|
||||
['name', 'phone', 'email', 'website', 'companyNumber']
|
||||
)
|
||||
|
||||
const addReceiptInfo = addSmthInfo(
|
||||
'receiptInfo',
|
||||
[
|
||||
const addReceiptInfo = receiptInfo => ret => {
|
||||
if (!receiptInfo) return ret
|
||||
|
||||
const fields = [
|
||||
'paper',
|
||||
'sms',
|
||||
'operatorWebsite',
|
||||
'operatorEmail',
|
||||
|
|
@ -43,10 +45,22 @@ const addReceiptInfo = addSmthInfo(
|
|||
'exchangeRate',
|
||||
'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. */
|
||||
const buildTriggers = (allTriggers) => {
|
||||
const buildTriggers = allTriggers => {
|
||||
const normalTriggers = []
|
||||
const customTriggers = _.filter(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(
|
||||
_.pick([
|
||||
'areThereAvailablePromoCodes',
|
||||
'coins',
|
||||
'configVersion',
|
||||
'timezone'
|
||||
|
|
@ -139,7 +152,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
const setZeroConfLimit = config => coin =>
|
||||
_.set(
|
||||
'zeroConfLimit',
|
||||
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit,
|
||||
configManager.getWalletSettings(coin.cryptoCode, config).zeroConfLimit ?? 0,
|
||||
coin
|
||||
)
|
||||
|
||||
|
|
@ -157,7 +170,7 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
state.pids = _.update(operatorId, _.set(deviceId, { pid, ts: Date.now() }), state.pids)
|
||||
|
||||
return _.flow(
|
||||
_.pick(['balances', 'cassettes', 'coins', 'rates']),
|
||||
_.pick(['areThereAvailablePromoCodes', 'balances', 'cassettes', 'coins', 'rates']),
|
||||
|
||||
_.update('cassettes', massageCassettes),
|
||||
|
||||
|
|
@ -172,7 +185,8 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
|
||||
/* Group the separate objects by cryptoCode */
|
||||
/* { balances, coins, rates } => { cryptoCode: { balance, ask, bid, cashIn, cashOut }, ... } */
|
||||
({ balances, cassettes, coins, rates }) => ({
|
||||
({ areThereAvailablePromoCodes, balances, cassettes, coins, rates }) => ({
|
||||
areThereAvailablePromoCodes,
|
||||
cassettes,
|
||||
coins: _.flow(
|
||||
_.reduce(
|
||||
|
|
@ -184,7 +198,10 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
|
|||
_.toPairs,
|
||||
|
||||
/* [[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))
|
||||
}),
|
||||
|
||||
|
|
@ -218,6 +235,7 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera
|
|||
|
||||
|
||||
const massageTerms = terms => (terms.active && terms.text) ? ({
|
||||
tcPhoto: Boolean(terms.tcPhoto),
|
||||
delay: Boolean(terms.delay),
|
||||
title: terms.title,
|
||||
text: nmd(terms.text),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ type Coin {
|
|||
cashInFee: String!
|
||||
cashInCommission: String!
|
||||
cashOutCommission: String!
|
||||
cryptoNetwork: Boolean!
|
||||
cryptoNetwork: String!
|
||||
cryptoUnits: String!
|
||||
batchable: Boolean!
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ type MachineInfo {
|
|||
}
|
||||
|
||||
type ReceiptInfo {
|
||||
paper: Boolean!
|
||||
sms: Boolean!
|
||||
operatorWebsite: Boolean!
|
||||
operatorEmail: Boolean!
|
||||
|
|
@ -57,6 +58,32 @@ type TriggersAutomation {
|
|||
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 {
|
||||
id: String!
|
||||
customInfoRequestId: String!
|
||||
|
|
@ -64,12 +91,14 @@ type Trigger {
|
|||
requirement: String!
|
||||
triggerType: String!
|
||||
|
||||
suspensionDays: Int
|
||||
suspensionDays: Float
|
||||
threshold: Int
|
||||
thresholdDays: Int
|
||||
customInfoRequest: CustomInfoRequest
|
||||
}
|
||||
|
||||
type TermsDetails {
|
||||
tcPhoto: Boolean!
|
||||
delay: Boolean!
|
||||
title: String!
|
||||
accept: String!
|
||||
|
|
@ -85,7 +114,6 @@ type Terms {
|
|||
type StaticConfig {
|
||||
configVersion: Int!
|
||||
|
||||
areThereAvailablePromoCodes: Boolean!
|
||||
coins: [Coin!]!
|
||||
enablePaperWalletOnly: Boolean!
|
||||
hasLightning: Boolean!
|
||||
|
|
@ -136,6 +164,7 @@ type Cassettes {
|
|||
}
|
||||
|
||||
type DynamicConfig {
|
||||
areThereAvailablePromoCodes: Boolean!
|
||||
cassettes: Cassettes
|
||||
coins: [DynamicCoinValues!]!
|
||||
reboot: Boolean!
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
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 db = require('./db')
|
||||
const logger = require('./logger')
|
||||
const pgp = require('pg-promise')()
|
||||
|
||||
const getMachineName = require('./machine-loader').getMachineName
|
||||
|
|
@ -118,6 +119,10 @@ function logDateFormat (timezone, logs, fields) {
|
|||
field =>
|
||||
{
|
||||
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])
|
||||
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 state = require('./state')
|
||||
const newSettingsLoader = require('../new-settings-loader')
|
||||
const helpers = require('../route-helpers')
|
||||
const logger = require('../logger')
|
||||
|
||||
db.connect({ direct: true }).then(sco => {
|
||||
|
|
@ -58,31 +55,54 @@ const populateSettings = function (req, res, next) {
|
|||
}
|
||||
|
||||
try {
|
||||
const operatorSettings = settingsCache.get(operatorId)
|
||||
if (!versionId && (!operatorSettings || !!needsSettingsReload[operatorId])) {
|
||||
// Priority of configs to retrieve
|
||||
// 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()
|
||||
.then(settings => {
|
||||
settingsCache.set(operatorId, settings)
|
||||
delete needsSettingsReload[operatorId]
|
||||
settingsCache.set(`${operatorId}-latest`, settings)
|
||||
if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId]
|
||||
req.settings = settings
|
||||
})
|
||||
.then(() => next())
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
if (!versionId && operatorSettings) {
|
||||
req.settings = operatorSettings
|
||||
return next()
|
||||
}
|
||||
|
||||
logger.debug('Fetching the latest config value from cache')
|
||||
req.settings = operatorSettings
|
||||
return next()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
newSettingsLoader.load(versionId)
|
||||
.then(settings => { req.settings = settings })
|
||||
.then(() => helpers.updateDeviceConfigVersion(versionId))
|
||||
.then(() => next())
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
module.exports = populateSettings
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ function updateOptionBasepath (result, optionName) {
|
|||
async function run () {
|
||||
// load current 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
|
||||
if (shouldMigrate) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const _ = require('lodash/fp')
|
|||
|
||||
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 TICKER = 'ticker'
|
||||
|
|
@ -29,8 +29,8 @@ const ALL_ACCOUNTS = [
|
|||
{ code: 'mock-ticker', display: 'Mock (Caution!)', class: TICKER, cryptos: ALL_CRYPTOS, dev: true },
|
||||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH] },
|
||||
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH] },
|
||||
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH, USDT] },
|
||||
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH, USDT] },
|
||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||
{ 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_out_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`
|
||||
|
||||
return db.any(sql)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,14 @@ const resolvers = {
|
|||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||
},
|
||||
Query: {
|
||||
transactions: (...[, { 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, excludeTestingCustomers),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, excludeTestingCustomers, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']))),
|
||||
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, swept, excludeTestingCustomers),
|
||||
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, swept, excludeTestingCustomers, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
|
||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTx(id, txClass).then(data =>
|
||||
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime']))
|
||||
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime', 'publishedAt']))
|
||||
),
|
||||
txAssociatedDataCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTxAssociatedData(id, txClass).then(data =>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ const typeDef = gql`
|
|||
batchError: String
|
||||
walletScore: Int
|
||||
profit: String
|
||||
swept: Boolean
|
||||
}
|
||||
|
||||
type Filter {
|
||||
|
|
@ -58,8 +59,8 @@ const typeDef = gql`
|
|||
}
|
||||
|
||||
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
|
||||
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
|
||||
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, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
|
||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
transactionFilters: [Filter] @auth
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ function batch (
|
|||
cryptoCode = null,
|
||||
toAddress = null,
|
||||
status = null,
|
||||
swept = null,
|
||||
excludeTestingCustomers = false,
|
||||
simplified
|
||||
) {
|
||||
|
|
@ -109,14 +110,33 @@ function batch (
|
|||
AND ($11 is null or txs.crypto_code = $11)
|
||||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
AND ($14 is null or txs.swept = $14)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
AND (fiat > 0)
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
||||
return Promise.all([
|
||||
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])
|
||||
])
|
||||
// The swept filter is cash-out only, so omit the cash-in query entirely
|
||||
const hasCashInOnlyFilters = false
|
||||
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(res => {
|
||||
if (simplified) return simplifiedBatch(res)
|
||||
|
|
@ -138,7 +158,7 @@ function advancedBatch (data) {
|
|||
'txVersion', 'publishedAt', 'termsAccepted', 'layer2Address',
|
||||
'commissionPercentage', 'rawTickerPrice', 'receivedCryptoAtoms',
|
||||
'discount', 'txHash', 'customerPhone', 'customerIdCardDataNumber',
|
||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName',
|
||||
'customerIdCardDataExpiration', 'customerIdCardData', 'customerName', 'sendTime',
|
||||
'customerFrontCameraPath', 'customerIdCardPhotoPath', 'expired', 'machineName', 'walletScore']
|
||||
|
||||
const addAdvancedFields = _.map(it => ({
|
||||
|
|
@ -169,8 +189,8 @@ function simplifiedBatch (data) {
|
|||
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode)
|
||||
|
||||
const getProfit = it => {
|
||||
/* fiat - crypto*tickerPrice + fee */
|
||||
const calcCashInProfit = (fiat, crypto, tickerPrice, fee) => fiat.minus(crypto.times(tickerPrice)).plus(fee)
|
||||
/* fiat - crypto*tickerPrice */
|
||||
const calcCashInProfit = (fiat, crypto, tickerPrice) => fiat.minus(crypto.times(tickerPrice))
|
||||
/* crypto*tickerPrice - fiat */
|
||||
const calcCashOutProfit = (fiat, crypto, tickerPrice) => crypto.times(tickerPrice).minus(fiat)
|
||||
|
||||
|
|
@ -180,7 +200,7 @@ const getProfit = it => {
|
|||
const isCashIn = it.txClass === 'cashIn'
|
||||
|
||||
return isCashIn
|
||||
? calcCashInProfit(fiat, crypto, tickerPrice, BN(it.cashInFee))
|
||||
? calcCashInProfit(fiat, crypto, tickerPrice)
|
||||
: calcCashOutProfit(fiat, crypto, tickerPrice)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,15 +145,13 @@ const getTriggersAutomation = (customInfoRequests, config) => {
|
|||
|
||||
const splitGetFirst = _.compose(_.head, _.split('_'))
|
||||
|
||||
const getCryptosFromWalletNamespace = config => {
|
||||
return _.uniq(_.map(splitGetFirst, _.keys(fromNamespace('wallets', config))))
|
||||
}
|
||||
const getCryptosFromWalletNamespace =
|
||||
_.compose(_.without(['advanced']), _.uniq, _.map(splitGetFirst), _.keys, fromNamespace('wallets'))
|
||||
|
||||
const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config)
|
||||
|
||||
const getCryptoUnits = (crypto, config) => {
|
||||
return getWalletSettings(crypto, config).cryptoUnits
|
||||
}
|
||||
const getCryptoUnits = (crypto, config) =>
|
||||
getWalletSettings(crypto, config).cryptoUnits ?? 'full'
|
||||
|
||||
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) {
|
||||
return loadLatestConfigOrNone()
|
||||
.then(currentConfig => {
|
||||
|
|
@ -221,5 +232,6 @@ module.exports = {
|
|||
loadLatestConfig,
|
||||
loadLatestConfigOrNone,
|
||||
load,
|
||||
migrate
|
||||
migrate,
|
||||
removeFromConfig
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const notificationCenter = require('./notificationCenter')
|
|||
const utils = require('./utils')
|
||||
const emailFuncs = require('./email')
|
||||
const smsFuncs = require('./sms')
|
||||
const webhookFuncs = require('./webhook')
|
||||
const { STALE, STALE_STATE } = require('./codes')
|
||||
|
||||
function buildMessage (alerts, notifications) {
|
||||
|
|
@ -185,6 +186,10 @@ function complianceNotify (customer, deviceId, action, period) {
|
|||
email: {
|
||||
subject: `Customer compliance`,
|
||||
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.compliance
|
||||
|
||||
const webhookActive = true
|
||||
|
||||
if (emailActive) promises.push(emailFuncs.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)
|
||||
|
||||
|
|
@ -220,6 +228,10 @@ function sendRedemptionMessage (txId, error) {
|
|||
email: {
|
||||
subject,
|
||||
body
|
||||
},
|
||||
webhook: {
|
||||
topic: `Transaction update`,
|
||||
content: body
|
||||
}
|
||||
}
|
||||
return sendTransactionMessage(rec)
|
||||
|
|
@ -241,6 +253,11 @@ function sendTransactionMessage (rec, isHighValueTx) {
|
|||
(notifications.sms.transactions || isHighValueTx)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
@ -259,6 +276,10 @@ function cashboxNotify (deviceId) {
|
|||
email: {
|
||||
subject: `Cashbox removal`,
|
||||
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 =
|
||||
notifications.sms.active &&
|
||||
notifications.sms.security
|
||||
|
||||
const webhookActive = true
|
||||
|
||||
if (emailActive) promises.push(emailFuncs.sendMessage(settings, rec))
|
||||
if (smsActive) promises.push(smsFuncs.sendMessage(settings, rec))
|
||||
if (webhookActive) promises.push(webhookFuncs.sendMessage(settings, rec))
|
||||
notifyIfActive('security', 'cashboxNotify', deviceId)
|
||||
|
||||
return Promise.all(promises)
|
||||
|
|
|
|||
|
|
@ -71,7 +71,9 @@ const fiatBalancesNotify = (fiatWarnings) => {
|
|||
const { cassette, deviceId } = o.detail
|
||||
return cassette === balance.cassette && deviceId === balance.deviceId
|
||||
}, 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 })
|
||||
return queries.addNotification(FIAT_BALANCE, message, detailB)
|
||||
})
|
||||
|
|
@ -111,11 +113,18 @@ const cryptoBalancesNotify = (cryptoWarnings) => {
|
|||
}
|
||||
|
||||
const balancesNotify = (balances) => {
|
||||
const cryptoFilter = o => o.code === 'HIGH_CRYPTO_BALANCE' || o.code === 'LOW_CRYPTO_BALANCE'
|
||||
const fiatFilter = o => o.code === 'LOW_CASH_OUT'
|
||||
const cryptoWarnings = _.filter(cryptoFilter, balances)
|
||||
const fiatWarnings = _.filter(fiatFilter, balances)
|
||||
return Promise.all([cryptoBalancesNotify(cryptoWarnings), fiatBalancesNotify(fiatWarnings)])
|
||||
const isCryptoCode = c => _.includes(c, ['HIGH_CRYPTO_BALANCE', 'LOW_CRYPTO_BALANCE'])
|
||||
const isFiatCode = c => _.includes(c, ['LOW_CASH_OUT', 'CASH_BOX_FULL'])
|
||||
const by = o =>
|
||||
isCryptoCode(o) ? 'crypto' :
|
||||
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 => {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,10 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
|||
email: {
|
||||
emailSubject,
|
||||
body
|
||||
},
|
||||
webhook: {
|
||||
topic: `New transaction`,
|
||||
content: body
|
||||
}
|
||||
}, 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 {
|
||||
cryptoCode,
|
||||
display: cryptoRec.display,
|
||||
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||
minimumTx: BN.max(minimumTx, cashInFee),
|
||||
cashInFee,
|
||||
cashInCommission,
|
||||
|
|
@ -788,9 +789,10 @@ function plugins (settings, deviceId) {
|
|||
}
|
||||
|
||||
function sweepHdRow (row) {
|
||||
const txId = row.id
|
||||
const cryptoCode = row.crypto_code
|
||||
|
||||
return wallet.sweep(settings, cryptoCode, row.hd_index)
|
||||
return wallet.sweep(settings, txId, cryptoCode, row.hd_index)
|
||||
.then(txHash => {
|
||||
if (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)
|
||||
}
|
||||
})
|
||||
.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 () {
|
||||
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')`
|
||||
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') AND created > now() - interval '1 week'`
|
||||
|
||||
return db.any(sql)
|
||||
.then(rows => Promise.all(rows.map(sweepHdRow)))
|
||||
|
|
@ -848,6 +850,10 @@ function plugins (settings, deviceId) {
|
|||
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
|
||||
}
|
||||
|
||||
function isWalletScoringEnabled (tx) {
|
||||
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
|
||||
}
|
||||
|
||||
return {
|
||||
getRates,
|
||||
recordPing,
|
||||
|
|
@ -880,7 +886,8 @@ function plugins (settings, deviceId) {
|
|||
rateWallet,
|
||||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses
|
||||
getInputAddresses,
|
||||
isWalletScoringEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const bitpay = require('../ticker/bitpay')
|
|||
const binance = require('../exchange/binance')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
|
||||
|
||||
const ALL = {
|
||||
cex: cex,
|
||||
|
|
@ -22,7 +22,7 @@ const ALL = {
|
|||
itbit: itbit,
|
||||
bitpay: bitpay,
|
||||
coinbase: {
|
||||
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH],
|
||||
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT],
|
||||
FIAT: 'ALL_CURRENCIES'
|
||||
},
|
||||
binance: binance
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
|||
const { ORDER_TYPES } = require('./consts')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH]
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT]
|
||||
const FIAT = ['USD']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
|||
const { ORDER_TYPES } = require('./consts')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, ETH, LTC, BCH } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH]
|
||||
const { BTC, ETH, LTC, BCH, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const AMOUNT_PRECISION = 8
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
|||
const { ORDER_TYPES } = require('./consts')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, BCH, DASH, ETH, LTC } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, BCH]
|
||||
const { BTC, BCH, DASH, ETH, LTC, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const _ = require('lodash/fp')
|
|||
const { ORDER_TYPES } = require('./consts')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, BCH, ETH, LTC } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH]
|
||||
const { BTC, BCH, ETH, LTC, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT]
|
||||
const FIAT = ['USD']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
|
|||
const { COINS } = require('@lamassu/coins')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.LIMIT
|
||||
const { BTC, ETH } = COINS
|
||||
const CRYPTO = [BTC, ETH]
|
||||
const { BTC, ETH, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, USDT]
|
||||
const FIAT = ['USD']
|
||||
const AMOUNT_PRECISION = 4
|
||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const { ORDER_TYPES } = require('./consts')
|
|||
const { COINS } = require('@lamassu/coins')
|
||||
|
||||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR]
|
||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const AMOUNT_PRECISION = 6
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
|
|
|
|||
|
|
@ -52,44 +52,62 @@ function isValidWalletScore (account, score) {
|
|||
return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
|
||||
}
|
||||
|
||||
function getTransactionHash (account, cryptoCode, receivingAddress) {
|
||||
const client = getClient(account)
|
||||
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
|
||||
|
||||
function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wallet) {
|
||||
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`)
|
||||
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`, {
|
||||
headers: authHeader
|
||||
}))
|
||||
.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))
|
||||
})
|
||||
.then(_.flow(
|
||||
_.get(['data', 'txHistory']),
|
||||
_.map(_.get(['txHash']))
|
||||
))
|
||||
.catch(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
|
||||
})
|
||||
}
|
||||
|
||||
function getInputAddresses (account, cryptoCode, txHashes) {
|
||||
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
|
||||
|
||||
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}`, {
|
||||
headers: authHeader
|
||||
})
|
||||
txHashes = _(txHashes).take(10).join(',')
|
||||
|
||||
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 => {
|
||||
const data = res.data
|
||||
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 = {
|
||||
NAME,
|
||||
rateWallet,
|
||||
isValidWalletScore,
|
||||
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 = {
|
||||
NAME,
|
||||
rateWallet,
|
||||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses
|
||||
getInputAddresses,
|
||||
isWalletScoringEnabled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,6 +129,13 @@ function checkBlockchainStatus (cryptoCode) {
|
|||
.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 = {
|
||||
balance,
|
||||
sendCoins,
|
||||
|
|
@ -136,5 +143,6 @@ module.exports = {
|
|||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,13 @@ function checkBlockchainStatus (cryptoCode) {
|
|||
.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 = {
|
||||
balance,
|
||||
sendCoins,
|
||||
|
|
@ -202,5 +209,6 @@ module.exports = {
|
|||
estimateFee,
|
||||
sendCoinsBatch,
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress,
|
||||
SUPPORTS_BATCHING
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,11 +125,19 @@ function checkBlockchainStatus (cryptoCode) {
|
|||
.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 = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@ const web3 = new Web3()
|
|||
const hdkey = require('ethereumjs-wallet/hdkey')
|
||||
const { FeeMarketEIP1559Transaction } = require('@ethereumjs/tx')
|
||||
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 coins = require('@lamassu/coins')
|
||||
const pify = require('pify')
|
||||
|
||||
const _pify = require('pify')
|
||||
const BN = require('../../../bn')
|
||||
const ABI = require('../../tokens')
|
||||
const logger = require('../../../logger')
|
||||
|
||||
exports.SUPPORTED_MODULES = ['wallet']
|
||||
|
||||
|
|
@ -30,7 +34,27 @@ module.exports = {
|
|||
privateKey,
|
||||
isStrictAddress,
|
||||
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) {
|
||||
|
|
@ -44,12 +68,19 @@ function privateKey (account) {
|
|||
}
|
||||
|
||||
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) {
|
||||
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(txid => {
|
||||
return pify(web3.eth.getTransaction)(txid)
|
||||
|
|
@ -77,14 +108,16 @@ function balance (account, cryptoCode, settings, operatorId) {
|
|||
|
||||
const pendingBalance = (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)
|
||||
|
||||
function _balance (includePending, address, cryptoCode) {
|
||||
if (coins.utils.isErc20Token(cryptoCode)) {
|
||||
const contract = web3.eth.contract(ABI.ERC20).at(coins.utils.getErc20Token(cryptoCode).contractAddress)
|
||||
return contract.balanceOf(address.toLowerCase())
|
||||
const contract = new web3.eth.Contract(ABI.ERC20, coins.utils.getErc20Token(cryptoCode).contractAddress)
|
||||
return contract.methods.balanceOf(address.toLowerCase()).call((_, balance) => {
|
||||
return contract.methods.decimals().call((_, decimals) => BN(balance).div(10 ** decimals))
|
||||
})
|
||||
}
|
||||
const block = includePending ? 'pending' : undefined
|
||||
return pify(web3.eth.getBalance)(address.toLowerCase(), block)
|
||||
|
|
@ -92,27 +125,78 @@ function _balance (includePending, address, cryptoCode) {
|
|||
.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 isErc20Token = coins.utils.isErc20Token(cryptoCode)
|
||||
const toAddress = isErc20Token ? coins.utils.getErc20Token(cryptoCode).contractAddress : _toAddress.toLowerCase()
|
||||
const toAddress = coins.utils.getErc20Token(cryptoCode).contractAddress
|
||||
|
||||
let contract, contractData
|
||||
if (isErc20Token) {
|
||||
contract = web3.eth.contract(ABI.ERC20).at(toAddress)
|
||||
contractData = isErc20Token && contract.transfer.getData(_toAddress.toLowerCase(), hex(toSend))
|
||||
const contract = new web3.eth.Contract(ABI.ERC20, toAddress)
|
||||
const contractData = contract.methods.transfer(_toAddress.toLowerCase(), hex(amount))
|
||||
|
||||
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 = {
|
||||
from: fromAddress,
|
||||
to: toAddress,
|
||||
value: amount.toString()
|
||||
}
|
||||
|
||||
if (isErc20Token) txTemplate.data = contractData
|
||||
|
||||
const common = new Common({ chain: Chain.Ropsten, hardfork: Hardfork.London })
|
||||
const common = new Common({ chain: Chain.Mainnet, hardfork: Hardfork.London })
|
||||
|
||||
const promises = [
|
||||
pify(web3.eth.estimateGas)(txTemplate),
|
||||
|
|
@ -122,34 +206,33 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
|||
]
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([gas, gasPrice, txCount]) => [
|
||||
.then(([gas, gasPrice, txCount, { baseFeePerGas }]) => [
|
||||
BN(gas),
|
||||
BN(gasPrice),
|
||||
_.max([0, txCount, lastUsedNonces[fromAddress] + 1])
|
||||
_.max([0, txCount, lastUsedNonces[fromAddress] + 1]),
|
||||
BN(baseFeePerGas)
|
||||
])
|
||||
.then(([gas, gasPrice, txCount, baseFeePerGas]) => {
|
||||
lastUsedNonces[fromAddress] = txCount
|
||||
|
||||
const toSend = includesFee
|
||||
? amount.minus(gasPrice.times(gas))
|
||||
: amount
|
||||
const maxPriorityFeePerGas = new BN(web3.utils.toWei('2.5', 'gwei')) // web3 default value
|
||||
const neededPriority = new BN(web3.utils.toWei('2.0', 'gwei'))
|
||||
const maxFeePerGas = baseFeePerGas.plus(neededPriority)
|
||||
const newGasPrice = BN.minimum(maxFeePerGas, baseFeePerGas.plus(maxPriorityFeePerGas))
|
||||
|
||||
const maxPriorityFeePerGas = new BN(2.5) // web3 default value
|
||||
const maxFeePerGas = new BN(2).times(baseFeePerGas).plus(maxPriorityFeePerGas)
|
||||
const toSend = includesFee
|
||||
? new BN(amount).minus(newGasPrice.times(gas))
|
||||
: amount
|
||||
|
||||
const rawTx = {
|
||||
chainId: 1,
|
||||
nonce: txCount,
|
||||
maxPriorityFeePerGas: web3.utils.toHex(web3.utils.toWei(maxPriorityFeePerGas.toString(), 'gwei')),
|
||||
maxFeePerGas: web3.utils.toHex(web3.utils.toWei(maxFeePerGas.toString(), 'gwei')),
|
||||
maxPriorityFeePerGas: web3.utils.toHex(maxPriorityFeePerGas),
|
||||
maxFeePerGas: web3.utils.toHex(maxFeePerGas),
|
||||
gasLimit: hex(gas),
|
||||
to: toAddress,
|
||||
from: fromAddress,
|
||||
value: isErc20Token ? hex(BN(0)) : hex(toSend)
|
||||
}
|
||||
|
||||
if (isErc20Token) {
|
||||
rawTx.data = contractData
|
||||
value: hex(toSend)
|
||||
}
|
||||
|
||||
const tx = FeeMarketEIP1559Transaction.fromTxData(rawTx, { common })
|
||||
|
|
@ -169,17 +252,18 @@ function defaultAddress (account) {
|
|||
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 fromAddress = wallet.getChecksumAddressString()
|
||||
|
||||
return confirmedBalance(fromAddress, cryptoCode)
|
||||
return SWEEP_QUEUE.add(() => confirmedBalance(fromAddress, cryptoCode)
|
||||
.then(r => {
|
||||
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))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function newAddress (account, info, tx, settings, operatorId) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
const _ = require('lodash/fp')
|
||||
const NodeCache = require('node-cache')
|
||||
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'
|
||||
|
||||
|
|
@ -12,4 +17,54 @@ function run (account) {
|
|||
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')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
throw new Error(`Transactions hash retrieval not implemented for this coin!`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,14 @@ const SECONDS = 1000
|
|||
const PUBLISH_TIME = 3 * SECONDS
|
||||
const AUTHORIZE_TIME = PUBLISH_TIME + 5 * SECONDS
|
||||
const CONFIRM_TIME = AUTHORIZE_TIME + 10 * SECONDS
|
||||
const SUPPORTED_COINS = coinUtils.cryptoCurrencies()
|
||||
|
||||
let t0
|
||||
|
||||
const checkCryptoCode = (cryptoCode) => !_.includes(cryptoCode, SUPPORTED_COINS)
|
||||
? Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
|
||||
: Promise.resolve()
|
||||
|
||||
function _balance (cryptoCode) {
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
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))
|
||||
|
||||
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) {
|
||||
|
|
@ -123,5 +136,6 @@ module.exports = {
|
|||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ function handleError (error, method) {
|
|||
|
||||
function openWallet () {
|
||||
return fetch('open_wallet', { filename: 'Wallet' })
|
||||
.catch(err => handleError(err, 'openWallet'))
|
||||
.catch(() => openWalletWithPassword())
|
||||
}
|
||||
|
||||
function openWalletWithPassword () {
|
||||
|
|
@ -164,7 +164,7 @@ function getStatus (account, tx, requested, settings, operatorId) {
|
|||
return checkCryptoCode(cryptoCode)
|
||||
.then(() => refreshWallet())
|
||||
.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 => {
|
||||
const confirmedToAddress = _.filter(it => it.address === toAddress, transferRes.in ?? [])
|
||||
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 = {
|
||||
balance,
|
||||
sendCoins,
|
||||
|
|
@ -242,5 +250,6 @@ module.exports = {
|
|||
getStatus,
|
||||
newFunding,
|
||||
cryptoNetwork,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function newFunding (account, cryptoCode, settings, operatorId) {
|
|||
throw new E.NotImplementedError()
|
||||
}
|
||||
|
||||
function sweep (account, cryptoCode, hdIndex, settings, operatorId) {
|
||||
function sweep (account, txId, cryptoCode, hdIndex, settings, operatorId) {
|
||||
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 })
|
||||
|
||||
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((res) => {
|
||||
return {
|
||||
|
|
@ -151,11 +151,17 @@ function checkBlockchainStatus (cryptoCode) {
|
|||
.then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing')
|
||||
}
|
||||
|
||||
function getTxHashesByAddress (cryptoCode, address) {
|
||||
checkCryptoCode(cryptoCode)
|
||||
.then(() => fetch('getaddresstxids', [address]))
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
balance,
|
||||
sendCoins,
|
||||
newAddress,
|
||||
getStatus,
|
||||
newFunding,
|
||||
checkBlockchainStatus
|
||||
checkBlockchainStatus,
|
||||
getTxHashesByAddress
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,8 @@ const processBatches = require('./tx-batching-processing')
|
|||
|
||||
const INCOMING_TX_INTERVAL = 30 * 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 SWEEP_HD_INTERVAL = T.minute
|
||||
const SWEEP_HD_INTERVAL = 5 * T.minute
|
||||
const TRADE_INTERVAL = 60 * T.seconds
|
||||
const PONG_INTERVAL = 10 * T.seconds
|
||||
const LOGS_CLEAR_INTERVAL = 1 * T.day
|
||||
|
|
@ -61,7 +59,6 @@ const QUEUE = {
|
|||
SLOW: SLOW_QUEUE
|
||||
}
|
||||
|
||||
const coinFilter = ['ETH']
|
||||
const schemaCallbacks = new Map()
|
||||
|
||||
const cachedVariables = new NodeCache({
|
||||
|
|
@ -168,12 +165,8 @@ function doPolling (schema) {
|
|||
pi().executeTrades()
|
||||
pi().pong()
|
||||
pi().clearOldLogs()
|
||||
cashOutTx.monitorLiveIncoming(settings(), false, coinFilter)
|
||||
cashOutTx.monitorStaleIncoming(settings(), false, coinFilter)
|
||||
if (!_.isEmpty(coinFilter)) {
|
||||
cashOutTx.monitorLiveIncoming(settings(), true, coinFilter)
|
||||
cashOutTx.monitorStaleIncoming(settings(), true, coinFilter)
|
||||
}
|
||||
cashOutTx.monitorLiveIncoming(settings())
|
||||
cashOutTx.monitorStaleIncoming(settings())
|
||||
cashOutTx.monitorUnnotified(settings())
|
||||
pi().sweepHd()
|
||||
notifier.checkNotification(pi())
|
||||
|
|
@ -181,12 +174,8 @@ function doPolling (schema) {
|
|||
|
||||
addToQueue(pi().getRawRates, TICKER_RATES_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.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter)
|
||||
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.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
||||
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
|
||||
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_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)
|
||||
|
|
|
|||
|
|
@ -85,14 +85,9 @@ function fetchStatusTx (txId, status) {
|
|||
})
|
||||
}
|
||||
|
||||
function updateDeviceConfigVersion (versionId) {
|
||||
return db.none('update devices set user_config_id=$1', [versionId])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stateChange,
|
||||
fetchPhoneTx,
|
||||
fetchStatusTx,
|
||||
updateDeviceConfigVersion,
|
||||
httpError
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,21 +6,36 @@ const notifier = require('../notifier')
|
|||
const { getMachine, setMachine } = require('../machine-loader')
|
||||
const { loadLatestConfig } = require('../new-settings-loader')
|
||||
const { getCashInSettings } = require('../new-config-manager')
|
||||
const { AUTOMATIC } = require('../constants.js')
|
||||
const { AUTOMATIC } = require('../constants')
|
||||
const logger = require('../logger')
|
||||
|
||||
function notifyCashboxRemoval (req, res, next) {
|
||||
const operatorId = res.locals.operatorId
|
||||
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`)
|
||||
|
||||
return notifier.cashboxNotify(req.deviceId)
|
||||
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
||||
.then(([machine, config]) => {
|
||||
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
|
||||
const cashInSettings = getCashInSettings(config)
|
||||
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' })
|
||||
}
|
||||
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)
|
||||
.then(() => setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId))
|
||||
.then(() => res.status(200).send({ status: 'OK' }))
|
||||
.then(() => {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const semver = require('semver')
|
||||
const sms = require('../sms')
|
||||
const _ = require('lodash/fp')
|
||||
const BN = require('../bn')
|
||||
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 complianceTriggers = require('../compliance-triggers')
|
||||
const configManager = require('../new-config-manager')
|
||||
|
|
@ -18,6 +19,7 @@ const { getTx } = require('../new-admin/services/transactions.js')
|
|||
const machineLoader = require('../machine-loader')
|
||||
const { loadLatestConfig } = require('../new-settings-loader')
|
||||
const customInfoRequestQueries = require('../new-admin/services/customInfoRequests')
|
||||
const T = require('../time')
|
||||
|
||||
function updateCustomerCustomInfoRequest (customerId, patch, req, res) {
|
||||
if (_.isNil(patch.data)) {
|
||||
|
|
@ -112,9 +114,9 @@ function triggerSuspend (req, res, next) {
|
|||
|
||||
const days = triggerId === 'no-ff-camera' ? 1 : getSuspendDays(triggers)
|
||||
|
||||
const date = new Date()
|
||||
date.setDate(date.getDate() + days)
|
||||
customers.update(id, { suspendedUntil: date })
|
||||
const suspensionDuration = intervalToDuration({ start: 0, end: T.day * days })
|
||||
|
||||
customers.update(id, { suspendedUntil: add(suspensionDuration, new Date()) })
|
||||
.then(customer => {
|
||||
notifier.complianceNotify(customer, req.deviceId, 'SUSPENDED', days)
|
||||
return respond(req, res, { customer })
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
const _ = require('lodash/fp')
|
||||
const mem = require('mem')
|
||||
const configManager = require('./new-config-manager')
|
||||
const logger = require('./logger')
|
||||
|
|
@ -9,6 +10,8 @@ const bitpay = require('./plugins/ticker/bitpay')
|
|||
|
||||
const FETCH_INTERVAL = 58000
|
||||
|
||||
const PEGGED_FIAT_CURRENCIES = { NAD: 'ZAR' }
|
||||
|
||||
function _getRates (settings, fiatCode, cryptoCode) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
|
|
@ -33,9 +36,12 @@ function _getRates (settings, fiatCode, cryptoCode) {
|
|||
}
|
||||
|
||||
function buildTicker (fiatCode, cryptoCode, tickerName) {
|
||||
if (tickerName === 'bitpay') return bitpay.ticker(fiatCode, cryptoCode)
|
||||
if (tickerName === 'mock-ticker') return mockTicker.ticker(fiatCode, cryptoCode)
|
||||
return ccxt.ticker(fiatCode, cryptoCode, tickerName)
|
||||
const fiatPeggedEquivalent = _.includes(fiatCode, _.keys(PEGGED_FIAT_CURRENCIES))
|
||||
? PEGGED_FIAT_CURRENCIES[fiatCode]
|
||||
: 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, {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
const ph = require('./plugin-helper')
|
||||
const _ = require('lodash/fp')
|
||||
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 wallet = cryptoCode ? ph.load(ph.WALLET, configManager.getWalletSettings(cryptoCode, settings.config).wallet) : null
|
||||
const plugin = ph.load(ph.WALLET_SCORING, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
return { plugin, account }
|
||||
return { plugin, account, wallet }
|
||||
}
|
||||
|
||||
function rateWallet (settings, cryptoCode, address) {
|
||||
|
|
@ -31,9 +32,9 @@ function isValidWalletScore (settings, score) {
|
|||
function getTransactionHash (settings, cryptoCode, receivingAddress) {
|
||||
return Promise.resolve()
|
||||
.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 = {
|
||||
rateWallet,
|
||||
isValidWalletScore,
|
||||
getTransactionHash,
|
||||
getInputAddresses
|
||||
getInputAddresses,
|
||||
isWalletScoringEnabled
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const httpError = require('./route-helpers').httpError
|
|||
const logger = require('./logger')
|
||||
const { getOpenBatchCryptoValue } = require('./tx-batching')
|
||||
const BN = require('./bn')
|
||||
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('./constants')
|
||||
|
||||
const FETCH_INTERVAL = 5000
|
||||
const INSUFFICIENT_FUNDS_CODE = 570
|
||||
|
|
@ -169,6 +170,11 @@ function authorizeZeroConf (settings, tx, machineId) {
|
|||
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)) {
|
||||
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)
|
||||
.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) {
|
||||
|
|
@ -255,20 +261,28 @@ function checkBlockchainStatus (settings, cryptoCode) {
|
|||
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
|
||||
}
|
||||
|
||||
const coinFilter = ['ETH']
|
||||
|
||||
const balance = (settings, cryptoCode) => {
|
||||
if (_.includes(coinFilter, cryptoCode)) return balanceFiltered(settings, cryptoCode)
|
||||
return balanceUnfiltered(settings, cryptoCode)
|
||||
return fetchWallet(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, {
|
||||
maxAge: FETCH_INTERVAL,
|
||||
const balanceNormal = mem(_balance, {
|
||||
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL * FETCH_INTERVAL,
|
||||
cacheKey: (settings, cryptoCode) => cryptoCode
|
||||
})
|
||||
|
||||
const balanceFiltered = mem(_balance, {
|
||||
maxAge: 3 * FETCH_INTERVAL,
|
||||
const balanceSlow = mem(_balance, {
|
||||
maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW * FETCH_INTERVAL,
|
||||
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 FilterIcon } from 'src/styling/icons/button/filter/white.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'
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ const SearchFilter = ({
|
|||
filters,
|
||||
onFilterDelete,
|
||||
deleteAllFilters,
|
||||
entries
|
||||
entries = 0
|
||||
}) => {
|
||||
const chipClasses = useChipStyles()
|
||||
const classes = useStyles()
|
||||
|
|
@ -40,8 +40,11 @@ const SearchFilter = ({
|
|||
</div>
|
||||
<div className={classes.deleteWrapper}>
|
||||
{
|
||||
<Label3 className={classes.entries}>{`${entries ??
|
||||
0} entries`}</Label3>
|
||||
<Label3 className={classes.entries}>{`${entries} ${singularOrPlural(
|
||||
entries,
|
||||
`entry`,
|
||||
`entries`
|
||||
)}`}</Label3>
|
||||
}
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import { compareAsc, differenceInDays, set } from 'date-fns/fp'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import Calendar from './Calendar'
|
||||
|
|
@ -37,7 +38,12 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
|||
set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day)
|
||||
)
|
||||
} else {
|
||||
setTo(from)
|
||||
setTo(
|
||||
set(
|
||||
{ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 },
|
||||
R.clone(from)
|
||||
)
|
||||
)
|
||||
setFrom(day)
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ const Header = () => {
|
|||
}
|
||||
|
||||
const mapElement = (
|
||||
{ name, width = DEFAULT_COL_SIZE, header, textAlign },
|
||||
{ name, display, width = DEFAULT_COL_SIZE, header, textAlign },
|
||||
idx
|
||||
) => {
|
||||
const orderClasses = classnames({
|
||||
|
|
@ -99,7 +99,7 @@ const Header = () => {
|
|||
<>{attachOrderedByToComplexHeader(header) ?? header}</>
|
||||
) : (
|
||||
<span className={orderClasses}>
|
||||
{startCase(name)}{' '}
|
||||
{!R.isNil(display) ? display : startCase(name)}{' '}
|
||||
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -257,6 +257,7 @@ const ERow = ({ editing, disabled, lastOfGroup, newRow }) => {
|
|||
size={rowSize}
|
||||
error={editing && hasErrors}
|
||||
newRow={newRow && !hasErrors}
|
||||
shouldShowError
|
||||
errorMessage={errorMessage}>
|
||||
{innerElements.map((it, idx) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ const Analytics = () => {
|
|||
case 'topMachines':
|
||||
return (
|
||||
<TopMachinesWrapper
|
||||
title="Transactions over time"
|
||||
title="Top 5 Machines"
|
||||
representing={representing}
|
||||
period={period}
|
||||
data={R.map(convertFiatToLocale)(filteredData(period.code).current)}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const Graph = ({
|
|||
const GRAPH_MARGIN = useMemo(
|
||||
() => ({
|
||||
top: 25,
|
||||
right: 0.5,
|
||||
right: 3.5,
|
||||
bottom: 27,
|
||||
left: 36.5
|
||||
}),
|
||||
|
|
@ -158,6 +158,12 @@ const Graph = ({
|
|||
.domain(periodDomains[period.code])
|
||||
.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
|
||||
.scaleLinear()
|
||||
.domain([
|
||||
|
|
@ -167,11 +173,11 @@ const Graph = ({
|
|||
.nice()
|
||||
.range([GRAPH_HEIGHT - GRAPH_MARGIN.bottom, GRAPH_MARGIN.top])
|
||||
|
||||
const getAreaInterval = (breakpoints, limits) => {
|
||||
const getAreaInterval = (breakpoints, dataLimits, graphLimits) => {
|
||||
const fullBreakpoints = [
|
||||
limits[1],
|
||||
...R.filter(it => it > limits[0] && it < limits[1], breakpoints),
|
||||
limits[0]
|
||||
graphLimits[1],
|
||||
...R.filter(it => it > dataLimits[0] && it < dataLimits[1], breakpoints),
|
||||
dataLimits[0]
|
||||
]
|
||||
|
||||
const intervals = []
|
||||
|
|
@ -238,7 +244,7 @@ const Graph = ({
|
|||
.selectAll('.tick line')
|
||||
.filter(d => d === 0)
|
||||
.clone()
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right - GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.left)
|
||||
.attr('stroke-width', 1)
|
||||
.attr('stroke', primaryColor)
|
||||
),
|
||||
|
|
@ -276,7 +282,7 @@ const Graph = ({
|
|||
.attr('y1', d => 0.5 + y(d))
|
||||
.attr('y2', d => 0.5 + y(d))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
// Vertical transparent rectangles for events
|
||||
.call(g =>
|
||||
|
|
@ -291,7 +297,8 @@ const Graph = ({
|
|||
const xValue = Math.round(x(d) * 100) / 100
|
||||
const intervals = getAreaInterval(
|
||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||
x.range()
|
||||
x.range(),
|
||||
x2.range()
|
||||
)
|
||||
const interval = getAreaIntervalByX(intervals, xValue)
|
||||
return Math.round((interval[0] - interval[1]) * 100) / 100
|
||||
|
|
@ -307,10 +314,12 @@ const Graph = ({
|
|||
const areas = buildAreas(x.domain())
|
||||
const intervals = getAreaInterval(
|
||||
buildAreas(x.domain()).map(it => Math.round(x(it) * 100) / 100),
|
||||
x.range()
|
||||
x.range(),
|
||||
x2.range()
|
||||
)
|
||||
|
||||
const dateInterval = getDateIntervalByX(areas, intervals, xValue)
|
||||
if (!dateInterval) return
|
||||
const filteredData = data.filter(it => {
|
||||
const created = new Date(it.created)
|
||||
const tzCreated = created.setTime(created.getTime() + offset)
|
||||
|
|
@ -426,6 +435,7 @@ const Graph = ({
|
|||
buildTicks,
|
||||
getPastAndCurrentDayLabels,
|
||||
x,
|
||||
x2,
|
||||
y,
|
||||
period,
|
||||
buildAreas,
|
||||
|
|
@ -482,7 +492,7 @@ const Graph = ({
|
|||
0.5 + y(d3.mean(data, d => new BigNumber(d.fiat).toNumber()) ?? 0)
|
||||
)
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
},
|
||||
[GRAPH_MARGIN, y, data]
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const BlackListModal = ({
|
|||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
||||
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
|
||||
XMR:
|
||||
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,13 +74,13 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
|||
}
|
||||
|
||||
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 notSameOverride = it => !R.eqProps('cryptoCurrencies', override, it)
|
||||
|
||||
const filterMachine = R.filter(R.both(sameMachine, notSameOverride))
|
||||
const removeCoin = removeCoinFromOverride(cryptoOverriden)
|
||||
const removeCoin = removeCoinFromOverride(cryptoOverridden)
|
||||
|
||||
const machineOverrides = R.map(removeCoin)(filterMachine(it))
|
||||
|
||||
|
|
|
|||
|
|
@ -318,7 +318,7 @@ const getOverridesSchema = (values, rawData, locale) => {
|
|||
'deviceId'
|
||||
)(machine)
|
||||
|
||||
const message = `${codes} already overriden for machine: ${machineView}`
|
||||
const message = `${codes} already overridden for machine: ${machineView}`
|
||||
|
||||
return this.createError({ message })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { fromNamespace, namespaces } from 'src/utils/config'
|
|||
|
||||
import CustomersList from './CustomersList'
|
||||
import CreateCustomerModal from './components/CreateCustomerModal'
|
||||
import { getAuthorizedStatus } from './helper'
|
||||
|
||||
const GET_CUSTOMER_FILTERS = gql`
|
||||
query filters {
|
||||
|
|
@ -130,9 +131,20 @@ const Customers = () => {
|
|||
R.path(['customInfoRequests'], customersResponse) ?? []
|
||||
const locale = configData && fromNamespace(namespaces.LOCALE, configData)
|
||||
const triggers = configData && fromNamespace(namespaces.TRIGGERS, configData)
|
||||
const customersData = R.sortWith([
|
||||
R.descend(it => new Date(R.prop('lastActive', it) ?? '0'))
|
||||
])(filteredCustomers ?? [])
|
||||
|
||||
const setAuthorizedStatus = c =>
|
||||
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 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 styles from './CustomersList.styles'
|
||||
import { getAuthorizedStatus, getFormattedPhone, getName } from './helper'
|
||||
import { getFormattedPhone, getName } from './helper'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
|
|
@ -73,11 +73,7 @@ const CustomersList = ({
|
|||
{
|
||||
header: 'Status',
|
||||
width: 191,
|
||||
view: it => (
|
||||
<MainStatus
|
||||
statuses={[getAuthorizedStatus(it, triggers, customRequests)]}
|
||||
/>
|
||||
)
|
||||
view: it => <MainStatus statuses={[it.authorizedStatus]} />
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
const GRAPH_MARGIN = useMemo(
|
||||
() => ({
|
||||
top: 20,
|
||||
right: 0.5,
|
||||
right: 3.5,
|
||||
bottom: 27,
|
||||
left: 33.5
|
||||
}),
|
||||
|
|
@ -211,7 +211,7 @@ const Graph = ({ data, timeFrame, timezone }) => {
|
|||
.attr('y1', d => 0.5 + y(d))
|
||||
.attr('y2', d => 0.5 + y(d))
|
||||
.attr('x1', GRAPH_MARGIN.left)
|
||||
.attr('x2', GRAPH_WIDTH - GRAPH_MARGIN.right)
|
||||
.attr('x2', GRAPH_WIDTH)
|
||||
)
|
||||
// Thick vertical lines
|
||||
.call(g =>
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ const allFields = (getData, onChange, auxElements = []) => {
|
|||
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 =>
|
||||
R.differenceWith((x, y) => x.deviceId === y, it, overridenMachines)
|
||||
R.differenceWith((x, y) => x.deviceId === y, it, overriddenMachines)
|
||||
|
||||
const machineData = getData(['machines'])
|
||||
const countryData = getData(['countries'])
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const GET_TRANSACTIONS = gql`
|
|||
customerId
|
||||
isAnonymous
|
||||
rawTickerPrice
|
||||
profit
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
|||
|
|
@ -74,11 +74,11 @@ const MachineRoute = () => {
|
|||
setLoading(false)
|
||||
},
|
||||
variables: {
|
||||
deviceId: id
|
||||
},
|
||||
billFilters: {
|
||||
deviceId: id,
|
||||
batch: 'none'
|
||||
billFilters: {
|
||||
deviceId: id,
|
||||
batch: 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import WizardSplash from './WizardSplash'
|
|||
import WizardStep from './WizardStep'
|
||||
|
||||
const MODAL_WIDTH = 554
|
||||
const MODAL_HEIGHT = 520
|
||||
const MODAL_HEIGHT = 535
|
||||
const CASHBOX_DEFAULT_CAPACITY = 500
|
||||
|
||||
const CASSETTE_FIELDS = R.map(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Formik, Form, Field } from 'formik'
|
|||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Stepper from 'src/components/Stepper'
|
||||
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||
import { Button } from 'src/components/buttons'
|
||||
|
|
@ -94,6 +95,10 @@ const styles = {
|
|||
},
|
||||
errorMessage: {
|
||||
color: errorColor
|
||||
},
|
||||
stepErrorMessage: {
|
||||
maxWidth: 275,
|
||||
marginTop: 25
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,6 +289,11 @@ const WizardStep = ({
|
|||
= {numberToFiatAmount(cassetteTotal(values))}{' '}
|
||||
{fiatCurrency}
|
||||
</P>
|
||||
{!R.isEmpty(errors) && (
|
||||
<ErrorMessage className={classes.stepErrorMessage}>
|
||||
{R.head(R.values(errors))}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ const CryptoBalanceOverrides = ({ section }) => {
|
|||
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(
|
||||
it => !R.contains(it.code, overridenCryptos)
|
||||
it => !R.contains(it.code, overriddenCryptos)
|
||||
)
|
||||
const suggestions = suggestionFilter(cryptoCurrencies)
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,11 @@ import styles from './FiatBalanceAlerts.styles.js'
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const NAME = 'fiatBalanceAlerts'
|
||||
const CASH_IN_KEY = 'fiatBalanceAlertsCashIn'
|
||||
const CASH_OUT_KEY = 'fiatBalanceAlertsCashOut'
|
||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||
const notesMin = 0
|
||||
const notesMax = 9999999
|
||||
|
||||
const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
||||
const {
|
||||
|
|
@ -36,9 +39,13 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
|||
DEFAULT_NUMBER_OF_CASSETTES
|
||||
)
|
||||
|
||||
const editing = isEditing(NAME)
|
||||
|
||||
const schema = Yup.object().shape({
|
||||
cashInAlertThreshold: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.integer()
|
||||
.min(notesMin)
|
||||
.max(notesMax)
|
||||
.nullable(),
|
||||
fillingPercentageCassette1: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.integer()
|
||||
|
|
@ -71,6 +78,7 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
|||
validateOnChange={false}
|
||||
enableReinitialize
|
||||
initialValues={{
|
||||
cashInAlertThreshold: data?.cashInAlertThreshold ?? '',
|
||||
fillingPercentageCassette1: data?.fillingPercentageCassette1 ?? '',
|
||||
fillingPercentageCassette2: data?.fillingPercentageCassette2 ?? '',
|
||||
fillingPercentageCassette3: data?.fillingPercentageCassette3 ?? '',
|
||||
|
|
@ -79,52 +87,80 @@ const FiatBalance = ({ section, min = 0, max = 100, fieldWidth = 80 }) => {
|
|||
validationSchema={schema}
|
||||
onSubmit={it => save(section, schema.cast(it))}
|
||||
onReset={() => {
|
||||
setEditing(NAME, false)
|
||||
setEditing(CASH_IN_KEY, false)
|
||||
setEditing(CASH_OUT_KEY, false)
|
||||
}}>
|
||||
{({ values }) => (
|
||||
<Form className={classes.form}>
|
||||
<PromptWhenDirty />
|
||||
<Header
|
||||
title="Cash out (Empty)"
|
||||
editing={editing}
|
||||
disabled={isDisabled(NAME)}
|
||||
setEditing={it => setEditing(NAME, 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
|
||||
<>
|
||||
<Form className={classes.form}>
|
||||
<PromptWhenDirty />
|
||||
<Header
|
||||
title="Cash box"
|
||||
editing={isEditing(CASH_IN_KEY)}
|
||||
disabled={isDisabled(CASH_IN_KEY)}
|
||||
setEditing={it => setEditing(CASH_IN_KEY, it)}
|
||||
/>
|
||||
<div className={classes.wrapper}>
|
||||
<div className={classes.first}>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.col2}>
|
||||
<EditableNumber
|
||||
label="Alert me over"
|
||||
name="cashInAlertThreshold"
|
||||
editing={isEditing(CASH_IN_KEY)}
|
||||
displayValue={x => (x === '' ? '-' : x)}
|
||||
decoration="notes"
|
||||
width={fieldWidth}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
),
|
||||
R.times(R.identity, maxNumberOfCassettes)
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<Form className={classes.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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import { transformNumber } from 'src/utils/number'
|
|||
|
||||
import NotificationsCtx from '../NotificationsContext'
|
||||
|
||||
const CASHBOX_KEY = 'cashbox'
|
||||
const CASSETTE_1_KEY = 'fillingPercentageCassette1'
|
||||
const CASSETTE_2_KEY = 'fillingPercentageCassette2'
|
||||
const CASSETTE_3_KEY = 'fillingPercentageCassette3'
|
||||
const CASSETTE_4_KEY = 'fillingPercentageCassette4'
|
||||
const MACHINE_KEY = 'machine'
|
||||
const NAME = 'fiatBalanceOverrides'
|
||||
const DEFAULT_NUMBER_OF_CASSETTES = 2
|
||||
|
||||
const CASSETTE_LIST = [
|
||||
CASSETTE_1_KEY,
|
||||
|
|
@ -25,9 +27,9 @@ const CASSETTE_LIST = [
|
|||
]
|
||||
|
||||
const widthsByNumberOfCassettes = {
|
||||
2: { machine: 230, cassette: 250 },
|
||||
3: { machine: 216, cassette: 270 },
|
||||
4: { machine: 210, cassette: 204 }
|
||||
2: { machine: 230, cashbox: 150, cassette: 250 },
|
||||
3: { machine: 216, cashbox: 150, cassette: 270 },
|
||||
4: { machine: 210, cashbox: 150, cassette: 204 }
|
||||
}
|
||||
|
||||
const FiatBalanceOverrides = ({ config, section }) => {
|
||||
|
|
@ -42,33 +44,35 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
|
||||
const setupValues = data?.fiatBalanceOverrides ?? []
|
||||
const innerSetEditing = it => setEditing(NAME, it)
|
||||
|
||||
const cashoutConfig = it => fromNamespace(it)(config)
|
||||
|
||||
const overridenMachines = R.map(override => override.machine, setupValues)
|
||||
const suggestionFilter = R.filter(
|
||||
it =>
|
||||
!R.includes(it.deviceId, overridenMachines) &&
|
||||
cashoutConfig(it.deviceId).active
|
||||
const overriddenMachines = R.map(override => override.machine, setupValues)
|
||||
const suggestions = R.differenceWith(
|
||||
(it, m) => it.deviceId === m,
|
||||
machines,
|
||||
overriddenMachines
|
||||
)
|
||||
const suggestions = suggestionFilter(machines)
|
||||
|
||||
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] : []
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
[MACHINE_KEY]: null,
|
||||
[CASHBOX_KEY]: '',
|
||||
[CASSETTE_1_KEY]: '',
|
||||
[CASSETTE_2_KEY]: '',
|
||||
[CASSETTE_3_KEY]: '',
|
||||
[CASSETTE_4_KEY]: ''
|
||||
}
|
||||
|
||||
const notesMin = 0
|
||||
const notesMax = 9999999
|
||||
|
||||
const maxNumberOfCassettes = Math.max(
|
||||
...R.map(it => it.numberOfCassettes, machines),
|
||||
2
|
||||
DEFAULT_NUMBER_OF_CASSETTES
|
||||
)
|
||||
|
||||
const percentMin = 0
|
||||
|
|
@ -77,8 +81,14 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
.shape({
|
||||
[MACHINE_KEY]: Yup.string()
|
||||
.label('Machine')
|
||||
.nullable()
|
||||
.required(),
|
||||
[CASHBOX_KEY]: Yup.number()
|
||||
.label('Cash box')
|
||||
.transform(transformNumber)
|
||||
.integer()
|
||||
.min(notesMin)
|
||||
.max(notesMax)
|
||||
.nullable(),
|
||||
[CASSETTE_1_KEY]: Yup.number()
|
||||
.label('Cassette 1')
|
||||
.transform(transformNumber)
|
||||
|
|
@ -108,39 +118,49 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
.max(percentMax)
|
||||
.nullable()
|
||||
})
|
||||
.test((values, context) => {
|
||||
const picked = R.pick(CASSETTE_LIST, values)
|
||||
|
||||
if (CASSETTE_LIST.some(it => !R.isNil(picked[it]))) return
|
||||
|
||||
return context.createError({
|
||||
path: CASSETTE_1_KEY,
|
||||
message: 'At least one of the cassettes must have a value'
|
||||
})
|
||||
})
|
||||
.test((values, context) =>
|
||||
R.any(key => !R.isNil(values[key]), R.prepend(CASHBOX_KEY, CASSETTE_LIST))
|
||||
? undefined
|
||||
: context.createError({
|
||||
path: CASHBOX_KEY,
|
||||
message:
|
||||
'The cash box or at least one of the cassettes must have a value'
|
||||
})
|
||||
)
|
||||
|
||||
const viewMachine = it =>
|
||||
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: MACHINE_KEY,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine,
|
||||
size: 'sm',
|
||||
view: viewMachine,
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: it => R.concat(suggestions, findSuggestion(it)),
|
||||
valueProp: 'deviceId',
|
||||
labelProp: 'name'
|
||||
const elements = R.concat(
|
||||
[
|
||||
{
|
||||
name: MACHINE_KEY,
|
||||
display: 'Machine',
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine,
|
||||
size: 'sm',
|
||||
view: viewMachine,
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
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.until(
|
||||
R.gt(R.__, maxNumberOfCassettes),
|
||||
it => {
|
||||
elements.push({
|
||||
],
|
||||
R.map(
|
||||
it => ({
|
||||
name: `fillingPercentageCassette${it}`,
|
||||
display: `Cash cassette ${it}`,
|
||||
width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette,
|
||||
|
|
@ -152,15 +172,18 @@ const FiatBalanceOverrides = ({ config, section }) => {
|
|||
inputProps: {
|
||||
decimalPlaces: 0
|
||||
},
|
||||
view: it => it?.toString() ?? '—',
|
||||
view: el => el?.toString() ?? '—',
|
||||
isHidden: value =>
|
||||
!cashoutConfig(value.machine).active ||
|
||||
it >
|
||||
machines.find(({ deviceId }) => deviceId === value.machine)
|
||||
?.numberOfCassettes
|
||||
})
|
||||
return R.add(1, it)
|
||||
},
|
||||
1
|
||||
R.defaultTo(
|
||||
0,
|
||||
machines.find(({ deviceId }) => deviceId === value.machine)
|
||||
?.numberOfCassettes
|
||||
)
|
||||
}),
|
||||
R.range(1, maxNumberOfCassettes + 1)
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -153,16 +153,18 @@ const TermsConditions = () => {
|
|||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
title: Yup.string()
|
||||
.required()
|
||||
title: Yup.string('The screen title must be a string')
|
||||
.required('The screen title is required')
|
||||
.max(50, 'Too long'),
|
||||
text: Yup.string().required(),
|
||||
acceptButtonText: Yup.string()
|
||||
.required()
|
||||
.max(50, 'Too long'),
|
||||
cancelButtonText: Yup.string()
|
||||
.required()
|
||||
.max(50, 'Too long')
|
||||
text: Yup.string('The text content must be a string').required(
|
||||
'The text content is required'
|
||||
),
|
||||
acceptButtonText: Yup.string('The accept button text must be a string')
|
||||
.required('The accept button text is required')
|
||||
.max(50, 'The accept button text is 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 (
|
||||
|
|
@ -236,37 +238,42 @@ const TermsConditions = () => {
|
|||
setEditing(false)
|
||||
setError(null)
|
||||
}}>
|
||||
<Form>
|
||||
<PromptWhenDirty />
|
||||
{fields.map((f, idx) => (
|
||||
<div className={classes.row} key={idx}>
|
||||
<Field
|
||||
editing={editing}
|
||||
name={f.name}
|
||||
width={f.width}
|
||||
placeholder={f.placeholder}
|
||||
label={f.label}
|
||||
value={f.value}
|
||||
multiline={f.multiline}
|
||||
rows={f.rows}
|
||||
onFocus={() => setError(null)}
|
||||
/>
|
||||
{({ errors }) => (
|
||||
<Form>
|
||||
<PromptWhenDirty />
|
||||
{fields.map((f, idx) => (
|
||||
<div className={classes.row} key={idx}>
|
||||
<Field
|
||||
editing={editing}
|
||||
name={f.name}
|
||||
width={f.width}
|
||||
placeholder={f.placeholder}
|
||||
label={f.label}
|
||||
value={f.value}
|
||||
multiline={f.multiline}
|
||||
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 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>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import CheckboxInput from 'src/components/inputs/formik/Checkbox'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import { Checkbox, TextInput, NumberInput } from 'src/components/inputs/formik'
|
||||
|
||||
export default {
|
||||
code: 'blockcypher',
|
||||
|
|
@ -11,19 +10,19 @@ export default {
|
|||
{
|
||||
code: 'token',
|
||||
display: 'API Token',
|
||||
component: TextInputFormik,
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'confidenceFactor',
|
||||
display: 'Confidence Factor',
|
||||
component: TextInputFormik,
|
||||
component: NumberInput,
|
||||
face: true
|
||||
},
|
||||
{
|
||||
code: 'rbf',
|
||||
component: CheckboxInput,
|
||||
component: Checkbox,
|
||||
settings: {
|
||||
field: 'wallets_BTC_wallet',
|
||||
enabled: true,
|
||||
|
|
@ -43,7 +42,8 @@ export default {
|
|||
.required('The token is required'),
|
||||
confidenceFactor: Yup.number('The confidence factor must be a number')
|
||||
.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')
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import {
|
|||
offErrorColor
|
||||
} from 'src/styling/variables'
|
||||
import { URI } from 'src/utils/apollo'
|
||||
import { SWEEPABLE_CRYPTOS } from 'src/utils/constants'
|
||||
import * as Customer from 'src/utils/customer'
|
||||
|
||||
import CopyToClipboard from './CopyToClipboard'
|
||||
|
|
@ -88,24 +89,6 @@ const CANCEL_CASH_IN_TRANSACTION = gql`
|
|||
const getCryptoAmount = tx =>
|
||||
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 = '') =>
|
||||
coinUtils.formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')
|
||||
|
||||
|
|
@ -124,7 +107,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
const zip = new JSZip()
|
||||
|
||||
const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
|
||||
onCompleted: data => createCsv(data)
|
||||
onCompleted: data => createCsv(R.filter(it => !R.isEmpty(it), data))
|
||||
})
|
||||
|
||||
const [cancelTransaction] = useMutation(
|
||||
|
|
@ -136,7 +119,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
}
|
||||
)
|
||||
|
||||
const commission = BigNumber(getCommission(tx))
|
||||
const commission = BigNumber(tx.profit)
|
||||
.abs()
|
||||
.toFixed(2, 1) // ROUND_DOWN
|
||||
const commissionPercentage =
|
||||
|
|
@ -407,6 +390,14 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
</ActionButton>
|
||||
)}
|
||||
</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>
|
||||
<Label>Other actions</Label>
|
||||
<div className={classes.otherActionsGroup}>
|
||||
|
|
|
|||
|
|
@ -131,5 +131,8 @@ export default {
|
|||
},
|
||||
error: {
|
||||
color: tomato
|
||||
},
|
||||
swept: {
|
||||
width: 250
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ const GET_TRANSACTIONS = gql`
|
|||
$cryptoCode: String
|
||||
$toAddress: String
|
||||
$status: String
|
||||
$swept: Boolean
|
||||
) {
|
||||
transactions(
|
||||
limit: $limit
|
||||
|
|
@ -87,6 +88,7 @@ const GET_TRANSACTIONS = gql`
|
|||
cryptoCode: $cryptoCode
|
||||
toAddress: $toAddress
|
||||
status: $status
|
||||
swept: $swept
|
||||
) {
|
||||
id
|
||||
txClass
|
||||
|
|
@ -121,6 +123,8 @@ const GET_TRANSACTIONS = gql`
|
|||
rawTickerPrice
|
||||
batchError
|
||||
walletScore
|
||||
profit
|
||||
swept
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -246,7 +250,8 @@ const Transactions = () => {
|
|||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
@ -269,7 +274,8 @@ const Transactions = () => {
|
|||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
@ -287,7 +293,8 @@ const Transactions = () => {
|
|||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status
|
||||
status: filtersObject.status,
|
||||
swept: filtersObject.swept === 'Swept'
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
|
|
|
|||
|
|
@ -173,7 +173,12 @@ const Wizard = ({
|
|||
const classes = useStyles()
|
||||
const isEditing = !R.isNil(toBeEdited)
|
||||
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 onContinue = (values, actions) => {
|
||||
|
|
|
|||
|
|
@ -47,13 +47,15 @@ const getOverridesSchema = (values, customInfoRequests) => {
|
|||
.required()
|
||||
.test({
|
||||
test() {
|
||||
const { requirement } = this.parent
|
||||
if (R.find(R.propEq('requirement', requirement))(values)) {
|
||||
const { id, requirement } = this.parent
|
||||
// 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({
|
||||
message: `Requirement ${displayRequirement(
|
||||
message: `Requirement '${displayRequirement(
|
||||
requirement,
|
||||
customInfoRequests
|
||||
)} already overriden`
|
||||
)}' already overridden`
|
||||
})
|
||||
}
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -253,7 +253,9 @@ const Schema = Yup.object()
|
|||
// TYPE
|
||||
const typeSchema = Yup.object()
|
||||
.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.number()
|
||||
.transform(transformNumber)
|
||||
|
|
@ -297,6 +299,8 @@ const typeSchema = Yup.object()
|
|||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||
}
|
||||
|
||||
if (!triggerType) return
|
||||
|
||||
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||
|
||||
return context.createError({
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ const AdvancedWallet = () => {
|
|||
|
||||
const AdvancedWalletSettingsOverrides = AdvancedWalletSettings.overrides ?? []
|
||||
|
||||
const overridenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(
|
||||
const overriddenCryptos = R.map(R.prop(CRYPTOCURRENCY_KEY))(
|
||||
AdvancedWalletSettingsOverrides
|
||||
)
|
||||
const suggestionFilter = R.filter(
|
||||
it => !R.contains(it.code, overridenCryptos)
|
||||
it => !R.contains(it.code, overriddenCryptos)
|
||||
)
|
||||
const coinSuggestions = suggestionFilter(cryptoCurrencies)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { utils as coinUtils } from '@lamassu/coins'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
|
@ -104,14 +103,7 @@ const Wizard = ({
|
|||
: accountsToSave
|
||||
|
||||
if (isLastStep) {
|
||||
const defaultCryptoUnit = R.head(
|
||||
R.keys(coinUtils.getCryptoCurrency(coin.code).units)
|
||||
)
|
||||
const configToSave = {
|
||||
...newConfig,
|
||||
cryptoUnits: defaultCryptoUnit
|
||||
}
|
||||
return save(toNamespace(coin.code, configToSave), newAccounts)
|
||||
return save(toNamespace(coin.code, newConfig), newAccounts)
|
||||
}
|
||||
|
||||
setState({
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
} from 'src/components/inputs/formik'
|
||||
import { disabledColor } from 'src/styling/variables'
|
||||
import { CURRENCY_MAX } from 'src/utils/constants'
|
||||
import { transformNumber } from 'src/utils/number'
|
||||
import { defaultToZero } from 'src/utils/number'
|
||||
|
||||
const classes = {
|
||||
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 WalletSchema = Yup.object().shape({
|
||||
ticker: Yup.string().required(),
|
||||
wallet: Yup.string().required(),
|
||||
exchange: Yup.string().required(),
|
||||
zeroConf: Yup.string(),
|
||||
zeroConfLimit: Yup.number()
|
||||
.integer()
|
||||
.min(0)
|
||||
ticker: Yup.string('The ticker must be a string').required(
|
||||
'The ticker is required'
|
||||
),
|
||||
wallet: Yup.string('The wallet must be a string').required(
|
||||
'The wallet is required'
|
||||
),
|
||||
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)
|
||||
.transform(transformNumber)
|
||||
.transform(defaultToZero)
|
||||
})
|
||||
|
||||
const AdvancedWalletSchema = Yup.object().shape({
|
||||
|
|
@ -195,7 +201,7 @@ const getAdvancedWalletElementsOverrides = (
|
|||
|
||||
const has0Conf = R.complement(
|
||||
/* 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) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { utils as coinUtils } from '@lamassu/coins'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
|
|
@ -54,13 +53,9 @@ const AllSet = ({ data: currentData, doContinue }) => {
|
|||
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
||||
|
||||
const save = () => {
|
||||
const defaultCryptoUnit = R.head(
|
||||
R.keys(coinUtils.getCryptoCurrency(coin).units)
|
||||
)
|
||||
const adjustedData = {
|
||||
zeroConfLimit: 0,
|
||||
...currentData,
|
||||
cryptoUnits: defaultCryptoUnit
|
||||
...currentData
|
||||
}
|
||||
if (!WalletSchema.isValidSync(adjustedData)) return setError(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ const Blockcypher = ({ addData }) => {
|
|||
value={accounts.blockcypher}
|
||||
save={save}
|
||||
elements={schema.blockcypher.elements}
|
||||
validationSchema={schema.blockcypher.validationSchema}
|
||||
validationSchema={schema.blockcypher.getValidationSchema}
|
||||
buttonLabel={'Continue'}
|
||||
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