Merge branch 'dev' into backport/binance-default-market

This commit is contained in:
Rafael Taranto 2024-11-29 13:50:10 +00:00 committed by GitHub
commit 5e2ac6ecbf
69 changed files with 1516 additions and 599 deletions

View file

@ -1,48 +1,49 @@
const _ = require('lodash/fp')
const db = require('./db') const db = require('./db')
const notifierQueries = require('./notifier/queries') const notifierQueries = require('./notifier/queries')
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator const getBlacklist = () =>
const getBlacklist = () => { db.any(
return db.any(`SELECT * FROM blacklist`).then(res => `SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
res.map(item => ({ FROM blacklist JOIN blacklist_messages
cryptoCode: item.crypto_code, ON blacklist.blacklist_message_id = blacklist_messages.id`
address: item.address
}))
) )
const deleteFromBlacklist = address => {
const sql = `DELETE FROM blacklist WHERE address = $1`
notifierQueries.clearBlacklistNotification(address)
return db.none(sql, [address])
} }
// Delete row from blacklist table by crypto code and address const insertIntoBlacklist = address => {
const deleteFromBlacklist = (cryptoCode, address) => {
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2`
notifierQueries.clearBlacklistNotification(cryptoCode, address)
return db.none(sql, [cryptoCode, address])
}
const insertIntoBlacklist = (cryptoCode, address) => {
return db return db
.none( .none(
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);', 'INSERT INTO blacklist (address) VALUES ($1);',
[cryptoCode, address] [address]
) )
} }
function blocked (address, cryptoCode) { function blocked (address) {
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2` const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
return db.any(sql, [address, cryptoCode]) return db.oneOrNone(sql, [address])
} }
function addToUsedAddresses (address, cryptoCode) { function getMessages () {
// ETH reuses addresses const sql = `SELECT * FROM blacklist_messages`
if (cryptoCode === 'ETH') return Promise.resolve() return db.any(sql)
}
const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)` function editBlacklistMessage (id, content) {
return db.oneOrNone(sql, [cryptoCode, address]) const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
return db.oneOrNone(sql, [content, id])
} }
module.exports = { module.exports = {
blocked, blocked,
addToUsedAddresses,
getBlacklist, getBlacklist,
deleteFromBlacklist, deleteFromBlacklist,
insertIntoBlacklist insertIntoBlacklist,
getMessages,
editBlacklistMessage
} }

View file

@ -27,6 +27,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Bitcoin Core. This may take a minute...') common.logger.info('Updating Bitcoin Core. This may take a minute...')
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`) !isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`) common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Bitcoin Core: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')
@ -55,6 +59,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
} }
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`fallbackfee already defined, skipping...`)
} else {
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
}
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
common.logger.info(`rpcworkqueue already defined, skipping...`)
} else {
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
}
if (isCurrentlyRunning && !isDevMode()) { if (isCurrentlyRunning && !isDevMode()) {
common.logger.info('Starting wallet...') common.logger.info('Starting wallet...')
common.es(`sudo supervisorctl start bitcoin`) common.es(`sudo supervisorctl start bitcoin`)

View file

@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Bitcoin Cash. This may take a minute...') common.logger.info('Updating Bitcoin Cash. This may take a minute...')
common.es(`sudo supervisorctl stop bitcoincash`) common.es(`sudo supervisorctl stop bitcoincash`)
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`) common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -29,37 +29,47 @@ module.exports = {
const BINARIES = { const BINARIES = {
BTC: { BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
defaultDir: 'bitcoin-0.20.1/bin', defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz', url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz',
UrlHash: 'c9840607d230d65f6938b81deaec0b98fe9cb14c3a41a5b13b2c05d044a48422',
dir: 'bitcoin-27.1/bin' dir: 'bitcoin-27.1/bin'
}, },
ETH: { ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz', url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz',
urlHash: 'fff507c90c180443456950e4fc0bf224d26ce5ea6896194ff864c3c3754c136b',
dir: 'geth-linux-amd64-1.14.8-a9523b64' dir: 'geth-linux-amd64-1.14.8-a9523b64'
}, },
ZEC: { ZEC: {
url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz', url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz',
urlHash: 'd385b9fbeeb145f60b0b339d256cabb342713ed3014cd634cf2d68078365abd2',
dir: 'zcash-5.9.0/bin' dir: 'zcash-5.9.0/bin'
}, },
DASH: { DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
defaultDir: 'dashcore-18.1.0/bin', defaultDir: 'dashcore-18.1.0/bin',
url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz', url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz',
urlHash: 'a7d0c1b04d53a9b1b3499eb82182c0fa57f4c8768c16163e5d05971bf45d7928',
dir: 'dashcore-21.1.0/bin' dir: 'dashcore-21.1.0/bin'
}, },
LTC: { LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
defaultDir: 'litecoin-0.18.1/bin', defaultDir: 'litecoin-0.18.1/bin',
url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz', url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz',
urlHash: 'ea231c630e2a243cb01affd4c2b95a2be71560f80b64b9f4bceaa13d736aa7cb',
dir: 'litecoin-0.21.3/bin' dir: 'litecoin-0.21.3/bin'
}, },
BCH: { BCH: {
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz', url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz',
urlHash: '0dcc387cbaa3a039c97ddc8fb99c1fa7bff5dc6e4bd3a01d3c3095f595ad2dce',
dir: 'bitcoin-cash-node-27.1.0/bin', dir: 'bitcoin-cash-node-27.1.0/bin',
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']] files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
}, },
XMR: { XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2', url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2',
urlHash: '47c7e6b4b88a57205800a2538065a7874174cd087eedc2526bee1ebcce0cc5e3',
dir: 'monero-x86_64-linux-gnu-v0.18.3.3', dir: 'monero-x86_64-linux-gnu-v0.18.3.3',
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']] files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
} }
@ -133,10 +143,15 @@ function fetchAndInstall (coinRec) {
if (!binaries) throw new Error(`No such coin: ${coinRec.code}`) if (!binaries) throw new Error(`No such coin: ${coinRec.code}`)
const url = requiresUpdate ? binaries.defaultUrl : binaries.url const url = requiresUpdate ? binaries.defaultUrl : binaries.url
const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash
const downloadFile = path.basename(url) const downloadFile = path.basename(url)
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
es(`wget -q ${url}`) es(`wget -q ${url}`)
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`)
return
}
es(`tar -xf ${downloadFile}`) es(`tar -xf ${downloadFile}`)
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin' const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'

View file

@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Dash Core. This may take a minute...') common.logger.info('Updating Dash Core. This may take a minute...')
common.es(`sudo supervisorctl stop dash`) common.es(`sudo supervisorctl stop dash`)
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`) common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Dash Core: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -8,6 +8,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...') common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...')
common.es(`sudo supervisorctl stop ethereum`) common.es(`sudo supervisorctl stop ethereum`)
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`) common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Geth: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Litecoin Core. This may take a minute...') common.logger.info('Updating Litecoin Core. This may take a minute...')
common.es(`sudo supervisorctl stop litecoin`) common.es(`sudo supervisorctl stop litecoin`)
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`) common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Litecoin Core: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -22,6 +22,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Monero. This may take a minute...') common.logger.info('Updating Monero. This may take a minute...')
common.es(`sudo supervisorctl stop monero monero-wallet`) common.es(`sudo supervisorctl stop monero monero-wallet`)
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`) common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Monero: Package signature do not match!')
return
}
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`) common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -13,6 +13,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating your Zcash wallet. This may take a minute...') common.logger.info('Updating your Zcash wallet. This may take a minute...')
common.es(`sudo supervisorctl stop zcash`) common.es(`sudo supervisorctl stop zcash`)
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`) common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
common.logger.info('Failed to update Zcash: Package signature do not match!')
return
}
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...') common.logger.info('Updating wallet...')

View file

@ -8,7 +8,7 @@ const E = require('../error')
const PENDING_INTERVAL_MS = 60 * T.minutes const PENDING_INTERVAL_MS = 60 * T.minutes
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto'] const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms') const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
const massage = _.flow(_.omit(massageFields), const massage = _.flow(_.omit(massageFields),

View file

@ -32,38 +32,41 @@ function post (machineTx, pi) {
return cashInAtomic.atomic(machineTx, pi) return cashInAtomic.atomic(machineTx, pi)
.then(r => { .then(r => {
const updatedTx = r.tx const updatedTx = r.tx
let blacklisted = false
let addressReuse = false let addressReuse = false
let walletScore = {}
const promises = [settingsLoader.loadLatestConfig()] const promises = [settingsLoader.loadLatestConfig()]
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero() const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
if (isFirstPost) { if (isFirstPost) {
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi)) promises.push(
checkForBlacklisted(updatedTx),
doesTxReuseAddress(updatedTx),
getWalletScore(updatedTx, pi)
)
} }
return Promise.all(promises) return Promise.all(promises)
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => { .then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse const { rejectAddressReuse } = configManager.getCompliance(config)
const isBlacklisted = !!blacklisted
walletScore = fetchedWalletScore if (isBlacklisted) {
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
blacklisted = true
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false) notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
} else if (isReusedAddress && rejectAddressReuse) { } else if (isReusedAddress && rejectAddressReuse) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true) notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
addressReuse = true addressReuse = true
} }
return postProcess(r, pi, blacklisted, addressReuse, walletScore) return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(_.flow(
_.set('bills', machineTx.bills),
_.set('blacklisted', isBlacklisted),
_.set('blacklistMessage', blacklisted?.content),
_.set('addressReuse', addressReuse),
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
))
}) })
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(tx => _.set('bills', machineTx.bills, tx))
.then(tx => _.set('blacklisted', blacklisted, tx))
.then(tx => _.set('addressReuse', addressReuse, tx))
.then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx))
}) })
} }
@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) {
} }
function checkForBlacklisted (tx) { function checkForBlacklisted (tx) {
return blacklist.blocked(tx.toAddress, tx.cryptoCode) return blacklist.blocked(tx.toAddress)
} }
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {

View file

@ -6,7 +6,7 @@ const camelize = require('./utils')
function createCashboxBatch (deviceId, cashboxCount) { function createCashboxBatch (deviceId, cashboxCount) {
if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.') if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.')
const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')` const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *`
const sql2 = ` const sql2 = `
UPDATE bills SET cashbox_batch_id=$1 UPDATE bills SET cashbox_batch_id=$1
FROM cash_in_txs FROM cash_in_txs
@ -25,6 +25,7 @@ function createCashboxBatch (deviceId, cashboxCount) {
const q2 = t.none(sql2, [batchId, deviceId]) const q2 = t.none(sql2, [batchId, deviceId])
const q3 = t.none(sql3, [batchId, deviceId]) const q3 = t.none(sql3, [batchId, deviceId])
return t.batch([q1, q2, q3]) return t.batch([q1, q2, q3])
.then(([it]) => it)
}) })
} }
@ -100,14 +101,6 @@ function editBatchById (id, performedBy) {
return db.none(sql, [performedBy, id]) return db.none(sql, [performedBy, id])
} }
function getBillsByBatchId (id) {
const sql = `SELECT bi.* FROM (
SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (SELECT id, device_id FROM cash_in_txs) AS cit ON cit.id = b.cash_in_txs_id UNION
SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills
) AS bi WHERE bi.cashbox_batch_id=$1`
return db.any(sql, [id])
}
function logFormatter (data) { function logFormatter (data) {
return _.map( return _.map(
it => { it => {
@ -124,11 +117,62 @@ function logFormatter (data) {
) )
} }
function getMachineUnbatchedBills (deviceId) {
const sql = `
SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs
ON b.cash_in_txs_id = cash_in_txs.id
WHERE b.cashbox_batch_id IS NULL AND cash_in_txs.device_id = $1
GROUP BY cash_in_txs.device_id
`
return db.oneOrNone(sql, [deviceId])
.then(res => _.mapKeys(it => _.camelCase(it), res))
.then(logFormatterSingle)
}
function getBatchById (id) {
const sql = `
SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills
FROM cashbox_batches AS cb
LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id
WHERE cb.id = $1
GROUP BY cb.id
`
return db.oneOrNone(sql, [id]).then(res => _.mapKeys(it => _.camelCase(it), res))
.then(logFormatterSingle)
}
function logFormatterSingle (data) {
const bills = _.filter(
it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)),
data.bills
)
return {
id: data.id,
deviceId: data.deviceId,
created: data.created,
operationType: data.operationType,
billCount: _.size(bills),
fiatTotals: _.reduce(
(acc, value) => {
acc[value.fiat_code] = (acc[value.fiat_code] || 0) + value.fiat
return acc
},
{},
bills
),
billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills)
}
}
module.exports = { module.exports = {
createCashboxBatch, createCashboxBatch,
updateMachineWithBatch, updateMachineWithBatch,
getBatches, getBatches,
getBillsByBatchId,
editBatchById, editBatchById,
getBatchById,
getMachineUnbatchedBills,
logFormatter logFormatter
} }

View file

@ -1,6 +1,10 @@
const _ = require('lodash/fp')
const { ALL_CRYPTOS } = require('@lamassu/coins')
const configManager = require('./new-config-manager') const configManager = require('./new-config-manager')
const ccxt = require('./plugins/exchange/ccxt') const ccxt = require('./plugins/exchange/ccxt')
const mockExchange = require('./plugins/exchange/mock-exchange') const mockExchange = require('./plugins/exchange/mock-exchange')
const accounts = require('./new-admin/config/accounts')
function lookupExchange (settings, cryptoCode) { function lookupExchange (settings, cryptoCode) {
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
@ -45,8 +49,26 @@ function active (settings, cryptoCode) {
return !!lookupExchange(settings, cryptoCode) return !!lookupExchange(settings, cryptoCode)
} }
function getMarkets () {
const filterExchanges = _.filter(it => it.class === 'exchange')
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
return _.reduce(
(acc, value) =>
Promise.all([acc, ccxt.getMarkets(value, ALL_CRYPTOS)])
.then(([a, markets]) => Promise.resolve({
...a,
[value]: markets
})),
Promise.resolve({}),
availableExchanges
)
}
module.exports = { module.exports = {
fetchExchange,
buy, buy,
sell, sell,
active active,
getMarkets
} }

View file

@ -61,6 +61,18 @@ const addReceiptInfo = receiptInfo => ret => {
} }
const addMachineScreenOpts = smth => _.update(
'screenOptions',
_.flow(
addSmthInfo(
'rates',
[
'active'
]
)(smth.rates)
)
)
/* TODO: Simplify this. */ /* TODO: Simplify this. */
const buildTriggers = allTriggers => { const buildTriggers = allTriggers => {
const normalTriggers = [] const normalTriggers = []
@ -103,7 +115,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
_.pick([ _.pick([
'coins', 'coins',
'configVersion', 'configVersion',
'timezone' 'timezone',
'screenOptions'
]), ]),
_.update('coins', massageCoins), _.update('coins', massageCoins),
_.set('serverVersion', VERSION), _.set('serverVersion', VERSION),
@ -117,6 +130,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
configManager.getLocale(deviceId, settings.config), configManager.getLocale(deviceId, settings.config),
configManager.getOperatorInfo(settings.config), configManager.getOperatorInfo(settings.config),
configManager.getReceipt(settings.config), configManager.getReceipt(settings.config),
configManager.getAllMachineScreenOpts(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active, !!configManager.getCashOut(deviceId, settings.config).active,
getMachine(deviceId, currentConfigVersion), getMachine(deviceId, currentConfigVersion),
configManager.getCustomerAuthenticationMethod(settings.config) configManager.getCustomerAuthenticationMethod(settings.config)
@ -129,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
localeInfo, localeInfo,
operatorInfo, operatorInfo,
receiptInfo, receiptInfo,
machineScreenOpts,
twoWayMode, twoWayMode,
{ numberOfCassettes, numberOfRecyclers }, { numberOfCassettes, numberOfRecyclers },
customerAuthentication, customerAuthentication,
@ -153,7 +168,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
urlsToPing, urlsToPing,
}), }),
addOperatorInfo(operatorInfo), addOperatorInfo(operatorInfo),
addReceiptInfo(receiptInfo) addReceiptInfo(receiptInfo),
addMachineScreenOpts(machineScreenOpts)
)(staticConf)) )(staticConf))
} }

View file

@ -49,6 +49,14 @@ type ReceiptInfo {
addressQRCode: Boolean! addressQRCode: Boolean!
} }
type MachineScreenOptions {
rates: RateScreenOptions!
}
type RateScreenOptions {
active: Boolean!
}
type SpeedtestFile { type SpeedtestFile {
url: String! url: String!
size: Int! size: Int!
@ -147,6 +155,7 @@ type StaticConfig {
operatorInfo: OperatorInfo operatorInfo: OperatorInfo
machineInfo: MachineInfo! machineInfo: MachineInfo!
receiptInfo: ReceiptInfo receiptInfo: ReceiptInfo
screenOptions: MachineScreenOptions
speedtestFiles: [SpeedtestFile!]! speedtestFiles: [SpeedtestFile!]!
urlsToPing: [String!]! urlsToPing: [String!]!

View file

@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
const resolvers = { const resolvers = {
Query: { Query: {
blacklist: () => blacklist.getBlacklist() blacklist: () => blacklist.getBlacklist(),
blacklistMessages: () => blacklist.getMessages()
}, },
Mutation: { Mutation: {
deleteBlacklistRow: (...[, { cryptoCode, address }]) => deleteBlacklistRow: (...[, { address }]) =>
blacklist.deleteFromBlacklist(cryptoCode, address), blacklist.deleteFromBlacklist(address),
insertBlacklistRow: (...[, { cryptoCode, address }]) => insertBlacklistRow: (...[, { address }]) =>
blacklist.insertIntoBlacklist(cryptoCode, address) blacklist.insertIntoBlacklist(address),
editBlacklistMessage: (...[, { id, content }]) =>
blacklist.editBlacklistMessage(id, content)
} }
} }

View file

@ -11,6 +11,7 @@ const funding = require('./funding.resolver')
const log = require('./log.resolver') const log = require('./log.resolver')
const loyalty = require('./loyalty.resolver') const loyalty = require('./loyalty.resolver')
const machine = require('./machine.resolver') const machine = require('./machine.resolver')
const market = require('./market.resolver')
const notification = require('./notification.resolver') const notification = require('./notification.resolver')
const pairing = require('./pairing.resolver') const pairing = require('./pairing.resolver')
const rates = require('./rates.resolver') const rates = require('./rates.resolver')
@ -35,6 +36,7 @@ const resolvers = [
log, log,
loyalty, loyalty,
machine, machine,
market,
notification, notification,
pairing, pairing,
rates, rates,

View file

@ -0,0 +1,9 @@
const exchange = require('../../../exchange')
const resolvers = {
Query: {
getMarkets: () => exchange.getMarkets()
}
}
module.exports = resolvers

View file

@ -2,17 +2,26 @@ const { gql } = require('apollo-server-express')
const typeDef = gql` const typeDef = gql`
type Blacklist { type Blacklist {
cryptoCode: String!
address: String! address: String!
blacklistMessage: BlacklistMessage!
}
type BlacklistMessage {
id: ID
label: String
content: String
allowToggle: Boolean
} }
type Query { type Query {
blacklist: [Blacklist] @auth blacklist: [Blacklist] @auth
blacklistMessages: [BlacklistMessage] @auth
} }
type Mutation { type Mutation {
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth deleteBlacklistRow(address: String!): Blacklist @auth
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth insertBlacklistRow(address: String!): Blacklist @auth
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
} }
` `

View file

@ -11,6 +11,7 @@ const funding = require('./funding.type')
const log = require('./log.type') const log = require('./log.type')
const loyalty = require('./loyalty.type') const loyalty = require('./loyalty.type')
const machine = require('./machine.type') const machine = require('./machine.type')
const market = require('./market.type')
const notification = require('./notification.type') const notification = require('./notification.type')
const pairing = require('./pairing.type') const pairing = require('./pairing.type')
const rates = require('./rates.type') const rates = require('./rates.type')
@ -35,6 +36,7 @@ const types = [
log, log,
loyalty, loyalty,
machine, machine,
market,
notification, notification,
pairing, pairing,
rates, rates,

View file

@ -0,0 +1,9 @@
const { gql } = require('apollo-server-express')
const typeDef = gql`
type Query {
getMarkets: JSONObject @auth
}
`
module.exports = typeDef

View file

@ -50,6 +50,7 @@ function batch (
excludeTestingCustomers = false, excludeTestingCustomers = false,
simplified simplified
) { ) {
const isCsvExport = _.isBoolean(simplified)
const packager = _.flow( const packager = _.flow(
_.flatten, _.flatten,
_.orderBy(_.property('created'), ['desc']), _.orderBy(_.property('created'), ['desc']),
@ -92,7 +93,7 @@ function batch (
AND ($12 is null or txs.to_address = $12) AND ($12 is null or txs.to_address = $12)
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0) ${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'}
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
const cashOutSql = `SELECT 'cashOut' AS tx_class, const cashOutSql = `SELECT 'cashOut' AS tx_class,
@ -126,7 +127,7 @@ function batch (
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
AND ($14 is null or txs.swept = $14) AND ($14 is null or txs.swept = $14)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (fiat > 0) ${isCsvExport ? '' : 'AND fiat > 0'}
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
// The swept filter is cash-out only, so omit the cash-in query entirely // The swept filter is cash-out only, so omit the cash-in query entirely
@ -152,14 +153,14 @@ function batch (
return Promise.all(promises) return Promise.all(promises)
.then(packager) .then(packager)
.then(res => { .then(res =>
if (simplified) return simplifiedBatch(res) !isCsvExport ? res :
// GQL transactions and transactionsCsv both use this function and // GQL transactions and transactionsCsv both use this function and
// if we don't check for the correct simplified value, the Transactions page polling // if we don't check for the correct simplified value, the Transactions page polling
// will continuously build a csv in the background // will continuously build a csv in the background
else if (simplified === false) return advancedBatch(res) simplified ? simplifiedBatch(res) :
return res advancedBatch(res)
}) )
} }
function advancedBatch (data) { function advancedBatch (data) {

View file

@ -13,7 +13,12 @@ const namespaces = {
TERMS_CONDITIONS: 'termsConditions', TERMS_CONDITIONS: 'termsConditions',
CASH_OUT: 'cashOut', CASH_OUT: 'cashOut',
CASH_IN: 'cashIn', CASH_IN: 'cashIn',
COMPLIANCE: 'compliance' COMPLIANCE: 'compliance',
MACHINE_SCREENS: 'machineScreens'
}
const machineScreens = {
RATES: 'rates'
} }
const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str) const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str)
@ -72,6 +77,8 @@ const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR)
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS) const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
const getReceipt = fromNamespace(namespaces.RECEIPT) const getReceipt = fromNamespace(namespaces.RECEIPT)
const getCompliance = fromNamespace(namespaces.COMPLIANCE) const getCompliance = fromNamespace(namespaces.COMPLIANCE)
const getMachineScreenOpts = (screenName, config) => _.compose(fromNamespace(screenName), fromNamespace(namespaces.MACHINE_SCREENS))(config)
const getAllMachineScreenOpts = config => _.reduce((acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), {}, _.values(machineScreens))
const getAllCryptoCurrencies = (config) => { const getAllCryptoCurrencies = (config) => {
const locale = fromNamespace(namespaces.LOCALE)(config) const locale = fromNamespace(namespaces.LOCALE)(config)
@ -180,6 +187,8 @@ module.exports = {
getWalletSettings, getWalletSettings,
getCashInSettings, getCashInSettings,
getOperatorInfo, getOperatorInfo,
getMachineScreenOpts,
getAllMachineScreenOpts,
getNotifications, getNotifications,
getGlobalNotifications, getGlobalNotifications,
getLocale, getLocale,

View file

@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) {
.then(_.compose(_.defaultTo({}), _.get('data.accounts'))) .then(_.compose(_.defaultTo({}), _.get('data.accounts')))
} }
function hideSecretFields (accounts) {
return _.flow(
_.filter(path => !_.isEmpty(_.get(path, accounts))),
_.reduce(
(accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts),
accounts
)
)(SECRET_FIELDS)
}
function showAccounts (schemaVersion) { function showAccounts (schemaVersion) {
return loadAccounts(schemaVersion) return loadAccounts(schemaVersion)
.then(accounts => { .then(hideSecretFields)
const filledSecretPaths = _.compact(_.map(path => {
if (!_.isEmpty(_.get(path, accounts))) {
return path
}
}, SECRET_FIELDS))
return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts)
})
} }
const insertConfigRow = (dbOrTx, data) => const insertConfigRow = (dbOrTx, data) =>

View file

@ -278,6 +278,7 @@ function plugins (settings, deviceId) {
const localeConfig = configManager.getLocale(deviceId, settings.config) const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies const cryptoCodes = localeConfig.cryptoCurrencies
const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config)
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
@ -327,7 +328,8 @@ function plugins (settings, deviceId) {
coins, coins,
configVersion, configVersion,
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
timezone timezone,
screenOptions: machineScreenOpts
} }
}) })
} }
@ -475,25 +477,28 @@ function plugins (settings, deviceId) {
function buyAndSell (rec, doBuy, tx) { function buyAndSell (rec, doBuy, tx) {
const cryptoCode = rec.cryptoCode const cryptoCode = rec.cryptoCode
const fiatCode = rec.fiatCode return exchange.fetchExchange(settings, cryptoCode)
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() .then(_exchange => {
const fiatCode = _exchange.account.currencyMarket
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
if (!exchange.active(settings, cryptoCode)) return if (!exchange.active(settings, cryptoCode)) return
const direction = doBuy ? 'cashIn' : 'cashOut' const direction = doBuy ? 'cashIn' : 'cashOut'
const internalTxId = tx ? tx.id : rec.id const internalTxId = tx ? tx.id : rec.id
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
if (!tradesQueues[market]) tradesQueues[market] = [] if (!tradesQueues[market]) tradesQueues[market] = []
tradesQueues[market].push({ tradesQueues[market].push({
direction, direction,
internalTxId, internalTxId,
fiatCode, fiatCode,
cryptoAtoms, cryptoAtoms,
cryptoCode, cryptoCode,
timestamp: Date.now() timestamp: Date.now()
}) })
})
} }
function consolidateTrades (cryptoCode, fiatCode) { function consolidateTrades (cryptoCode, fiatCode) {
@ -550,19 +555,22 @@ function plugins (settings, deviceId) {
const deviceIds = devices.map(device => device.deviceId) const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => { const lists = deviceIds.map(deviceId => {
const localeConfig = configManager.getLocale(deviceId, settings.config) const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies const cryptoCodes = localeConfig.cryptoCurrencies
return cryptoCodes.map(cryptoCode => ({ return Promise.all(cryptoCodes.map(cryptoCode => {
fiatCode, return exchange.fetchExchange(settings, cryptoCode)
cryptoCode .then(exchange => ({
fiatCode: exchange.account.currencyMarket,
cryptoCode
}))
})) }))
}) })
const tradesPromises = _.uniq(_.flatten(lists)) return Promise.all(lists)
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)) })
.then(lists => {
return Promise.all(tradesPromises) return Promise.all(_.uniq(_.flatten(lists))
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)))
}) })
.catch(logger.error) .catch(logger.error)
} }

View file

@ -34,11 +34,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) {
if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) {
throw new Error('Unsupported crypto: ' + cryptoCode) throw new Error('Unsupported crypto: ' + cryptoCode)
} }
const fiatSupported = ALL[serviceName].FIAT
if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) { if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code')
logger.info('Building a market for an unsupported fiat. Defaulting to EUR market')
return cryptoCode + '/' + 'EUR'
}
return cryptoCode + '/' + fiatCode return cryptoCode + '/' + fiatCode
} }

View file

@ -8,7 +8,7 @@ const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN] const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN]
const FIAT = ['EUR'] const FIAT = ['EUR']
const DEFAULT_FIAT_MARKET = 'EUR' const DEFAULT_FIAT_MARKET = 'EUR'
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
const loadConfig = (account) => { const loadConfig = (account) => {
const mapper = { const mapper = {

View file

@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN] const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
const FIAT = ['USD'] const FIAT = ['USD']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] const DEFAULT_FIAT_MARKET = 'USD'
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
const loadConfig = (account) => { const loadConfig = (account) => {
const mapper = { const mapper = {
@ -17,4 +18,4 @@ const loadConfig = (account) => {
return { ...mapped, timeout: 3000 } return { ...mapped, timeout: 3000 }
} }
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }

View file

@ -7,6 +7,7 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
const FIAT = ['USD', 'EUR'] const FIAT = ['USD', 'EUR']
const DEFAULT_FIAT_MARKET = 'EUR'
const AMOUNT_PRECISION = 8 const AMOUNT_PRECISION = 8
const REQUIRED_CONFIG_FIELDS = ['key', 'secret'] const REQUIRED_CONFIG_FIELDS = ['key', 'secret']
@ -18,4 +19,4 @@ const loadConfig = (account) => {
return { ...mapped, timeout: 3000 } return { ...mapped, timeout: 3000 }
} }
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }

View file

@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
const FIAT = ['USD', 'EUR'] const FIAT = ['USD', 'EUR']
const DEFAULT_FIAT_MARKET = 'EUR'
const AMOUNT_PRECISION = 8 const AMOUNT_PRECISION = 8
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId'] const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket']
const loadConfig = (account) => { const loadConfig = (account) => {
const mapper = { const mapper = {
@ -19,4 +20,4 @@ const loadConfig = (account) => {
return { ...mapped, timeout: 3000 } return { ...mapped, timeout: 3000 }
} }
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }

View file

@ -1,9 +1,13 @@
const { utils: coinUtils } = require('@lamassu/coins') const { utils: coinUtils } = require('@lamassu/coins')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const ccxt = require('ccxt') const ccxt = require('ccxt')
const mem = require('mem')
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
const { ORDER_TYPES } = require('./consts') const { ORDER_TYPES } = require('./consts')
const logger = require('../../logger')
const { currencies } = require('../../new-admin/config')
const T = require('../../time')
const DEFAULT_PRICE_PRECISION = 2 const DEFAULT_PRICE_PRECISION = 2
const DEFAULT_AMOUNT_PRECISION = 8 const DEFAULT_AMOUNT_PRECISION = 8
@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) {
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config') if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
const symbol = buildMarket(fiatCode, cryptoCode, exchangeName) const selectedFiatMarket = account.currencyMarket
const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName)
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
@ -50,4 +55,36 @@ function calculatePrice (side, amount, orderBook) {
throw new Error('Insufficient market depth') throw new Error('Insufficient market depth')
} }
module.exports = { trade } function _getMarkets (exchangeName, availableCryptos) {
try {
const exchange = new ccxt[exchangeName]()
const cryptosToQuoteAgainst = ['USDT']
const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst)
return exchange.fetchMarkets()
.then(_.filter(it => (it.type === 'spot' || it.spot)))
.then(res =>
_.reduce((acc, value) => {
if (_.includes(value.base, availableCryptos) && _.includes(value.quote, currencyCodes)) {
if (value.quote === value.base) return acc
if (_.isNil(acc[value.quote])) {
return { ...acc, [value.quote]: [value.base] }
}
acc[value.quote].push(value.base)
}
return acc
}, {}, res)
)
} catch (e) {
logger.debug(`No CCXT exchange found for ${exchangeName}`)
}
}
const getMarkets = mem(_getMarkets, {
maxAge: T.week,
cacheKey: (exchangeName, availableCryptos) => exchangeName
})
module.exports = { trade, getMarkets }

View file

@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN] const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
const FIAT = ['USD', 'EUR'] const FIAT = ['USD', 'EUR']
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] const DEFAULT_FIAT_MARKET = 'EUR'
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
const loadConfig = (account) => { const loadConfig = (account) => {
const mapper = { const mapper = {
@ -17,4 +18,4 @@ const loadConfig = (account) => {
return { ...mapped, timeout: 3000 } return { ...mapped, timeout: 3000 }
} }
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }

View file

@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.LIMIT
const { BTC, ETH, USDT, LN } = COINS const { BTC, ETH, USDT, LN } = COINS
const CRYPTO = [BTC, ETH, USDT, LN] const CRYPTO = [BTC, ETH, USDT, LN]
const FIAT = ['USD'] const FIAT = ['USD']
const DEFAULT_FIAT_MARKET = 'USD'
const AMOUNT_PRECISION = 4 const AMOUNT_PRECISION = 4
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId'] const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket']
const loadConfig = (account) => { const loadConfig = (account) => {
const mapper = { const mapper = {
@ -21,4 +22,4 @@ const loadConfig = (account) => {
} }
const loadOptions = ({ walletId }) => ({ walletId }) const loadOptions = ({ walletId }) => ({ walletId })
module.exports = { loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }

View file

@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN] const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
const FIAT = ['USD', 'EUR'] const FIAT = ['USD', 'EUR']
const DEFAULT_FIAT_MARKET = 'EUR'
const AMOUNT_PRECISION = 6 const AMOUNT_PRECISION = 6
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
const USER_REF = 'userref' const USER_REF = 'userref'
const loadConfig = (account) => { const loadConfig = (account) => {
@ -26,4 +27,4 @@ const loadConfig = (account) => {
const loadOptions = () => ({ expiretm: '+60' }) const loadOptions = () => ({ expiretm: '+60' })
module.exports = { USER_REF, loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }

View file

@ -61,6 +61,15 @@ morgan.token('bytesRead', (_req, res) => res.bytesRead)
morgan.token('bytesWritten', (_req, res) => res.bytesWritten) morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream })) app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
app.use('/robots.txt', (req, res) => {
res.type('text/plain')
res.send("User-agent: *\nDisallow: /")
})
app.get('/', (req, res) => {
res.sendStatus(404)
})
// app /pair and /ca routes // app /pair and /ca routes
app.use('/', pairingRoutes) app.use('/', pairingRoutes)

View file

@ -1,40 +1,41 @@
const express = require('express') const express = require('express')
const _ = require('lodash/fp')
const router = express.Router() const router = express.Router()
const cashbox = require('../cashbox-batches') const cashbox = require('../cashbox-batches')
const notifier = require('../notifier') const notifier = require('../notifier')
const { getMachine, setMachine } = require('../machine-loader') const { getMachine, setMachine, getMachineName } = require('../machine-loader')
const { loadLatestConfig } = require('../new-settings-loader') const { loadLatestConfig } = require('../new-settings-loader')
const { getCashInSettings } = require('../new-config-manager') const { getCashInSettings } = require('../new-config-manager')
const { AUTOMATIC } = require('../constants') const { AUTOMATIC } = require('../constants')
const logger = require('../logger') const logger = require('../logger')
function notifyCashboxRemoval (req, res, next) {
function cashboxRemoval (req, res, next) {
const operatorId = res.locals.operatorId const operatorId = res.locals.operatorId
logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`) notifier.cashboxNotify(req.deviceId).catch(logger.error)
return notifier.cashboxNotify(req.deviceId) return Promise.all([getMachine(req.deviceId), loadLatestConfig()])
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
.then(([machine, config]) => { .then(([machine, config]) => {
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
const cashInSettings = getCashInSettings(config) const cashInSettings = getCashInSettings(config)
if (cashInSettings.cashboxReset !== AUTOMATIC) { if (cashInSettings.cashboxReset !== AUTOMATIC) {
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created') return Promise.all([
logger.info(`** DEBUG ** - Cashbox removal - Process finished`) cashbox.getMachineUnbatchedBills(req.deviceId),
return res.status(200).send({ status: 'OK' }) getMachineName(req.deviceId)
])
} }
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created') return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...') .then(batch => Promise.all([
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox) cashbox.getBatchById(batch.id),
.then(() => { getMachineName(batch.device_id),
logger.info(`** DEBUG ** - Cashbox removal - Process finished`) setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
return res.status(200).send({ status: 'OK' }) ]))
})
}) })
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
.catch(next) .catch(next)
} }
router.post('/removal', notifyCashboxRemoval) router.post('/removal', cashboxRemoval)
module.exports = router module.exports = router

View 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()
}

View 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()
}

View 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()
}

View 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()
}

View file

@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View file

@ -105,20 +105,29 @@ const Popover = ({
const classes = useStyles() const classes = useStyles()
const arrowClasses = { const getArrowClasses = placement => ({
[classes.arrow]: true, [classes.arrow]: true,
[classes.arrowBottom]: props.placement === 'bottom', [classes.arrowBottom]: placement === 'bottom',
[classes.arrowTop]: props.placement === 'top', [classes.arrowTop]: placement === 'top',
[classes.arrowRight]: props.placement === 'right', [classes.arrowRight]: placement === 'right',
[classes.arrowLeft]: props.placement === 'left' [classes.arrowLeft]: placement === 'left'
})
const flipPlacements = {
top: ['bottom'],
bottom: ['top'],
left: ['right'],
right: ['left']
} }
const modifiers = R.merge(props.modifiers, { const modifiers = R.mergeDeepLeft(props.modifiers, {
flip: { flip: {
enabled: false enabled: R.defaultTo(false, props.flip),
allowedAutoPlacements: flipPlacements[props.placement],
boundary: 'clippingParents'
}, },
preventOverflow: { preventOverflow: {
enabled: true, enabled: R.defaultTo(true, props.preventOverflow),
boundariesElement: 'scrollParent' boundariesElement: 'scrollParent'
}, },
offset: { offset: {
@ -126,7 +135,7 @@ const Popover = ({
offset: '0, 10' offset: '0, 10'
}, },
arrow: { arrow: {
enabled: true, enabled: R.defaultTo(true, props.showArrow),
element: arrowRef element: arrowRef
}, },
computeStyle: { computeStyle: {
@ -134,6 +143,12 @@ const Popover = ({
} }
}) })
if (props.preventOverflow === false) {
modifiers.hide = {
enabled: false
}
}
return ( return (
<> <>
<MaterialPopper <MaterialPopper
@ -141,10 +156,15 @@ const Popover = ({
modifiers={modifiers} modifiers={modifiers}
className={classes.popover} className={classes.popover}
{...props}> {...props}>
<Paper className={classnames(classes.root, className)}> {({ placement }) => (
<span className={classnames(arrowClasses)} ref={setArrowRef} /> <Paper className={classnames(classes.root, className)}>
{children} <span
</Paper> className={classnames(getArrowClasses(placement))}
ref={setArrowRef}
/>
{children}
</Paper>
)}
</MaterialPopper> </MaterialPopper>
</> </>
) )

View file

@ -56,7 +56,8 @@ const styles = {
alignItems: 'center', alignItems: 'center',
borderRadius: 4, borderRadius: 4,
'& img': { '& img': {
maxHeight: 145 height: 145,
minWidth: 200
} }
} }
} }
@ -127,7 +128,8 @@ const IDButton = memo(
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={handleClose} onClose={handleClose}
arrowSize={3} arrowSize={3}
placement="top"> placement="top"
flip>
<div className={classes.popoverContent}> <div className={classes.popoverContent}>
<div>{children}</div> <div>{children}</div>
</div> </div>

View file

@ -1,8 +1,13 @@
import { Box } from '@material-ui/core'
import MAutocomplete from '@material-ui/lab/Autocomplete' import MAutocomplete from '@material-ui/lab/Autocomplete'
import sort from 'match-sorter' import sort from 'match-sorter'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import { HoverableTooltip } from 'src/components/Tooltip'
import { P } from 'src/components/typography'
import { errorColor, orangeYellow, spring4 } from 'src/styling/variables'
import TextInput from './TextInput' import TextInput from './TextInput'
const Autocomplete = ({ const Autocomplete = ({
@ -95,6 +100,39 @@ const Autocomplete = ({
/> />
) )
}} }}
renderOption={props => {
if (!props.warning && !props.warningMessage)
return R.path([labelProp])(props)
const warningColors = {
clean: spring4,
partial: orangeYellow,
important: errorColor
}
const hoverableElement = (
<Box
width={18}
height={18}
borderRadius={6}
bgcolor={warningColors[props.warning]}
/>
)
return (
<Box
width="100%"
display="flex"
flexDirection="row"
justifyContent="space-between"
alignItems="center">
<Box>{R.path([labelProp])(props)}</Box>
<HoverableTooltip parentElements={hoverableElement} width={250}>
<P>{props.warningMessage}</P>
</HoverableTooltip>
</Box>
)
}}
/> />
) )
} }

View file

@ -1,38 +1,31 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { utils as coinUtils } from '@lamassu/coins' import { addressDetector } from '@lamassu/coins'
import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core' import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { HelpTooltip } from 'src/components/Tooltip' import { HelpTooltip } from 'src/components/Tooltip'
import { import { Link, Button, IconButton } from 'src/components/buttons'
Link,
Button,
IconButton,
SupportLinkButton
} from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { H4, H2, Label2, P, Info3, Info2 } from 'src/components/typography' import { H2, Label2, P, Info3, Info2 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
import { fromNamespace, toNamespace } from 'src/utils/config' import { fromNamespace, toNamespace } from 'src/utils/config'
import styles from './Blacklist.styles' import styles from './Blacklist.styles'
import BlackListAdvanced from './BlacklistAdvanced'
import BlackListModal from './BlacklistModal' import BlackListModal from './BlacklistModal'
import BlacklistTable from './BlacklistTable' import BlacklistTable from './BlacklistTable'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const groupByCode = R.groupBy(obj => obj.cryptoCode)
const DELETE_ROW = gql` const DELETE_ROW = gql`
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) { mutation DeleteBlacklistRow($address: String!) {
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) { deleteBlacklistRow(address: $address) {
cryptoCode
address address
} }
} }
@ -41,7 +34,6 @@ const DELETE_ROW = gql`
const GET_BLACKLIST = gql` const GET_BLACKLIST = gql`
query getBlacklistData { query getBlacklistData {
blacklist { blacklist {
cryptoCode
address address
} }
cryptoCurrencies { cryptoCurrencies {
@ -64,14 +56,32 @@ const GET_INFO = gql`
` `
const ADD_ROW = gql` const ADD_ROW = gql`
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) { mutation InsertBlacklistRow($address: String!) {
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) { insertBlacklistRow(address: $address) {
cryptoCode
address address
} }
} }
` `
const GET_BLACKLIST_MESSAGES = gql`
query getBlacklistMessages {
blacklistMessages {
id
label
content
allowToggle
}
}
`
const EDIT_BLACKLIST_MESSAGE = gql`
mutation editBlacklistMessage($id: ID, $content: String) {
editBlacklistMessage(id: $id, content: $content) {
id
}
}
`
const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => { const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
const classes = useStyles() const classes = useStyles()
@ -117,14 +127,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
const Blacklist = () => { const Blacklist = () => {
const { data: blacklistResponse } = useQuery(GET_BLACKLIST) const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
const { data: configData } = useQuery(GET_INFO) const { data: configData } = useQuery(GET_INFO)
const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES)
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [clickedItem, setClickedItem] = useState({
code: 'BTC',
display: 'Bitcoin'
})
const [errorMsg, setErrorMsg] = useState(null) const [errorMsg, setErrorMsg] = useState(null)
const [editMessageError, setEditMessageError] = useState(null)
const [deleteDialog, setDeleteDialog] = useState(false) const [deleteDialog, setDeleteDialog] = useState(false)
const [confirmDialog, setConfirmDialog] = useState(false) const [confirmDialog, setConfirmDialog] = useState(false)
const [advancedSettings, setAdvancedSettings] = useState(false)
const [deleteEntry] = useMutation(DELETE_ROW, { const [deleteEntry] = useMutation(DELETE_ROW, {
onError: ({ message }) => { onError: ({ message }) => {
@ -144,14 +153,14 @@ const Blacklist = () => {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, {
onError: e => setEditMessageError(e),
refetchQueries: () => ['getBlacklistData']
})
const classes = useStyles() const classes = useStyles()
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? [] const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
const availableCurrencies = R.filter(
coin => coinUtils.getEquivalentCode(coin.code) === coin.code
)(R.path(['cryptoCurrencies'], blacklistResponse) ?? [])
const formattedData = groupByCode(blacklistData)
const complianceConfig = const complianceConfig =
configData?.config && fromNamespace('compliance')(configData.config) configData?.config && fromNamespace('compliance')(configData.config)
@ -165,12 +174,8 @@ const Blacklist = () => {
return saveConfig({ variables: { config } }) return saveConfig({ variables: { config } })
} }
const onClickSidebarItem = e => { const handleDeleteEntry = address => {
setClickedItem({ code: e.code, display: e.display }) deleteEntry({ variables: { address } })
}
const handleDeleteEntry = (cryptoCode, address) => {
deleteEntry({ variables: { cryptoCode, address } })
} }
const handleConfirmDialog = confirm => { const handleConfirmDialog = confirm => {
@ -180,21 +185,21 @@ const Blacklist = () => {
setConfirmDialog(false) setConfirmDialog(false)
} }
const validateAddress = (cryptoCode, address) => { const validateAddress = address => {
try { try {
return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address)) return !R.isEmpty(addressDetector.detectAddress(address).matches)
} catch { } catch {
return false return false
} }
} }
const addToBlacklist = async (cryptoCode, address) => { const addToBlacklist = async address => {
setErrorMsg(null) setErrorMsg(null)
if (!validateAddress(cryptoCode, address)) { if (!validateAddress(address)) {
setErrorMsg('Invalid address') setErrorMsg('Invalid address')
return return
} }
const res = await addEntry({ variables: { cryptoCode, address } }) const res = await addEntry({ variables: { address } })
if (!res.errors) { if (!res.errors) {
return setShowModal(false) return setShowModal(false)
} }
@ -208,6 +213,15 @@ const Blacklist = () => {
} }
} }
const editBlacklistMessage = r => {
editMessage({
variables: {
id: r.id,
content: r.content
}
})
}
return ( return (
<> <>
<PaperWalletDialog <PaperWalletDialog
@ -217,32 +231,23 @@ const Blacklist = () => {
setConfirmDialog(false) setConfirmDialog(false)
}} }}
/> />
<TitleSection title="Blacklisted addresses"> <TitleSection
<Box display="flex" justifyContent="flex-end"> title="Blacklisted addresses"
<Link color="primary" onClick={() => setShowModal(true)}> buttons={[
Blacklist new addresses {
</Link> text: 'Advanced settings',
</Box> icon: SettingsIcon,
</TitleSection> inverseIcon: ReverseSettingsIcon,
<Grid container className={classes.grid}> toggle: setAdvancedSettings
<Sidebar }
data={availableCurrencies} ]}>
isSelected={R.propEq('code', clickedItem.code)} {!advancedSettings && (
displayName={it => it.display} <Box display="flex" alignItems="center" justifyContent="flex-end">
onClick={onClickSidebarItem}
/>
<div className={classes.content}>
<Box display="flex" justifyContent="space-between" mb={3}>
<H4 noMargin className={classes.subtitle}>
{clickedItem.display
? `${clickedItem.display} blacklisted addresses`
: ''}{' '}
</H4>
<Box <Box
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="end" justifyContent="end"
mr="-140px"> mr="15px">
<P>Enable paper wallet (only)</P> <P>Enable paper wallet (only)</P>
<Switch <Switch
checked={enablePaperWalletOnly} checked={enablePaperWalletOnly}
@ -268,7 +273,7 @@ const Blacklist = () => {
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="flex-end" justifyContent="flex-end"
mr="-5px"> mr="15px">
<P>Reject reused addresses</P> <P>Reject reused addresses</P>
<Switch <Switch
checked={rejectAddressReuse} checked={rejectAddressReuse}
@ -280,24 +285,22 @@ const Blacklist = () => {
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2> <Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
<HelpTooltip width={304}> <HelpTooltip width={304}>
<P> <P>
The "Reject reused addresses" option means that all addresses This option requires a user to scan a fresh wallet address if
that are used once will be automatically rejected if there's they attempt to scan one that had been previously used for a
an attempt to use them again on a new transaction. transaction in your network.
</P> </P>
<P>
For details please read the relevant knowledgebase article:
</P>
<SupportLinkButton
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
label="Reject Address Reuse"
bottomSpace="1"
/>
</HelpTooltip> </HelpTooltip>
</Box> </Box>
<Link color="primary" onClick={() => setShowModal(true)}>
Blacklist new addresses
</Link>
</Box> </Box>
)}
</TitleSection>
{!advancedSettings && (
<div className={classes.content}>
<BlacklistTable <BlacklistTable
data={formattedData} data={blacklistData}
selectedCoin={clickedItem}
handleDeleteEntry={handleDeleteEntry} handleDeleteEntry={handleDeleteEntry}
errorMessage={errorMsg} errorMessage={errorMsg}
setErrorMessage={setErrorMsg} setErrorMessage={setErrorMsg}
@ -305,7 +308,15 @@ const Blacklist = () => {
setDeleteDialog={setDeleteDialog} setDeleteDialog={setDeleteDialog}
/> />
</div> </div>
</Grid> )}
{advancedSettings && (
<BlackListAdvanced
data={messagesResponse}
editBlacklistMessage={editBlacklistMessage}
mutationError={editMessageError}
onClose={() => refetch()}
/>
)}
{showModal && ( {showModal && (
<BlackListModal <BlackListModal
onClose={() => { onClose={() => {
@ -313,7 +324,6 @@ const Blacklist = () => {
setShowModal(false) setShowModal(false)
}} }}
errorMsg={errorMsg} errorMsg={errorMsg}
selectedCoin={clickedItem}
addToBlacklist={addToBlacklist} addToBlacklist={addToBlacklist}
/> />
)} )}

View file

@ -7,11 +7,23 @@ const styles = {
content: { content: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1
marginLeft: spacer * 6 },
advancedForm: {
'& > *': {
marginTop: 20
},
display: 'flex',
flexDirection: 'column',
height: '100%'
}, },
footer: { footer: {
margin: [['auto', 0, spacer * 3, 'auto']] display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, spacer * 3, 0]]
},
submit: {
margin: [['auto', 0, 0, 'auto']]
}, },
modalTitle: { modalTitle: {
margin: [['auto', 0, 8.5, 'auto']] margin: [['auto', 0, 8.5, 'auto']]
@ -54,6 +66,9 @@ const styles = {
cancelButton: { cancelButton: {
marginRight: 8, marginRight: 8,
padding: 0 padding: 0
},
resetToDefault: {
width: 145
} }
} }

View 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

View file

@ -14,31 +14,14 @@ import { H3 } from 'src/components/typography'
import styles from './Blacklist.styles' import styles from './Blacklist.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const BlackListModal = ({ const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => {
onClose,
selectedCoin,
addToBlacklist,
errorMsg
}) => {
const classes = useStyles() const classes = useStyles()
const handleAddToBlacklist = address => { const handleAddToBlacklist = address => {
if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) { addToBlacklist(address)
address = 'bitcoincash:' + address
}
addToBlacklist(selectedCoin.code, address)
}
const placeholderAddress = {
BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD',
ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
XMR:
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
} }
const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD'
return ( return (
<Modal <Modal
closeOnBackdropClick={true} closeOnBackdropClick={true}
@ -61,26 +44,20 @@ const BlackListModal = ({
handleAddToBlacklist(address.trim()) handleAddToBlacklist(address.trim())
}}> }}>
<Form id="address-form"> <Form id="address-form">
<H3 className={classes.modalTitle}> <H3 className={classes.modalTitle}>Blacklist new address</H3>
{selectedCoin.display
? `Blacklist ${R.toLower(selectedCoin.display)} address`
: ''}
</H3>
<Field <Field
name="address" name="address"
fullWidth fullWidth
autoComplete="off" autoComplete="off"
label="Paste new address to blacklist here" label="Paste new address to blacklist here"
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`} placeholder={`ex: ${placeholderAddress}`}
component={TextInput} component={TextInput}
/> />
{!R.isNil(errorMsg) && (
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
)}
</Form> </Form>
</Formik> </Formik>
<div className={classes.footer}> <div className={classes.footer}>
<Box display="flex" justifyContent="flex-end"> {!R.isNil(errorMsg) && <ErrorMessage>{errorMsg}</ErrorMessage>}
<Box className={classes.submit}>
<Link type="submit" form="address-form"> <Link type="submit" form="address-form">
Blacklist address Blacklist address
</Link> </Link>

View file

@ -5,7 +5,6 @@ import React, { useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog' import { DeleteDialog } from 'src/components/DeleteDialog'
import { IconButton } from 'src/components/buttons' import { IconButton } from 'src/components/buttons'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { Label1 } from 'src/components/typography'
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
@ -15,7 +14,6 @@ const useStyles = makeStyles(styles)
const BlacklistTable = ({ const BlacklistTable = ({
data, data,
selectedCoin,
handleDeleteEntry, handleDeleteEntry,
errorMessage, errorMessage,
setErrorMessage, setErrorMessage,
@ -29,8 +27,8 @@ const BlacklistTable = ({
const elements = [ const elements = [
{ {
name: 'address', name: 'address',
header: <Label1 className={classes.white}>{'Addresses'}</Label1>, header: 'Address',
width: 800, width: 1070,
textAlign: 'left', textAlign: 'left',
size: 'sm', size: 'sm',
view: it => ( view: it => (
@ -41,7 +39,7 @@ const BlacklistTable = ({
}, },
{ {
name: 'deleteButton', name: 'deleteButton',
header: <Label1 className={classes.white}>{'Delete'}</Label1>, header: 'Delete',
width: 130, width: 130,
textAlign: 'center', textAlign: 'center',
size: 'sm', size: 'sm',
@ -57,14 +55,11 @@ const BlacklistTable = ({
) )
} }
] ]
const dataToShow = selectedCoin
? data[selectedCoin.code]
: data[R.keys(data)[0]]
return ( return (
<> <>
<DataTable <DataTable
data={dataToShow} data={data}
elements={elements} elements={elements}
emptyText="No blacklisted addresses so far" emptyText="No blacklisted addresses so far"
name="blacklistTable" name="blacklistTable"
@ -77,10 +72,7 @@ const BlacklistTable = ({
}} }}
onConfirmed={() => { onConfirmed={() => {
setErrorMessage(null) setErrorMessage(null)
handleDeleteEntry( handleDeleteEntry(R.path(['address'], toBeDeleted))
R.path(['cryptoCode'], toBeDeleted),
R.path(['address'], toBeDeleted)
)
}} }}
errorMessage={errorMessage} errorMessage={errorMessage}
/> />

View file

@ -330,7 +330,7 @@ const EditableCard = ({
{editing && ( {editing && (
<div className={classes.editingWrapper}> <div className={classes.editingWrapper}>
<div className={classes.replace}> <div className={classes.replace}>
{hasImage && ( {hasImage && state !== OVERRIDE_PENDING && (
<ActionButton <ActionButton
color="secondary" color="secondary"
type="button" type="button"

View file

@ -5,7 +5,7 @@ export default {
height: 36 height: 36
}, },
tBody: { tBody: {
maxHeight: '65vh', maxHeight: 'calc(100vh - 350px)',
overflow: 'auto' overflow: 'auto'
}, },
tableWidth: { tableWidth: {

View file

@ -51,6 +51,12 @@ const styles = {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between' justifyContent: 'space-between'
},
tableWrapper: {
display: 'flex',
flexDirection: 'column',
flex: 1,
marginBottom: 80
} }
} }
@ -243,13 +249,15 @@ const CashboxHistory = ({ machines, currency, timezone }) => {
] ]
return ( return (
<DataTable <div className={classes.tableWrapper}>
loading={loading} <DataTable
name="cashboxHistory" loading={loading}
elements={elements} name="cashboxHistory"
data={batches} elements={elements}
emptyText="No cash box batches so far" data={batches}
/> emptyText="No cash box batches so far"
/>
</div>
) )
} }

View 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

View file

@ -12,7 +12,7 @@ import SingleRowTable from 'src/components/single-row-table/SingleRowTable'
import { formatLong } from 'src/utils/string' import { formatLong } from 'src/utils/string'
import FormRenderer from './FormRenderer' import FormRenderer from './FormRenderer'
import schemas from './schemas' import _schemas from './schemas'
const GET_INFO = gql` const GET_INFO = gql`
query getData { query getData {
@ -21,6 +21,12 @@ const GET_INFO = gql`
} }
` `
const GET_MARKETS = gql`
query getMarkets {
getMarkets
}
`
const SAVE_ACCOUNT = gql` const SAVE_ACCOUNT = gql`
mutation Save($accounts: JSONObject) { mutation Save($accounts: JSONObject) {
saveAccounts(accounts: $accounts) saveAccounts(accounts: $accounts)
@ -40,12 +46,17 @@ const useStyles = makeStyles(styles)
const Services = () => { const Services = () => {
const [editingSchema, setEditingSchema] = useState(null) const [editingSchema, setEditingSchema] = useState(null)
const { data } = useQuery(GET_INFO) const { data, loading: configLoading } = useQuery(GET_INFO)
const { data: marketsData, loading: marketsLoading } = useQuery(GET_MARKETS)
const [saveAccount] = useMutation(SAVE_ACCOUNT, { const [saveAccount] = useMutation(SAVE_ACCOUNT, {
onCompleted: () => setEditingSchema(null), onCompleted: () => setEditingSchema(null),
refetchQueries: ['getData'] refetchQueries: ['getData']
}) })
const markets = marketsData?.getMarkets
const schemas = _schemas(markets)
const classes = useStyles() const classes = useStyles()
const accounts = data?.accounts ?? {} const accounts = data?.accounts ?? {}
@ -101,40 +112,44 @@ const Services = () => {
const getValidationSchema = ({ code, getValidationSchema }) => const getValidationSchema = ({ code, getValidationSchema }) =>
getValidationSchema(accounts[code]) getValidationSchema(accounts[code])
const loading = marketsLoading || configLoading
return ( return (
<div className={classes.wrapper}> !loading && (
<TitleSection title="Third-Party services" /> <div className={classes.wrapper}>
<Grid container spacing={4}> <TitleSection title="Third-Party services" />
{R.values(schemas).map(schema => ( <Grid container spacing={4}>
<Grid item key={schema.code}> {R.values(schemas).map(schema => (
<SingleRowTable <Grid item key={schema.code}>
editMessage={'Configure ' + schema.title} <SingleRowTable
title={schema.title} editMessage={'Configure ' + schema.title}
onEdit={() => setEditingSchema(schema)} title={schema.title}
items={getItems(schema.code, schema.elements)} onEdit={() => setEditingSchema(schema)}
items={getItems(schema.code, schema.elements)}
/>
</Grid>
))}
</Grid>
{editingSchema && (
<Modal
title={`Edit ${editingSchema.name}`}
width={525}
handleClose={() => setEditingSchema(null)}
open={true}>
<FormRenderer
save={it =>
saveAccount({
variables: { accounts: { [editingSchema.code]: it } }
})
}
elements={getElements(editingSchema)}
validationSchema={getValidationSchema(editingSchema)}
value={getAccounts(editingSchema)}
/> />
</Grid> </Modal>
))} )}
</Grid> </div>
{editingSchema && ( )
<Modal
title={`Edit ${editingSchema.name}`}
width={525}
handleClose={() => setEditingSchema(null)}
open={true}>
<FormRenderer
save={it =>
saveAccount({
variables: { accounts: { [editingSchema.code]: it } }
})
}
elements={getElements(editingSchema)}
validationSchema={getValidationSchema(editingSchema)}
value={getAccounts(editingSchema)}
/>
</Modal>
)}
</div>
) )
} }

View file

@ -1,36 +1,57 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'binance', return {
name: 'Binance', code: 'binance',
title: 'Binance (Exchange)', name: 'Binance',
elements: [ title: 'Binance (Exchange)',
{ elements: [
code: 'apiKey', {
display: 'API key', code: 'apiKey',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'privateKey', {
display: 'Private key', code: 'privateKey',
component: SecretInputFormik display: 'Private key',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key'))
})
} }
} }
export default schema

View file

@ -1,36 +1,57 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'binanceus', return {
name: 'Binance.us', code: 'binanceus',
title: 'Binance.us (Exchange)', name: 'Binance.us',
elements: [ title: 'Binance.us (Exchange)',
{ elements: [
code: 'apiKey', {
display: 'API key', code: 'apiKey',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'privateKey', {
display: 'Private key', code: 'privateKey',
component: SecretInputFormik display: 'Private key',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key'))
})
} }
} }
export default schema

View file

@ -1,36 +1,57 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'bitfinex', return {
name: 'Bitfinex', code: 'bitfinex',
title: 'Bitfinex (Exchange)', name: 'Bitfinex',
elements: [ title: 'Bitfinex (Exchange)',
{ elements: [
code: 'key', {
display: 'API Key', code: 'key',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'secret', {
display: 'API Secret', code: 'secret',
component: SecretInputFormik display: 'API secret',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency Market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
key: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
secret: Yup.string('The API secret must be a string')
.max(100, 'The API secret is too long')
.test(secretTest(account?.secret, 'API secret')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
key: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
secret: Yup.string('The API secret must be a string')
.max(100, 'The API secret is too long')
.test(secretTest(account?.secret, 'API secret'))
})
} }
} }
export default schema

View file

@ -1,46 +1,67 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'bitstamp', return {
name: 'Bitstamp', code: 'bitstamp',
title: 'Bitstamp (Exchange)', name: 'Bitstamp',
elements: [ title: 'Bitstamp (Exchange)',
{ elements: [
code: 'clientId', {
display: 'Client ID', code: 'clientId',
component: TextInputFormik, display: 'Client ID',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'key', {
display: 'API key', code: 'key',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'secret', {
display: 'API secret', code: 'secret',
component: SecretInputFormik display: 'API secret',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
clientId: Yup.string('The client ID must be a string')
.max(100, 'The client ID is too long')
.required('The client ID is required'),
key: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
secret: Yup.string('The API secret must be a string')
.max(100, 'The API secret is too long')
.test(secretTest(account?.secret, 'API secret')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
clientId: Yup.string('The client ID must be a string')
.max(100, 'The client ID is too long')
.required('The client ID is required'),
key: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
secret: Yup.string('The API secret must be a string')
.max(100, 'The API secret is too long')
.test(secretTest(account?.secret, 'API secret'))
})
} }
} }
export default schema

View file

@ -1,46 +1,67 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'cex', return {
name: 'CEX.IO', code: 'cex',
title: 'CEX.IO (Exchange)', name: 'CEX.IO',
elements: [ title: 'CEX.IO (Exchange)',
{ elements: [
code: 'apiKey', {
display: 'API key', code: 'apiKey',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'uid', {
display: 'User ID', code: 'uid',
component: TextInputFormik, display: 'User ID',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'privateKey', {
display: 'Private key', code: 'privateKey',
component: SecretInputFormik display: 'Private key',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency Market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
uid: Yup.string('The User ID must be a string')
.max(100, 'The User ID is too long')
.required('The User ID is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
uid: Yup.string('The User ID must be a string')
.max(100, 'The User ID is too long')
.required('The User ID is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key'))
})
} }
} }
export default schema

View file

@ -1,5 +1,12 @@
import { ALL_CRYPTOS } from '@lamassu/coins'
import * as R from 'ramda' import * as R from 'ramda'
const WARNING_LEVELS = {
CLEAN: 'clean',
PARTIAL: 'partial',
IMPORTANT: 'important'
}
const secretTest = (secret, message) => ({ const secretTest = (secret, message) => ({
name: 'secret-test', name: 'secret-test',
message: message ? `The ${message} is invalid` : 'Invalid field', message: message ? `The ${message} is invalid` : 'Invalid field',
@ -21,4 +28,35 @@ const leadingZerosTest = (value, context) => {
return true return true
} }
export { secretTest, leadingZerosTest } const buildCurrencyOptions = markets => {
return R.map(it => {
const unavailableCryptos = R.difference(ALL_CRYPTOS, markets[it])
const unavailableCryptosFiltered = R.difference(unavailableCryptos, [it]) // As the markets can have stablecoins to trade against other crypto, filter them out, as there can't be pairs such as USDT/USDT
const unavailableMarketsStr =
R.length(unavailableCryptosFiltered) > 1
? `${R.join(
', ',
R.slice(0, -1, unavailableCryptosFiltered)
)} and ${R.last(unavailableCryptosFiltered)}`
: unavailableCryptosFiltered[0]
const warningLevel = R.isEmpty(unavailableCryptosFiltered)
? WARNING_LEVELS.CLEAN
: !R.isEmpty(unavailableCryptosFiltered) &&
R.length(unavailableCryptosFiltered) < R.length(ALL_CRYPTOS)
? WARNING_LEVELS.PARTIAL
: WARNING_LEVELS.IMPORTANT
return {
code: R.toUpper(it),
display: R.toUpper(it),
warning: warningLevel,
warningMessage: !R.isEmpty(unavailableCryptosFiltered)
? `No market pairs available for ${unavailableMarketsStr}`
: `All market pairs are available`
}
}, R.keys(markets))
}
export { secretTest, leadingZerosTest, buildCurrencyOptions }

View file

@ -1,16 +1,16 @@
import binance from './binance' import _binance from './binance'
import binanceus from './binanceus' import _binanceus from './binanceus'
import bitfinex from './bitfinex' import _bitfinex from './bitfinex'
import bitgo from './bitgo' import bitgo from './bitgo'
import bitstamp from './bitstamp' import _bitstamp from './bitstamp'
import blockcypher from './blockcypher' import blockcypher from './blockcypher'
import cex from './cex' import _cex from './cex'
import elliptic from './elliptic' import elliptic from './elliptic'
import galoy from './galoy' import galoy from './galoy'
import inforu from './inforu' import inforu from './inforu'
import infura from './infura' import infura from './infura'
import itbit from './itbit' import _itbit from './itbit'
import kraken from './kraken' import _kraken from './kraken'
import mailgun from './mailgun' import mailgun from './mailgun'
import scorechain from './scorechain' import scorechain from './scorechain'
import sumsub from './sumsub' import sumsub from './sumsub'
@ -19,25 +19,37 @@ import trongrid from './trongrid'
import twilio from './twilio' import twilio from './twilio'
import vonage from './vonage' import vonage from './vonage'
export default { const schemas = (markets = {}) => {
[bitgo.code]: bitgo, const binance = _binance(markets?.binance)
[galoy.code]: galoy, const bitfinex = _bitfinex(markets?.bitfinex)
[bitstamp.code]: bitstamp, const binanceus = _binanceus(markets?.binanceus)
[blockcypher.code]: blockcypher, const bitstamp = _bitstamp(markets?.bitstamp)
[elliptic.code]: elliptic, const cex = _cex(markets?.cex)
[inforu.code]: inforu, const itbit = _itbit(markets?.itbit)
[infura.code]: infura, const kraken = _kraken(markets?.kraken)
[itbit.code]: itbit,
[kraken.code]: kraken, return {
[mailgun.code]: mailgun, [bitgo.code]: bitgo,
[telnyx.code]: telnyx, [galoy.code]: galoy,
[vonage.code]: vonage, [bitstamp.code]: bitstamp,
[twilio.code]: twilio, [blockcypher.code]: blockcypher,
[binanceus.code]: binanceus, [elliptic.code]: elliptic,
[cex.code]: cex, [inforu.code]: inforu,
[scorechain.code]: scorechain, [infura.code]: infura,
[trongrid.code]: trongrid, [itbit.code]: itbit,
[binance.code]: binance, [kraken.code]: kraken,
[bitfinex.code]: bitfinex, [mailgun.code]: mailgun,
[sumsub.code]: sumsub [telnyx.code]: telnyx,
[vonage.code]: vonage,
[twilio.code]: twilio,
[binanceus.code]: binanceus,
[cex.code]: cex,
[scorechain.code]: scorechain,
[trongrid.code]: trongrid,
[binance.code]: binance,
[bitfinex.code]: bitfinex,
[sumsub.code]: sumsub
}
} }
export default schemas

View file

@ -1,54 +1,75 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { buildCurrencyOptions, secretTest } from './helper'
export default { const schema = markets => {
code: 'itbit', return {
name: 'itBit', code: 'itbit',
title: 'itBit (Exchange)', name: 'itBit',
elements: [ title: 'itBit (Exchange)',
{ elements: [
code: 'userId', {
display: 'User ID', code: 'userId',
component: TextInputFormik, display: 'User ID',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'walletId', {
display: 'Wallet ID', code: 'walletId',
component: TextInputFormik, display: 'Wallet ID',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'clientKey', {
display: 'Client key', code: 'clientKey',
component: TextInputFormik display: 'Client key',
}, component: TextInput
{ },
code: 'clientSecret', {
display: 'Client secret', code: 'clientSecret',
component: SecretInputFormik display: 'Client secret',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
userId: Yup.string('The user ID must be a string')
.max(100, 'The user ID is too long')
.required('The user ID is required'),
walletId: Yup.string('The wallet ID must be a string')
.max(100, 'The wallet ID is too long')
.required('The wallet ID is required'),
clientKey: Yup.string('The client key must be a string')
.max(100, 'The client key is too long')
.required('The client key is required'),
clientSecret: Yup.string('The client secret must be a string')
.max(100, 'The client secret is too long')
.test(secretTest(account?.clientSecret, 'client secret')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
userId: Yup.string('The user ID must be a string')
.max(100, 'The user ID is too long')
.required('The user ID is required'),
walletId: Yup.string('The wallet ID must be a string')
.max(100, 'The wallet ID is too long')
.required('The wallet ID is required'),
clientKey: Yup.string('The client key must be a string')
.max(100, 'The client key is too long')
.required('The client key is required'),
clientSecret: Yup.string('The client secret must be a string')
.max(100, 'The client secret is too long')
.test(secretTest(account?.clientSecret, 'client secret'))
})
} }
} }
export default schema

View file

@ -1,36 +1,57 @@
import * as Yup from 'yup' import * as Yup from 'yup'
import SecretInputFormik from 'src/components/inputs/formik/SecretInput' import {
import TextInputFormik from 'src/components/inputs/formik/TextInput' SecretInput,
TextInput,
Autocomplete
} from 'src/components/inputs/formik'
import { secretTest } from './helper' import { secretTest, buildCurrencyOptions } from './helper'
export default { const schema = markets => {
code: 'kraken', return {
name: 'Kraken', code: 'kraken',
title: 'Kraken (Exchange)', name: 'Kraken',
elements: [ title: 'Kraken (Exchange)',
{ elements: [
code: 'apiKey', {
display: 'API key', code: 'apiKey',
component: TextInputFormik, display: 'API key',
face: true, component: TextInput,
long: true face: true,
}, long: true
{ },
code: 'privateKey', {
display: 'Private key', code: 'privateKey',
component: SecretInputFormik display: 'Private key',
component: SecretInput
},
{
code: 'currencyMarket',
display: 'Currency market',
component: Autocomplete,
inputProps: {
options: buildCurrencyOptions(markets),
labelProp: 'display',
valueProp: 'code'
},
face: true
}
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key')),
currencyMarket: Yup.string(
'The currency market must be a string'
).required('The currency market is required')
})
} }
],
getValidationSchema: account => {
return Yup.object().shape({
apiKey: Yup.string('The API key must be a string')
.max(100, 'The API key is too long')
.required('The API key is required'),
privateKey: Yup.string('The private key must be a string')
.max(100, 'The private key is too long')
.test(secretTest(account?.privateKey, 'private key'))
})
} }
} }
export default schema

View file

@ -131,8 +131,9 @@ const DetailsRow = ({ it: tx, timezone }) => {
) )
const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN
const commissionPercentage = const commissionPercentage = BigNumber(
Number.parseFloat(tx.commissionPercentage, 2) * 100 Number.parseFloat(tx.commissionPercentage, 2) * 100
).toFixed(2, 1) // ROUND_DOWN
const fixedFee = Number.parseFloat(tx.fixedFee) || 0 const fixedFee = Number.parseFloat(tx.fixedFee) || 0
const fiat = BigNumber(tx.fiat) const fiat = BigNumber(tx.fiat)
.minus(fixedFee) .minus(fixedFee)

View file

@ -10,7 +10,7 @@ import { SupportLinkButton } from 'src/components/buttons'
import { NamespacedTable as EditableTable } from 'src/components/editableTable' import { NamespacedTable as EditableTable } from 'src/components/editableTable'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import FormRenderer from 'src/pages/Services/FormRenderer' import FormRenderer from 'src/pages/Services/FormRenderer'
import schemas from 'src/pages/Services/schemas' import _schemas from 'src/pages/Services/schemas'
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg' import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg' import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
import { fromNamespace, toNamespace } from 'src/utils/config' import { fromNamespace, toNamespace } from 'src/utils/config'
@ -54,6 +54,12 @@ const GET_INFO = gql`
} }
` `
const GET_MARKETS = gql`
query getMarkets {
getMarkets
}
`
const LOCALE = 'locale' const LOCALE = 'locale'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -71,6 +77,8 @@ const Wallet = ({ name: SCREEN_KEY }) => {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const { data: marketsData } = useQuery(GET_MARKETS)
const [saveAccount] = useMutation(SAVE_ACCOUNT, { const [saveAccount] = useMutation(SAVE_ACCOUNT, {
onCompleted: () => setEditingSchema(null), onCompleted: () => setEditingSchema(null),
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
@ -89,6 +97,10 @@ const Wallet = ({ name: SCREEN_KEY }) => {
const cryptoCurrencies = data?.cryptoCurrencies ?? [] const cryptoCurrencies = data?.cryptoCurrencies ?? []
const accounts = data?.accounts ?? [] const accounts = data?.accounts ?? []
const markets = marketsData?.getMarkets
const schemas = _schemas(markets)
const onChange = (previous, current, setValue) => { const onChange = (previous, current, setValue) => {
if (!current) return setValue(current) if (!current) return setValue(current)

View file

@ -16,6 +16,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
import Notifications from 'src/pages/Notifications/Notifications' import Notifications from 'src/pages/Notifications/Notifications'
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
@ -193,6 +194,13 @@ const getLamassuRoutes = () => [
route: '/settings/operator-info/terms-conditions', route: '/settings/operator-info/terms-conditions',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER], allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: TermsConditions component: TermsConditions
},
{
key: 'machine-screens',
label: 'Machine screens',
route: '/settings/operator-info/machine-screens',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: MachineScreens
} }
] ]
} }

View file

@ -18,6 +18,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus'
import Notifications from 'src/pages/Notifications/Notifications' import Notifications from 'src/pages/Notifications/Notifications'
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
import MachineScreens from 'src/pages/OperatorInfo/MachineScreens'
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices'
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
@ -172,6 +173,13 @@ const getPazuzRoutes = () => [
route: '/settings/operator-info/terms-conditions', route: '/settings/operator-info/terms-conditions',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER], allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: TermsConditions component: TermsConditions
},
{
key: 'machine-screens',
label: 'Machine screens',
route: '/settings/operator-info/machine-screens',
allowedRoles: [ROLES.USER, ROLES.SUPERUSER],
component: MachineScreens
} }
] ]
} }

View file

@ -32,6 +32,9 @@ const mistyRose = '#ffeceb'
const pumpkin = '#ff7311' const pumpkin = '#ff7311'
const linen = '#fbf3ec' const linen = '#fbf3ec'
// Warning
const orangeYellow = '#ffcc00'
// Color Variables // Color Variables
const primaryColor = zodiac const primaryColor = zodiac
@ -136,6 +139,7 @@ export {
java, java,
neon, neon,
linen, linen,
orangeYellow,
// named colors // named colors
primaryColor, primaryColor,
secondaryColor, secondaryColor,

View file

@ -12,7 +12,8 @@ const namespaces = {
RECEIPT: 'receipt', RECEIPT: 'receipt',
COIN_ATM_RADAR: 'coinAtmRadar', COIN_ATM_RADAR: 'coinAtmRadar',
TERMS_CONDITIONS: 'termsConditions', TERMS_CONDITIONS: 'termsConditions',
TRIGGERS: 'triggersConfig' TRIGGERS: 'triggersConfig',
MACHINE_SCREENS: 'machineScreens'
} }
const mapKeys = R.curry((fn, obj) => const mapKeys = R.curry((fn, obj) =>