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 notifierQueries = require('./notifier/queries')
|
||||
|
||||
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
|
||||
const getBlacklist = () => {
|
||||
return db.any(`SELECT * FROM blacklist`).then(res =>
|
||||
res.map(item => ({
|
||||
cryptoCode: item.crypto_code,
|
||||
address: item.address
|
||||
}))
|
||||
const getBlacklist = () =>
|
||||
db.any(
|
||||
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
|
||||
FROM blacklist JOIN blacklist_messages
|
||||
ON blacklist.blacklist_message_id = blacklist_messages.id`
|
||||
)
|
||||
|
||||
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 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) => {
|
||||
const insertIntoBlacklist = address => {
|
||||
return db
|
||||
.none(
|
||||
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);',
|
||||
[cryptoCode, address]
|
||||
'INSERT INTO blacklist (address) VALUES ($1);',
|
||||
[address]
|
||||
)
|
||||
}
|
||||
|
||||
function blocked (address, cryptoCode) {
|
||||
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2`
|
||||
return db.any(sql, [address, cryptoCode])
|
||||
function blocked (address) {
|
||||
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.oneOrNone(sql, [address])
|
||||
}
|
||||
|
||||
function addToUsedAddresses (address, cryptoCode) {
|
||||
// ETH reuses addresses
|
||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||
function getMessages () {
|
||||
const sql = `SELECT * FROM blacklist_messages`
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)`
|
||||
return db.oneOrNone(sql, [cryptoCode, address])
|
||||
function editBlacklistMessage (id, content) {
|
||||
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
||||
return db.oneOrNone(sql, [content, id])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
blocked,
|
||||
addToUsedAddresses,
|
||||
getBlacklist,
|
||||
deleteFromBlacklist,
|
||||
insertIntoBlacklist
|
||||
insertIntoBlacklist,
|
||||
getMessages,
|
||||
editBlacklistMessage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
||||
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
||||
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.logger.info('Updating wallet...')
|
||||
|
|
@ -55,6 +59,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
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()) {
|
||||
common.logger.info('Starting wallet...')
|
||||
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.es(`sudo supervisorctl stop bitcoincash`)
|
||||
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.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -29,37 +29,47 @@ module.exports = {
|
|||
const BINARIES = {
|
||||
BTC: {
|
||||
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',
|
||||
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'
|
||||
},
|
||||
ETH: {
|
||||
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'
|
||||
},
|
||||
ZEC: {
|
||||
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'
|
||||
},
|
||||
DASH: {
|
||||
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',
|
||||
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'
|
||||
},
|
||||
LTC: {
|
||||
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',
|
||||
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'
|
||||
},
|
||||
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',
|
||||
urlHash: '0dcc387cbaa3a039c97ddc8fb99c1fa7bff5dc6e4bd3a01d3c3095f595ad2dce',
|
||||
dir: 'bitcoin-cash-node-27.1.0/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
},
|
||||
XMR: {
|
||||
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',
|
||||
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}`)
|
||||
|
||||
const url = requiresUpdate ? binaries.defaultUrl : binaries.url
|
||||
const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash
|
||||
const downloadFile = path.basename(url)
|
||||
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
|
||||
|
||||
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}`)
|
||||
|
||||
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.es(`sudo supervisorctl stop dash`)
|
||||
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.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.es(`sudo supervisorctl stop ethereum`)
|
||||
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.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop litecoin`)
|
||||
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.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Monero. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
||||
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.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.es(`sudo supervisorctl stop zcash`)
|
||||
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.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const E = require('../error')
|
|||
|
||||
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 massage = _.flow(_.omit(massageFields),
|
||||
|
|
|
|||
|
|
@ -32,38 +32,41 @@ function post (machineTx, pi) {
|
|||
return cashInAtomic.atomic(machineTx, pi)
|
||||
.then(r => {
|
||||
const updatedTx = r.tx
|
||||
let blacklisted = false
|
||||
let addressReuse = false
|
||||
let walletScore = {}
|
||||
|
||||
const promises = [settingsLoader.loadLatestConfig()]
|
||||
|
||||
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||
if (isFirstPost) {
|
||||
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi))
|
||||
promises.push(
|
||||
checkForBlacklisted(updatedTx),
|
||||
doesTxReuseAddress(updatedTx),
|
||||
getWalletScore(updatedTx, pi)
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
|
||||
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
||||
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
||||
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||
const isBlacklisted = !!blacklisted
|
||||
|
||||
walletScore = fetchedWalletScore
|
||||
|
||||
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
|
||||
blacklisted = true
|
||||
if (isBlacklisted) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
||||
} else if (isReusedAddress && rejectAddressReuse) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, 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) {
|
||||
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
||||
return blacklist.blocked(tx.toAddress)
|
||||
}
|
||||
|
||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const camelize = require('./utils')
|
|||
|
||||
function createCashboxBatch (deviceId, cashboxCount) {
|
||||
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 = `
|
||||
UPDATE bills SET cashbox_batch_id=$1
|
||||
FROM cash_in_txs
|
||||
|
|
@ -25,6 +25,7 @@ function createCashboxBatch (deviceId, cashboxCount) {
|
|||
const q2 = t.none(sql2, [batchId, deviceId])
|
||||
const q3 = t.none(sql3, [batchId, deviceId])
|
||||
return t.batch([q1, q2, q3])
|
||||
.then(([it]) => it)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -100,14 +101,6 @@ function editBatchById (id, performedBy) {
|
|||
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) {
|
||||
return _.map(
|
||||
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 = {
|
||||
createCashboxBatch,
|
||||
updateMachineWithBatch,
|
||||
getBatches,
|
||||
getBillsByBatchId,
|
||||
editBatchById,
|
||||
getBatchById,
|
||||
getMachineUnbatchedBills,
|
||||
logFormatter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { ALL_CRYPTOS } = require('@lamassu/coins')
|
||||
|
||||
const configManager = require('./new-config-manager')
|
||||
const ccxt = require('./plugins/exchange/ccxt')
|
||||
const mockExchange = require('./plugins/exchange/mock-exchange')
|
||||
const accounts = require('./new-admin/config/accounts')
|
||||
|
||||
function lookupExchange (settings, cryptoCode) {
|
||||
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
|
||||
|
|
@ -45,8 +49,26 @@ function active (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 = {
|
||||
fetchExchange,
|
||||
buy,
|
||||
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. */
|
||||
const buildTriggers = allTriggers => {
|
||||
const normalTriggers = []
|
||||
|
|
@ -103,7 +115,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
_.pick([
|
||||
'coins',
|
||||
'configVersion',
|
||||
'timezone'
|
||||
'timezone',
|
||||
'screenOptions'
|
||||
]),
|
||||
_.update('coins', massageCoins),
|
||||
_.set('serverVersion', VERSION),
|
||||
|
|
@ -117,6 +130,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
configManager.getLocale(deviceId, settings.config),
|
||||
configManager.getOperatorInfo(settings.config),
|
||||
configManager.getReceipt(settings.config),
|
||||
configManager.getAllMachineScreenOpts(settings.config),
|
||||
!!configManager.getCashOut(deviceId, settings.config).active,
|
||||
getMachine(deviceId, currentConfigVersion),
|
||||
configManager.getCustomerAuthenticationMethod(settings.config)
|
||||
|
|
@ -129,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
localeInfo,
|
||||
operatorInfo,
|
||||
receiptInfo,
|
||||
machineScreenOpts,
|
||||
twoWayMode,
|
||||
{ numberOfCassettes, numberOfRecyclers },
|
||||
customerAuthentication,
|
||||
|
|
@ -153,7 +168,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
urlsToPing,
|
||||
}),
|
||||
addOperatorInfo(operatorInfo),
|
||||
addReceiptInfo(receiptInfo)
|
||||
addReceiptInfo(receiptInfo),
|
||||
addMachineScreenOpts(machineScreenOpts)
|
||||
)(staticConf))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,14 @@ type ReceiptInfo {
|
|||
addressQRCode: Boolean!
|
||||
}
|
||||
|
||||
type MachineScreenOptions {
|
||||
rates: RateScreenOptions!
|
||||
}
|
||||
|
||||
type RateScreenOptions {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
type SpeedtestFile {
|
||||
url: String!
|
||||
size: Int!
|
||||
|
|
@ -147,6 +155,7 @@ type StaticConfig {
|
|||
operatorInfo: OperatorInfo
|
||||
machineInfo: MachineInfo!
|
||||
receiptInfo: ReceiptInfo
|
||||
screenOptions: MachineScreenOptions
|
||||
|
||||
speedtestFiles: [SpeedtestFile!]!
|
||||
urlsToPing: [String!]!
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
blacklist: () => blacklist.getBlacklist()
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
blacklistMessages: () => blacklist.getMessages()
|
||||
},
|
||||
Mutation: {
|
||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address)
|
||||
deleteBlacklistRow: (...[, { address }]) =>
|
||||
blacklist.deleteFromBlacklist(address),
|
||||
insertBlacklistRow: (...[, { 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 loyalty = require('./loyalty.resolver')
|
||||
const machine = require('./machine.resolver')
|
||||
const market = require('./market.resolver')
|
||||
const notification = require('./notification.resolver')
|
||||
const pairing = require('./pairing.resolver')
|
||||
const rates = require('./rates.resolver')
|
||||
|
|
@ -35,6 +36,7 @@ const resolvers = [
|
|||
log,
|
||||
loyalty,
|
||||
machine,
|
||||
market,
|
||||
notification,
|
||||
pairing,
|
||||
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`
|
||||
type Blacklist {
|
||||
cryptoCode: String!
|
||||
address: String!
|
||||
blacklistMessage: BlacklistMessage!
|
||||
}
|
||||
|
||||
type BlacklistMessage {
|
||||
id: ID
|
||||
label: String
|
||||
content: String
|
||||
allowToggle: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
blacklist: [Blacklist] @auth
|
||||
blacklistMessages: [BlacklistMessage] @auth
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
deleteBlacklistRow(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 loyalty = require('./loyalty.type')
|
||||
const machine = require('./machine.type')
|
||||
const market = require('./market.type')
|
||||
const notification = require('./notification.type')
|
||||
const pairing = require('./pairing.type')
|
||||
const rates = require('./rates.type')
|
||||
|
|
@ -35,6 +36,7 @@ const types = [
|
|||
log,
|
||||
loyalty,
|
||||
machine,
|
||||
market,
|
||||
notification,
|
||||
pairing,
|
||||
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,
|
||||
simplified
|
||||
) {
|
||||
const isCsvExport = _.isBoolean(simplified)
|
||||
const packager = _.flow(
|
||||
_.flatten,
|
||||
_.orderBy(_.property('created'), ['desc']),
|
||||
|
|
@ -92,7 +93,7 @@ function batch (
|
|||
AND ($12 is null or txs.to_address = $12)
|
||||
AND ($13 is null or txs.txStatus = $13)
|
||||
${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`
|
||||
|
||||
const cashOutSql = `SELECT 'cashOut' AS tx_class,
|
||||
|
|
@ -126,7 +127,7 @@ function batch (
|
|||
AND ($13 is null or txs.txStatus = $13)
|
||||
AND ($14 is null or txs.swept = $14)
|
||||
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
|
||||
AND (fiat > 0)
|
||||
${isCsvExport ? '' : 'AND fiat > 0'}
|
||||
ORDER BY created DESC limit $4 offset $5`
|
||||
|
||||
// The swept filter is cash-out only, so omit the cash-in query entirely
|
||||
|
|
@ -152,14 +153,14 @@ function batch (
|
|||
|
||||
return Promise.all(promises)
|
||||
.then(packager)
|
||||
.then(res => {
|
||||
if (simplified) return simplifiedBatch(res)
|
||||
.then(res =>
|
||||
!isCsvExport ? res :
|
||||
// GQL transactions and transactionsCsv both use this function and
|
||||
// if we don't check for the correct simplified value, the Transactions page polling
|
||||
// will continuously build a csv in the background
|
||||
else if (simplified === false) return advancedBatch(res)
|
||||
return res
|
||||
})
|
||||
simplified ? simplifiedBatch(res) :
|
||||
advancedBatch(res)
|
||||
)
|
||||
}
|
||||
|
||||
function advancedBatch (data) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ const namespaces = {
|
|||
TERMS_CONDITIONS: 'termsConditions',
|
||||
CASH_OUT: 'cashOut',
|
||||
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)
|
||||
|
|
@ -72,6 +77,8 @@ const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR)
|
|||
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
|
||||
const getReceipt = fromNamespace(namespaces.RECEIPT)
|
||||
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 locale = fromNamespace(namespaces.LOCALE)(config)
|
||||
|
|
@ -180,6 +187,8 @@ module.exports = {
|
|||
getWalletSettings,
|
||||
getCashInSettings,
|
||||
getOperatorInfo,
|
||||
getMachineScreenOpts,
|
||||
getAllMachineScreenOpts,
|
||||
getNotifications,
|
||||
getGlobalNotifications,
|
||||
getLocale,
|
||||
|
|
|
|||
|
|
@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) {
|
|||
.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) {
|
||||
return loadAccounts(schemaVersion)
|
||||
.then(accounts => {
|
||||
const filledSecretPaths = _.compact(_.map(path => {
|
||||
if (!_.isEmpty(_.get(path, accounts))) {
|
||||
return path
|
||||
}
|
||||
}, SECRET_FIELDS))
|
||||
return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts)
|
||||
})
|
||||
.then(hideSecretFields)
|
||||
}
|
||||
|
||||
const insertConfigRow = (dbOrTx, data) =>
|
||||
|
|
|
|||
|
|
@ -278,6 +278,7 @@ function plugins (settings, deviceId) {
|
|||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const fiatCode = localeConfig.fiatCurrency
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config)
|
||||
|
||||
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
|
||||
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
|
||||
|
|
@ -327,7 +328,8 @@ function plugins (settings, deviceId) {
|
|||
coins,
|
||||
configVersion,
|
||||
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
|
||||
timezone
|
||||
timezone,
|
||||
screenOptions: machineScreenOpts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -475,25 +477,28 @@ function plugins (settings, deviceId) {
|
|||
|
||||
function buyAndSell (rec, doBuy, tx) {
|
||||
const cryptoCode = rec.cryptoCode
|
||||
const fiatCode = rec.fiatCode
|
||||
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
|
||||
return exchange.fetchExchange(settings, cryptoCode)
|
||||
.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 internalTxId = tx ? tx.id : rec.id
|
||||
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
|
||||
if (!tradesQueues[market]) tradesQueues[market] = []
|
||||
tradesQueues[market].push({
|
||||
direction,
|
||||
internalTxId,
|
||||
fiatCode,
|
||||
cryptoAtoms,
|
||||
cryptoCode,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
const direction = doBuy ? 'cashIn' : 'cashOut'
|
||||
const internalTxId = tx ? tx.id : rec.id
|
||||
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
|
||||
if (!tradesQueues[market]) tradesQueues[market] = []
|
||||
tradesQueues[market].push({
|
||||
direction,
|
||||
internalTxId,
|
||||
fiatCode,
|
||||
cryptoAtoms,
|
||||
cryptoCode,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function consolidateTrades (cryptoCode, fiatCode) {
|
||||
|
|
@ -550,19 +555,22 @@ function plugins (settings, deviceId) {
|
|||
const deviceIds = devices.map(device => device.deviceId)
|
||||
const lists = deviceIds.map(deviceId => {
|
||||
const localeConfig = configManager.getLocale(deviceId, settings.config)
|
||||
const fiatCode = localeConfig.fiatCurrency
|
||||
const cryptoCodes = localeConfig.cryptoCurrencies
|
||||
|
||||
return cryptoCodes.map(cryptoCode => ({
|
||||
fiatCode,
|
||||
cryptoCode
|
||||
return Promise.all(cryptoCodes.map(cryptoCode => {
|
||||
return exchange.fetchExchange(settings, cryptoCode)
|
||||
.then(exchange => ({
|
||||
fiatCode: exchange.account.currencyMarket,
|
||||
cryptoCode
|
||||
}))
|
||||
}))
|
||||
})
|
||||
|
||||
const tradesPromises = _.uniq(_.flatten(lists))
|
||||
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))
|
||||
|
||||
return Promise.all(tradesPromises)
|
||||
|
||||
return Promise.all(lists)
|
||||
})
|
||||
.then(lists => {
|
||||
return Promise.all(_.uniq(_.flatten(lists))
|
||||
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)))
|
||||
})
|
||||
.catch(logger.error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) {
|
|||
if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) {
|
||||
throw new Error('Unsupported crypto: ' + cryptoCode)
|
||||
}
|
||||
const fiatSupported = ALL[serviceName].FIAT
|
||||
if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) {
|
||||
logger.info('Building a market for an unsupported fiat. Defaulting to EUR market')
|
||||
return cryptoCode + '/' + 'EUR'
|
||||
}
|
||||
|
||||
if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code')
|
||||
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 FIAT = ['EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
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 CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
|
||||
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 mapper = {
|
||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
|||
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 CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 8
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret']
|
||||
|
||||
|
|
@ -18,4 +19,4 @@ const loadConfig = (account) => {
|
|||
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 CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 8
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -19,4 +20,4 @@ const loadConfig = (account) => {
|
|||
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 _ = require('lodash/fp')
|
||||
const ccxt = require('ccxt')
|
||||
const mem = require('mem')
|
||||
|
||||
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
|
||||
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_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
|
||||
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 amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
|
||||
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
|
||||
|
|
@ -50,4 +55,36 @@ function calculatePrice (side, amount, orderBook) {
|
|||
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 CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
|
||||
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 mapper = {
|
||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
|||
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 CRYPTO = [BTC, ETH, USDT, LN]
|
||||
const FIAT = ['USD']
|
||||
const DEFAULT_FIAT_MARKET = 'USD'
|
||||
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 mapper = {
|
||||
|
|
@ -21,4 +22,4 @@ const loadConfig = (account) => {
|
|||
}
|
||||
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 CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 6
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
const USER_REF = 'userref'
|
||||
|
||||
const loadConfig = (account) => {
|
||||
|
|
@ -26,4 +27,4 @@ const loadConfig = (account) => {
|
|||
|
||||
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)
|
||||
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.use('/', pairingRoutes)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +1,41 @@
|
|||
const express = require('express')
|
||||
const _ = require('lodash/fp')
|
||||
const router = express.Router()
|
||||
|
||||
const cashbox = require('../cashbox-batches')
|
||||
const notifier = require('../notifier')
|
||||
const { getMachine, setMachine } = require('../machine-loader')
|
||||
const { getMachine, setMachine, getMachineName } = require('../machine-loader')
|
||||
const { loadLatestConfig } = require('../new-settings-loader')
|
||||
const { getCashInSettings } = require('../new-config-manager')
|
||||
const { AUTOMATIC } = require('../constants')
|
||||
const logger = require('../logger')
|
||||
|
||||
function notifyCashboxRemoval (req, res, next) {
|
||||
|
||||
function cashboxRemoval (req, res, next) {
|
||||
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)
|
||||
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
||||
return Promise.all([getMachine(req.deviceId), loadLatestConfig()])
|
||||
.then(([machine, config]) => {
|
||||
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
|
||||
const cashInSettings = getCashInSettings(config)
|
||||
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created')
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||
return res.status(200).send({ status: 'OK' })
|
||||
return Promise.all([
|
||||
cashbox.getMachineUnbatchedBills(req.deviceId),
|
||||
getMachineName(req.deviceId)
|
||||
])
|
||||
}
|
||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
||||
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox)
|
||||
.then(() => {
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||
return res.status(200).send({ status: 'OK' })
|
||||
})
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||
.then(batch => Promise.all([
|
||||
cashbox.getBatchById(batch.id),
|
||||
getMachineName(batch.device_id),
|
||||
setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
|
||||
]))
|
||||
})
|
||||
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
router.post('/removal', notifyCashboxRemoval)
|
||||
router.post('/removal', cashboxRemoval)
|
||||
|
||||
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 arrowClasses = {
|
||||
const getArrowClasses = placement => ({
|
||||
[classes.arrow]: true,
|
||||
[classes.arrowBottom]: props.placement === 'bottom',
|
||||
[classes.arrowTop]: props.placement === 'top',
|
||||
[classes.arrowRight]: props.placement === 'right',
|
||||
[classes.arrowLeft]: props.placement === 'left'
|
||||
[classes.arrowBottom]: placement === 'bottom',
|
||||
[classes.arrowTop]: placement === 'top',
|
||||
[classes.arrowRight]: placement === 'right',
|
||||
[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: {
|
||||
enabled: false
|
||||
enabled: R.defaultTo(false, props.flip),
|
||||
allowedAutoPlacements: flipPlacements[props.placement],
|
||||
boundary: 'clippingParents'
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
enabled: R.defaultTo(true, props.preventOverflow),
|
||||
boundariesElement: 'scrollParent'
|
||||
},
|
||||
offset: {
|
||||
|
|
@ -126,7 +135,7 @@ const Popover = ({
|
|||
offset: '0, 10'
|
||||
},
|
||||
arrow: {
|
||||
enabled: true,
|
||||
enabled: R.defaultTo(true, props.showArrow),
|
||||
element: arrowRef
|
||||
},
|
||||
computeStyle: {
|
||||
|
|
@ -134,6 +143,12 @@ const Popover = ({
|
|||
}
|
||||
})
|
||||
|
||||
if (props.preventOverflow === false) {
|
||||
modifiers.hide = {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MaterialPopper
|
||||
|
|
@ -141,10 +156,15 @@ const Popover = ({
|
|||
modifiers={modifiers}
|
||||
className={classes.popover}
|
||||
{...props}>
|
||||
<Paper className={classnames(classes.root, className)}>
|
||||
<span className={classnames(arrowClasses)} ref={setArrowRef} />
|
||||
{children}
|
||||
</Paper>
|
||||
{({ placement }) => (
|
||||
<Paper className={classnames(classes.root, className)}>
|
||||
<span
|
||||
className={classnames(getArrowClasses(placement))}
|
||||
ref={setArrowRef}
|
||||
/>
|
||||
{children}
|
||||
</Paper>
|
||||
)}
|
||||
</MaterialPopper>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,8 @@ const styles = {
|
|||
alignItems: 'center',
|
||||
borderRadius: 4,
|
||||
'& img': {
|
||||
maxHeight: 145
|
||||
height: 145,
|
||||
minWidth: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +128,8 @@ const IDButton = memo(
|
|||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
arrowSize={3}
|
||||
placement="top">
|
||||
placement="top"
|
||||
flip>
|
||||
<div className={classes.popoverContent}>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { Box } from '@material-ui/core'
|
||||
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
||||
import sort from 'match-sorter'
|
||||
import * as R from 'ramda'
|
||||
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'
|
||||
|
||||
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 { utils as coinUtils } from '@lamassu/coins'
|
||||
import { addressDetector } from '@lamassu/coins'
|
||||
import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
|
||||
import Grid from '@material-ui/core/Grid'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import { HelpTooltip } from 'src/components/Tooltip'
|
||||
import {
|
||||
Link,
|
||||
Button,
|
||||
IconButton,
|
||||
SupportLinkButton
|
||||
} from 'src/components/buttons'
|
||||
import { Link, Button, IconButton } from 'src/components/buttons'
|
||||
import { Switch } from 'src/components/inputs'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
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 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 styles from './Blacklist.styles'
|
||||
import BlackListAdvanced from './BlacklistAdvanced'
|
||||
import BlackListModal from './BlacklistModal'
|
||||
import BlacklistTable from './BlacklistTable'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const groupByCode = R.groupBy(obj => obj.cryptoCode)
|
||||
|
||||
const DELETE_ROW = gql`
|
||||
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||
cryptoCode
|
||||
mutation DeleteBlacklistRow($address: String!) {
|
||||
deleteBlacklistRow(address: $address) {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +34,6 @@ const DELETE_ROW = gql`
|
|||
const GET_BLACKLIST = gql`
|
||||
query getBlacklistData {
|
||||
blacklist {
|
||||
cryptoCode
|
||||
address
|
||||
}
|
||||
cryptoCurrencies {
|
||||
|
|
@ -64,14 +56,32 @@ const GET_INFO = gql`
|
|||
`
|
||||
|
||||
const ADD_ROW = gql`
|
||||
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||
cryptoCode
|
||||
mutation InsertBlacklistRow($address: String!) {
|
||||
insertBlacklistRow(address: $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 classes = useStyles()
|
||||
|
||||
|
|
@ -117,14 +127,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
|||
const Blacklist = () => {
|
||||
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
||||
const { data: configData } = useQuery(GET_INFO)
|
||||
const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [clickedItem, setClickedItem] = useState({
|
||||
code: 'BTC',
|
||||
display: 'Bitcoin'
|
||||
})
|
||||
const [errorMsg, setErrorMsg] = useState(null)
|
||||
const [editMessageError, setEditMessageError] = useState(null)
|
||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||
const [confirmDialog, setConfirmDialog] = useState(false)
|
||||
const [advancedSettings, setAdvancedSettings] = useState(false)
|
||||
|
||||
const [deleteEntry] = useMutation(DELETE_ROW, {
|
||||
onError: ({ message }) => {
|
||||
|
|
@ -144,14 +153,14 @@ const Blacklist = () => {
|
|||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, {
|
||||
onError: e => setEditMessageError(e),
|
||||
refetchQueries: () => ['getBlacklistData']
|
||||
})
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
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 =
|
||||
configData?.config && fromNamespace('compliance')(configData.config)
|
||||
|
|
@ -165,12 +174,8 @@ const Blacklist = () => {
|
|||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
const onClickSidebarItem = e => {
|
||||
setClickedItem({ code: e.code, display: e.display })
|
||||
}
|
||||
|
||||
const handleDeleteEntry = (cryptoCode, address) => {
|
||||
deleteEntry({ variables: { cryptoCode, address } })
|
||||
const handleDeleteEntry = address => {
|
||||
deleteEntry({ variables: { address } })
|
||||
}
|
||||
|
||||
const handleConfirmDialog = confirm => {
|
||||
|
|
@ -180,21 +185,21 @@ const Blacklist = () => {
|
|||
setConfirmDialog(false)
|
||||
}
|
||||
|
||||
const validateAddress = (cryptoCode, address) => {
|
||||
const validateAddress = address => {
|
||||
try {
|
||||
return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address))
|
||||
return !R.isEmpty(addressDetector.detectAddress(address).matches)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const addToBlacklist = async (cryptoCode, address) => {
|
||||
const addToBlacklist = async address => {
|
||||
setErrorMsg(null)
|
||||
if (!validateAddress(cryptoCode, address)) {
|
||||
if (!validateAddress(address)) {
|
||||
setErrorMsg('Invalid address')
|
||||
return
|
||||
}
|
||||
const res = await addEntry({ variables: { cryptoCode, address } })
|
||||
const res = await addEntry({ variables: { address } })
|
||||
if (!res.errors) {
|
||||
return setShowModal(false)
|
||||
}
|
||||
|
|
@ -208,6 +213,15 @@ const Blacklist = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const editBlacklistMessage = r => {
|
||||
editMessage({
|
||||
variables: {
|
||||
id: r.id,
|
||||
content: r.content
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaperWalletDialog
|
||||
|
|
@ -217,32 +231,23 @@ const Blacklist = () => {
|
|||
setConfirmDialog(false)
|
||||
}}
|
||||
/>
|
||||
<TitleSection title="Blacklisted addresses">
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||
Blacklist new addresses
|
||||
</Link>
|
||||
</Box>
|
||||
</TitleSection>
|
||||
<Grid container className={classes.grid}>
|
||||
<Sidebar
|
||||
data={availableCurrencies}
|
||||
isSelected={R.propEq('code', clickedItem.code)}
|
||||
displayName={it => it.display}
|
||||
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>
|
||||
<TitleSection
|
||||
title="Blacklisted addresses"
|
||||
buttons={[
|
||||
{
|
||||
text: 'Advanced settings',
|
||||
icon: SettingsIcon,
|
||||
inverseIcon: ReverseSettingsIcon,
|
||||
toggle: setAdvancedSettings
|
||||
}
|
||||
]}>
|
||||
{!advancedSettings && (
|
||||
<Box display="flex" alignItems="center" justifyContent="flex-end">
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
mr="-140px">
|
||||
mr="15px">
|
||||
<P>Enable paper wallet (only)</P>
|
||||
<Switch
|
||||
checked={enablePaperWalletOnly}
|
||||
|
|
@ -268,7 +273,7 @@ const Blacklist = () => {
|
|||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
mr="-5px">
|
||||
mr="15px">
|
||||
<P>Reject reused addresses</P>
|
||||
<Switch
|
||||
checked={rejectAddressReuse}
|
||||
|
|
@ -280,24 +285,22 @@ const Blacklist = () => {
|
|||
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||
<HelpTooltip width={304}>
|
||||
<P>
|
||||
The "Reject reused addresses" option means that all addresses
|
||||
that are used once will be automatically rejected if there's
|
||||
an attempt to use them again on a new transaction.
|
||||
This option requires a user to scan a fresh wallet address if
|
||||
they attempt to scan one that had been previously used for a
|
||||
transaction in your network.
|
||||
</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>
|
||||
</Box>
|
||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||
Blacklist new addresses
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</TitleSection>
|
||||
{!advancedSettings && (
|
||||
<div className={classes.content}>
|
||||
<BlacklistTable
|
||||
data={formattedData}
|
||||
selectedCoin={clickedItem}
|
||||
data={blacklistData}
|
||||
handleDeleteEntry={handleDeleteEntry}
|
||||
errorMessage={errorMsg}
|
||||
setErrorMessage={setErrorMsg}
|
||||
|
|
@ -305,7 +308,15 @@ const Blacklist = () => {
|
|||
setDeleteDialog={setDeleteDialog}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
{advancedSettings && (
|
||||
<BlackListAdvanced
|
||||
data={messagesResponse}
|
||||
editBlacklistMessage={editBlacklistMessage}
|
||||
mutationError={editMessageError}
|
||||
onClose={() => refetch()}
|
||||
/>
|
||||
)}
|
||||
{showModal && (
|
||||
<BlackListModal
|
||||
onClose={() => {
|
||||
|
|
@ -313,7 +324,6 @@ const Blacklist = () => {
|
|||
setShowModal(false)
|
||||
}}
|
||||
errorMsg={errorMsg}
|
||||
selectedCoin={clickedItem}
|
||||
addToBlacklist={addToBlacklist}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,23 @@ const styles = {
|
|||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
marginLeft: spacer * 6
|
||||
flex: 1
|
||||
},
|
||||
advancedForm: {
|
||||
'& > *': {
|
||||
marginTop: 20
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
},
|
||||
modalTitle: {
|
||||
margin: [['auto', 0, 8.5, 'auto']]
|
||||
|
|
@ -54,6 +66,9 @@ const styles = {
|
|||
cancelButton: {
|
||||
marginRight: 8,
|
||||
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'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const BlackListModal = ({
|
||||
onClose,
|
||||
selectedCoin,
|
||||
addToBlacklist,
|
||||
errorMsg
|
||||
}) => {
|
||||
const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => {
|
||||
const classes = useStyles()
|
||||
const handleAddToBlacklist = address => {
|
||||
if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) {
|
||||
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'
|
||||
addToBlacklist(address)
|
||||
}
|
||||
|
||||
const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
|
|
@ -61,26 +44,20 @@ const BlackListModal = ({
|
|||
handleAddToBlacklist(address.trim())
|
||||
}}>
|
||||
<Form id="address-form">
|
||||
<H3 className={classes.modalTitle}>
|
||||
{selectedCoin.display
|
||||
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
||||
: ''}
|
||||
</H3>
|
||||
<H3 className={classes.modalTitle}>Blacklist new address</H3>
|
||||
<Field
|
||||
name="address"
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
label="Paste new address to blacklist here"
|
||||
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
||||
placeholder={`ex: ${placeholderAddress}`}
|
||||
component={TextInput}
|
||||
/>
|
||||
{!R.isNil(errorMsg) && (
|
||||
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
|
||||
)}
|
||||
</Form>
|
||||
</Formik>
|
||||
<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">
|
||||
Blacklist address
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import React, { useState } from 'react'
|
|||
import { DeleteDialog } from 'src/components/DeleteDialog'
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { Label1 } from 'src/components/typography'
|
||||
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
|
||||
|
|
@ -15,7 +14,6 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const BlacklistTable = ({
|
||||
data,
|
||||
selectedCoin,
|
||||
handleDeleteEntry,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
|
|
@ -29,8 +27,8 @@ const BlacklistTable = ({
|
|||
const elements = [
|
||||
{
|
||||
name: 'address',
|
||||
header: <Label1 className={classes.white}>{'Addresses'}</Label1>,
|
||||
width: 800,
|
||||
header: 'Address',
|
||||
width: 1070,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
|
|
@ -41,7 +39,7 @@ const BlacklistTable = ({
|
|||
},
|
||||
{
|
||||
name: 'deleteButton',
|
||||
header: <Label1 className={classes.white}>{'Delete'}</Label1>,
|
||||
header: 'Delete',
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
|
|
@ -57,14 +55,11 @@ const BlacklistTable = ({
|
|||
)
|
||||
}
|
||||
]
|
||||
const dataToShow = selectedCoin
|
||||
? data[selectedCoin.code]
|
||||
: data[R.keys(data)[0]]
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={dataToShow}
|
||||
data={data}
|
||||
elements={elements}
|
||||
emptyText="No blacklisted addresses so far"
|
||||
name="blacklistTable"
|
||||
|
|
@ -77,10 +72,7 @@ const BlacklistTable = ({
|
|||
}}
|
||||
onConfirmed={() => {
|
||||
setErrorMessage(null)
|
||||
handleDeleteEntry(
|
||||
R.path(['cryptoCode'], toBeDeleted),
|
||||
R.path(['address'], toBeDeleted)
|
||||
)
|
||||
handleDeleteEntry(R.path(['address'], toBeDeleted))
|
||||
}}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ const EditableCard = ({
|
|||
{editing && (
|
||||
<div className={classes.editingWrapper}>
|
||||
<div className={classes.replace}>
|
||||
{hasImage && (
|
||||
{hasImage && state !== OVERRIDE_PENDING && (
|
||||
<ActionButton
|
||||
color="secondary"
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default {
|
|||
height: 36
|
||||
},
|
||||
tBody: {
|
||||
maxHeight: '65vh',
|
||||
maxHeight: 'calc(100vh - 350px)',
|
||||
overflow: 'auto'
|
||||
},
|
||||
tableWidth: {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,12 @@ const styles = {
|
|||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
},
|
||||
tableWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
marginBottom: 80
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -243,13 +249,15 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
|
|||
]
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
loading={loading}
|
||||
name="cashboxHistory"
|
||||
elements={elements}
|
||||
data={batches}
|
||||
emptyText="No cash box batches so far"
|
||||
/>
|
||||
<div className={classes.tableWrapper}>
|
||||
<DataTable
|
||||
loading={loading}
|
||||
name="cashboxHistory"
|
||||
elements={elements}
|
||||
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 FormRenderer from './FormRenderer'
|
||||
import schemas from './schemas'
|
||||
import _schemas from './schemas'
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getData {
|
||||
|
|
@ -21,6 +21,12 @@ const GET_INFO = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const GET_MARKETS = gql`
|
||||
query getMarkets {
|
||||
getMarkets
|
||||
}
|
||||
`
|
||||
|
||||
const SAVE_ACCOUNT = gql`
|
||||
mutation Save($accounts: JSONObject) {
|
||||
saveAccounts(accounts: $accounts)
|
||||
|
|
@ -40,12 +46,17 @@ const useStyles = makeStyles(styles)
|
|||
const Services = () => {
|
||||
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, {
|
||||
onCompleted: () => setEditingSchema(null),
|
||||
refetchQueries: ['getData']
|
||||
})
|
||||
|
||||
const markets = marketsData?.getMarkets
|
||||
|
||||
const schemas = _schemas(markets)
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const accounts = data?.accounts ?? {}
|
||||
|
|
@ -101,40 +112,44 @@ const Services = () => {
|
|||
const getValidationSchema = ({ code, getValidationSchema }) =>
|
||||
getValidationSchema(accounts[code])
|
||||
|
||||
const loading = marketsLoading || configLoading
|
||||
|
||||
return (
|
||||
<div className={classes.wrapper}>
|
||||
<TitleSection title="Third-Party services" />
|
||||
<Grid container spacing={4}>
|
||||
{R.values(schemas).map(schema => (
|
||||
<Grid item key={schema.code}>
|
||||
<SingleRowTable
|
||||
editMessage={'Configure ' + schema.title}
|
||||
title={schema.title}
|
||||
onEdit={() => setEditingSchema(schema)}
|
||||
items={getItems(schema.code, schema.elements)}
|
||||
!loading && (
|
||||
<div className={classes.wrapper}>
|
||||
<TitleSection title="Third-Party services" />
|
||||
<Grid container spacing={4}>
|
||||
{R.values(schemas).map(schema => (
|
||||
<Grid item key={schema.code}>
|
||||
<SingleRowTable
|
||||
editMessage={'Configure ' + schema.title}
|
||||
title={schema.title}
|
||||
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>
|
||||
))}
|
||||
</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)}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +1,57 @@
|
|||
import * as Yup from 'yup'
|
||||
|
||||
import SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'binance',
|
||||
name: 'Binance',
|
||||
title: 'Binance (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'binance',
|
||||
name: 'Binance',
|
||||
title: 'Binance (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'binanceus',
|
||||
name: 'Binance.us',
|
||||
title: 'Binance.us (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'binanceus',
|
||||
name: 'Binance.us',
|
||||
title: 'Binance.us (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'bitfinex',
|
||||
name: 'Bitfinex',
|
||||
title: 'Bitfinex (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'key',
|
||||
display: 'API Key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'secret',
|
||||
display: 'API Secret',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'bitfinex',
|
||||
name: 'Bitfinex',
|
||||
title: 'Bitfinex (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'key',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'secret',
|
||||
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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'bitstamp',
|
||||
name: 'Bitstamp',
|
||||
title: 'Bitstamp (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'clientId',
|
||||
display: 'Client ID',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'key',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'secret',
|
||||
display: 'API secret',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'bitstamp',
|
||||
name: 'Bitstamp',
|
||||
title: 'Bitstamp (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'clientId',
|
||||
display: 'Client ID',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'key',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'secret',
|
||||
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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'cex',
|
||||
name: 'CEX.IO',
|
||||
title: 'CEX.IO (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'uid',
|
||||
display: 'User ID',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'cex',
|
||||
name: 'CEX.IO',
|
||||
title: 'CEX.IO (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'uid',
|
||||
display: 'User ID',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
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'
|
||||
|
||||
const WARNING_LEVELS = {
|
||||
CLEAN: 'clean',
|
||||
PARTIAL: 'partial',
|
||||
IMPORTANT: 'important'
|
||||
}
|
||||
|
||||
const secretTest = (secret, message) => ({
|
||||
name: 'secret-test',
|
||||
message: message ? `The ${message} is invalid` : 'Invalid field',
|
||||
|
|
@ -21,4 +28,35 @@ const leadingZerosTest = (value, context) => {
|
|||
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 binanceus from './binanceus'
|
||||
import bitfinex from './bitfinex'
|
||||
import _binance from './binance'
|
||||
import _binanceus from './binanceus'
|
||||
import _bitfinex from './bitfinex'
|
||||
import bitgo from './bitgo'
|
||||
import bitstamp from './bitstamp'
|
||||
import _bitstamp from './bitstamp'
|
||||
import blockcypher from './blockcypher'
|
||||
import cex from './cex'
|
||||
import _cex from './cex'
|
||||
import elliptic from './elliptic'
|
||||
import galoy from './galoy'
|
||||
import inforu from './inforu'
|
||||
import infura from './infura'
|
||||
import itbit from './itbit'
|
||||
import kraken from './kraken'
|
||||
import _itbit from './itbit'
|
||||
import _kraken from './kraken'
|
||||
import mailgun from './mailgun'
|
||||
import scorechain from './scorechain'
|
||||
import sumsub from './sumsub'
|
||||
|
|
@ -19,25 +19,37 @@ import trongrid from './trongrid'
|
|||
import twilio from './twilio'
|
||||
import vonage from './vonage'
|
||||
|
||||
export default {
|
||||
[bitgo.code]: bitgo,
|
||||
[galoy.code]: galoy,
|
||||
[bitstamp.code]: bitstamp,
|
||||
[blockcypher.code]: blockcypher,
|
||||
[elliptic.code]: elliptic,
|
||||
[inforu.code]: inforu,
|
||||
[infura.code]: infura,
|
||||
[itbit.code]: itbit,
|
||||
[kraken.code]: kraken,
|
||||
[mailgun.code]: mailgun,
|
||||
[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
|
||||
const schemas = (markets = {}) => {
|
||||
const binance = _binance(markets?.binance)
|
||||
const bitfinex = _bitfinex(markets?.bitfinex)
|
||||
const binanceus = _binanceus(markets?.binanceus)
|
||||
const bitstamp = _bitstamp(markets?.bitstamp)
|
||||
const cex = _cex(markets?.cex)
|
||||
const itbit = _itbit(markets?.itbit)
|
||||
const kraken = _kraken(markets?.kraken)
|
||||
|
||||
return {
|
||||
[bitgo.code]: bitgo,
|
||||
[galoy.code]: galoy,
|
||||
[bitstamp.code]: bitstamp,
|
||||
[blockcypher.code]: blockcypher,
|
||||
[elliptic.code]: elliptic,
|
||||
[inforu.code]: inforu,
|
||||
[infura.code]: infura,
|
||||
[itbit.code]: itbit,
|
||||
[kraken.code]: kraken,
|
||||
[mailgun.code]: mailgun,
|
||||
[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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { buildCurrencyOptions, secretTest } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'itbit',
|
||||
name: 'itBit',
|
||||
title: 'itBit (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'userId',
|
||||
display: 'User ID',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'walletId',
|
||||
display: 'Wallet ID',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'clientKey',
|
||||
display: 'Client key',
|
||||
component: TextInputFormik
|
||||
},
|
||||
{
|
||||
code: 'clientSecret',
|
||||
display: 'Client secret',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'itbit',
|
||||
name: 'itBit',
|
||||
title: 'itBit (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'userId',
|
||||
display: 'User ID',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'walletId',
|
||||
display: 'Wallet ID',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'clientKey',
|
||||
display: 'Client key',
|
||||
component: TextInput
|
||||
},
|
||||
{
|
||||
code: 'clientSecret',
|
||||
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 SecretInputFormik from 'src/components/inputs/formik/SecretInput'
|
||||
import TextInputFormik from 'src/components/inputs/formik/TextInput'
|
||||
import {
|
||||
SecretInput,
|
||||
TextInput,
|
||||
Autocomplete
|
||||
} from 'src/components/inputs/formik'
|
||||
|
||||
import { secretTest } from './helper'
|
||||
import { secretTest, buildCurrencyOptions } from './helper'
|
||||
|
||||
export default {
|
||||
code: 'kraken',
|
||||
name: 'Kraken',
|
||||
title: 'Kraken (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInputFormik,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
display: 'Private key',
|
||||
component: SecretInputFormik
|
||||
const schema = markets => {
|
||||
return {
|
||||
code: 'kraken',
|
||||
name: 'Kraken',
|
||||
title: 'Kraken (Exchange)',
|
||||
elements: [
|
||||
{
|
||||
code: 'apiKey',
|
||||
display: 'API key',
|
||||
component: TextInput,
|
||||
face: true,
|
||||
long: true
|
||||
},
|
||||
{
|
||||
code: 'privateKey',
|
||||
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 commissionPercentage =
|
||||
const commissionPercentage = BigNumber(
|
||||
Number.parseFloat(tx.commissionPercentage, 2) * 100
|
||||
).toFixed(2, 1) // ROUND_DOWN
|
||||
const fixedFee = Number.parseFloat(tx.fixedFee) || 0
|
||||
const fiat = BigNumber(tx.fiat)
|
||||
.minus(fixedFee)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { SupportLinkButton } from 'src/components/buttons'
|
|||
import { NamespacedTable as EditableTable } from 'src/components/editableTable'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
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 SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||
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 useStyles = makeStyles(styles)
|
||||
|
|
@ -71,6 +77,8 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const { data: marketsData } = useQuery(GET_MARKETS)
|
||||
|
||||
const [saveAccount] = useMutation(SAVE_ACCOUNT, {
|
||||
onCompleted: () => setEditingSchema(null),
|
||||
refetchQueries: () => ['getData']
|
||||
|
|
@ -89,6 +97,10 @@ const Wallet = ({ name: SCREEN_KEY }) => {
|
|||
const cryptoCurrencies = data?.cryptoCurrencies ?? []
|
||||
const accounts = data?.accounts ?? []
|
||||
|
||||
const markets = marketsData?.getMarkets
|
||||
|
||||
const schemas = _schemas(markets)
|
||||
|
||||
const onChange = (previous, current, setValue) => {
|
||||
if (!current) return setValue(current)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
|||
import Notifications from 'src/pages/Notifications/Notifications'
|
||||
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
|
||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
|
|
@ -193,6 +194,13 @@ const getLamassuRoutes = () => [
|
|||
route: '/settings/operator-info/terms-conditions',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
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 CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
|
||||
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
|
||||
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||
|
|
@ -172,6 +173,13 @@ const getPazuzRoutes = () => [
|
|||
route: '/settings/operator-info/terms-conditions',
|
||||
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
|
||||
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 linen = '#fbf3ec'
|
||||
|
||||
// Warning
|
||||
const orangeYellow = '#ffcc00'
|
||||
|
||||
// Color Variables
|
||||
const primaryColor = zodiac
|
||||
|
||||
|
|
@ -136,6 +139,7 @@ export {
|
|||
java,
|
||||
neon,
|
||||
linen,
|
||||
orangeYellow,
|
||||
// named colors
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ const namespaces = {
|
|||
RECEIPT: 'receipt',
|
||||
COIN_ATM_RADAR: 'coinAtmRadar',
|
||||
TERMS_CONDITIONS: 'termsConditions',
|
||||
TRIGGERS: 'triggersConfig'
|
||||
TRIGGERS: 'triggersConfig',
|
||||
MACHINE_SCREENS: 'machineScreens'
|
||||
}
|
||||
|
||||
const mapKeys = R.curry((fn, obj) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue