Merge branch 'dev' into backport/binance-default-market
This commit is contained in:
commit
5e2ac6ecbf
69 changed files with 1516 additions and 599 deletions
|
|
@ -1,48 +1,49 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const notifierQueries = require('./notifier/queries')
|
const notifierQueries = require('./notifier/queries')
|
||||||
|
|
||||||
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
|
const getBlacklist = () =>
|
||||||
const getBlacklist = () => {
|
db.any(
|
||||||
return db.any(`SELECT * FROM blacklist`).then(res =>
|
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
|
||||||
res.map(item => ({
|
FROM blacklist JOIN blacklist_messages
|
||||||
cryptoCode: item.crypto_code,
|
ON blacklist.blacklist_message_id = blacklist_messages.id`
|
||||||
address: item.address
|
|
||||||
}))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const deleteFromBlacklist = address => {
|
||||||
|
const sql = `DELETE FROM blacklist WHERE address = $1`
|
||||||
|
notifierQueries.clearBlacklistNotification(address)
|
||||||
|
return db.none(sql, [address])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete row from blacklist table by crypto code and address
|
const insertIntoBlacklist = address => {
|
||||||
const deleteFromBlacklist = (cryptoCode, address) => {
|
|
||||||
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2`
|
|
||||||
notifierQueries.clearBlacklistNotification(cryptoCode, address)
|
|
||||||
return db.none(sql, [cryptoCode, address])
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertIntoBlacklist = (cryptoCode, address) => {
|
|
||||||
return db
|
return db
|
||||||
.none(
|
.none(
|
||||||
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);',
|
'INSERT INTO blacklist (address) VALUES ($1);',
|
||||||
[cryptoCode, address]
|
[address]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function blocked (address, cryptoCode) {
|
function blocked (address) {
|
||||||
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2`
|
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
|
||||||
return db.any(sql, [address, cryptoCode])
|
return db.oneOrNone(sql, [address])
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToUsedAddresses (address, cryptoCode) {
|
function getMessages () {
|
||||||
// ETH reuses addresses
|
const sql = `SELECT * FROM blacklist_messages`
|
||||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)`
|
function editBlacklistMessage (id, content) {
|
||||||
return db.oneOrNone(sql, [cryptoCode, address])
|
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
||||||
|
return db.oneOrNone(sql, [content, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
blocked,
|
blocked,
|
||||||
addToUsedAddresses,
|
|
||||||
getBlacklist,
|
getBlacklist,
|
||||||
deleteFromBlacklist,
|
deleteFromBlacklist,
|
||||||
insertIntoBlacklist
|
insertIntoBlacklist,
|
||||||
|
getMessages,
|
||||||
|
editBlacklistMessage
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
||||||
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
||||||
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Bitcoin Core: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
@ -55,6 +59,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||||
|
common.logger.info(`fallbackfee already defined, skipping...`)
|
||||||
|
} else {
|
||||||
|
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
|
||||||
|
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||||
|
common.logger.info(`rpcworkqueue already defined, skipping...`)
|
||||||
|
} else {
|
||||||
|
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
|
||||||
|
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||||
|
}
|
||||||
|
|
||||||
if (isCurrentlyRunning && !isDevMode()) {
|
if (isCurrentlyRunning && !isDevMode()) {
|
||||||
common.logger.info('Starting wallet...')
|
common.logger.info('Starting wallet...')
|
||||||
common.es(`sudo supervisorctl start bitcoin`)
|
common.es(`sudo supervisorctl start bitcoin`)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
|
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop bitcoincash`)
|
common.es(`sudo supervisorctl stop bitcoincash`)
|
||||||
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -29,37 +29,47 @@ module.exports = {
|
||||||
const BINARIES = {
|
const BINARIES = {
|
||||||
BTC: {
|
BTC: {
|
||||||
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
|
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
|
||||||
|
defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
|
||||||
defaultDir: 'bitcoin-0.20.1/bin',
|
defaultDir: 'bitcoin-0.20.1/bin',
|
||||||
url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz',
|
url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz',
|
||||||
|
UrlHash: 'c9840607d230d65f6938b81deaec0b98fe9cb14c3a41a5b13b2c05d044a48422',
|
||||||
dir: 'bitcoin-27.1/bin'
|
dir: 'bitcoin-27.1/bin'
|
||||||
},
|
},
|
||||||
ETH: {
|
ETH: {
|
||||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz',
|
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz',
|
||||||
|
urlHash: 'fff507c90c180443456950e4fc0bf224d26ce5ea6896194ff864c3c3754c136b',
|
||||||
dir: 'geth-linux-amd64-1.14.8-a9523b64'
|
dir: 'geth-linux-amd64-1.14.8-a9523b64'
|
||||||
},
|
},
|
||||||
ZEC: {
|
ZEC: {
|
||||||
url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz',
|
url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz',
|
||||||
|
urlHash: 'd385b9fbeeb145f60b0b339d256cabb342713ed3014cd634cf2d68078365abd2',
|
||||||
dir: 'zcash-5.9.0/bin'
|
dir: 'zcash-5.9.0/bin'
|
||||||
},
|
},
|
||||||
DASH: {
|
DASH: {
|
||||||
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
|
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
|
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
|
||||||
defaultDir: 'dashcore-18.1.0/bin',
|
defaultDir: 'dashcore-18.1.0/bin',
|
||||||
url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
|
urlHash: 'a7d0c1b04d53a9b1b3499eb82182c0fa57f4c8768c16163e5d05971bf45d7928',
|
||||||
dir: 'dashcore-21.1.0/bin'
|
dir: 'dashcore-21.1.0/bin'
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||||
|
defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
|
||||||
defaultDir: 'litecoin-0.18.1/bin',
|
defaultDir: 'litecoin-0.18.1/bin',
|
||||||
url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz',
|
url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz',
|
||||||
|
urlHash: 'ea231c630e2a243cb01affd4c2b95a2be71560f80b64b9f4bceaa13d736aa7cb',
|
||||||
dir: 'litecoin-0.21.3/bin'
|
dir: 'litecoin-0.21.3/bin'
|
||||||
},
|
},
|
||||||
BCH: {
|
BCH: {
|
||||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz',
|
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz',
|
||||||
|
urlHash: '0dcc387cbaa3a039c97ddc8fb99c1fa7bff5dc6e4bd3a01d3c3095f595ad2dce',
|
||||||
dir: 'bitcoin-cash-node-27.1.0/bin',
|
dir: 'bitcoin-cash-node-27.1.0/bin',
|
||||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||||
},
|
},
|
||||||
XMR: {
|
XMR: {
|
||||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2',
|
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2',
|
||||||
|
urlHash: '47c7e6b4b88a57205800a2538065a7874174cd087eedc2526bee1ebcce0cc5e3',
|
||||||
dir: 'monero-x86_64-linux-gnu-v0.18.3.3',
|
dir: 'monero-x86_64-linux-gnu-v0.18.3.3',
|
||||||
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
||||||
}
|
}
|
||||||
|
|
@ -133,10 +143,15 @@ function fetchAndInstall (coinRec) {
|
||||||
if (!binaries) throw new Error(`No such coin: ${coinRec.code}`)
|
if (!binaries) throw new Error(`No such coin: ${coinRec.code}`)
|
||||||
|
|
||||||
const url = requiresUpdate ? binaries.defaultUrl : binaries.url
|
const url = requiresUpdate ? binaries.defaultUrl : binaries.url
|
||||||
|
const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash
|
||||||
const downloadFile = path.basename(url)
|
const downloadFile = path.basename(url)
|
||||||
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
|
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
|
||||||
|
|
||||||
es(`wget -q ${url}`)
|
es(`wget -q ${url}`)
|
||||||
|
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
|
||||||
|
logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
es(`tar -xf ${downloadFile}`)
|
es(`tar -xf ${downloadFile}`)
|
||||||
|
|
||||||
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
|
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Dash Core. This may take a minute...')
|
common.logger.info('Updating Dash Core. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop dash`)
|
common.es(`sudo supervisorctl stop dash`)
|
||||||
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Dash Core: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...')
|
common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop ethereum`)
|
common.es(`sudo supervisorctl stop ethereum`)
|
||||||
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Geth: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop litecoin`)
|
common.es(`sudo supervisorctl stop litecoin`)
|
||||||
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Litecoin Core: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating Monero. This may take a minute...')
|
common.logger.info('Updating Monero. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
||||||
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
|
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Monero: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
|
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
||||||
common.logger.info('Updating your Zcash wallet. This may take a minute...')
|
common.logger.info('Updating your Zcash wallet. This may take a minute...')
|
||||||
common.es(`sudo supervisorctl stop zcash`)
|
common.es(`sudo supervisorctl stop zcash`)
|
||||||
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
|
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
|
||||||
|
if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||||
|
common.logger.info('Failed to update Zcash: Package signature do not match!')
|
||||||
|
return
|
||||||
|
}
|
||||||
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
|
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
|
||||||
|
|
||||||
common.logger.info('Updating wallet...')
|
common.logger.info('Updating wallet...')
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const E = require('../error')
|
||||||
|
|
||||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
const PENDING_INTERVAL_MS = 60 * T.minutes
|
||||||
|
|
||||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||||
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||||
|
|
||||||
const massage = _.flow(_.omit(massageFields),
|
const massage = _.flow(_.omit(massageFields),
|
||||||
|
|
|
||||||
|
|
@ -32,38 +32,41 @@ function post (machineTx, pi) {
|
||||||
return cashInAtomic.atomic(machineTx, pi)
|
return cashInAtomic.atomic(machineTx, pi)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
const updatedTx = r.tx
|
const updatedTx = r.tx
|
||||||
let blacklisted = false
|
|
||||||
let addressReuse = false
|
let addressReuse = false
|
||||||
let walletScore = {}
|
|
||||||
|
|
||||||
const promises = [settingsLoader.loadLatestConfig()]
|
const promises = [settingsLoader.loadLatestConfig()]
|
||||||
|
|
||||||
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||||
if (isFirstPost) {
|
if (isFirstPost) {
|
||||||
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi))
|
promises.push(
|
||||||
|
checkForBlacklisted(updatedTx),
|
||||||
|
doesTxReuseAddress(updatedTx),
|
||||||
|
getWalletScore(updatedTx, pi)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
|
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
||||||
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||||
|
const isBlacklisted = !!blacklisted
|
||||||
|
|
||||||
walletScore = fetchedWalletScore
|
if (isBlacklisted) {
|
||||||
|
|
||||||
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
|
|
||||||
blacklisted = true
|
|
||||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
||||||
} else if (isReusedAddress && rejectAddressReuse) {
|
} else if (isReusedAddress && rejectAddressReuse) {
|
||||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
||||||
addressReuse = true
|
addressReuse = true
|
||||||
}
|
}
|
||||||
return postProcess(r, pi, blacklisted, addressReuse, walletScore)
|
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
||||||
|
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||||
|
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||||
|
.then(_.flow(
|
||||||
|
_.set('bills', machineTx.bills),
|
||||||
|
_.set('blacklisted', isBlacklisted),
|
||||||
|
_.set('blacklistMessage', blacklisted?.content),
|
||||||
|
_.set('addressReuse', addressReuse),
|
||||||
|
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
|
||||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
|
||||||
.then(tx => _.set('bills', machineTx.bills, tx))
|
|
||||||
.then(tx => _.set('blacklisted', blacklisted, tx))
|
|
||||||
.then(tx => _.set('addressReuse', addressReuse, tx))
|
|
||||||
.then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForBlacklisted (tx) {
|
function checkForBlacklisted (tx) {
|
||||||
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
return blacklist.blocked(tx.toAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const camelize = require('./utils')
|
||||||
|
|
||||||
function createCashboxBatch (deviceId, cashboxCount) {
|
function createCashboxBatch (deviceId, cashboxCount) {
|
||||||
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
|
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
|
||||||
const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')`
|
const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
|
||||||
const sql2 = `
|
const sql2 = `
|
||||||
UPDATE bills SET cashbox_batch_id=$1
|
UPDATE bills SET cashbox_batch_id=$1
|
||||||
FROM cash_in_txs
|
FROM cash_in_txs
|
||||||
|
|
@ -25,6 +25,7 @@ function createCashboxBatch (deviceId, cashboxCount) {
|
||||||
const q2 = t.none(sql2, [batchId, deviceId])
|
const q2 = t.none(sql2, [batchId, deviceId])
|
||||||
const q3 = t.none(sql3, [batchId, deviceId])
|
const q3 = t.none(sql3, [batchId, deviceId])
|
||||||
return t.batch([q1, q2, q3])
|
return t.batch([q1, q2, q3])
|
||||||
|
.then(([it]) => it)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,14 +101,6 @@ function editBatchById (id, performedBy) {
|
||||||
return db.none(sql, [performedBy, id])
|
return db.none(sql, [performedBy, id])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBillsByBatchId (id) {
|
|
||||||
const sql = `SELECT bi.* FROM (
|
|
||||||
SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (SELECT id, device_id FROM cash_in_txs) AS cit ON cit.id = b.cash_in_txs_id UNION
|
|
||||||
SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills
|
|
||||||
) AS bi WHERE bi.cashbox_batch_id=$1`
|
|
||||||
return db.any(sql, [id])
|
|
||||||
}
|
|
||||||
|
|
||||||
function logFormatter (data) {
|
function logFormatter (data) {
|
||||||
return _.map(
|
return _.map(
|
||||||
it => {
|
it => {
|
||||||
|
|
@ -124,11 +117,62 @@ function logFormatter (data) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMachineUnbatchedBills (deviceId) {
|
||||||
|
const sql = `
|
||||||
|
SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs
|
||||||
|
ON b.cash_in_txs_id = cash_in_txs.id
|
||||||
|
WHERE b.cashbox_batch_id IS NULL AND cash_in_txs.device_id = $1
|
||||||
|
GROUP BY cash_in_txs.device_id
|
||||||
|
`
|
||||||
|
|
||||||
|
return db.oneOrNone(sql, [deviceId])
|
||||||
|
.then(res => _.mapKeys(it => _.camelCase(it), res))
|
||||||
|
.then(logFormatterSingle)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBatchById (id) {
|
||||||
|
const sql = `
|
||||||
|
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
|
||||||
|
FROM cashbox_batches AS cb
|
||||||
|
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
|
||||||
|
WHERE cb.id = $1
|
||||||
|
GROUP BY cb.id
|
||||||
|
`
|
||||||
|
|
||||||
|
return db.oneOrNone(sql, [id]).then(res => _.mapKeys(it => _.camelCase(it), res))
|
||||||
|
.then(logFormatterSingle)
|
||||||
|
}
|
||||||
|
|
||||||
|
function logFormatterSingle (data) {
|
||||||
|
const bills = _.filter(
|
||||||
|
it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)),
|
||||||
|
data.bills
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: data.id,
|
||||||
|
deviceId: data.deviceId,
|
||||||
|
created: data.created,
|
||||||
|
operationType: data.operationType,
|
||||||
|
billCount: _.size(bills),
|
||||||
|
fiatTotals: _.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
acc[value.fiat_code] = (acc[value.fiat_code] || 0) + value.fiat
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
bills
|
||||||
|
),
|
||||||
|
billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createCashboxBatch,
|
createCashboxBatch,
|
||||||
updateMachineWithBatch,
|
updateMachineWithBatch,
|
||||||
getBatches,
|
getBatches,
|
||||||
getBillsByBatchId,
|
|
||||||
editBatchById,
|
editBatchById,
|
||||||
|
getBatchById,
|
||||||
|
getMachineUnbatchedBills,
|
||||||
logFormatter
|
logFormatter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const { ALL_CRYPTOS } = require('@lamassu/coins')
|
||||||
|
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const ccxt = require('./plugins/exchange/ccxt')
|
const ccxt = require('./plugins/exchange/ccxt')
|
||||||
const mockExchange = require('./plugins/exchange/mock-exchange')
|
const mockExchange = require('./plugins/exchange/mock-exchange')
|
||||||
|
const accounts = require('./new-admin/config/accounts')
|
||||||
|
|
||||||
function lookupExchange (settings, cryptoCode) {
|
function lookupExchange (settings, cryptoCode) {
|
||||||
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
|
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
|
||||||
|
|
@ -45,8 +49,26 @@ function active (settings, cryptoCode) {
|
||||||
return !!lookupExchange(settings, cryptoCode)
|
return !!lookupExchange(settings, cryptoCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMarkets () {
|
||||||
|
const filterExchanges = _.filter(it => it.class === 'exchange')
|
||||||
|
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
|
||||||
|
|
||||||
|
return _.reduce(
|
||||||
|
(acc, value) =>
|
||||||
|
Promise.all([acc, ccxt.getMarkets(value, ALL_CRYPTOS)])
|
||||||
|
.then(([a, markets]) => Promise.resolve({
|
||||||
|
...a,
|
||||||
|
[value]: markets
|
||||||
|
})),
|
||||||
|
Promise.resolve({}),
|
||||||
|
availableExchanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
fetchExchange,
|
||||||
buy,
|
buy,
|
||||||
sell,
|
sell,
|
||||||
active
|
active,
|
||||||
|
getMarkets
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,18 @@ const addReceiptInfo = receiptInfo => ret => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const addMachineScreenOpts = smth => _.update(
|
||||||
|
'screenOptions',
|
||||||
|
_.flow(
|
||||||
|
addSmthInfo(
|
||||||
|
'rates',
|
||||||
|
[
|
||||||
|
'active'
|
||||||
|
]
|
||||||
|
)(smth.rates)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
/* TODO: Simplify this. */
|
/* TODO: Simplify this. */
|
||||||
const buildTriggers = allTriggers => {
|
const buildTriggers = allTriggers => {
|
||||||
const normalTriggers = []
|
const normalTriggers = []
|
||||||
|
|
@ -103,7 +115,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
_.pick([
|
_.pick([
|
||||||
'coins',
|
'coins',
|
||||||
'configVersion',
|
'configVersion',
|
||||||
'timezone'
|
'timezone',
|
||||||
|
'screenOptions'
|
||||||
]),
|
]),
|
||||||
_.update('coins', massageCoins),
|
_.update('coins', massageCoins),
|
||||||
_.set('serverVersion', VERSION),
|
_.set('serverVersion', VERSION),
|
||||||
|
|
@ -117,6 +130,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
configManager.getLocale(deviceId, settings.config),
|
configManager.getLocale(deviceId, settings.config),
|
||||||
configManager.getOperatorInfo(settings.config),
|
configManager.getOperatorInfo(settings.config),
|
||||||
configManager.getReceipt(settings.config),
|
configManager.getReceipt(settings.config),
|
||||||
|
configManager.getAllMachineScreenOpts(settings.config),
|
||||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||||
getMachine(deviceId, currentConfigVersion),
|
getMachine(deviceId, currentConfigVersion),
|
||||||
configManager.getCustomerAuthenticationMethod(settings.config)
|
configManager.getCustomerAuthenticationMethod(settings.config)
|
||||||
|
|
@ -129,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
localeInfo,
|
localeInfo,
|
||||||
operatorInfo,
|
operatorInfo,
|
||||||
receiptInfo,
|
receiptInfo,
|
||||||
|
machineScreenOpts,
|
||||||
twoWayMode,
|
twoWayMode,
|
||||||
{ numberOfCassettes, numberOfRecyclers },
|
{ numberOfCassettes, numberOfRecyclers },
|
||||||
customerAuthentication,
|
customerAuthentication,
|
||||||
|
|
@ -153,7 +168,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
||||||
urlsToPing,
|
urlsToPing,
|
||||||
}),
|
}),
|
||||||
addOperatorInfo(operatorInfo),
|
addOperatorInfo(operatorInfo),
|
||||||
addReceiptInfo(receiptInfo)
|
addReceiptInfo(receiptInfo),
|
||||||
|
addMachineScreenOpts(machineScreenOpts)
|
||||||
)(staticConf))
|
)(staticConf))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@ type ReceiptInfo {
|
||||||
addressQRCode: Boolean!
|
addressQRCode: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MachineScreenOptions {
|
||||||
|
rates: RateScreenOptions!
|
||||||
|
}
|
||||||
|
|
||||||
|
type RateScreenOptions {
|
||||||
|
active: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
type SpeedtestFile {
|
type SpeedtestFile {
|
||||||
url: String!
|
url: String!
|
||||||
size: Int!
|
size: Int!
|
||||||
|
|
@ -147,6 +155,7 @@ type StaticConfig {
|
||||||
operatorInfo: OperatorInfo
|
operatorInfo: OperatorInfo
|
||||||
machineInfo: MachineInfo!
|
machineInfo: MachineInfo!
|
||||||
receiptInfo: ReceiptInfo
|
receiptInfo: ReceiptInfo
|
||||||
|
screenOptions: MachineScreenOptions
|
||||||
|
|
||||||
speedtestFiles: [SpeedtestFile!]!
|
speedtestFiles: [SpeedtestFile!]!
|
||||||
urlsToPing: [String!]!
|
urlsToPing: [String!]!
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
|
||||||
|
|
||||||
const resolvers = {
|
const resolvers = {
|
||||||
Query: {
|
Query: {
|
||||||
blacklist: () => blacklist.getBlacklist()
|
blacklist: () => blacklist.getBlacklist(),
|
||||||
|
blacklistMessages: () => blacklist.getMessages()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
deleteBlacklistRow: (...[, { address }]) =>
|
||||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
blacklist.deleteFromBlacklist(address),
|
||||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
insertBlacklistRow: (...[, { address }]) =>
|
||||||
blacklist.insertIntoBlacklist(cryptoCode, address)
|
blacklist.insertIntoBlacklist(address),
|
||||||
|
editBlacklistMessage: (...[, { id, content }]) =>
|
||||||
|
blacklist.editBlacklistMessage(id, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const funding = require('./funding.resolver')
|
||||||
const log = require('./log.resolver')
|
const log = require('./log.resolver')
|
||||||
const loyalty = require('./loyalty.resolver')
|
const loyalty = require('./loyalty.resolver')
|
||||||
const machine = require('./machine.resolver')
|
const machine = require('./machine.resolver')
|
||||||
|
const market = require('./market.resolver')
|
||||||
const notification = require('./notification.resolver')
|
const notification = require('./notification.resolver')
|
||||||
const pairing = require('./pairing.resolver')
|
const pairing = require('./pairing.resolver')
|
||||||
const rates = require('./rates.resolver')
|
const rates = require('./rates.resolver')
|
||||||
|
|
@ -35,6 +36,7 @@ const resolvers = [
|
||||||
log,
|
log,
|
||||||
loyalty,
|
loyalty,
|
||||||
machine,
|
machine,
|
||||||
|
market,
|
||||||
notification,
|
notification,
|
||||||
pairing,
|
pairing,
|
||||||
rates,
|
rates,
|
||||||
|
|
|
||||||
9
lib/new-admin/graphql/resolvers/market.resolver.js
Normal file
9
lib/new-admin/graphql/resolvers/market.resolver.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const exchange = require('../../../exchange')
|
||||||
|
|
||||||
|
const resolvers = {
|
||||||
|
Query: {
|
||||||
|
getMarkets: () => exchange.getMarkets()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = resolvers
|
||||||
|
|
@ -2,17 +2,26 @@ const { gql } = require('apollo-server-express')
|
||||||
|
|
||||||
const typeDef = gql`
|
const typeDef = gql`
|
||||||
type Blacklist {
|
type Blacklist {
|
||||||
cryptoCode: String!
|
|
||||||
address: String!
|
address: String!
|
||||||
|
blacklistMessage: BlacklistMessage!
|
||||||
|
}
|
||||||
|
|
||||||
|
type BlacklistMessage {
|
||||||
|
id: ID
|
||||||
|
label: String
|
||||||
|
content: String
|
||||||
|
allowToggle: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
blacklist: [Blacklist] @auth
|
blacklist: [Blacklist] @auth
|
||||||
|
blacklistMessages: [BlacklistMessage] @auth
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
deleteBlacklistRow(address: String!): Blacklist @auth
|
||||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
insertBlacklistRow(address: String!): Blacklist @auth
|
||||||
|
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ const funding = require('./funding.type')
|
||||||
const log = require('./log.type')
|
const log = require('./log.type')
|
||||||
const loyalty = require('./loyalty.type')
|
const loyalty = require('./loyalty.type')
|
||||||
const machine = require('./machine.type')
|
const machine = require('./machine.type')
|
||||||
|
const market = require('./market.type')
|
||||||
const notification = require('./notification.type')
|
const notification = require('./notification.type')
|
||||||
const pairing = require('./pairing.type')
|
const pairing = require('./pairing.type')
|
||||||
const rates = require('./rates.type')
|
const rates = require('./rates.type')
|
||||||
|
|
@ -35,6 +36,7 @@ const types = [
|
||||||
log,
|
log,
|
||||||
loyalty,
|
loyalty,
|
||||||
machine,
|
machine,
|
||||||
|
market,
|
||||||
notification,
|
notification,
|
||||||
pairing,
|
pairing,
|
||||||
rates,
|
rates,
|
||||||
|
|
|
||||||
9
lib/new-admin/graphql/types/market.type.js
Normal file
9
lib/new-admin/graphql/types/market.type.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
const { gql } = require('apollo-server-express')
|
||||||
|
|
||||||
|
const typeDef = gql`
|
||||||
|
type Query {
|
||||||
|
getMarkets: JSONObject @auth
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
module.exports = typeDef
|
||||||
|
|
@ -50,6 +50,7 @@ function batch (
|
||||||
excludeTestingCustomers = false,
|
excludeTestingCustomers = false,
|
||||||
simplified
|
simplified
|
||||||
) {
|
) {
|
||||||
|
const isCsvExport = _.isBoolean(simplified)
|
||||||
const packager = _.flow(
|
const packager = _.flow(
|
||||||
_.flatten,
|
_.flatten,
|
||||||
_.orderBy(_.property('created'), ['desc']),
|
_.orderBy(_.property('created'), ['desc']),
|
||||||
|
|
@ -92,7 +93,7 @@ function batch (
|
||||||
AND ($12 is null or txs.to_address = $12)
|
AND ($12 is null or txs.to_address = $12)
|
||||||
AND ($13 is null or txs.txStatus = $13)
|
AND ($13 is null or txs.txStatus = $13)
|
||||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||||
AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)
|
${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'}
|
||||||
ORDER BY created DESC limit $4 offset $5`
|
ORDER BY created DESC limit $4 offset $5`
|
||||||
|
|
||||||
const cashOutSql = `SELECT 'cashOut' AS tx_class,
|
const cashOutSql = `SELECT 'cashOut' AS tx_class,
|
||||||
|
|
@ -126,7 +127,7 @@ function batch (
|
||||||
AND ($13 is null or txs.txStatus = $13)
|
AND ($13 is null or txs.txStatus = $13)
|
||||||
AND ($14 is null or txs.swept = $14)
|
AND ($14 is null or txs.swept = $14)
|
||||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||||
AND (fiat > 0)
|
${isCsvExport ? '' : 'AND fiat > 0'}
|
||||||
ORDER BY created DESC limit $4 offset $5`
|
ORDER BY created DESC limit $4 offset $5`
|
||||||
|
|
||||||
// The swept filter is cash-out only, so omit the cash-in query entirely
|
// The swept filter is cash-out only, so omit the cash-in query entirely
|
||||||
|
|
@ -152,14 +153,14 @@ function batch (
|
||||||
|
|
||||||
return Promise.all(promises)
|
return Promise.all(promises)
|
||||||
.then(packager)
|
.then(packager)
|
||||||
.then(res => {
|
.then(res =>
|
||||||
if (simplified) return simplifiedBatch(res)
|
!isCsvExport ? res :
|
||||||
// GQL transactions and transactionsCsv both use this function and
|
// GQL transactions and transactionsCsv both use this function and
|
||||||
// if we don't check for the correct simplified value, the Transactions page polling
|
// if we don't check for the correct simplified value, the Transactions page polling
|
||||||
// will continuously build a csv in the background
|
// will continuously build a csv in the background
|
||||||
else if (simplified === false) return advancedBatch(res)
|
simplified ? simplifiedBatch(res) :
|
||||||
return res
|
advancedBatch(res)
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function advancedBatch (data) {
|
function advancedBatch (data) {
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,12 @@ const namespaces = {
|
||||||
TERMS_CONDITIONS: 'termsConditions',
|
TERMS_CONDITIONS: 'termsConditions',
|
||||||
CASH_OUT: 'cashOut',
|
CASH_OUT: 'cashOut',
|
||||||
CASH_IN: 'cashIn',
|
CASH_IN: 'cashIn',
|
||||||
COMPLIANCE: 'compliance'
|
COMPLIANCE: 'compliance',
|
||||||
|
MACHINE_SCREENS: 'machineScreens'
|
||||||
|
}
|
||||||
|
|
||||||
|
const machineScreens = {
|
||||||
|
RATES: 'rates'
|
||||||
}
|
}
|
||||||
|
|
||||||
const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str)
|
const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str)
|
||||||
|
|
@ -72,6 +77,8 @@ const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR)
|
||||||
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
|
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
|
||||||
const getReceipt = fromNamespace(namespaces.RECEIPT)
|
const getReceipt = fromNamespace(namespaces.RECEIPT)
|
||||||
const getCompliance = fromNamespace(namespaces.COMPLIANCE)
|
const getCompliance = fromNamespace(namespaces.COMPLIANCE)
|
||||||
|
const getMachineScreenOpts = (screenName, config) => _.compose(fromNamespace(screenName), fromNamespace(namespaces.MACHINE_SCREENS))(config)
|
||||||
|
const getAllMachineScreenOpts = config => _.reduce((acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), {}, _.values(machineScreens))
|
||||||
|
|
||||||
const getAllCryptoCurrencies = (config) => {
|
const getAllCryptoCurrencies = (config) => {
|
||||||
const locale = fromNamespace(namespaces.LOCALE)(config)
|
const locale = fromNamespace(namespaces.LOCALE)(config)
|
||||||
|
|
@ -180,6 +187,8 @@ module.exports = {
|
||||||
getWalletSettings,
|
getWalletSettings,
|
||||||
getCashInSettings,
|
getCashInSettings,
|
||||||
getOperatorInfo,
|
getOperatorInfo,
|
||||||
|
getMachineScreenOpts,
|
||||||
|
getAllMachineScreenOpts,
|
||||||
getNotifications,
|
getNotifications,
|
||||||
getGlobalNotifications,
|
getGlobalNotifications,
|
||||||
getLocale,
|
getLocale,
|
||||||
|
|
|
||||||
|
|
@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) {
|
||||||
.then(_.compose(_.defaultTo({}), _.get('data.accounts')))
|
.then(_.compose(_.defaultTo({}), _.get('data.accounts')))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideSecretFields (accounts) {
|
||||||
|
return _.flow(
|
||||||
|
_.filter(path => !_.isEmpty(_.get(path, accounts))),
|
||||||
|
_.reduce(
|
||||||
|
(accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts),
|
||||||
|
accounts
|
||||||
|
)
|
||||||
|
)(SECRET_FIELDS)
|
||||||
|
}
|
||||||
|
|
||||||
function showAccounts (schemaVersion) {
|
function showAccounts (schemaVersion) {
|
||||||
return loadAccounts(schemaVersion)
|
return loadAccounts(schemaVersion)
|
||||||
.then(accounts => {
|
.then(hideSecretFields)
|
||||||
const filledSecretPaths = _.compact(_.map(path => {
|
|
||||||
if (!_.isEmpty(_.get(path, accounts))) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}, SECRET_FIELDS))
|
|
||||||
return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const insertConfigRow = (dbOrTx, data) =>
|
const insertConfigRow = (dbOrTx, data) =>
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ function plugins (settings, deviceId) {
|
||||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
const fiatCode = localeConfig.fiatCurrency
|
||||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||||
|
const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config)
|
||||||
|
|
||||||
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
|
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
|
||||||
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
||||||
|
|
@ -327,7 +328,8 @@ function plugins (settings, deviceId) {
|
||||||
coins,
|
coins,
|
||||||
configVersion,
|
configVersion,
|
||||||
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
|
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
|
||||||
timezone
|
timezone,
|
||||||
|
screenOptions: machineScreenOpts
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -475,25 +477,28 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
function buyAndSell (rec, doBuy, tx) {
|
function buyAndSell (rec, doBuy, tx) {
|
||||||
const cryptoCode = rec.cryptoCode
|
const cryptoCode = rec.cryptoCode
|
||||||
const fiatCode = rec.fiatCode
|
return exchange.fetchExchange(settings, cryptoCode)
|
||||||
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
|
.then(_exchange => {
|
||||||
|
const fiatCode = _exchange.account.currencyMarket
|
||||||
|
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
|
||||||
|
|
||||||
const market = [fiatCode, cryptoCode].join('')
|
const market = [fiatCode, cryptoCode].join('')
|
||||||
|
|
||||||
if (!exchange.active(settings, cryptoCode)) return
|
if (!exchange.active(settings, cryptoCode)) return
|
||||||
|
|
||||||
const direction = doBuy ? 'cashIn' : 'cashOut'
|
const direction = doBuy ? 'cashIn' : 'cashOut'
|
||||||
const internalTxId = tx ? tx.id : rec.id
|
const internalTxId = tx ? tx.id : rec.id
|
||||||
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
|
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
|
||||||
if (!tradesQueues[market]) tradesQueues[market] = []
|
if (!tradesQueues[market]) tradesQueues[market] = []
|
||||||
tradesQueues[market].push({
|
tradesQueues[market].push({
|
||||||
direction,
|
direction,
|
||||||
internalTxId,
|
internalTxId,
|
||||||
fiatCode,
|
fiatCode,
|
||||||
cryptoAtoms,
|
cryptoAtoms,
|
||||||
cryptoCode,
|
cryptoCode,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function consolidateTrades (cryptoCode, fiatCode) {
|
function consolidateTrades (cryptoCode, fiatCode) {
|
||||||
|
|
@ -550,19 +555,22 @@ function plugins (settings, deviceId) {
|
||||||
const deviceIds = devices.map(device => device.deviceId)
|
const deviceIds = devices.map(device => device.deviceId)
|
||||||
const lists = deviceIds.map(deviceId => {
|
const lists = deviceIds.map(deviceId => {
|
||||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||||
const fiatCode = localeConfig.fiatCurrency
|
|
||||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||||
|
|
||||||
return cryptoCodes.map(cryptoCode => ({
|
return Promise.all(cryptoCodes.map(cryptoCode => {
|
||||||
fiatCode,
|
return exchange.fetchExchange(settings, cryptoCode)
|
||||||
cryptoCode
|
.then(exchange => ({
|
||||||
|
fiatCode: exchange.account.currencyMarket,
|
||||||
|
cryptoCode
|
||||||
|
}))
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
const tradesPromises = _.uniq(_.flatten(lists))
|
return Promise.all(lists)
|
||||||
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
|
})
|
||||||
|
.then(lists => {
|
||||||
return Promise.all(tradesPromises)
|
return Promise.all(_.uniq(_.flatten(lists))
|
||||||
|
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)))
|
||||||
})
|
})
|
||||||
.catch(logger.error)
|
.catch(logger.error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,11 +34,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) {
|
||||||
if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) {
|
if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) {
|
||||||
throw new Error('Unsupported crypto: ' + cryptoCode)
|
throw new Error('Unsupported crypto: ' + cryptoCode)
|
||||||
}
|
}
|
||||||
const fiatSupported = ALL[serviceName].FIAT
|
|
||||||
if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) {
|
if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code')
|
||||||
logger.info('Building a market for an unsupported fiat. Defaulting to EUR market')
|
|
||||||
return cryptoCode + '/' + 'EUR'
|
|
||||||
}
|
|
||||||
return cryptoCode + '/' + fiatCode
|
return cryptoCode + '/' + fiatCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN]
|
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN]
|
||||||
const FIAT = ['EUR']
|
const FIAT = ['EUR']
|
||||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const DEFAULT_FIAT_MARKET = 'USD'
|
||||||
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
||||||
return { ...mapped, timeout: 3000 }
|
return { ...mapped, timeout: 3000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 8
|
const AMOUNT_PRECISION = 8
|
||||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret']
|
const REQUIRED_CONFIG_FIELDS = ['key', 'secret']
|
||||||
|
|
||||||
|
|
@ -18,4 +19,4 @@ const loadConfig = (account) => {
|
||||||
return { ...mapped, timeout: 3000 }
|
return { ...mapped, timeout: 3000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 8
|
const AMOUNT_PRECISION = 8
|
||||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket']
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
|
|
@ -19,4 +20,4 @@ const loadConfig = (account) => {
|
||||||
return { ...mapped, timeout: 3000 }
|
return { ...mapped, timeout: 3000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
const { utils: coinUtils } = require('@lamassu/coins')
|
const { utils: coinUtils } = require('@lamassu/coins')
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const ccxt = require('ccxt')
|
const ccxt = require('ccxt')
|
||||||
|
const mem = require('mem')
|
||||||
|
|
||||||
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
|
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
|
||||||
const { ORDER_TYPES } = require('./consts')
|
const { ORDER_TYPES } = require('./consts')
|
||||||
|
const logger = require('../../logger')
|
||||||
|
const { currencies } = require('../../new-admin/config')
|
||||||
|
const T = require('../../time')
|
||||||
|
|
||||||
const DEFAULT_PRICE_PRECISION = 2
|
const DEFAULT_PRICE_PRECISION = 2
|
||||||
const DEFAULT_AMOUNT_PRECISION = 8
|
const DEFAULT_AMOUNT_PRECISION = 8
|
||||||
|
|
@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) {
|
||||||
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
|
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
|
||||||
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
|
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
|
||||||
|
|
||||||
const symbol = buildMarket(fiatCode, cryptoCode, exchangeName)
|
const selectedFiatMarket = account.currencyMarket
|
||||||
|
const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName)
|
||||||
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
|
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
|
||||||
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
|
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
|
||||||
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
|
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
|
||||||
|
|
@ -50,4 +55,36 @@ function calculatePrice (side, amount, orderBook) {
|
||||||
throw new Error('Insufficient market depth')
|
throw new Error('Insufficient market depth')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { trade }
|
function _getMarkets (exchangeName, availableCryptos) {
|
||||||
|
try {
|
||||||
|
const exchange = new ccxt[exchangeName]()
|
||||||
|
const cryptosToQuoteAgainst = ['USDT']
|
||||||
|
const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst)
|
||||||
|
|
||||||
|
return exchange.fetchMarkets()
|
||||||
|
.then(_.filter(it => (it.type === 'spot' || it.spot)))
|
||||||
|
.then(res =>
|
||||||
|
_.reduce((acc, value) => {
|
||||||
|
if (_.includes(value.base, availableCryptos) && _.includes(value.quote, currencyCodes)) {
|
||||||
|
if (value.quote === value.base) return acc
|
||||||
|
|
||||||
|
if (_.isNil(acc[value.quote])) {
|
||||||
|
return { ...acc, [value.quote]: [value.base] }
|
||||||
|
}
|
||||||
|
|
||||||
|
acc[value.quote].push(value.base)
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {}, res)
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`No CCXT exchange found for ${exchangeName}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMarkets = mem(_getMarkets, {
|
||||||
|
maxAge: T.week,
|
||||||
|
cacheKey: (exchangeName, availableCryptos) => exchangeName
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = { trade, getMarkets }
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
|
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
||||||
return { ...mapped, timeout: 3000 }
|
return { ...mapped, timeout: 3000 }
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.LIMIT
|
||||||
const { BTC, ETH, USDT, LN } = COINS
|
const { BTC, ETH, USDT, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, USDT, LN]
|
const CRYPTO = [BTC, ETH, USDT, LN]
|
||||||
const FIAT = ['USD']
|
const FIAT = ['USD']
|
||||||
|
const DEFAULT_FIAT_MARKET = 'USD'
|
||||||
const AMOUNT_PRECISION = 4
|
const AMOUNT_PRECISION = 4
|
||||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']
|
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket']
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
const mapper = {
|
const mapper = {
|
||||||
|
|
@ -21,4 +22,4 @@ const loadConfig = (account) => {
|
||||||
}
|
}
|
||||||
const loadOptions = ({ walletId }) => ({ walletId })
|
const loadOptions = ({ walletId }) => ({ walletId })
|
||||||
|
|
||||||
module.exports = { loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
|
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
|
||||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
|
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
|
||||||
const FIAT = ['USD', 'EUR']
|
const FIAT = ['USD', 'EUR']
|
||||||
|
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||||
const AMOUNT_PRECISION = 6
|
const AMOUNT_PRECISION = 6
|
||||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||||
const USER_REF = 'userref'
|
const USER_REF = 'userref'
|
||||||
|
|
||||||
const loadConfig = (account) => {
|
const loadConfig = (account) => {
|
||||||
|
|
@ -26,4 +27,4 @@ const loadConfig = (account) => {
|
||||||
|
|
||||||
const loadOptions = () => ({ expiretm: '+60' })
|
const loadOptions = () => ({ expiretm: '+60' })
|
||||||
|
|
||||||
module.exports = { USER_REF, loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,15 @@ morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||||
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
||||||
|
|
||||||
|
app.use('/robots.txt', (req, res) => {
|
||||||
|
res.type('text/plain')
|
||||||
|
res.send("User-agent: *\nDisallow: /")
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/', (req, res) => {
|
||||||
|
res.sendStatus(404)
|
||||||
|
})
|
||||||
|
|
||||||
// app /pair and /ca routes
|
// app /pair and /ca routes
|
||||||
app.use('/', pairingRoutes)
|
app.use('/', pairingRoutes)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,41 @@
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
|
const _ = require('lodash/fp')
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
const cashbox = require('../cashbox-batches')
|
const cashbox = require('../cashbox-batches')
|
||||||
const notifier = require('../notifier')
|
const notifier = require('../notifier')
|
||||||
const { getMachine, setMachine } = require('../machine-loader')
|
const { getMachine, setMachine, getMachineName } = require('../machine-loader')
|
||||||
const { loadLatestConfig } = require('../new-settings-loader')
|
const { loadLatestConfig } = require('../new-settings-loader')
|
||||||
const { getCashInSettings } = require('../new-config-manager')
|
const { getCashInSettings } = require('../new-config-manager')
|
||||||
const { AUTOMATIC } = require('../constants')
|
const { AUTOMATIC } = require('../constants')
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
function notifyCashboxRemoval (req, res, next) {
|
|
||||||
|
function cashboxRemoval (req, res, next) {
|
||||||
const operatorId = res.locals.operatorId
|
const operatorId = res.locals.operatorId
|
||||||
|
|
||||||
logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`)
|
notifier.cashboxNotify(req.deviceId).catch(logger.error)
|
||||||
|
|
||||||
return notifier.cashboxNotify(req.deviceId)
|
return Promise.all([getMachine(req.deviceId), loadLatestConfig()])
|
||||||
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
|
||||||
.then(([machine, config]) => {
|
.then(([machine, config]) => {
|
||||||
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
|
|
||||||
const cashInSettings = getCashInSettings(config)
|
const cashInSettings = getCashInSettings(config)
|
||||||
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
||||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created')
|
return Promise.all([
|
||||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
cashbox.getMachineUnbatchedBills(req.deviceId),
|
||||||
return res.status(200).send({ status: 'OK' })
|
getMachineName(req.deviceId)
|
||||||
|
])
|
||||||
}
|
}
|
||||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||||
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
.then(batch => Promise.all([
|
||||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox)
|
cashbox.getBatchById(batch.id),
|
||||||
.then(() => {
|
getMachineName(batch.device_id),
|
||||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
|
||||||
return res.status(200).send({ status: 'OK' })
|
]))
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
|
||||||
.catch(next)
|
.catch(next)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post('/removal', notifyCashboxRemoval)
|
router.post('/removal', cashboxRemoval)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
||||||
30
migrations/1732874039534-market-currency.js
Normal file
30
migrations/1732874039534-market-currency.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
|
const { loadLatest, saveAccounts } = require('../lib/new-settings-loader')
|
||||||
|
const { ACCOUNT_LIST } = require('../lib/new-admin/config/accounts')
|
||||||
|
const { ALL } = require('../lib/plugins/common/ccxt')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
return loadLatest()
|
||||||
|
.then(({ accounts }) => {
|
||||||
|
const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST))
|
||||||
|
const configuredExchanges = _.intersection(allExchanges, _.keys(accounts))
|
||||||
|
|
||||||
|
const newAccounts = _.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
if (!_.isNil(accounts[value].currencyMarket)) return acc
|
||||||
|
if (_.includes('EUR', ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: 'EUR' } }
|
||||||
|
return { ...acc, [value]: { currencyMarket: ALL[value].DEFAULT_FIAT_CURRENCY } }
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
configuredExchanges
|
||||||
|
)
|
||||||
|
|
||||||
|
return saveAccounts(newAccounts)
|
||||||
|
})
|
||||||
|
.then(next)
|
||||||
|
.catch(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
`CREATE TABLE blacklist_temp (
|
||||||
|
address TEXT NOT NULL UNIQUE
|
||||||
|
)`,
|
||||||
|
`INSERT INTO blacklist_temp (address) SELECT DISTINCT address FROM blacklist`,
|
||||||
|
`DROP TABLE blacklist`,
|
||||||
|
`ALTER TABLE blacklist_temp RENAME TO blacklist`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const uuid = require('uuid')
|
||||||
|
|
||||||
|
var db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
const defaultMessageId = uuid.v4()
|
||||||
|
|
||||||
|
var sql = [
|
||||||
|
`CREATE TABLE blacklist_messages (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
label TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
allow_toggle BOOLEAN NOT NULL DEFAULT true
|
||||||
|
)`,
|
||||||
|
`INSERT INTO blacklist_messages (id, label, content, allow_toggle) VALUES ('${defaultMessageId}', 'Suspicious address', 'This address may be associated with a deceptive offer or a prohibited group. Please make sure you''re using an address from your own wallet.', false)`,
|
||||||
|
`ALTER TABLE blacklist ADD COLUMN blacklist_message_id UUID REFERENCES blacklist_messages(id) NOT NULL DEFAULT '${defaultMessageId}'`
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
20
migrations/1732881659436-rates-screen.js
Normal file
20
migrations/1732881659436-rates-screen.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
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.machineScreens_rates_active)) return
|
||||||
|
newConfig[`machineScreens_rates_active`] = true
|
||||||
|
return saveConfig(newConfig)
|
||||||
|
})
|
||||||
|
.then(next)
|
||||||
|
.catch(err => {
|
||||||
|
return next(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
2
new-lamassu-admin/public/robots.txt
Normal file
2
new-lamassu-admin/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
@ -105,20 +105,29 @@ const Popover = ({
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const arrowClasses = {
|
const getArrowClasses = placement => ({
|
||||||
[classes.arrow]: true,
|
[classes.arrow]: true,
|
||||||
[classes.arrowBottom]: props.placement === 'bottom',
|
[classes.arrowBottom]: placement === 'bottom',
|
||||||
[classes.arrowTop]: props.placement === 'top',
|
[classes.arrowTop]: placement === 'top',
|
||||||
[classes.arrowRight]: props.placement === 'right',
|
[classes.arrowRight]: placement === 'right',
|
||||||
[classes.arrowLeft]: props.placement === 'left'
|
[classes.arrowLeft]: placement === 'left'
|
||||||
|
})
|
||||||
|
|
||||||
|
const flipPlacements = {
|
||||||
|
top: ['bottom'],
|
||||||
|
bottom: ['top'],
|
||||||
|
left: ['right'],
|
||||||
|
right: ['left']
|
||||||
}
|
}
|
||||||
|
|
||||||
const modifiers = R.merge(props.modifiers, {
|
const modifiers = R.mergeDeepLeft(props.modifiers, {
|
||||||
flip: {
|
flip: {
|
||||||
enabled: false
|
enabled: R.defaultTo(false, props.flip),
|
||||||
|
allowedAutoPlacements: flipPlacements[props.placement],
|
||||||
|
boundary: 'clippingParents'
|
||||||
},
|
},
|
||||||
preventOverflow: {
|
preventOverflow: {
|
||||||
enabled: true,
|
enabled: R.defaultTo(true, props.preventOverflow),
|
||||||
boundariesElement: 'scrollParent'
|
boundariesElement: 'scrollParent'
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
|
|
@ -126,7 +135,7 @@ const Popover = ({
|
||||||
offset: '0, 10'
|
offset: '0, 10'
|
||||||
},
|
},
|
||||||
arrow: {
|
arrow: {
|
||||||
enabled: true,
|
enabled: R.defaultTo(true, props.showArrow),
|
||||||
element: arrowRef
|
element: arrowRef
|
||||||
},
|
},
|
||||||
computeStyle: {
|
computeStyle: {
|
||||||
|
|
@ -134,6 +143,12 @@ const Popover = ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (props.preventOverflow === false) {
|
||||||
|
modifiers.hide = {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MaterialPopper
|
<MaterialPopper
|
||||||
|
|
@ -141,10 +156,15 @@ const Popover = ({
|
||||||
modifiers={modifiers}
|
modifiers={modifiers}
|
||||||
className={classes.popover}
|
className={classes.popover}
|
||||||
{...props}>
|
{...props}>
|
||||||
<Paper className={classnames(classes.root, className)}>
|
{({ placement }) => (
|
||||||
<span className={classnames(arrowClasses)} ref={setArrowRef} />
|
<Paper className={classnames(classes.root, className)}>
|
||||||
{children}
|
<span
|
||||||
</Paper>
|
className={classnames(getArrowClasses(placement))}
|
||||||
|
ref={setArrowRef}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
)}
|
||||||
</MaterialPopper>
|
</MaterialPopper>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,8 @@ const styles = {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
'& img': {
|
'& img': {
|
||||||
maxHeight: 145
|
height: 145,
|
||||||
|
minWidth: 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -127,7 +128,8 @@ const IDButton = memo(
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
arrowSize={3}
|
arrowSize={3}
|
||||||
placement="top">
|
placement="top"
|
||||||
|
flip>
|
||||||
<div className={classes.popoverContent}>
|
<div className={classes.popoverContent}>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
||||||
import sort from 'match-sorter'
|
import sort from 'match-sorter'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import { HoverableTooltip } from 'src/components/Tooltip'
|
||||||
|
import { P } from 'src/components/typography'
|
||||||
|
import { errorColor, orangeYellow, spring4 } from 'src/styling/variables'
|
||||||
|
|
||||||
import TextInput from './TextInput'
|
import TextInput from './TextInput'
|
||||||
|
|
||||||
const Autocomplete = ({
|
const Autocomplete = ({
|
||||||
|
|
@ -95,6 +100,39 @@ const Autocomplete = ({
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
renderOption={props => {
|
||||||
|
if (!props.warning && !props.warningMessage)
|
||||||
|
return R.path([labelProp])(props)
|
||||||
|
|
||||||
|
const warningColors = {
|
||||||
|
clean: spring4,
|
||||||
|
partial: orangeYellow,
|
||||||
|
important: errorColor
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoverableElement = (
|
||||||
|
<Box
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
borderRadius={6}
|
||||||
|
bgcolor={warningColors[props.warning]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
display="flex"
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
alignItems="center">
|
||||||
|
<Box>{R.path([labelProp])(props)}</Box>
|
||||||
|
<HoverableTooltip parentElements={hoverableElement} width={250}>
|
||||||
|
<P>{props.warningMessage}</P>
|
||||||
|
</HoverableTooltip>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,31 @@
|
||||||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
import { utils as coinUtils } from '@lamassu/coins'
|
import { addressDetector } from '@lamassu/coins'
|
||||||
import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
|
import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
|
||||||
import Grid from '@material-ui/core/Grid'
|
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import { HelpTooltip } from 'src/components/Tooltip'
|
import { HelpTooltip } from 'src/components/Tooltip'
|
||||||
import {
|
import { Link, Button, IconButton } from 'src/components/buttons'
|
||||||
Link,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
SupportLinkButton
|
|
||||||
} from 'src/components/buttons'
|
|
||||||
import { Switch } from 'src/components/inputs'
|
import { Switch } from 'src/components/inputs'
|
||||||
import Sidebar from 'src/components/layout/Sidebar'
|
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import { H4, H2, Label2, P, Info3, Info2 } from 'src/components/typography'
|
import { H2, Label2, P, Info3, Info2 } from 'src/components/typography'
|
||||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||||
|
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
|
||||||
|
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
|
|
||||||
import styles from './Blacklist.styles'
|
import styles from './Blacklist.styles'
|
||||||
|
import BlackListAdvanced from './BlacklistAdvanced'
|
||||||
import BlackListModal from './BlacklistModal'
|
import BlackListModal from './BlacklistModal'
|
||||||
import BlacklistTable from './BlacklistTable'
|
import BlacklistTable from './BlacklistTable'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const groupByCode = R.groupBy(obj => obj.cryptoCode)
|
|
||||||
|
|
||||||
const DELETE_ROW = gql`
|
const DELETE_ROW = gql`
|
||||||
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) {
|
mutation DeleteBlacklistRow($address: String!) {
|
||||||
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
deleteBlacklistRow(address: $address) {
|
||||||
cryptoCode
|
|
||||||
address
|
address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +34,6 @@ const DELETE_ROW = gql`
|
||||||
const GET_BLACKLIST = gql`
|
const GET_BLACKLIST = gql`
|
||||||
query getBlacklistData {
|
query getBlacklistData {
|
||||||
blacklist {
|
blacklist {
|
||||||
cryptoCode
|
|
||||||
address
|
address
|
||||||
}
|
}
|
||||||
cryptoCurrencies {
|
cryptoCurrencies {
|
||||||
|
|
@ -64,14 +56,32 @@ const GET_INFO = gql`
|
||||||
`
|
`
|
||||||
|
|
||||||
const ADD_ROW = gql`
|
const ADD_ROW = gql`
|
||||||
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) {
|
mutation InsertBlacklistRow($address: String!) {
|
||||||
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
insertBlacklistRow(address: $address) {
|
||||||
cryptoCode
|
|
||||||
address
|
address
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GET_BLACKLIST_MESSAGES = gql`
|
||||||
|
query getBlacklistMessages {
|
||||||
|
blacklistMessages {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
content
|
||||||
|
allowToggle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const EDIT_BLACKLIST_MESSAGE = gql`
|
||||||
|
mutation editBlacklistMessage($id: ID, $content: String) {
|
||||||
|
editBlacklistMessage(id: $id, content: $content) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
|
|
@ -117,14 +127,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
||||||
const Blacklist = () => {
|
const Blacklist = () => {
|
||||||
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
||||||
const { data: configData } = useQuery(GET_INFO)
|
const { data: configData } = useQuery(GET_INFO)
|
||||||
|
const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES)
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const [clickedItem, setClickedItem] = useState({
|
|
||||||
code: 'BTC',
|
|
||||||
display: 'Bitcoin'
|
|
||||||
})
|
|
||||||
const [errorMsg, setErrorMsg] = useState(null)
|
const [errorMsg, setErrorMsg] = useState(null)
|
||||||
|
const [editMessageError, setEditMessageError] = useState(null)
|
||||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||||
const [confirmDialog, setConfirmDialog] = useState(false)
|
const [confirmDialog, setConfirmDialog] = useState(false)
|
||||||
|
const [advancedSettings, setAdvancedSettings] = useState(false)
|
||||||
|
|
||||||
const [deleteEntry] = useMutation(DELETE_ROW, {
|
const [deleteEntry] = useMutation(DELETE_ROW, {
|
||||||
onError: ({ message }) => {
|
onError: ({ message }) => {
|
||||||
|
|
@ -144,14 +153,14 @@ const Blacklist = () => {
|
||||||
refetchQueries: () => ['getData']
|
refetchQueries: () => ['getData']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, {
|
||||||
|
onError: e => setEditMessageError(e),
|
||||||
|
refetchQueries: () => ['getBlacklistData']
|
||||||
|
})
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
|
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
|
||||||
const availableCurrencies = R.filter(
|
|
||||||
coin => coinUtils.getEquivalentCode(coin.code) === coin.code
|
|
||||||
)(R.path(['cryptoCurrencies'], blacklistResponse) ?? [])
|
|
||||||
|
|
||||||
const formattedData = groupByCode(blacklistData)
|
|
||||||
|
|
||||||
const complianceConfig =
|
const complianceConfig =
|
||||||
configData?.config && fromNamespace('compliance')(configData.config)
|
configData?.config && fromNamespace('compliance')(configData.config)
|
||||||
|
|
@ -165,12 +174,8 @@ const Blacklist = () => {
|
||||||
return saveConfig({ variables: { config } })
|
return saveConfig({ variables: { config } })
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClickSidebarItem = e => {
|
const handleDeleteEntry = address => {
|
||||||
setClickedItem({ code: e.code, display: e.display })
|
deleteEntry({ variables: { address } })
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteEntry = (cryptoCode, address) => {
|
|
||||||
deleteEntry({ variables: { cryptoCode, address } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmDialog = confirm => {
|
const handleConfirmDialog = confirm => {
|
||||||
|
|
@ -180,21 +185,21 @@ const Blacklist = () => {
|
||||||
setConfirmDialog(false)
|
setConfirmDialog(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateAddress = (cryptoCode, address) => {
|
const validateAddress = address => {
|
||||||
try {
|
try {
|
||||||
return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address))
|
return !R.isEmpty(addressDetector.detectAddress(address).matches)
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addToBlacklist = async (cryptoCode, address) => {
|
const addToBlacklist = async address => {
|
||||||
setErrorMsg(null)
|
setErrorMsg(null)
|
||||||
if (!validateAddress(cryptoCode, address)) {
|
if (!validateAddress(address)) {
|
||||||
setErrorMsg('Invalid address')
|
setErrorMsg('Invalid address')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const res = await addEntry({ variables: { cryptoCode, address } })
|
const res = await addEntry({ variables: { address } })
|
||||||
if (!res.errors) {
|
if (!res.errors) {
|
||||||
return setShowModal(false)
|
return setShowModal(false)
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +213,15 @@ const Blacklist = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editBlacklistMessage = r => {
|
||||||
|
editMessage({
|
||||||
|
variables: {
|
||||||
|
id: r.id,
|
||||||
|
content: r.content
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaperWalletDialog
|
<PaperWalletDialog
|
||||||
|
|
@ -217,32 +231,23 @@ const Blacklist = () => {
|
||||||
setConfirmDialog(false)
|
setConfirmDialog(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TitleSection title="Blacklisted addresses">
|
<TitleSection
|
||||||
<Box display="flex" justifyContent="flex-end">
|
title="Blacklisted addresses"
|
||||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
buttons={[
|
||||||
Blacklist new addresses
|
{
|
||||||
</Link>
|
text: 'Advanced settings',
|
||||||
</Box>
|
icon: SettingsIcon,
|
||||||
</TitleSection>
|
inverseIcon: ReverseSettingsIcon,
|
||||||
<Grid container className={classes.grid}>
|
toggle: setAdvancedSettings
|
||||||
<Sidebar
|
}
|
||||||
data={availableCurrencies}
|
]}>
|
||||||
isSelected={R.propEq('code', clickedItem.code)}
|
{!advancedSettings && (
|
||||||
displayName={it => it.display}
|
<Box display="flex" alignItems="center" justifyContent="flex-end">
|
||||||
onClick={onClickSidebarItem}
|
|
||||||
/>
|
|
||||||
<div className={classes.content}>
|
|
||||||
<Box display="flex" justifyContent="space-between" mb={3}>
|
|
||||||
<H4 noMargin className={classes.subtitle}>
|
|
||||||
{clickedItem.display
|
|
||||||
? `${clickedItem.display} blacklisted addresses`
|
|
||||||
: ''}{' '}
|
|
||||||
</H4>
|
|
||||||
<Box
|
<Box
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="end"
|
justifyContent="end"
|
||||||
mr="-140px">
|
mr="15px">
|
||||||
<P>Enable paper wallet (only)</P>
|
<P>Enable paper wallet (only)</P>
|
||||||
<Switch
|
<Switch
|
||||||
checked={enablePaperWalletOnly}
|
checked={enablePaperWalletOnly}
|
||||||
|
|
@ -268,7 +273,7 @@ const Blacklist = () => {
|
||||||
display="flex"
|
display="flex"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="flex-end"
|
justifyContent="flex-end"
|
||||||
mr="-5px">
|
mr="15px">
|
||||||
<P>Reject reused addresses</P>
|
<P>Reject reused addresses</P>
|
||||||
<Switch
|
<Switch
|
||||||
checked={rejectAddressReuse}
|
checked={rejectAddressReuse}
|
||||||
|
|
@ -280,24 +285,22 @@ const Blacklist = () => {
|
||||||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||||
<HelpTooltip width={304}>
|
<HelpTooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
The "Reject reused addresses" option means that all addresses
|
This option requires a user to scan a fresh wallet address if
|
||||||
that are used once will be automatically rejected if there's
|
they attempt to scan one that had been previously used for a
|
||||||
an attempt to use them again on a new transaction.
|
transaction in your network.
|
||||||
</P>
|
</P>
|
||||||
<P>
|
|
||||||
For details please read the relevant knowledgebase article:
|
|
||||||
</P>
|
|
||||||
<SupportLinkButton
|
|
||||||
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
|
|
||||||
label="Reject Address Reuse"
|
|
||||||
bottomSpace="1"
|
|
||||||
/>
|
|
||||||
</HelpTooltip>
|
</HelpTooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||||
|
Blacklist new addresses
|
||||||
|
</Link>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
</TitleSection>
|
||||||
|
{!advancedSettings && (
|
||||||
|
<div className={classes.content}>
|
||||||
<BlacklistTable
|
<BlacklistTable
|
||||||
data={formattedData}
|
data={blacklistData}
|
||||||
selectedCoin={clickedItem}
|
|
||||||
handleDeleteEntry={handleDeleteEntry}
|
handleDeleteEntry={handleDeleteEntry}
|
||||||
errorMessage={errorMsg}
|
errorMessage={errorMsg}
|
||||||
setErrorMessage={setErrorMsg}
|
setErrorMessage={setErrorMsg}
|
||||||
|
|
@ -305,7 +308,15 @@ const Blacklist = () => {
|
||||||
setDeleteDialog={setDeleteDialog}
|
setDeleteDialog={setDeleteDialog}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
)}
|
||||||
|
{advancedSettings && (
|
||||||
|
<BlackListAdvanced
|
||||||
|
data={messagesResponse}
|
||||||
|
editBlacklistMessage={editBlacklistMessage}
|
||||||
|
mutationError={editMessageError}
|
||||||
|
onClose={() => refetch()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showModal && (
|
{showModal && (
|
||||||
<BlackListModal
|
<BlackListModal
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
@ -313,7 +324,6 @@ const Blacklist = () => {
|
||||||
setShowModal(false)
|
setShowModal(false)
|
||||||
}}
|
}}
|
||||||
errorMsg={errorMsg}
|
errorMsg={errorMsg}
|
||||||
selectedCoin={clickedItem}
|
|
||||||
addToBlacklist={addToBlacklist}
|
addToBlacklist={addToBlacklist}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,23 @@ const styles = {
|
||||||
content: {
|
content: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
flex: 1,
|
flex: 1
|
||||||
marginLeft: spacer * 6
|
},
|
||||||
|
advancedForm: {
|
||||||
|
'& > *': {
|
||||||
|
marginTop: 20
|
||||||
|
},
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%'
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
margin: [['auto', 0, spacer * 3, 0]]
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
margin: [['auto', 0, 0, 'auto']]
|
||||||
},
|
},
|
||||||
modalTitle: {
|
modalTitle: {
|
||||||
margin: [['auto', 0, 8.5, 'auto']]
|
margin: [['auto', 0, 8.5, 'auto']]
|
||||||
|
|
@ -54,6 +66,9 @@ const styles = {
|
||||||
cancelButton: {
|
cancelButton: {
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
padding: 0
|
padding: 0
|
||||||
|
},
|
||||||
|
resetToDefault: {
|
||||||
|
width: 145
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
178
new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js
Normal file
178
new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { Form, Formik, Field } from 'formik'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
import ErrorMessage from 'src/components/ErrorMessage'
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { ActionButton, IconButton, Button } from 'src/components/buttons'
|
||||||
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg'
|
||||||
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||||
|
import { ReactComponent as DefaultIconReverse } from 'src/styling/icons/button/retry/white.svg'
|
||||||
|
import { ReactComponent as DefaultIcon } from 'src/styling/icons/button/retry/zodiac.svg'
|
||||||
|
|
||||||
|
import styles from './Blacklist.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const DEFAULT_MESSAGE = `This address may be associated with a deceptive offer or a prohibited group. Please make sure you're using an address from your own wallet.`
|
||||||
|
|
||||||
|
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||||
|
if (mutationError) return 'Internal server error'
|
||||||
|
if (!formikErrors || !formikTouched) return null
|
||||||
|
if (formikErrors.event && formikTouched.event) return formikErrors.event
|
||||||
|
if (formikErrors.message && formikTouched.message) return formikErrors.message
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlacklistAdvanced = ({
|
||||||
|
data,
|
||||||
|
editBlacklistMessage,
|
||||||
|
onClose,
|
||||||
|
mutationError
|
||||||
|
}) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const [selectedMessage, setSelectedMessage] = useState(null)
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
header: 'Label',
|
||||||
|
width: 250,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => R.path(['label'], it)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'content',
|
||||||
|
header: 'Content',
|
||||||
|
width: 690,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => R.path(['content'], it)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit',
|
||||||
|
header: 'Edit',
|
||||||
|
width: 130,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => (
|
||||||
|
<IconButton
|
||||||
|
className={classes.deleteButton}
|
||||||
|
onClick={() => setSelectedMessage(it)}>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deleteButton',
|
||||||
|
header: 'Delete',
|
||||||
|
width: 130,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => (
|
||||||
|
<IconButton
|
||||||
|
className={classes.deleteButton}
|
||||||
|
disabled={
|
||||||
|
!R.isNil(R.path(['allowToggle'], it)) &&
|
||||||
|
!R.path(['allowToggle'], it)
|
||||||
|
}>
|
||||||
|
{R.path(['allowToggle'], it) ? (
|
||||||
|
<DeleteIcon />
|
||||||
|
) : (
|
||||||
|
<DisabledDeleteIcon />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleModalClose = () => {
|
||||||
|
setSelectedMessage(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = values => {
|
||||||
|
editBlacklistMessage(values)
|
||||||
|
handleModalClose()
|
||||||
|
!R.isNil(onClose) && onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
label: !R.isNil(selectedMessage) ? selectedMessage.label : '',
|
||||||
|
content: !R.isNil(selectedMessage) ? selectedMessage.content : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
label: Yup.string().required('A label is required!'),
|
||||||
|
content: Yup.string()
|
||||||
|
.required('The message content is required!')
|
||||||
|
.trim()
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataTable
|
||||||
|
data={R.path(['blacklistMessages'], data)}
|
||||||
|
elements={elements}
|
||||||
|
emptyText="No blacklisted addresses so far"
|
||||||
|
name="blacklistTable"
|
||||||
|
/>
|
||||||
|
{selectedMessage && (
|
||||||
|
<Modal
|
||||||
|
title={`Blacklist message - ${selectedMessage?.label}`}
|
||||||
|
open={true}
|
||||||
|
width={676}
|
||||||
|
height={400}
|
||||||
|
handleClose={handleModalClose}>
|
||||||
|
<Formik
|
||||||
|
validateOnBlur={false}
|
||||||
|
validateOnChange={false}
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={values =>
|
||||||
|
handleSubmit({ id: selectedMessage.id, ...values })
|
||||||
|
}>
|
||||||
|
{({ errors, touched, setFieldValue }) => (
|
||||||
|
<Form className={classes.advancedForm}>
|
||||||
|
<ActionButton
|
||||||
|
color="primary"
|
||||||
|
Icon={DefaultIcon}
|
||||||
|
InverseIcon={DefaultIconReverse}
|
||||||
|
className={classes.resetToDefault}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFieldValue('content', DEFAULT_MESSAGE)}>
|
||||||
|
Reset to default
|
||||||
|
</ActionButton>
|
||||||
|
<Field
|
||||||
|
name="content"
|
||||||
|
label="Message content"
|
||||||
|
fullWidth
|
||||||
|
multiline={true}
|
||||||
|
rows={6}
|
||||||
|
component={TextInput}
|
||||||
|
/>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
{getErrorMsg(errors, touched, mutationError) && (
|
||||||
|
<ErrorMessage>
|
||||||
|
{getErrorMsg(errors, touched, mutationError)}
|
||||||
|
</ErrorMessage>
|
||||||
|
)}
|
||||||
|
<Button type="submit" className={classes.submit}>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlacklistAdvanced
|
||||||
|
|
@ -14,31 +14,14 @@ import { H3 } from 'src/components/typography'
|
||||||
import styles from './Blacklist.styles'
|
import styles from './Blacklist.styles'
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const BlackListModal = ({
|
const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => {
|
||||||
onClose,
|
|
||||||
selectedCoin,
|
|
||||||
addToBlacklist,
|
|
||||||
errorMsg
|
|
||||||
}) => {
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const handleAddToBlacklist = address => {
|
const handleAddToBlacklist = address => {
|
||||||
if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) {
|
addToBlacklist(address)
|
||||||
address = 'bitcoincash:' + address
|
|
||||||
}
|
|
||||||
addToBlacklist(selectedCoin.code, address)
|
|
||||||
}
|
|
||||||
const placeholderAddress = {
|
|
||||||
BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD',
|
|
||||||
ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
|
|
||||||
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
|
|
||||||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
|
||||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
|
||||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
|
||||||
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
|
|
||||||
XMR:
|
|
||||||
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
closeOnBackdropClick={true}
|
closeOnBackdropClick={true}
|
||||||
|
|
@ -61,26 +44,20 @@ const BlackListModal = ({
|
||||||
handleAddToBlacklist(address.trim())
|
handleAddToBlacklist(address.trim())
|
||||||
}}>
|
}}>
|
||||||
<Form id="address-form">
|
<Form id="address-form">
|
||||||
<H3 className={classes.modalTitle}>
|
<H3 className={classes.modalTitle}>Blacklist new address</H3>
|
||||||
{selectedCoin.display
|
|
||||||
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
|
||||||
: ''}
|
|
||||||
</H3>
|
|
||||||
<Field
|
<Field
|
||||||
name="address"
|
name="address"
|
||||||
fullWidth
|
fullWidth
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
label="Paste new address to blacklist here"
|
label="Paste new address to blacklist here"
|
||||||
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
placeholder={`ex: ${placeholderAddress}`}
|
||||||
component={TextInput}
|
component={TextInput}
|
||||||
/>
|
/>
|
||||||
{!R.isNil(errorMsg) && (
|
|
||||||
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
<div className={classes.footer}>
|
<div className={classes.footer}>
|
||||||
<Box display="flex" justifyContent="flex-end">
|
{!R.isNil(errorMsg) && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||||
|
<Box className={classes.submit}>
|
||||||
<Link type="submit" form="address-form">
|
<Link type="submit" form="address-form">
|
||||||
Blacklist address
|
Blacklist address
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import React, { useState } from 'react'
|
||||||
import { DeleteDialog } from 'src/components/DeleteDialog'
|
import { DeleteDialog } from 'src/components/DeleteDialog'
|
||||||
import { IconButton } from 'src/components/buttons'
|
import { IconButton } from 'src/components/buttons'
|
||||||
import DataTable from 'src/components/tables/DataTable'
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
import { Label1 } from 'src/components/typography'
|
|
||||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
|
||||||
|
|
@ -15,7 +14,6 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const BlacklistTable = ({
|
const BlacklistTable = ({
|
||||||
data,
|
data,
|
||||||
selectedCoin,
|
|
||||||
handleDeleteEntry,
|
handleDeleteEntry,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
|
|
@ -29,8 +27,8 @@ const BlacklistTable = ({
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
name: 'address',
|
name: 'address',
|
||||||
header: <Label1 className={classes.white}>{'Addresses'}</Label1>,
|
header: 'Address',
|
||||||
width: 800,
|
width: 1070,
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
view: it => (
|
view: it => (
|
||||||
|
|
@ -41,7 +39,7 @@ const BlacklistTable = ({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'deleteButton',
|
name: 'deleteButton',
|
||||||
header: <Label1 className={classes.white}>{'Delete'}</Label1>,
|
header: 'Delete',
|
||||||
width: 130,
|
width: 130,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
|
|
@ -57,14 +55,11 @@ const BlacklistTable = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const dataToShow = selectedCoin
|
|
||||||
? data[selectedCoin.code]
|
|
||||||
: data[R.keys(data)[0]]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={dataToShow}
|
data={data}
|
||||||
elements={elements}
|
elements={elements}
|
||||||
emptyText="No blacklisted addresses so far"
|
emptyText="No blacklisted addresses so far"
|
||||||
name="blacklistTable"
|
name="blacklistTable"
|
||||||
|
|
@ -77,10 +72,7 @@ const BlacklistTable = ({
|
||||||
}}
|
}}
|
||||||
onConfirmed={() => {
|
onConfirmed={() => {
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
handleDeleteEntry(
|
handleDeleteEntry(R.path(['address'], toBeDeleted))
|
||||||
R.path(['cryptoCode'], toBeDeleted),
|
|
||||||
R.path(['address'], toBeDeleted)
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ const EditableCard = ({
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className={classes.editingWrapper}>
|
<div className={classes.editingWrapper}>
|
||||||
<div className={classes.replace}>
|
<div className={classes.replace}>
|
||||||
{hasImage && (
|
{hasImage && state !== OVERRIDE_PENDING && (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ export default {
|
||||||
height: 36
|
height: 36
|
||||||
},
|
},
|
||||||
tBody: {
|
tBody: {
|
||||||
maxHeight: '65vh',
|
maxHeight: 'calc(100vh - 350px)',
|
||||||
overflow: 'auto'
|
overflow: 'auto'
|
||||||
},
|
},
|
||||||
tableWidth: {
|
tableWidth: {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ const styles = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between'
|
justifyContent: 'space-between'
|
||||||
|
},
|
||||||
|
tableWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
marginBottom: 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,13 +249,15 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable
|
<div className={classes.tableWrapper}>
|
||||||
loading={loading}
|
<DataTable
|
||||||
name="cashboxHistory"
|
loading={loading}
|
||||||
elements={elements}
|
name="cashboxHistory"
|
||||||
data={batches}
|
elements={elements}
|
||||||
emptyText="No cash box batches so far"
|
data={batches}
|
||||||
/>
|
emptyText="No cash box batches so far"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
80
new-lamassu-admin/src/pages/OperatorInfo/MachineScreens.js
Normal file
80
new-lamassu-admin/src/pages/OperatorInfo/MachineScreens.js
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { memo } from 'react'
|
||||||
|
|
||||||
|
import { Switch } from 'src/components/inputs'
|
||||||
|
import { H4, P, Label2 } from 'src/components/typography'
|
||||||
|
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
||||||
|
|
||||||
|
import { global } from './OperatorInfo.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(global)
|
||||||
|
|
||||||
|
const GET_CONFIG = gql`
|
||||||
|
query getData {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SAVE_CONFIG = gql`
|
||||||
|
mutation Save($config: JSONObject) {
|
||||||
|
saveConfig(config: $config)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MachineScreens = memo(({ wizard }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const { data } = useQuery(GET_CONFIG)
|
||||||
|
|
||||||
|
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||||
|
refetchQueries: () => ['getData']
|
||||||
|
})
|
||||||
|
|
||||||
|
const machineScreensConfig =
|
||||||
|
data?.config && fromNamespace(namespaces.MACHINE_SCREENS, data.config)
|
||||||
|
|
||||||
|
const ratesScreenConfig =
|
||||||
|
data?.config &&
|
||||||
|
R.compose(
|
||||||
|
fromNamespace('rates'),
|
||||||
|
fromNamespace(namespaces.MACHINE_SCREENS)
|
||||||
|
)(data.config)
|
||||||
|
|
||||||
|
if (!machineScreensConfig) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.header}>
|
||||||
|
<H4>Rates screen</H4>
|
||||||
|
</div>
|
||||||
|
<div className={classes.switchRow}>
|
||||||
|
<P>Enable rates screen</P>
|
||||||
|
<div className={classes.switch}>
|
||||||
|
<Switch
|
||||||
|
checked={ratesScreenConfig.active}
|
||||||
|
onChange={event =>
|
||||||
|
saveConfig({
|
||||||
|
variables: {
|
||||||
|
config: R.compose(
|
||||||
|
toNamespace(namespaces.MACHINE_SCREENS),
|
||||||
|
toNamespace('rates')
|
||||||
|
)(
|
||||||
|
R.merge(ratesScreenConfig, {
|
||||||
|
active: event.target.checked
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Label2>{ratesScreenConfig.active ? 'Yes' : 'No'}</Label2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MachineScreens
|
||||||
|
|
@ -12,7 +12,7 @@ import SingleRowTable from 'src/components/single-row-table/SingleRowTable'
|
||||||
import { formatLong } from 'src/utils/string'
|
import { formatLong } from 'src/utils/string'
|
||||||
|
|
||||||
import FormRenderer from './FormRenderer'
|
import FormRenderer from './FormRenderer'
|
||||||
import schemas from './schemas'
|
import _schemas from './schemas'
|
||||||
|
|
||||||
const GET_INFO = gql`
|
const GET_INFO = gql`
|
||||||
query getData {
|
query getData {
|
||||||
|
|
@ -21,6 +21,12 @@ const GET_INFO = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GET_MARKETS = gql`
|
||||||
|
query getMarkets {
|
||||||
|
getMarkets
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const SAVE_ACCOUNT = gql`
|
const SAVE_ACCOUNT = gql`
|
||||||
mutation Save($accounts: JSONObject) {
|
mutation Save($accounts: JSONObject) {
|
||||||
saveAccounts(accounts: $accounts)
|
saveAccounts(accounts: $accounts)
|
||||||
|
|
@ -40,12 +46,17 @@ const useStyles = makeStyles(styles)
|
||||||
const Services = () => {
|
const Services = () => {
|
||||||
const [editingSchema, setEditingSchema] = useState(null)
|
const [editingSchema, setEditingSchema] = useState(null)
|
||||||
|
|
||||||
const { data } = useQuery(GET_INFO)
|
const { data, loading: configLoading } = useQuery(GET_INFO)
|
||||||
|
const { data: marketsData, loading: marketsLoading } = useQuery(GET_MARKETS)
|
||||||
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
||||||
onCompleted: () => setEditingSchema(null),
|
onCompleted: () => setEditingSchema(null),
|
||||||
refetchQueries: ['getData']
|
refetchQueries: ['getData']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const markets = marketsData?.getMarkets
|
||||||
|
|
||||||
|
const schemas = _schemas(markets)
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const accounts = data?.accounts ?? {}
|
const accounts = data?.accounts ?? {}
|
||||||
|
|
@ -101,40 +112,44 @@ const Services = () => {
|
||||||
const getValidationSchema = ({ code, getValidationSchema }) =>
|
const getValidationSchema = ({ code, getValidationSchema }) =>
|
||||||
getValidationSchema(accounts[code])
|
getValidationSchema(accounts[code])
|
||||||
|
|
||||||
|
const loading = marketsLoading || configLoading
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.wrapper}>
|
!loading && (
|
||||||
<TitleSection title="Third-Party services" />
|
<div className={classes.wrapper}>
|
||||||
<Grid container spacing={4}>
|
<TitleSection title="Third-Party services" />
|
||||||
{R.values(schemas).map(schema => (
|
<Grid container spacing={4}>
|
||||||
<Grid item key={schema.code}>
|
{R.values(schemas).map(schema => (
|
||||||
<SingleRowTable
|
<Grid item key={schema.code}>
|
||||||
editMessage={'Configure ' + schema.title}
|
<SingleRowTable
|
||||||
title={schema.title}
|
editMessage={'Configure ' + schema.title}
|
||||||
onEdit={() => setEditingSchema(schema)}
|
title={schema.title}
|
||||||
items={getItems(schema.code, schema.elements)}
|
onEdit={() => setEditingSchema(schema)}
|
||||||
|
items={getItems(schema.code, schema.elements)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
{editingSchema && (
|
||||||
|
<Modal
|
||||||
|
title={`Edit ${editingSchema.name}`}
|
||||||
|
width={525}
|
||||||
|
handleClose={() => setEditingSchema(null)}
|
||||||
|
open={true}>
|
||||||
|
<FormRenderer
|
||||||
|
save={it =>
|
||||||
|
saveAccount({
|
||||||
|
variables: { accounts: { [editingSchema.code]: it } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
elements={getElements(editingSchema)}
|
||||||
|
validationSchema={getValidationSchema(editingSchema)}
|
||||||
|
value={getAccounts(editingSchema)}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Modal>
|
||||||
))}
|
)}
|
||||||
</Grid>
|
</div>
|
||||||
{editingSchema && (
|
)
|
||||||
<Modal
|
|
||||||
title={`Edit ${editingSchema.name}`}
|
|
||||||
width={525}
|
|
||||||
handleClose={() => setEditingSchema(null)}
|
|
||||||
open={true}>
|
|
||||||
<FormRenderer
|
|
||||||
save={it =>
|
|
||||||
saveAccount({
|
|
||||||
variables: { accounts: { [editingSchema.code]: it } }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
elements={getElements(editingSchema)}
|
|
||||||
validationSchema={getValidationSchema(editingSchema)}
|
|
||||||
value={getAccounts(editingSchema)}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,57 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'binance',
|
return {
|
||||||
name: 'Binance',
|
code: 'binance',
|
||||||
title: 'Binance (Exchange)',
|
name: 'Binance',
|
||||||
elements: [
|
title: 'Binance (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'apiKey',
|
{
|
||||||
display: 'API key',
|
code: 'apiKey',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'privateKey',
|
{
|
||||||
display: 'Private key',
|
code: 'privateKey',
|
||||||
component: SecretInputFormik
|
display: 'Private key',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
apiKey: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
privateKey: Yup.string('The private key must be a string')
|
||||||
|
.max(100, 'The private key is too long')
|
||||||
|
.test(secretTest(account?.privateKey, 'private key')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
apiKey: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
privateKey: Yup.string('The private key must be a string')
|
|
||||||
.max(100, 'The private key is too long')
|
|
||||||
.test(secretTest(account?.privateKey, 'private key'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,57 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'binanceus',
|
return {
|
||||||
name: 'Binance.us',
|
code: 'binanceus',
|
||||||
title: 'Binance.us (Exchange)',
|
name: 'Binance.us',
|
||||||
elements: [
|
title: 'Binance.us (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'apiKey',
|
{
|
||||||
display: 'API key',
|
code: 'apiKey',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'privateKey',
|
{
|
||||||
display: 'Private key',
|
code: 'privateKey',
|
||||||
component: SecretInputFormik
|
display: 'Private key',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
apiKey: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
privateKey: Yup.string('The private key must be a string')
|
||||||
|
.max(100, 'The private key is too long')
|
||||||
|
.test(secretTest(account?.privateKey, 'private key')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
apiKey: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
privateKey: Yup.string('The private key must be a string')
|
|
||||||
.max(100, 'The private key is too long')
|
|
||||||
.test(secretTest(account?.privateKey, 'private key'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,57 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'bitfinex',
|
return {
|
||||||
name: 'Bitfinex',
|
code: 'bitfinex',
|
||||||
title: 'Bitfinex (Exchange)',
|
name: 'Bitfinex',
|
||||||
elements: [
|
title: 'Bitfinex (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'key',
|
{
|
||||||
display: 'API Key',
|
code: 'key',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'secret',
|
{
|
||||||
display: 'API Secret',
|
code: 'secret',
|
||||||
component: SecretInputFormik
|
display: 'API secret',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency Market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
key: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
secret: Yup.string('The API secret must be a string')
|
||||||
|
.max(100, 'The API secret is too long')
|
||||||
|
.test(secretTest(account?.secret, 'API secret')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
key: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
secret: Yup.string('The API secret must be a string')
|
|
||||||
.max(100, 'The API secret is too long')
|
|
||||||
.test(secretTest(account?.secret, 'API secret'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,67 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'bitstamp',
|
return {
|
||||||
name: 'Bitstamp',
|
code: 'bitstamp',
|
||||||
title: 'Bitstamp (Exchange)',
|
name: 'Bitstamp',
|
||||||
elements: [
|
title: 'Bitstamp (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'clientId',
|
{
|
||||||
display: 'Client ID',
|
code: 'clientId',
|
||||||
component: TextInputFormik,
|
display: 'Client ID',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'key',
|
{
|
||||||
display: 'API key',
|
code: 'key',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'secret',
|
{
|
||||||
display: 'API secret',
|
code: 'secret',
|
||||||
component: SecretInputFormik
|
display: 'API secret',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
clientId: Yup.string('The client ID must be a string')
|
||||||
|
.max(100, 'The client ID is too long')
|
||||||
|
.required('The client ID is required'),
|
||||||
|
key: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
secret: Yup.string('The API secret must be a string')
|
||||||
|
.max(100, 'The API secret is too long')
|
||||||
|
.test(secretTest(account?.secret, 'API secret')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
clientId: Yup.string('The client ID must be a string')
|
|
||||||
.max(100, 'The client ID is too long')
|
|
||||||
.required('The client ID is required'),
|
|
||||||
key: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
secret: Yup.string('The API secret must be a string')
|
|
||||||
.max(100, 'The API secret is too long')
|
|
||||||
.test(secretTest(account?.secret, 'API secret'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,46 +1,67 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'cex',
|
return {
|
||||||
name: 'CEX.IO',
|
code: 'cex',
|
||||||
title: 'CEX.IO (Exchange)',
|
name: 'CEX.IO',
|
||||||
elements: [
|
title: 'CEX.IO (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'apiKey',
|
{
|
||||||
display: 'API key',
|
code: 'apiKey',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'uid',
|
{
|
||||||
display: 'User ID',
|
code: 'uid',
|
||||||
component: TextInputFormik,
|
display: 'User ID',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'privateKey',
|
{
|
||||||
display: 'Private key',
|
code: 'privateKey',
|
||||||
component: SecretInputFormik
|
display: 'Private key',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency Market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
apiKey: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
uid: Yup.string('The User ID must be a string')
|
||||||
|
.max(100, 'The User ID is too long')
|
||||||
|
.required('The User ID is required'),
|
||||||
|
privateKey: Yup.string('The private key must be a string')
|
||||||
|
.max(100, 'The private key is too long')
|
||||||
|
.test(secretTest(account?.privateKey, 'private key')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
apiKey: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
uid: Yup.string('The User ID must be a string')
|
|
||||||
.max(100, 'The User ID is too long')
|
|
||||||
.required('The User ID is required'),
|
|
||||||
privateKey: Yup.string('The private key must be a string')
|
|
||||||
.max(100, 'The private key is too long')
|
|
||||||
.test(secretTest(account?.privateKey, 'private key'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,12 @@
|
||||||
|
import { ALL_CRYPTOS } from '@lamassu/coins'
|
||||||
import * as R from 'ramda'
|
import * as R from 'ramda'
|
||||||
|
|
||||||
|
const WARNING_LEVELS = {
|
||||||
|
CLEAN: 'clean',
|
||||||
|
PARTIAL: 'partial',
|
||||||
|
IMPORTANT: 'important'
|
||||||
|
}
|
||||||
|
|
||||||
const secretTest = (secret, message) => ({
|
const secretTest = (secret, message) => ({
|
||||||
name: 'secret-test',
|
name: 'secret-test',
|
||||||
message: message ? `The ${message} is invalid` : 'Invalid field',
|
message: message ? `The ${message} is invalid` : 'Invalid field',
|
||||||
|
|
@ -21,4 +28,35 @@ const leadingZerosTest = (value, context) => {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export { secretTest, leadingZerosTest }
|
const buildCurrencyOptions = markets => {
|
||||||
|
return R.map(it => {
|
||||||
|
const unavailableCryptos = R.difference(ALL_CRYPTOS, markets[it])
|
||||||
|
const unavailableCryptosFiltered = R.difference(unavailableCryptos, [it]) // As the markets can have stablecoins to trade against other crypto, filter them out, as there can't be pairs such as USDT/USDT
|
||||||
|
|
||||||
|
const unavailableMarketsStr =
|
||||||
|
R.length(unavailableCryptosFiltered) > 1
|
||||||
|
? `${R.join(
|
||||||
|
', ',
|
||||||
|
R.slice(0, -1, unavailableCryptosFiltered)
|
||||||
|
)} and ${R.last(unavailableCryptosFiltered)}`
|
||||||
|
: unavailableCryptosFiltered[0]
|
||||||
|
|
||||||
|
const warningLevel = R.isEmpty(unavailableCryptosFiltered)
|
||||||
|
? WARNING_LEVELS.CLEAN
|
||||||
|
: !R.isEmpty(unavailableCryptosFiltered) &&
|
||||||
|
R.length(unavailableCryptosFiltered) < R.length(ALL_CRYPTOS)
|
||||||
|
? WARNING_LEVELS.PARTIAL
|
||||||
|
: WARNING_LEVELS.IMPORTANT
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: R.toUpper(it),
|
||||||
|
display: R.toUpper(it),
|
||||||
|
warning: warningLevel,
|
||||||
|
warningMessage: !R.isEmpty(unavailableCryptosFiltered)
|
||||||
|
? `No market pairs available for ${unavailableMarketsStr}`
|
||||||
|
: `All market pairs are available`
|
||||||
|
}
|
||||||
|
}, R.keys(markets))
|
||||||
|
}
|
||||||
|
|
||||||
|
export { secretTest, leadingZerosTest, buildCurrencyOptions }
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import binance from './binance'
|
import _binance from './binance'
|
||||||
import binanceus from './binanceus'
|
import _binanceus from './binanceus'
|
||||||
import bitfinex from './bitfinex'
|
import _bitfinex from './bitfinex'
|
||||||
import bitgo from './bitgo'
|
import bitgo from './bitgo'
|
||||||
import bitstamp from './bitstamp'
|
import _bitstamp from './bitstamp'
|
||||||
import blockcypher from './blockcypher'
|
import blockcypher from './blockcypher'
|
||||||
import cex from './cex'
|
import _cex from './cex'
|
||||||
import elliptic from './elliptic'
|
import elliptic from './elliptic'
|
||||||
import galoy from './galoy'
|
import galoy from './galoy'
|
||||||
import inforu from './inforu'
|
import inforu from './inforu'
|
||||||
import infura from './infura'
|
import infura from './infura'
|
||||||
import itbit from './itbit'
|
import _itbit from './itbit'
|
||||||
import kraken from './kraken'
|
import _kraken from './kraken'
|
||||||
import mailgun from './mailgun'
|
import mailgun from './mailgun'
|
||||||
import scorechain from './scorechain'
|
import scorechain from './scorechain'
|
||||||
import sumsub from './sumsub'
|
import sumsub from './sumsub'
|
||||||
|
|
@ -19,25 +19,37 @@ import trongrid from './trongrid'
|
||||||
import twilio from './twilio'
|
import twilio from './twilio'
|
||||||
import vonage from './vonage'
|
import vonage from './vonage'
|
||||||
|
|
||||||
export default {
|
const schemas = (markets = {}) => {
|
||||||
[bitgo.code]: bitgo,
|
const binance = _binance(markets?.binance)
|
||||||
[galoy.code]: galoy,
|
const bitfinex = _bitfinex(markets?.bitfinex)
|
||||||
[bitstamp.code]: bitstamp,
|
const binanceus = _binanceus(markets?.binanceus)
|
||||||
[blockcypher.code]: blockcypher,
|
const bitstamp = _bitstamp(markets?.bitstamp)
|
||||||
[elliptic.code]: elliptic,
|
const cex = _cex(markets?.cex)
|
||||||
[inforu.code]: inforu,
|
const itbit = _itbit(markets?.itbit)
|
||||||
[infura.code]: infura,
|
const kraken = _kraken(markets?.kraken)
|
||||||
[itbit.code]: itbit,
|
|
||||||
[kraken.code]: kraken,
|
return {
|
||||||
[mailgun.code]: mailgun,
|
[bitgo.code]: bitgo,
|
||||||
[telnyx.code]: telnyx,
|
[galoy.code]: galoy,
|
||||||
[vonage.code]: vonage,
|
[bitstamp.code]: bitstamp,
|
||||||
[twilio.code]: twilio,
|
[blockcypher.code]: blockcypher,
|
||||||
[binanceus.code]: binanceus,
|
[elliptic.code]: elliptic,
|
||||||
[cex.code]: cex,
|
[inforu.code]: inforu,
|
||||||
[scorechain.code]: scorechain,
|
[infura.code]: infura,
|
||||||
[trongrid.code]: trongrid,
|
[itbit.code]: itbit,
|
||||||
[binance.code]: binance,
|
[kraken.code]: kraken,
|
||||||
[bitfinex.code]: bitfinex,
|
[mailgun.code]: mailgun,
|
||||||
[sumsub.code]: sumsub
|
[telnyx.code]: telnyx,
|
||||||
|
[vonage.code]: vonage,
|
||||||
|
[twilio.code]: twilio,
|
||||||
|
[binanceus.code]: binanceus,
|
||||||
|
[cex.code]: cex,
|
||||||
|
[scorechain.code]: scorechain,
|
||||||
|
[trongrid.code]: trongrid,
|
||||||
|
[binance.code]: binance,
|
||||||
|
[bitfinex.code]: bitfinex,
|
||||||
|
[sumsub.code]: sumsub
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schemas
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,75 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { buildCurrencyOptions, secretTest } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'itbit',
|
return {
|
||||||
name: 'itBit',
|
code: 'itbit',
|
||||||
title: 'itBit (Exchange)',
|
name: 'itBit',
|
||||||
elements: [
|
title: 'itBit (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'userId',
|
{
|
||||||
display: 'User ID',
|
code: 'userId',
|
||||||
component: TextInputFormik,
|
display: 'User ID',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'walletId',
|
{
|
||||||
display: 'Wallet ID',
|
code: 'walletId',
|
||||||
component: TextInputFormik,
|
display: 'Wallet ID',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'clientKey',
|
{
|
||||||
display: 'Client key',
|
code: 'clientKey',
|
||||||
component: TextInputFormik
|
display: 'Client key',
|
||||||
},
|
component: TextInput
|
||||||
{
|
},
|
||||||
code: 'clientSecret',
|
{
|
||||||
display: 'Client secret',
|
code: 'clientSecret',
|
||||||
component: SecretInputFormik
|
display: 'Client secret',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
userId: Yup.string('The user ID must be a string')
|
||||||
|
.max(100, 'The user ID is too long')
|
||||||
|
.required('The user ID is required'),
|
||||||
|
walletId: Yup.string('The wallet ID must be a string')
|
||||||
|
.max(100, 'The wallet ID is too long')
|
||||||
|
.required('The wallet ID is required'),
|
||||||
|
clientKey: Yup.string('The client key must be a string')
|
||||||
|
.max(100, 'The client key is too long')
|
||||||
|
.required('The client key is required'),
|
||||||
|
clientSecret: Yup.string('The client secret must be a string')
|
||||||
|
.max(100, 'The client secret is too long')
|
||||||
|
.test(secretTest(account?.clientSecret, 'client secret')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
userId: Yup.string('The user ID must be a string')
|
|
||||||
.max(100, 'The user ID is too long')
|
|
||||||
.required('The user ID is required'),
|
|
||||||
walletId: Yup.string('The wallet ID must be a string')
|
|
||||||
.max(100, 'The wallet ID is too long')
|
|
||||||
.required('The wallet ID is required'),
|
|
||||||
clientKey: Yup.string('The client key must be a string')
|
|
||||||
.max(100, 'The client key is too long')
|
|
||||||
.required('The client key is required'),
|
|
||||||
clientSecret: Yup.string('The client secret must be a string')
|
|
||||||
.max(100, 'The client secret is too long')
|
|
||||||
.test(secretTest(account?.clientSecret, 'client secret'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,57 @@
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
import {
|
||||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
SecretInput,
|
||||||
|
TextInput,
|
||||||
|
Autocomplete
|
||||||
|
} from 'src/components/inputs/formik'
|
||||||
|
|
||||||
import { secretTest } from './helper'
|
import { secretTest, buildCurrencyOptions } from './helper'
|
||||||
|
|
||||||
export default {
|
const schema = markets => {
|
||||||
code: 'kraken',
|
return {
|
||||||
name: 'Kraken',
|
code: 'kraken',
|
||||||
title: 'Kraken (Exchange)',
|
name: 'Kraken',
|
||||||
elements: [
|
title: 'Kraken (Exchange)',
|
||||||
{
|
elements: [
|
||||||
code: 'apiKey',
|
{
|
||||||
display: 'API key',
|
code: 'apiKey',
|
||||||
component: TextInputFormik,
|
display: 'API key',
|
||||||
face: true,
|
component: TextInput,
|
||||||
long: true
|
face: true,
|
||||||
},
|
long: true
|
||||||
{
|
},
|
||||||
code: 'privateKey',
|
{
|
||||||
display: 'Private key',
|
code: 'privateKey',
|
||||||
component: SecretInputFormik
|
display: 'Private key',
|
||||||
|
component: SecretInput
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'currencyMarket',
|
||||||
|
display: 'Currency market',
|
||||||
|
component: Autocomplete,
|
||||||
|
inputProps: {
|
||||||
|
options: buildCurrencyOptions(markets),
|
||||||
|
labelProp: 'display',
|
||||||
|
valueProp: 'code'
|
||||||
|
},
|
||||||
|
face: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
getValidationSchema: account => {
|
||||||
|
return Yup.object().shape({
|
||||||
|
apiKey: Yup.string('The API key must be a string')
|
||||||
|
.max(100, 'The API key is too long')
|
||||||
|
.required('The API key is required'),
|
||||||
|
privateKey: Yup.string('The private key must be a string')
|
||||||
|
.max(100, 'The private key is too long')
|
||||||
|
.test(secretTest(account?.privateKey, 'private key')),
|
||||||
|
currencyMarket: Yup.string(
|
||||||
|
'The currency market must be a string'
|
||||||
|
).required('The currency market is required')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
],
|
|
||||||
getValidationSchema: account => {
|
|
||||||
return Yup.object().shape({
|
|
||||||
apiKey: Yup.string('The API key must be a string')
|
|
||||||
.max(100, 'The API key is too long')
|
|
||||||
.required('The API key is required'),
|
|
||||||
privateKey: Yup.string('The private key must be a string')
|
|
||||||
.max(100, 'The private key is too long')
|
|
||||||
.test(secretTest(account?.privateKey, 'private key'))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default schema
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,9 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
||||||
)
|
)
|
||||||
|
|
||||||
const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN
|
const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN
|
||||||
const commissionPercentage =
|
const commissionPercentage = BigNumber(
|
||||||
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
||||||
|
).toFixed(2, 1) // ROUND_DOWN
|
||||||
const fixedFee = Number.parseFloat(tx.fixedFee) || 0
|
const fixedFee = Number.parseFloat(tx.fixedFee) || 0
|
||||||
const fiat = BigNumber(tx.fiat)
|
const fiat = BigNumber(tx.fiat)
|
||||||
.minus(fixedFee)
|
.minus(fixedFee)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import { SupportLinkButton } from 'src/components/buttons'
|
||||||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import FormRenderer from 'src/pages/Services/FormRenderer'
|
import FormRenderer from 'src/pages/Services/FormRenderer'
|
||||||
import schemas from 'src/pages/Services/schemas'
|
import _schemas from 'src/pages/Services/schemas'
|
||||||
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
|
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
|
||||||
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
|
|
@ -54,6 +54,12 @@ const GET_INFO = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const GET_MARKETS = gql`
|
||||||
|
query getMarkets {
|
||||||
|
getMarkets
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const LOCALE = 'locale'
|
const LOCALE = 'locale'
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
@ -71,6 +77,8 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
||||||
refetchQueries: () => ['getData']
|
refetchQueries: () => ['getData']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { data: marketsData } = useQuery(GET_MARKETS)
|
||||||
|
|
||||||
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
||||||
onCompleted: () => setEditingSchema(null),
|
onCompleted: () => setEditingSchema(null),
|
||||||
refetchQueries: () => ['getData']
|
refetchQueries: () => ['getData']
|
||||||
|
|
@ -89,6 +97,10 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
||||||
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
||||||
const accounts = data?.accounts ?? []
|
const accounts = data?.accounts ?? []
|
||||||
|
|
||||||
|
const markets = marketsData?.getMarkets
|
||||||
|
|
||||||
|
const schemas = _schemas(markets)
|
||||||
|
|
||||||
const onChange = (previous, current, setValue) => {
|
const onChange = (previous, current, setValue) => {
|
||||||
if (!current) return setValue(current)
|
if (!current) return setValue(current)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||||
|
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
|
||||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||||
|
|
@ -193,6 +194,13 @@ const getLamassuRoutes = () => [
|
||||||
route: '/settings/operator-info/terms-conditions',
|
route: '/settings/operator-info/terms-conditions',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: TermsConditions
|
component: TermsConditions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'machine-screens',
|
||||||
|
label: 'Machine screens',
|
||||||
|
route: '/settings/operator-info/machine-screens',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
|
component: MachineScreens
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||||
|
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
|
||||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||||
|
|
@ -172,6 +173,13 @@ const getPazuzRoutes = () => [
|
||||||
route: '/settings/operator-info/terms-conditions',
|
route: '/settings/operator-info/terms-conditions',
|
||||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
component: TermsConditions
|
component: TermsConditions
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'machine-screens',
|
||||||
|
label: 'Machine screens',
|
||||||
|
route: '/settings/operator-info/machine-screens',
|
||||||
|
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||||
|
component: MachineScreens
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ const mistyRose = '#ffeceb'
|
||||||
const pumpkin = '#ff7311'
|
const pumpkin = '#ff7311'
|
||||||
const linen = '#fbf3ec'
|
const linen = '#fbf3ec'
|
||||||
|
|
||||||
|
// Warning
|
||||||
|
const orangeYellow = '#ffcc00'
|
||||||
|
|
||||||
// Color Variables
|
// Color Variables
|
||||||
const primaryColor = zodiac
|
const primaryColor = zodiac
|
||||||
|
|
||||||
|
|
@ -136,6 +139,7 @@ export {
|
||||||
java,
|
java,
|
||||||
neon,
|
neon,
|
||||||
linen,
|
linen,
|
||||||
|
orangeYellow,
|
||||||
// named colors
|
// named colors
|
||||||
primaryColor,
|
primaryColor,
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ const namespaces = {
|
||||||
RECEIPT: 'receipt',
|
RECEIPT: 'receipt',
|
||||||
COIN_ATM_RADAR: 'coinAtmRadar',
|
COIN_ATM_RADAR: 'coinAtmRadar',
|
||||||
TERMS_CONDITIONS: 'termsConditions',
|
TERMS_CONDITIONS: 'termsConditions',
|
||||||
TRIGGERS: 'triggersConfig'
|
TRIGGERS: 'triggersConfig',
|
||||||
|
MACHINE_SCREENS: 'machineScreens'
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapKeys = R.curry((fn, obj) =>
|
const mapKeys = R.curry((fn, obj) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue