diff --git a/lib/blacklist.js b/lib/blacklist.js index 4c3d8595..8324fc20 100644 --- a/lib/blacklist.js +++ b/lib/blacklist.js @@ -1,48 +1,49 @@ +const _ = require('lodash/fp') + const db = require('./db') const notifierQueries = require('./notifier/queries') -// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator -const getBlacklist = () => { - return db.any(`SELECT * FROM blacklist`).then(res => - res.map(item => ({ - cryptoCode: item.crypto_code, - address: item.address - })) +const getBlacklist = () => + db.any( + `SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage + FROM blacklist JOIN blacklist_messages + ON blacklist.blacklist_message_id = blacklist_messages.id` ) + +const deleteFromBlacklist = address => { + const sql = `DELETE FROM blacklist WHERE address = $1` + notifierQueries.clearBlacklistNotification(address) + return db.none(sql, [address]) } -// Delete row from blacklist table by crypto code and address -const deleteFromBlacklist = (cryptoCode, address) => { - const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2` - notifierQueries.clearBlacklistNotification(cryptoCode, address) - return db.none(sql, [cryptoCode, address]) -} - -const insertIntoBlacklist = (cryptoCode, address) => { +const insertIntoBlacklist = address => { return db .none( - 'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);', - [cryptoCode, address] + 'INSERT INTO blacklist (address) VALUES ($1);', + [address] ) } -function blocked (address, cryptoCode) { - const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2` - return db.any(sql, [address, cryptoCode]) +function blocked (address) { + const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1` + return db.oneOrNone(sql, [address]) } -function addToUsedAddresses (address, cryptoCode) { - // ETH reuses addresses - if (cryptoCode === 'ETH') return Promise.resolve() +function getMessages () { + const sql = `SELECT * FROM blacklist_messages` + return db.any(sql) +} - const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)` - return db.oneOrNone(sql, [cryptoCode, address]) +function editBlacklistMessage (id, content) { + const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id` + return db.oneOrNone(sql, [content, id]) } module.exports = { blocked, - addToUsedAddresses, getBlacklist, deleteFromBlacklist, - insertIntoBlacklist + insertIntoBlacklist, + getMessages, + editBlacklistMessage } diff --git a/lib/blockchain/bitcoin.js b/lib/blockchain/bitcoin.js index 12c96815..9d91ecb9 100644 --- a/lib/blockchain/bitcoin.js +++ b/lib/blockchain/bitcoin.js @@ -27,6 +27,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating Bitcoin Core. This may take a minute...') !isDevMode() && common.es(`sudo supervisorctl stop bitcoin`) common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Bitcoin Core: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') @@ -55,6 +59,20 @@ function updateCore (coinRec, isCurrentlyRunning) { common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) } + if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + common.logger.info(`fallbackfee already defined, skipping...`) + } else { + common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`) + common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + } + + if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) { + common.logger.info(`rpcworkqueue already defined, skipping...`) + } else { + common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`) + common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`) + } + if (isCurrentlyRunning && !isDevMode()) { common.logger.info('Starting wallet...') common.es(`sudo supervisorctl start bitcoin`) diff --git a/lib/blockchain/bitcoincash.js b/lib/blockchain/bitcoincash.js index d8ac0efe..53e4ab8e 100644 --- a/lib/blockchain/bitcoincash.js +++ b/lib/blockchain/bitcoincash.js @@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating Bitcoin Cash. This may take a minute...') common.es(`sudo supervisorctl stop bitcoincash`) common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/blockchain/common.js b/lib/blockchain/common.js index 8db73a72..3e844f6c 100644 --- a/lib/blockchain/common.js +++ b/lib/blockchain/common.js @@ -29,37 +29,47 @@ module.exports = { const BINARIES = { BTC: { defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', + defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397', defaultDir: 'bitcoin-0.20.1/bin', url: 'https://bitcoincore.org/bin/bitcoin-core-27.1/bitcoin-27.1-x86_64-linux-gnu.tar.gz', + UrlHash: 'c9840607d230d65f6938b81deaec0b98fe9cb14c3a41a5b13b2c05d044a48422', dir: 'bitcoin-27.1/bin' }, ETH: { url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.8-a9523b64.tar.gz', + urlHash: 'fff507c90c180443456950e4fc0bf224d26ce5ea6896194ff864c3c3754c136b', dir: 'geth-linux-amd64-1.14.8-a9523b64' }, ZEC: { url: 'https://github.com/zcash/artifacts/raw/master/v5.9.0/bullseye/zcash-5.9.0-linux64-debian-bullseye.tar.gz', + urlHash: 'd385b9fbeeb145f60b0b339d256cabb342713ed3014cd634cf2d68078365abd2', dir: 'zcash-5.9.0/bin' }, DASH: { defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', + defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219', defaultDir: 'dashcore-18.1.0/bin', url: 'https://github.com/dashpay/dash/releases/download/v21.1.0/dashcore-21.1.0-x86_64-linux-gnu.tar.gz', + urlHash: 'a7d0c1b04d53a9b1b3499eb82182c0fa57f4c8768c16163e5d05971bf45d7928', dir: 'dashcore-21.1.0/bin' }, LTC: { defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', + defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751', defaultDir: 'litecoin-0.18.1/bin', url: 'https://download.litecoin.org/litecoin-0.21.3/linux/litecoin-0.21.3-x86_64-linux-gnu.tar.gz', + urlHash: 'ea231c630e2a243cb01affd4c2b95a2be71560f80b64b9f4bceaa13d736aa7cb', dir: 'litecoin-0.21.3/bin' }, BCH: { url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v27.1.0/bitcoin-cash-node-27.1.0-x86_64-linux-gnu.tar.gz', + urlHash: '0dcc387cbaa3a039c97ddc8fb99c1fa7bff5dc6e4bd3a01d3c3095f595ad2dce', dir: 'bitcoin-cash-node-27.1.0/bin', files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']] }, XMR: { url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.3.tar.bz2', + urlHash: '47c7e6b4b88a57205800a2538065a7874174cd087eedc2526bee1ebcce0cc5e3', dir: 'monero-x86_64-linux-gnu-v0.18.3.3', files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']] } @@ -133,10 +143,15 @@ function fetchAndInstall (coinRec) { if (!binaries) throw new Error(`No such coin: ${coinRec.code}`) const url = requiresUpdate ? binaries.defaultUrl : binaries.url + const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash const downloadFile = path.basename(url) const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir es(`wget -q ${url}`) + if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) { + logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`) + return + } es(`tar -xf ${downloadFile}`) const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin' diff --git a/lib/blockchain/dash.js b/lib/blockchain/dash.js index 05ace3a7..51ed159f 100644 --- a/lib/blockchain/dash.js +++ b/lib/blockchain/dash.js @@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating Dash Core. This may take a minute...') common.es(`sudo supervisorctl stop dash`) common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Dash Core: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/blockchain/ethereum.js b/lib/blockchain/ethereum.js index dd39468a..9434ebdc 100644 --- a/lib/blockchain/ethereum.js +++ b/lib/blockchain/ethereum.js @@ -8,6 +8,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...') common.es(`sudo supervisorctl stop ethereum`) common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Geth: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/blockchain/litecoin.js b/lib/blockchain/litecoin.js index cd02a77f..ce128dd0 100644 --- a/lib/blockchain/litecoin.js +++ b/lib/blockchain/litecoin.js @@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating Litecoin Core. This may take a minute...') common.es(`sudo supervisorctl stop litecoin`) common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Litecoin Core: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/blockchain/monero.js b/lib/blockchain/monero.js index 447b2722..870f3920 100644 --- a/lib/blockchain/monero.js +++ b/lib/blockchain/monero.js @@ -22,6 +22,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating Monero. This may take a minute...') common.es(`sudo supervisorctl stop monero monero-wallet`) common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Monero: Package signature do not match!') + return + } common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/blockchain/zcash.js b/lib/blockchain/zcash.js index 51430969..a6baed51 100644 --- a/lib/blockchain/zcash.js +++ b/lib/blockchain/zcash.js @@ -13,6 +13,10 @@ function updateCore (coinRec, isCurrentlyRunning) { common.logger.info('Updating your Zcash wallet. This may take a minute...') common.es(`sudo supervisorctl stop zcash`) common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`) + if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) { + common.logger.info('Failed to update Zcash: Package signature do not match!') + return + } common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`) common.logger.info('Updating wallet...') diff --git a/lib/cash-in/cash-in-low.js b/lib/cash-in/cash-in-low.js index 5b8066a0..0f2942df 100644 --- a/lib/cash-in/cash-in-low.js +++ b/lib/cash-in/cash-in-low.js @@ -8,7 +8,7 @@ const E = require('../error') const PENDING_INTERVAL_MS = 60 * T.minutes -const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto'] +const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto'] const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms') const massage = _.flow(_.omit(massageFields), diff --git a/lib/cash-in/cash-in-tx.js b/lib/cash-in/cash-in-tx.js index cc676554..a4e8c228 100644 --- a/lib/cash-in/cash-in-tx.js +++ b/lib/cash-in/cash-in-tx.js @@ -32,38 +32,41 @@ function post (machineTx, pi) { return cashInAtomic.atomic(machineTx, pi) .then(r => { const updatedTx = r.tx - let blacklisted = false let addressReuse = false - let walletScore = {} const promises = [settingsLoader.loadLatestConfig()] const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero() if (isFirstPost) { - promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi)) + promises.push( + checkForBlacklisted(updatedTx), + doesTxReuseAddress(updatedTx), + getWalletScore(updatedTx, pi) + ) } return Promise.all(promises) - .then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => { - const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse + .then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => { + const { rejectAddressReuse } = configManager.getCompliance(config) + const isBlacklisted = !!blacklisted - walletScore = fetchedWalletScore - - if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) { - blacklisted = true + if (isBlacklisted) { notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false) } else if (isReusedAddress && rejectAddressReuse) { notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true) addressReuse = true } - return postProcess(r, pi, blacklisted, addressReuse, walletScore) + return postProcess(r, pi, isBlacklisted, addressReuse, walletScore) + .then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes)) + .then(changes => cashInLow.update(db, updatedTx, changes)) + .then(_.flow( + _.set('bills', machineTx.bills), + _.set('blacklisted', isBlacklisted), + _.set('blacklistMessage', blacklisted?.content), + _.set('addressReuse', addressReuse), + _.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid), + )) }) - .then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes)) - .then(changes => cashInLow.update(db, updatedTx, changes)) - .then(tx => _.set('bills', machineTx.bills, tx)) - .then(tx => _.set('blacklisted', blacklisted, tx)) - .then(tx => _.set('addressReuse', addressReuse, tx)) - .then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx)) }) } @@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) { } function checkForBlacklisted (tx) { - return blacklist.blocked(tx.toAddress, tx.cryptoCode) + return blacklist.blocked(tx.toAddress) } function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { diff --git a/lib/cashbox-batches.js b/lib/cashbox-batches.js index 52f41dc6..35a03d9e 100644 --- a/lib/cashbox-batches.js +++ b/lib/cashbox-batches.js @@ -6,7 +6,7 @@ const camelize = require('./utils') function createCashboxBatch (deviceId, cashboxCount) { if (_.isEqual(0, cashboxCount)) throw new Error('Cash box is empty. Cash box batch could not be created.') - const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty')` + const sql = `INSERT INTO cash_unit_operation (id, device_id, created, operation_type) VALUES ($1, $2, now(), 'cash-box-empty') RETURNING *` const sql2 = ` UPDATE bills SET cashbox_batch_id=$1 FROM cash_in_txs @@ -25,6 +25,7 @@ function createCashboxBatch (deviceId, cashboxCount) { const q2 = t.none(sql2, [batchId, deviceId]) const q3 = t.none(sql3, [batchId, deviceId]) return t.batch([q1, q2, q3]) + .then(([it]) => it) }) } @@ -100,14 +101,6 @@ function editBatchById (id, performedBy) { return db.none(sql, [performedBy, id]) } -function getBillsByBatchId (id) { - const sql = `SELECT bi.* FROM ( - SELECT b.id, b.fiat, b.fiat_code, b.created, b.cashbox_batch_id, cit.device_id AS device_id FROM bills b LEFT OUTER JOIN (SELECT id, device_id FROM cash_in_txs) AS cit ON cit.id = b.cash_in_txs_id UNION - SELECT id, fiat, fiat_code, created, cashbox_batch_id, device_id FROM empty_unit_bills - ) AS bi WHERE bi.cashbox_batch_id=$1` - return db.any(sql, [id]) -} - function logFormatter (data) { return _.map( it => { @@ -124,11 +117,62 @@ function logFormatter (data) { ) } +function getMachineUnbatchedBills (deviceId) { + const sql = ` + SELECT now() AS created, cash_in_txs.device_id, json_agg(b.*) AS bills FROM bills b LEFT OUTER JOIN cash_in_txs + ON b.cash_in_txs_id = cash_in_txs.id + WHERE b.cashbox_batch_id IS NULL AND cash_in_txs.device_id = $1 + GROUP BY cash_in_txs.device_id + ` + + return db.oneOrNone(sql, [deviceId]) + .then(res => _.mapKeys(it => _.camelCase(it), res)) + .then(logFormatterSingle) +} + +function getBatchById (id) { + const sql = ` + SELECT cb.id, cb.device_id, cb.created, cb.operation_type, cb.bill_count_override, cb.performed_by, json_agg(b.*) AS bills + FROM cashbox_batches AS cb + LEFT JOIN bills AS b ON cb.id = b.cashbox_batch_id + WHERE cb.id = $1 + GROUP BY cb.id + ` + + return db.oneOrNone(sql, [id]).then(res => _.mapKeys(it => _.camelCase(it), res)) + .then(logFormatterSingle) +} + +function logFormatterSingle (data) { + const bills = _.filter( + it => !(_.isNil(it) || _.isNil(it.fiat_code) || _.isNil(it.fiat) || _.isNaN(it.fiat)), + data.bills + ) + + return { + id: data.id, + deviceId: data.deviceId, + created: data.created, + operationType: data.operationType, + billCount: _.size(bills), + fiatTotals: _.reduce( + (acc, value) => { + acc[value.fiat_code] = (acc[value.fiat_code] || 0) + value.fiat + return acc + }, + {}, + bills + ), + billsByDenomination: _.countBy(it => `${it.fiat} ${it.fiat_code}`, bills) + } +} + module.exports = { createCashboxBatch, updateMachineWithBatch, getBatches, - getBillsByBatchId, editBatchById, + getBatchById, + getMachineUnbatchedBills, logFormatter } diff --git a/lib/exchange.js b/lib/exchange.js index 0431a7d5..f9811bb8 100644 --- a/lib/exchange.js +++ b/lib/exchange.js @@ -1,6 +1,10 @@ +const _ = require('lodash/fp') +const { ALL_CRYPTOS } = require('@lamassu/coins') + const configManager = require('./new-config-manager') const ccxt = require('./plugins/exchange/ccxt') const mockExchange = require('./plugins/exchange/mock-exchange') +const accounts = require('./new-admin/config/accounts') function lookupExchange (settings, cryptoCode) { const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange @@ -45,8 +49,26 @@ function active (settings, cryptoCode) { return !!lookupExchange(settings, cryptoCode) } +function getMarkets () { + const filterExchanges = _.filter(it => it.class === 'exchange') + const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST)) + + return _.reduce( + (acc, value) => + Promise.all([acc, ccxt.getMarkets(value, ALL_CRYPTOS)]) + .then(([a, markets]) => Promise.resolve({ + ...a, + [value]: markets + })), + Promise.resolve({}), + availableExchanges + ) +} + module.exports = { + fetchExchange, buy, sell, - active + active, + getMarkets } diff --git a/lib/graphql/resolvers.js b/lib/graphql/resolvers.js index 1270f1f3..5d1f6323 100644 --- a/lib/graphql/resolvers.js +++ b/lib/graphql/resolvers.js @@ -61,6 +61,18 @@ const addReceiptInfo = receiptInfo => ret => { } +const addMachineScreenOpts = smth => _.update( + 'screenOptions', + _.flow( + addSmthInfo( + 'rates', + [ + 'active' + ] + )(smth.rates) + ) +) + /* TODO: Simplify this. */ const buildTriggers = allTriggers => { const normalTriggers = [] @@ -103,7 +115,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings _.pick([ 'coins', 'configVersion', - 'timezone' + 'timezone', + 'screenOptions' ]), _.update('coins', massageCoins), _.set('serverVersion', VERSION), @@ -117,6 +130,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings configManager.getLocale(deviceId, settings.config), configManager.getOperatorInfo(settings.config), configManager.getReceipt(settings.config), + configManager.getAllMachineScreenOpts(settings.config), !!configManager.getCashOut(deviceId, settings.config).active, getMachine(deviceId, currentConfigVersion), configManager.getCustomerAuthenticationMethod(settings.config) @@ -129,6 +143,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings localeInfo, operatorInfo, receiptInfo, + machineScreenOpts, twoWayMode, { numberOfCassettes, numberOfRecyclers }, customerAuthentication, @@ -153,7 +168,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings urlsToPing, }), addOperatorInfo(operatorInfo), - addReceiptInfo(receiptInfo) + addReceiptInfo(receiptInfo), + addMachineScreenOpts(machineScreenOpts) )(staticConf)) } diff --git a/lib/graphql/types.js b/lib/graphql/types.js index 89296c6c..c0c72b1e 100644 --- a/lib/graphql/types.js +++ b/lib/graphql/types.js @@ -49,6 +49,14 @@ type ReceiptInfo { addressQRCode: Boolean! } +type MachineScreenOptions { + rates: RateScreenOptions! +} + +type RateScreenOptions { + active: Boolean! +} + type SpeedtestFile { url: String! size: Int! @@ -147,6 +155,7 @@ type StaticConfig { operatorInfo: OperatorInfo machineInfo: MachineInfo! receiptInfo: ReceiptInfo + screenOptions: MachineScreenOptions speedtestFiles: [SpeedtestFile!]! urlsToPing: [String!]! diff --git a/lib/new-admin/graphql/resolvers/blacklist.resolver.js b/lib/new-admin/graphql/resolvers/blacklist.resolver.js index 8c15e43f..e0b63d53 100644 --- a/lib/new-admin/graphql/resolvers/blacklist.resolver.js +++ b/lib/new-admin/graphql/resolvers/blacklist.resolver.js @@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist') const resolvers = { Query: { - blacklist: () => blacklist.getBlacklist() + blacklist: () => blacklist.getBlacklist(), + blacklistMessages: () => blacklist.getMessages() }, Mutation: { - deleteBlacklistRow: (...[, { cryptoCode, address }]) => - blacklist.deleteFromBlacklist(cryptoCode, address), - insertBlacklistRow: (...[, { cryptoCode, address }]) => - blacklist.insertIntoBlacklist(cryptoCode, address) + deleteBlacklistRow: (...[, { address }]) => + blacklist.deleteFromBlacklist(address), + insertBlacklistRow: (...[, { address }]) => + blacklist.insertIntoBlacklist(address), + editBlacklistMessage: (...[, { id, content }]) => + blacklist.editBlacklistMessage(id, content) } } diff --git a/lib/new-admin/graphql/resolvers/index.js b/lib/new-admin/graphql/resolvers/index.js index a20d9216..ea3cb3fa 100644 --- a/lib/new-admin/graphql/resolvers/index.js +++ b/lib/new-admin/graphql/resolvers/index.js @@ -11,6 +11,7 @@ const funding = require('./funding.resolver') const log = require('./log.resolver') const loyalty = require('./loyalty.resolver') const machine = require('./machine.resolver') +const market = require('./market.resolver') const notification = require('./notification.resolver') const pairing = require('./pairing.resolver') const rates = require('./rates.resolver') @@ -35,6 +36,7 @@ const resolvers = [ log, loyalty, machine, + market, notification, pairing, rates, diff --git a/lib/new-admin/graphql/resolvers/market.resolver.js b/lib/new-admin/graphql/resolvers/market.resolver.js new file mode 100644 index 00000000..49864417 --- /dev/null +++ b/lib/new-admin/graphql/resolvers/market.resolver.js @@ -0,0 +1,9 @@ +const exchange = require('../../../exchange') + +const resolvers = { + Query: { + getMarkets: () => exchange.getMarkets() + } +} + +module.exports = resolvers diff --git a/lib/new-admin/graphql/types/blacklist.type.js b/lib/new-admin/graphql/types/blacklist.type.js index 3cd1bfa1..7cc34721 100644 --- a/lib/new-admin/graphql/types/blacklist.type.js +++ b/lib/new-admin/graphql/types/blacklist.type.js @@ -2,17 +2,26 @@ const { gql } = require('apollo-server-express') const typeDef = gql` type Blacklist { - cryptoCode: String! address: String! + blacklistMessage: BlacklistMessage! + } + + type BlacklistMessage { + id: ID + label: String + content: String + allowToggle: Boolean } type Query { blacklist: [Blacklist] @auth + blacklistMessages: [BlacklistMessage] @auth } type Mutation { - deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth - insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth + deleteBlacklistRow(address: String!): Blacklist @auth + insertBlacklistRow(address: String!): Blacklist @auth + editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth } ` diff --git a/lib/new-admin/graphql/types/index.js b/lib/new-admin/graphql/types/index.js index f4794b67..e33c50b5 100644 --- a/lib/new-admin/graphql/types/index.js +++ b/lib/new-admin/graphql/types/index.js @@ -11,6 +11,7 @@ const funding = require('./funding.type') const log = require('./log.type') const loyalty = require('./loyalty.type') const machine = require('./machine.type') +const market = require('./market.type') const notification = require('./notification.type') const pairing = require('./pairing.type') const rates = require('./rates.type') @@ -35,6 +36,7 @@ const types = [ log, loyalty, machine, + market, notification, pairing, rates, diff --git a/lib/new-admin/graphql/types/market.type.js b/lib/new-admin/graphql/types/market.type.js new file mode 100644 index 00000000..2413a9fe --- /dev/null +++ b/lib/new-admin/graphql/types/market.type.js @@ -0,0 +1,9 @@ +const { gql } = require('apollo-server-express') + +const typeDef = gql` + type Query { + getMarkets: JSONObject @auth + } +` + +module.exports = typeDef diff --git a/lib/new-admin/services/transactions.js b/lib/new-admin/services/transactions.js index 546ef0b3..8a838b64 100644 --- a/lib/new-admin/services/transactions.js +++ b/lib/new-admin/services/transactions.js @@ -50,6 +50,7 @@ function batch ( excludeTestingCustomers = false, simplified ) { + const isCsvExport = _.isBoolean(simplified) const packager = _.flow( _.flatten, _.orderBy(_.property('created'), ['desc']), @@ -92,7 +93,7 @@ function batch ( AND ($12 is null or txs.to_address = $12) AND ($13 is null or txs.txStatus = $13) ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} - AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0) + ${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'} ORDER BY created DESC limit $4 offset $5` const cashOutSql = `SELECT 'cashOut' AS tx_class, @@ -126,7 +127,7 @@ function batch ( AND ($13 is null or txs.txStatus = $13) AND ($14 is null or txs.swept = $14) ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} - AND (fiat > 0) + ${isCsvExport ? '' : 'AND fiat > 0'} ORDER BY created DESC limit $4 offset $5` // The swept filter is cash-out only, so omit the cash-in query entirely @@ -152,14 +153,14 @@ function batch ( return Promise.all(promises) .then(packager) - .then(res => { - if (simplified) return simplifiedBatch(res) + .then(res => + !isCsvExport ? res : // GQL transactions and transactionsCsv both use this function and // if we don't check for the correct simplified value, the Transactions page polling // will continuously build a csv in the background - else if (simplified === false) return advancedBatch(res) - return res - }) + simplified ? simplifiedBatch(res) : + advancedBatch(res) + ) } function advancedBatch (data) { diff --git a/lib/new-config-manager.js b/lib/new-config-manager.js index 680b57a9..239bfa20 100644 --- a/lib/new-config-manager.js +++ b/lib/new-config-manager.js @@ -13,7 +13,12 @@ const namespaces = { TERMS_CONDITIONS: 'termsConditions', CASH_OUT: 'cashOut', CASH_IN: 'cashIn', - COMPLIANCE: 'compliance' + COMPLIANCE: 'compliance', + MACHINE_SCREENS: 'machineScreens' +} + +const machineScreens = { + RATES: 'rates' } const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str) @@ -72,6 +77,8 @@ const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR) const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS) const getReceipt = fromNamespace(namespaces.RECEIPT) const getCompliance = fromNamespace(namespaces.COMPLIANCE) +const getMachineScreenOpts = (screenName, config) => _.compose(fromNamespace(screenName), fromNamespace(namespaces.MACHINE_SCREENS))(config) +const getAllMachineScreenOpts = config => _.reduce((acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), {}, _.values(machineScreens)) const getAllCryptoCurrencies = (config) => { const locale = fromNamespace(namespaces.LOCALE)(config) @@ -180,6 +187,8 @@ module.exports = { getWalletSettings, getCashInSettings, getOperatorInfo, + getMachineScreenOpts, + getAllMachineScreenOpts, getNotifications, getGlobalNotifications, getLocale, diff --git a/lib/new-settings-loader.js b/lib/new-settings-loader.js index 76cd120c..e571d1e2 100644 --- a/lib/new-settings-loader.js +++ b/lib/new-settings-loader.js @@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) { .then(_.compose(_.defaultTo({}), _.get('data.accounts'))) } +function hideSecretFields (accounts) { + return _.flow( + _.filter(path => !_.isEmpty(_.get(path, accounts))), + _.reduce( + (accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts), + accounts + ) + )(SECRET_FIELDS) +} + function showAccounts (schemaVersion) { return loadAccounts(schemaVersion) - .then(accounts => { - const filledSecretPaths = _.compact(_.map(path => { - if (!_.isEmpty(_.get(path, accounts))) { - return path - } - }, SECRET_FIELDS)) - return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts) - }) + .then(hideSecretFields) } const insertConfigRow = (dbOrTx, data) => diff --git a/lib/plugins.js b/lib/plugins.js index d5bfcb4f..9c9d1a29 100644 --- a/lib/plugins.js +++ b/lib/plugins.js @@ -278,6 +278,7 @@ function plugins (settings, deviceId) { const localeConfig = configManager.getLocale(deviceId, settings.config) const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies + const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) @@ -327,7 +328,8 @@ function plugins (settings, deviceId) { coins, configVersion, areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, - timezone + timezone, + screenOptions: machineScreenOpts } }) } @@ -475,25 +477,28 @@ function plugins (settings, deviceId) { function buyAndSell (rec, doBuy, tx) { const cryptoCode = rec.cryptoCode - const fiatCode = rec.fiatCode - const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() + return exchange.fetchExchange(settings, cryptoCode) + .then(_exchange => { + const fiatCode = _exchange.account.currencyMarket + const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() - const market = [fiatCode, cryptoCode].join('') + const market = [fiatCode, cryptoCode].join('') - if (!exchange.active(settings, cryptoCode)) return + if (!exchange.active(settings, cryptoCode)) return - const direction = doBuy ? 'cashIn' : 'cashOut' - const internalTxId = tx ? tx.id : rec.id - logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) - if (!tradesQueues[market]) tradesQueues[market] = [] - tradesQueues[market].push({ - direction, - internalTxId, - fiatCode, - cryptoAtoms, - cryptoCode, - timestamp: Date.now() - }) + const direction = doBuy ? 'cashIn' : 'cashOut' + const internalTxId = tx ? tx.id : rec.id + logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) + if (!tradesQueues[market]) tradesQueues[market] = [] + tradesQueues[market].push({ + direction, + internalTxId, + fiatCode, + cryptoAtoms, + cryptoCode, + timestamp: Date.now() + }) + }) } function consolidateTrades (cryptoCode, fiatCode) { @@ -550,19 +555,22 @@ function plugins (settings, deviceId) { const deviceIds = devices.map(device => device.deviceId) const lists = deviceIds.map(deviceId => { const localeConfig = configManager.getLocale(deviceId, settings.config) - const fiatCode = localeConfig.fiatCurrency const cryptoCodes = localeConfig.cryptoCurrencies - return cryptoCodes.map(cryptoCode => ({ - fiatCode, - cryptoCode + return Promise.all(cryptoCodes.map(cryptoCode => { + return exchange.fetchExchange(settings, cryptoCode) + .then(exchange => ({ + fiatCode: exchange.account.currencyMarket, + cryptoCode + })) })) }) - - const tradesPromises = _.uniq(_.flatten(lists)) - .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)) - - return Promise.all(tradesPromises) + + return Promise.all(lists) + }) + .then(lists => { + return Promise.all(_.uniq(_.flatten(lists)) + .map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode))) }) .catch(logger.error) } diff --git a/lib/plugins/common/ccxt.js b/lib/plugins/common/ccxt.js index 5312b35c..825c30b7 100644 --- a/lib/plugins/common/ccxt.js +++ b/lib/plugins/common/ccxt.js @@ -34,11 +34,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) { if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { throw new Error('Unsupported crypto: ' + cryptoCode) } - const fiatSupported = ALL[serviceName].FIAT - if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) { - logger.info('Building a market for an unsupported fiat. Defaulting to EUR market') - return cryptoCode + '/' + 'EUR' - } + + if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code') return cryptoCode + '/' + fiatCode } diff --git a/lib/plugins/exchange/binance.js b/lib/plugins/exchange/binance.js index e1b4a4c4..dc2a4c52 100644 --- a/lib/plugins/exchange/binance.js +++ b/lib/plugins/exchange/binance.js @@ -8,7 +8,7 @@ const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN] const FIAT = ['EUR'] const DEFAULT_FIAT_MARKET = 'EUR' -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { diff --git a/lib/plugins/exchange/binanceus.js b/lib/plugins/exchange/binanceus.js index ecf058b6..e8f0c371 100644 --- a/lib/plugins/exchange/binanceus.js +++ b/lib/plugins/exchange/binanceus.js @@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN] const FIAT = ['USD'] -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const DEFAULT_FIAT_MARKET = 'USD' +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -17,4 +18,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } diff --git a/lib/plugins/exchange/bitfinex.js b/lib/plugins/exchange/bitfinex.js index 4feccb0c..4e4d85ce 100644 --- a/lib/plugins/exchange/bitfinex.js +++ b/lib/plugins/exchange/bitfinex.js @@ -7,6 +7,7 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 8 const REQUIRED_CONFIG_FIELDS = ['key', 'secret'] @@ -18,4 +19,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/bitstamp.js b/lib/plugins/exchange/bitstamp.js index 5494ff1c..bd745d49 100644 --- a/lib/plugins/exchange/bitstamp.js +++ b/lib/plugins/exchange/bitstamp.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, ETH, LTC, BCH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 8 -const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId'] +const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -19,4 +20,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/ccxt.js b/lib/plugins/exchange/ccxt.js index 5de324f5..63b57fa9 100644 --- a/lib/plugins/exchange/ccxt.js +++ b/lib/plugins/exchange/ccxt.js @@ -1,9 +1,13 @@ const { utils: coinUtils } = require('@lamassu/coins') const _ = require('lodash/fp') const ccxt = require('ccxt') +const mem = require('mem') const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') const { ORDER_TYPES } = require('./consts') +const logger = require('../../logger') +const { currencies } = require('../../new-admin/config') +const T = require('../../time') const DEFAULT_PRICE_PRECISION = 2 const DEFAULT_AMOUNT_PRECISION = 8 @@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) { const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config') - const symbol = buildMarket(fiatCode, cryptoCode, exchangeName) + const selectedFiatMarket = account.currencyMarket + const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName) const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} @@ -50,4 +55,36 @@ function calculatePrice (side, amount, orderBook) { throw new Error('Insufficient market depth') } -module.exports = { trade } +function _getMarkets (exchangeName, availableCryptos) { + try { + const exchange = new ccxt[exchangeName]() + const cryptosToQuoteAgainst = ['USDT'] + const currencyCodes = _.concat(_.map(it => it.code, currencies), cryptosToQuoteAgainst) + + return exchange.fetchMarkets() + .then(_.filter(it => (it.type === 'spot' || it.spot))) + .then(res => + _.reduce((acc, value) => { + if (_.includes(value.base, availableCryptos) && _.includes(value.quote, currencyCodes)) { + if (value.quote === value.base) return acc + + if (_.isNil(acc[value.quote])) { + return { ...acc, [value.quote]: [value.base] } + } + + acc[value.quote].push(value.base) + } + return acc + }, {}, res) + ) + } catch (e) { + logger.debug(`No CCXT exchange found for ${exchangeName}`) + } +} + +const getMarkets = mem(_getMarkets, { + maxAge: T.week, + cacheKey: (exchangeName, availableCryptos) => exchangeName +}) + +module.exports = { trade, getMarkets } diff --git a/lib/plugins/exchange/cex.js b/lib/plugins/exchange/cex.js index 525eb427..b9687e15 100644 --- a/lib/plugins/exchange/cex.js +++ b/lib/plugins/exchange/cex.js @@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN] const FIAT = ['USD', 'EUR'] -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const DEFAULT_FIAT_MARKET = 'EUR' +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -17,4 +18,4 @@ const loadConfig = (account) => { return { ...mapped, timeout: 3000 } } -module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } +module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE } diff --git a/lib/plugins/exchange/itbit.js b/lib/plugins/exchange/itbit.js index 02572335..d80268e1 100644 --- a/lib/plugins/exchange/itbit.js +++ b/lib/plugins/exchange/itbit.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.LIMIT const { BTC, ETH, USDT, LN } = COINS const CRYPTO = [BTC, ETH, USDT, LN] const FIAT = ['USD'] +const DEFAULT_FIAT_MARKET = 'USD' const AMOUNT_PRECISION = 4 -const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId'] +const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket'] const loadConfig = (account) => { const mapper = { @@ -21,4 +22,4 @@ const loadConfig = (account) => { } const loadOptions = ({ walletId }) => ({ walletId }) -module.exports = { loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/plugins/exchange/kraken.js b/lib/plugins/exchange/kraken.js index 849af0e5..0f050ccf 100644 --- a/lib/plugins/exchange/kraken.js +++ b/lib/plugins/exchange/kraken.js @@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN] const FIAT = ['USD', 'EUR'] +const DEFAULT_FIAT_MARKET = 'EUR' const AMOUNT_PRECISION = 6 -const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey'] +const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket'] const USER_REF = 'userref' const loadConfig = (account) => { @@ -26,4 +27,4 @@ const loadConfig = (account) => { const loadOptions = () => ({ expiretm: '+60' }) -module.exports = { USER_REF, loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } +module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION } diff --git a/lib/routes.js b/lib/routes.js index e6654121..599c77c9 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -61,6 +61,15 @@ morgan.token('bytesRead', (_req, res) => res.bytesRead) morgan.token('bytesWritten', (_req, res) => res.bytesWritten) app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream })) +app.use('/robots.txt', (req, res) => { + res.type('text/plain') + res.send("User-agent: *\nDisallow: /") +}) + +app.get('/', (req, res) => { + res.sendStatus(404) +}) + // app /pair and /ca routes app.use('/', pairingRoutes) diff --git a/lib/routes/cashboxRoutes.js b/lib/routes/cashboxRoutes.js index cec84416..6fcc88bf 100644 --- a/lib/routes/cashboxRoutes.js +++ b/lib/routes/cashboxRoutes.js @@ -1,40 +1,41 @@ const express = require('express') +const _ = require('lodash/fp') const router = express.Router() const cashbox = require('../cashbox-batches') const notifier = require('../notifier') -const { getMachine, setMachine } = require('../machine-loader') +const { getMachine, setMachine, getMachineName } = require('../machine-loader') const { loadLatestConfig } = require('../new-settings-loader') const { getCashInSettings } = require('../new-config-manager') const { AUTOMATIC } = require('../constants') const logger = require('../logger') -function notifyCashboxRemoval (req, res, next) { + +function cashboxRemoval (req, res, next) { const operatorId = res.locals.operatorId - logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`) + notifier.cashboxNotify(req.deviceId).catch(logger.error) - return notifier.cashboxNotify(req.deviceId) - .then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()])) + return Promise.all([getMachine(req.deviceId), loadLatestConfig()]) .then(([machine, config]) => { - logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in') const cashInSettings = getCashInSettings(config) if (cashInSettings.cashboxReset !== AUTOMATIC) { - logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created') - logger.info(`** DEBUG ** - Cashbox removal - Process finished`) - return res.status(200).send({ status: 'OK' }) + return Promise.all([ + cashbox.getMachineUnbatchedBills(req.deviceId), + getMachineName(req.deviceId) + ]) } - logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created') - logger.info('** DEBUG ** - Cashbox removal - Creating new batch...') - return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox) - .then(() => { - logger.info(`** DEBUG ** - Cashbox removal - Process finished`) - return res.status(200).send({ status: 'OK' }) - }) + return cashbox.createCashboxBatch(req.deviceId, machine.cashbox) + .then(batch => Promise.all([ + cashbox.getBatchById(batch.id), + getMachineName(batch.device_id), + setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId) + ])) }) + .then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' })) .catch(next) } -router.post('/removal', notifyCashboxRemoval) +router.post('/removal', cashboxRemoval) module.exports = router diff --git a/migrations/1732874039534-market-currency.js b/migrations/1732874039534-market-currency.js new file mode 100644 index 00000000..359db4bd --- /dev/null +++ b/migrations/1732874039534-market-currency.js @@ -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() +} diff --git a/migrations/1732881489395-coin-agnostic-blacklist.js b/migrations/1732881489395-coin-agnostic-blacklist.js new file mode 100644 index 00000000..9a227308 --- /dev/null +++ b/migrations/1732881489395-coin-agnostic-blacklist.js @@ -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() +} diff --git a/migrations/1732881489396-advanced-blacklisting.js b/migrations/1732881489396-advanced-blacklisting.js new file mode 100644 index 00000000..4648f182 --- /dev/null +++ b/migrations/1732881489396-advanced-blacklisting.js @@ -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() +} diff --git a/migrations/1732881659436-rates-screen.js b/migrations/1732881659436-rates-screen.js new file mode 100644 index 00000000..94db1542 --- /dev/null +++ b/migrations/1732881659436-rates-screen.js @@ -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() +} diff --git a/new-lamassu-admin/public/robots.txt b/new-lamassu-admin/public/robots.txt new file mode 100644 index 00000000..77470cb3 --- /dev/null +++ b/new-lamassu-admin/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/new-lamassu-admin/src/components/Popper.js b/new-lamassu-admin/src/components/Popper.js index db01740d..a933e39d 100644 --- a/new-lamassu-admin/src/components/Popper.js +++ b/new-lamassu-admin/src/components/Popper.js @@ -105,20 +105,29 @@ const Popover = ({ const classes = useStyles() - const arrowClasses = { + const getArrowClasses = placement => ({ [classes.arrow]: true, - [classes.arrowBottom]: props.placement === 'bottom', - [classes.arrowTop]: props.placement === 'top', - [classes.arrowRight]: props.placement === 'right', - [classes.arrowLeft]: props.placement === 'left' + [classes.arrowBottom]: placement === 'bottom', + [classes.arrowTop]: placement === 'top', + [classes.arrowRight]: placement === 'right', + [classes.arrowLeft]: placement === 'left' + }) + + const flipPlacements = { + top: ['bottom'], + bottom: ['top'], + left: ['right'], + right: ['left'] } - const modifiers = R.merge(props.modifiers, { + const modifiers = R.mergeDeepLeft(props.modifiers, { flip: { - enabled: false + enabled: R.defaultTo(false, props.flip), + allowedAutoPlacements: flipPlacements[props.placement], + boundary: 'clippingParents' }, preventOverflow: { - enabled: true, + enabled: R.defaultTo(true, props.preventOverflow), boundariesElement: 'scrollParent' }, offset: { @@ -126,7 +135,7 @@ const Popover = ({ offset: '0, 10' }, arrow: { - enabled: true, + enabled: R.defaultTo(true, props.showArrow), element: arrowRef }, computeStyle: { @@ -134,6 +143,12 @@ const Popover = ({ } }) + if (props.preventOverflow === false) { + modifiers.hide = { + enabled: false + } + } + return ( <> - - - {children} - + {({ placement }) => ( + + + {children} + + )} ) diff --git a/new-lamassu-admin/src/components/buttons/IDButton.js b/new-lamassu-admin/src/components/buttons/IDButton.js index e6e9b55a..15c55602 100644 --- a/new-lamassu-admin/src/components/buttons/IDButton.js +++ b/new-lamassu-admin/src/components/buttons/IDButton.js @@ -56,7 +56,8 @@ const styles = { alignItems: 'center', borderRadius: 4, '& img': { - maxHeight: 145 + height: 145, + minWidth: 200 } } } @@ -127,7 +128,8 @@ const IDButton = memo( anchorEl={anchorEl} onClose={handleClose} arrowSize={3} - placement="top"> + placement="top" + flip>
{children}
diff --git a/new-lamassu-admin/src/components/inputs/base/Autocomplete.js b/new-lamassu-admin/src/components/inputs/base/Autocomplete.js index 996fb909..e5f9b941 100644 --- a/new-lamassu-admin/src/components/inputs/base/Autocomplete.js +++ b/new-lamassu-admin/src/components/inputs/base/Autocomplete.js @@ -1,8 +1,13 @@ +import { Box } from '@material-ui/core' import MAutocomplete from '@material-ui/lab/Autocomplete' import sort from 'match-sorter' import * as R from 'ramda' import React from 'react' +import { HoverableTooltip } from 'src/components/Tooltip' +import { P } from 'src/components/typography' +import { errorColor, orangeYellow, spring4 } from 'src/styling/variables' + import TextInput from './TextInput' const Autocomplete = ({ @@ -95,6 +100,39 @@ const Autocomplete = ({ /> ) }} + renderOption={props => { + if (!props.warning && !props.warningMessage) + return R.path([labelProp])(props) + + const warningColors = { + clean: spring4, + partial: orangeYellow, + important: errorColor + } + + const hoverableElement = ( + + ) + + return ( + + {R.path([labelProp])(props)} + +

{props.warningMessage}

+
+
+ ) + }} /> ) } diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js index dec75592..ebfea5c0 100644 --- a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js @@ -1,38 +1,31 @@ import { useQuery, useMutation } from '@apollo/react-hooks' -import { utils as coinUtils } from '@lamassu/coins' +import { addressDetector } from '@lamassu/coins' import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core' -import Grid from '@material-ui/core/Grid' import { makeStyles } from '@material-ui/core/styles' import gql from 'graphql-tag' import * as R from 'ramda' import React, { useState } from 'react' import { HelpTooltip } from 'src/components/Tooltip' -import { - Link, - Button, - IconButton, - SupportLinkButton -} from 'src/components/buttons' +import { Link, Button, IconButton } from 'src/components/buttons' import { Switch } from 'src/components/inputs' -import Sidebar from 'src/components/layout/Sidebar' import TitleSection from 'src/components/layout/TitleSection' -import { H4, H2, Label2, P, Info3, Info2 } from 'src/components/typography' +import { H2, Label2, P, Info3, Info2 } from 'src/components/typography' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' +import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg' +import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg' import { fromNamespace, toNamespace } from 'src/utils/config' import styles from './Blacklist.styles' +import BlackListAdvanced from './BlacklistAdvanced' import BlackListModal from './BlacklistModal' import BlacklistTable from './BlacklistTable' const useStyles = makeStyles(styles) -const groupByCode = R.groupBy(obj => obj.cryptoCode) - const DELETE_ROW = gql` - mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) { - deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) { - cryptoCode + mutation DeleteBlacklistRow($address: String!) { + deleteBlacklistRow(address: $address) { address } } @@ -41,7 +34,6 @@ const DELETE_ROW = gql` const GET_BLACKLIST = gql` query getBlacklistData { blacklist { - cryptoCode address } cryptoCurrencies { @@ -64,14 +56,32 @@ const GET_INFO = gql` ` const ADD_ROW = gql` - mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) { - insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) { - cryptoCode + mutation InsertBlacklistRow($address: String!) { + insertBlacklistRow(address: $address) { address } } ` +const GET_BLACKLIST_MESSAGES = gql` + query getBlacklistMessages { + blacklistMessages { + id + label + content + allowToggle + } + } +` + +const EDIT_BLACKLIST_MESSAGE = gql` + mutation editBlacklistMessage($id: ID, $content: String) { + editBlacklistMessage(id: $id, content: $content) { + id + } + } +` + const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => { const classes = useStyles() @@ -117,14 +127,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => { const Blacklist = () => { const { data: blacklistResponse } = useQuery(GET_BLACKLIST) const { data: configData } = useQuery(GET_INFO) + const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES) const [showModal, setShowModal] = useState(false) - const [clickedItem, setClickedItem] = useState({ - code: 'BTC', - display: 'Bitcoin' - }) const [errorMsg, setErrorMsg] = useState(null) + const [editMessageError, setEditMessageError] = useState(null) const [deleteDialog, setDeleteDialog] = useState(false) const [confirmDialog, setConfirmDialog] = useState(false) + const [advancedSettings, setAdvancedSettings] = useState(false) const [deleteEntry] = useMutation(DELETE_ROW, { onError: ({ message }) => { @@ -144,14 +153,14 @@ const Blacklist = () => { refetchQueries: () => ['getData'] }) + const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, { + onError: e => setEditMessageError(e), + refetchQueries: () => ['getBlacklistData'] + }) + const classes = useStyles() const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? [] - const availableCurrencies = R.filter( - coin => coinUtils.getEquivalentCode(coin.code) === coin.code - )(R.path(['cryptoCurrencies'], blacklistResponse) ?? []) - - const formattedData = groupByCode(blacklistData) const complianceConfig = configData?.config && fromNamespace('compliance')(configData.config) @@ -165,12 +174,8 @@ const Blacklist = () => { return saveConfig({ variables: { config } }) } - const onClickSidebarItem = e => { - setClickedItem({ code: e.code, display: e.display }) - } - - const handleDeleteEntry = (cryptoCode, address) => { - deleteEntry({ variables: { cryptoCode, address } }) + const handleDeleteEntry = address => { + deleteEntry({ variables: { address } }) } const handleConfirmDialog = confirm => { @@ -180,21 +185,21 @@ const Blacklist = () => { setConfirmDialog(false) } - const validateAddress = (cryptoCode, address) => { + const validateAddress = address => { try { - return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address)) + return !R.isEmpty(addressDetector.detectAddress(address).matches) } catch { return false } } - const addToBlacklist = async (cryptoCode, address) => { + const addToBlacklist = async address => { setErrorMsg(null) - if (!validateAddress(cryptoCode, address)) { + if (!validateAddress(address)) { setErrorMsg('Invalid address') return } - const res = await addEntry({ variables: { cryptoCode, address } }) + const res = await addEntry({ variables: { address } }) if (!res.errors) { return setShowModal(false) } @@ -208,6 +213,15 @@ const Blacklist = () => { } } + const editBlacklistMessage = r => { + editMessage({ + variables: { + id: r.id, + content: r.content + } + }) + } + return ( <> { setConfirmDialog(false) }} /> - - - setShowModal(true)}> - Blacklist new addresses - - - - - it.display} - onClick={onClickSidebarItem} - /> -
- -

- {clickedItem.display - ? `${clickedItem.display} blacklisted addresses` - : ''}{' '} -

+ + {!advancedSettings && ( + + mr="15px">

Enable paper wallet (only)

{ display="flex" alignItems="center" justifyContent="flex-end" - mr="-5px"> + mr="15px">

Reject reused addresses

{ {rejectAddressReuse ? 'On' : 'Off'}

- The "Reject reused addresses" option means that all addresses - that are used once will be automatically rejected if there's - an attempt to use them again on a new transaction. + This option requires a user to scan a fresh wallet address if + they attempt to scan one that had been previously used for a + transaction in your network.

-

- For details please read the relevant knowledgebase article: -

-
+ setShowModal(true)}> + Blacklist new addresses +
+ )} +
+ {!advancedSettings && ( +
{ setDeleteDialog={setDeleteDialog} />
- + )} + {advancedSettings && ( + refetch()} + /> + )} {showModal && ( { @@ -313,7 +324,6 @@ const Blacklist = () => { setShowModal(false) }} errorMsg={errorMsg} - selectedCoin={clickedItem} addToBlacklist={addToBlacklist} /> )} diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js index 9c44bb1d..3cfc09ec 100644 --- a/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js @@ -7,11 +7,23 @@ const styles = { content: { display: 'flex', flexDirection: 'column', - flex: 1, - marginLeft: spacer * 6 + flex: 1 + }, + advancedForm: { + '& > *': { + marginTop: 20 + }, + display: 'flex', + flexDirection: 'column', + height: '100%' }, footer: { - margin: [['auto', 0, spacer * 3, 'auto']] + display: 'flex', + flexDirection: 'row', + margin: [['auto', 0, spacer * 3, 0]] + }, + submit: { + margin: [['auto', 0, 0, 'auto']] }, modalTitle: { margin: [['auto', 0, 8.5, 'auto']] @@ -54,6 +66,9 @@ const styles = { cancelButton: { marginRight: 8, padding: 0 + }, + resetToDefault: { + width: 145 } } diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js new file mode 100644 index 00000000..f3790959 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js @@ -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 => ( + setSelectedMessage(it)}> + + + ) + }, + { + name: 'deleteButton', + header: 'Delete', + width: 130, + textAlign: 'center', + size: 'sm', + view: it => ( + + {R.path(['allowToggle'], it) ? ( + + ) : ( + + )} + + ) + } + ] + + 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 ( + <> + + {selectedMessage && ( + + + handleSubmit({ id: selectedMessage.id, ...values }) + }> + {({ errors, touched, setFieldValue }) => ( +
+ setFieldValue('content', DEFAULT_MESSAGE)}> + Reset to default + + +
+ {getErrorMsg(errors, touched, mutationError) && ( + + {getErrorMsg(errors, touched, mutationError)} + + )} + +
+ + )} +
+
+ )} + + ) +} + +export default BlacklistAdvanced diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js index 4c13881b..5e9eb95a 100644 --- a/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js @@ -14,31 +14,14 @@ import { H3 } from 'src/components/typography' import styles from './Blacklist.styles' const useStyles = makeStyles(styles) -const BlackListModal = ({ - onClose, - selectedCoin, - addToBlacklist, - errorMsg -}) => { +const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => { const classes = useStyles() const handleAddToBlacklist = address => { - if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) { - address = 'bitcoincash:' + address - } - addToBlacklist(selectedCoin.code, address) - } - const placeholderAddress = { - BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD', - ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F', - LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa', - DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn', - ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR', - BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm', - USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949', - XMR: - '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H' + addToBlacklist(address) } + const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD' + return (
-

- {selectedCoin.display - ? `Blacklist ${R.toLower(selectedCoin.display)} address` - : ''} -

+

Blacklist new address

- {!R.isNil(errorMsg) && ( - {errorMsg} - )}
- + {!R.isNil(errorMsg) && {errorMsg}} + Blacklist address diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js index 9ce480a3..9e929a0c 100644 --- a/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js @@ -5,7 +5,6 @@ import React, { useState } from 'react' import { DeleteDialog } from 'src/components/DeleteDialog' import { IconButton } from 'src/components/buttons' import DataTable from 'src/components/tables/DataTable' -import { Label1 } from 'src/components/typography' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' @@ -15,7 +14,6 @@ const useStyles = makeStyles(styles) const BlacklistTable = ({ data, - selectedCoin, handleDeleteEntry, errorMessage, setErrorMessage, @@ -29,8 +27,8 @@ const BlacklistTable = ({ const elements = [ { name: 'address', - header: {'Addresses'}, - width: 800, + header: 'Address', + width: 1070, textAlign: 'left', size: 'sm', view: it => ( @@ -41,7 +39,7 @@ const BlacklistTable = ({ }, { name: 'deleteButton', - header: {'Delete'}, + header: 'Delete', width: 130, textAlign: 'center', size: 'sm', @@ -57,14 +55,11 @@ const BlacklistTable = ({ ) } ] - const dataToShow = selectedCoin - ? data[selectedCoin.code] - : data[R.keys(data)[0]] return ( <> { setErrorMessage(null) - handleDeleteEntry( - R.path(['cryptoCode'], toBeDeleted), - R.path(['address'], toBeDeleted) - ) + handleDeleteEntry(R.path(['address'], toBeDeleted)) }} errorMessage={errorMessage} /> diff --git a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js index 89479a02..503e1e39 100644 --- a/new-lamassu-admin/src/pages/Customers/components/EditableCard.js +++ b/new-lamassu-admin/src/pages/Customers/components/EditableCard.js @@ -330,7 +330,7 @@ const EditableCard = ({ {editing && (
- {hasImage && ( + {hasImage && state !== OVERRIDE_PENDING && ( { ] return ( - +
+ +
) } diff --git a/new-lamassu-admin/src/pages/OperatorInfo/MachineScreens.js b/new-lamassu-admin/src/pages/OperatorInfo/MachineScreens.js new file mode 100644 index 00000000..c3ca3b53 --- /dev/null +++ b/new-lamassu-admin/src/pages/OperatorInfo/MachineScreens.js @@ -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 ( + <> +
+

Rates screen

+
+
+

Enable rates screen

+
+ + saveConfig({ + variables: { + config: R.compose( + toNamespace(namespaces.MACHINE_SCREENS), + toNamespace('rates') + )( + R.merge(ratesScreenConfig, { + active: event.target.checked + }) + ) + } + }) + } + /> + {ratesScreenConfig.active ? 'Yes' : 'No'} +
+
+ + ) +}) + +export default MachineScreens diff --git a/new-lamassu-admin/src/pages/Services/Services.js b/new-lamassu-admin/src/pages/Services/Services.js index 72eab97b..c1d5b408 100644 --- a/new-lamassu-admin/src/pages/Services/Services.js +++ b/new-lamassu-admin/src/pages/Services/Services.js @@ -12,7 +12,7 @@ import SingleRowTable from 'src/components/single-row-table/SingleRowTable' import { formatLong } from 'src/utils/string' import FormRenderer from './FormRenderer' -import schemas from './schemas' +import _schemas from './schemas' const GET_INFO = gql` query getData { @@ -21,6 +21,12 @@ const GET_INFO = gql` } ` +const GET_MARKETS = gql` + query getMarkets { + getMarkets + } +` + const SAVE_ACCOUNT = gql` mutation Save($accounts: JSONObject) { saveAccounts(accounts: $accounts) @@ -40,12 +46,17 @@ const useStyles = makeStyles(styles) const Services = () => { const [editingSchema, setEditingSchema] = useState(null) - const { data } = useQuery(GET_INFO) + const { data, loading: configLoading } = useQuery(GET_INFO) + const { data: marketsData, loading: marketsLoading } = useQuery(GET_MARKETS) const [saveAccount] = useMutation(SAVE_ACCOUNT, { onCompleted: () => setEditingSchema(null), refetchQueries: ['getData'] }) + const markets = marketsData?.getMarkets + + const schemas = _schemas(markets) + const classes = useStyles() const accounts = data?.accounts ?? {} @@ -101,40 +112,44 @@ const Services = () => { const getValidationSchema = ({ code, getValidationSchema }) => getValidationSchema(accounts[code]) + const loading = marketsLoading || configLoading + return ( -
- - - {R.values(schemas).map(schema => ( - - setEditingSchema(schema)} - items={getItems(schema.code, schema.elements)} + !loading && ( +
+ + + {R.values(schemas).map(schema => ( + + setEditingSchema(schema)} + items={getItems(schema.code, schema.elements)} + /> + + ))} + + {editingSchema && ( + setEditingSchema(null)} + open={true}> + + saveAccount({ + variables: { accounts: { [editingSchema.code]: it } } + }) + } + elements={getElements(editingSchema)} + validationSchema={getValidationSchema(editingSchema)} + value={getAccounts(editingSchema)} /> - - ))} - - {editingSchema && ( - setEditingSchema(null)} - open={true}> - - saveAccount({ - variables: { accounts: { [editingSchema.code]: it } } - }) - } - elements={getElements(editingSchema)} - validationSchema={getValidationSchema(editingSchema)} - value={getAccounts(editingSchema)} - /> - - )} -
+ + )} +
+ ) ) } diff --git a/new-lamassu-admin/src/pages/Services/schemas/binance.js b/new-lamassu-admin/src/pages/Services/schemas/binance.js index 6be4be26..faec0e35 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/binance.js +++ b/new-lamassu-admin/src/pages/Services/schemas/binance.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'binance', - name: 'Binance', - title: 'Binance (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'binance', + name: 'Binance', + title: 'Binance (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/binanceus.js b/new-lamassu-admin/src/pages/Services/schemas/binanceus.js index 7afd724b..74795e24 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/binanceus.js +++ b/new-lamassu-admin/src/pages/Services/schemas/binanceus.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'binanceus', - name: 'Binance.us', - title: 'Binance.us (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'binanceus', + name: 'Binance.us', + title: 'Binance.us (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js b/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js index 0609807a..c0485af1 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js +++ b/new-lamassu-admin/src/pages/Services/schemas/bitfinex.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'bitfinex', - name: 'Bitfinex', - title: 'Bitfinex (Exchange)', - elements: [ - { - code: 'key', - display: 'API Key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'secret', - display: 'API Secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'bitfinex', + name: 'Bitfinex', + title: 'Bitfinex (Exchange)', + elements: [ + { + code: 'key', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'secret', + display: 'API secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency Market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + key: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + secret: Yup.string('The API secret must be a string') + .max(100, 'The API secret is too long') + .test(secretTest(account?.secret, 'API secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - key: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - secret: Yup.string('The API secret must be a string') - .max(100, 'The API secret is too long') - .test(secretTest(account?.secret, 'API secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js b/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js index 431fcfb5..e9061e9e 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js +++ b/new-lamassu-admin/src/pages/Services/schemas/bitstamp.js @@ -1,46 +1,67 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'bitstamp', - name: 'Bitstamp', - title: 'Bitstamp (Exchange)', - elements: [ - { - code: 'clientId', - display: 'Client ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'key', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'secret', - display: 'API secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'bitstamp', + name: 'Bitstamp', + title: 'Bitstamp (Exchange)', + elements: [ + { + code: 'clientId', + display: 'Client ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'key', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'secret', + display: 'API secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + clientId: Yup.string('The client ID must be a string') + .max(100, 'The client ID is too long') + .required('The client ID is required'), + key: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + secret: Yup.string('The API secret must be a string') + .max(100, 'The API secret is too long') + .test(secretTest(account?.secret, 'API secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - clientId: Yup.string('The client ID must be a string') - .max(100, 'The client ID is too long') - .required('The client ID is required'), - key: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - secret: Yup.string('The API secret must be a string') - .max(100, 'The API secret is too long') - .test(secretTest(account?.secret, 'API secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/cex.js b/new-lamassu-admin/src/pages/Services/schemas/cex.js index f8374c6f..b887db93 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/cex.js +++ b/new-lamassu-admin/src/pages/Services/schemas/cex.js @@ -1,46 +1,67 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'cex', - name: 'CEX.IO', - title: 'CEX.IO (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'uid', - display: 'User ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'cex', + name: 'CEX.IO', + title: 'CEX.IO (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'uid', + display: 'User ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency Market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + uid: Yup.string('The User ID must be a string') + .max(100, 'The User ID is too long') + .required('The User ID is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - uid: Yup.string('The User ID must be a string') - .max(100, 'The User ID is too long') - .required('The User ID is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/helper.js b/new-lamassu-admin/src/pages/Services/schemas/helper.js index ccb49a79..c1c97870 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/helper.js +++ b/new-lamassu-admin/src/pages/Services/schemas/helper.js @@ -1,5 +1,12 @@ +import { ALL_CRYPTOS } from '@lamassu/coins' import * as R from 'ramda' +const WARNING_LEVELS = { + CLEAN: 'clean', + PARTIAL: 'partial', + IMPORTANT: 'important' +} + const secretTest = (secret, message) => ({ name: 'secret-test', message: message ? `The ${message} is invalid` : 'Invalid field', @@ -21,4 +28,35 @@ const leadingZerosTest = (value, context) => { return true } -export { secretTest, leadingZerosTest } +const buildCurrencyOptions = markets => { + return R.map(it => { + const unavailableCryptos = R.difference(ALL_CRYPTOS, markets[it]) + const unavailableCryptosFiltered = R.difference(unavailableCryptos, [it]) // As the markets can have stablecoins to trade against other crypto, filter them out, as there can't be pairs such as USDT/USDT + + const unavailableMarketsStr = + R.length(unavailableCryptosFiltered) > 1 + ? `${R.join( + ', ', + R.slice(0, -1, unavailableCryptosFiltered) + )} and ${R.last(unavailableCryptosFiltered)}` + : unavailableCryptosFiltered[0] + + const warningLevel = R.isEmpty(unavailableCryptosFiltered) + ? WARNING_LEVELS.CLEAN + : !R.isEmpty(unavailableCryptosFiltered) && + R.length(unavailableCryptosFiltered) < R.length(ALL_CRYPTOS) + ? WARNING_LEVELS.PARTIAL + : WARNING_LEVELS.IMPORTANT + + return { + code: R.toUpper(it), + display: R.toUpper(it), + warning: warningLevel, + warningMessage: !R.isEmpty(unavailableCryptosFiltered) + ? `No market pairs available for ${unavailableMarketsStr}` + : `All market pairs are available` + } + }, R.keys(markets)) +} + +export { secretTest, leadingZerosTest, buildCurrencyOptions } diff --git a/new-lamassu-admin/src/pages/Services/schemas/index.js b/new-lamassu-admin/src/pages/Services/schemas/index.js index 22368537..e952771b 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/index.js +++ b/new-lamassu-admin/src/pages/Services/schemas/index.js @@ -1,16 +1,16 @@ -import binance from './binance' -import binanceus from './binanceus' -import bitfinex from './bitfinex' +import _binance from './binance' +import _binanceus from './binanceus' +import _bitfinex from './bitfinex' import bitgo from './bitgo' -import bitstamp from './bitstamp' +import _bitstamp from './bitstamp' import blockcypher from './blockcypher' -import cex from './cex' +import _cex from './cex' import elliptic from './elliptic' import galoy from './galoy' import inforu from './inforu' import infura from './infura' -import itbit from './itbit' -import kraken from './kraken' +import _itbit from './itbit' +import _kraken from './kraken' import mailgun from './mailgun' import scorechain from './scorechain' import sumsub from './sumsub' @@ -19,25 +19,37 @@ import trongrid from './trongrid' import twilio from './twilio' import vonage from './vonage' -export default { - [bitgo.code]: bitgo, - [galoy.code]: galoy, - [bitstamp.code]: bitstamp, - [blockcypher.code]: blockcypher, - [elliptic.code]: elliptic, - [inforu.code]: inforu, - [infura.code]: infura, - [itbit.code]: itbit, - [kraken.code]: kraken, - [mailgun.code]: mailgun, - [telnyx.code]: telnyx, - [vonage.code]: vonage, - [twilio.code]: twilio, - [binanceus.code]: binanceus, - [cex.code]: cex, - [scorechain.code]: scorechain, - [trongrid.code]: trongrid, - [binance.code]: binance, - [bitfinex.code]: bitfinex, - [sumsub.code]: sumsub +const schemas = (markets = {}) => { + const binance = _binance(markets?.binance) + const bitfinex = _bitfinex(markets?.bitfinex) + const binanceus = _binanceus(markets?.binanceus) + const bitstamp = _bitstamp(markets?.bitstamp) + const cex = _cex(markets?.cex) + const itbit = _itbit(markets?.itbit) + const kraken = _kraken(markets?.kraken) + + return { + [bitgo.code]: bitgo, + [galoy.code]: galoy, + [bitstamp.code]: bitstamp, + [blockcypher.code]: blockcypher, + [elliptic.code]: elliptic, + [inforu.code]: inforu, + [infura.code]: infura, + [itbit.code]: itbit, + [kraken.code]: kraken, + [mailgun.code]: mailgun, + [telnyx.code]: telnyx, + [vonage.code]: vonage, + [twilio.code]: twilio, + [binanceus.code]: binanceus, + [cex.code]: cex, + [scorechain.code]: scorechain, + [trongrid.code]: trongrid, + [binance.code]: binance, + [bitfinex.code]: bitfinex, + [sumsub.code]: sumsub + } } + +export default schemas diff --git a/new-lamassu-admin/src/pages/Services/schemas/itbit.js b/new-lamassu-admin/src/pages/Services/schemas/itbit.js index 949ba692..d6607461 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/itbit.js +++ b/new-lamassu-admin/src/pages/Services/schemas/itbit.js @@ -1,54 +1,75 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { buildCurrencyOptions, secretTest } from './helper' -export default { - code: 'itbit', - name: 'itBit', - title: 'itBit (Exchange)', - elements: [ - { - code: 'userId', - display: 'User ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'walletId', - display: 'Wallet ID', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'clientKey', - display: 'Client key', - component: TextInputFormik - }, - { - code: 'clientSecret', - display: 'Client secret', - component: SecretInputFormik +const schema = markets => { + return { + code: 'itbit', + name: 'itBit', + title: 'itBit (Exchange)', + elements: [ + { + code: 'userId', + display: 'User ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'walletId', + display: 'Wallet ID', + component: TextInput, + face: true, + long: true + }, + { + code: 'clientKey', + display: 'Client key', + component: TextInput + }, + { + code: 'clientSecret', + display: 'Client secret', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + userId: Yup.string('The user ID must be a string') + .max(100, 'The user ID is too long') + .required('The user ID is required'), + walletId: Yup.string('The wallet ID must be a string') + .max(100, 'The wallet ID is too long') + .required('The wallet ID is required'), + clientKey: Yup.string('The client key must be a string') + .max(100, 'The client key is too long') + .required('The client key is required'), + clientSecret: Yup.string('The client secret must be a string') + .max(100, 'The client secret is too long') + .test(secretTest(account?.clientSecret, 'client secret')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - userId: Yup.string('The user ID must be a string') - .max(100, 'The user ID is too long') - .required('The user ID is required'), - walletId: Yup.string('The wallet ID must be a string') - .max(100, 'The wallet ID is too long') - .required('The wallet ID is required'), - clientKey: Yup.string('The client key must be a string') - .max(100, 'The client key is too long') - .required('The client key is required'), - clientSecret: Yup.string('The client secret must be a string') - .max(100, 'The client secret is too long') - .test(secretTest(account?.clientSecret, 'client secret')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Services/schemas/kraken.js b/new-lamassu-admin/src/pages/Services/schemas/kraken.js index 733cebe4..2c0ee271 100644 --- a/new-lamassu-admin/src/pages/Services/schemas/kraken.js +++ b/new-lamassu-admin/src/pages/Services/schemas/kraken.js @@ -1,36 +1,57 @@ import * as Yup from 'yup' -import SecretInputFormik from 'src/components/inputs/formik/SecretInput' -import TextInputFormik from 'src/components/inputs/formik/TextInput' +import { + SecretInput, + TextInput, + Autocomplete +} from 'src/components/inputs/formik' -import { secretTest } from './helper' +import { secretTest, buildCurrencyOptions } from './helper' -export default { - code: 'kraken', - name: 'Kraken', - title: 'Kraken (Exchange)', - elements: [ - { - code: 'apiKey', - display: 'API key', - component: TextInputFormik, - face: true, - long: true - }, - { - code: 'privateKey', - display: 'Private key', - component: SecretInputFormik +const schema = markets => { + return { + code: 'kraken', + name: 'Kraken', + title: 'Kraken (Exchange)', + elements: [ + { + code: 'apiKey', + display: 'API key', + component: TextInput, + face: true, + long: true + }, + { + code: 'privateKey', + display: 'Private key', + component: SecretInput + }, + { + code: 'currencyMarket', + display: 'Currency market', + component: Autocomplete, + inputProps: { + options: buildCurrencyOptions(markets), + labelProp: 'display', + valueProp: 'code' + }, + face: true + } + ], + getValidationSchema: account => { + return Yup.object().shape({ + apiKey: Yup.string('The API key must be a string') + .max(100, 'The API key is too long') + .required('The API key is required'), + privateKey: Yup.string('The private key must be a string') + .max(100, 'The private key is too long') + .test(secretTest(account?.privateKey, 'private key')), + currencyMarket: Yup.string( + 'The currency market must be a string' + ).required('The currency market is required') + }) } - ], - getValidationSchema: account => { - return Yup.object().shape({ - apiKey: Yup.string('The API key must be a string') - .max(100, 'The API key is too long') - .required('The API key is required'), - privateKey: Yup.string('The private key must be a string') - .max(100, 'The private key is too long') - .test(secretTest(account?.privateKey, 'private key')) - }) } } + +export default schema diff --git a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js index 364a56b5..4e65014d 100644 --- a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js +++ b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js @@ -131,8 +131,9 @@ const DetailsRow = ({ it: tx, timezone }) => { ) const commission = BigNumber(tx.profit).toFixed(2, 1) // ROUND_DOWN - const commissionPercentage = + const commissionPercentage = BigNumber( Number.parseFloat(tx.commissionPercentage, 2) * 100 + ).toFixed(2, 1) // ROUND_DOWN const fixedFee = Number.parseFloat(tx.fixedFee) || 0 const fiat = BigNumber(tx.fiat) .minus(fixedFee) diff --git a/new-lamassu-admin/src/pages/Wallet/Wallet.js b/new-lamassu-admin/src/pages/Wallet/Wallet.js index eaf1148e..67b02bba 100644 --- a/new-lamassu-admin/src/pages/Wallet/Wallet.js +++ b/new-lamassu-admin/src/pages/Wallet/Wallet.js @@ -10,7 +10,7 @@ import { SupportLinkButton } from 'src/components/buttons' import { NamespacedTable as EditableTable } from 'src/components/editableTable' import TitleSection from 'src/components/layout/TitleSection' import FormRenderer from 'src/pages/Services/FormRenderer' -import schemas from 'src/pages/Services/schemas' +import _schemas from 'src/pages/Services/schemas' import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg' import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg' import { fromNamespace, toNamespace } from 'src/utils/config' @@ -54,6 +54,12 @@ const GET_INFO = gql` } ` +const GET_MARKETS = gql` + query getMarkets { + getMarkets + } +` + const LOCALE = 'locale' const useStyles = makeStyles(styles) @@ -71,6 +77,8 @@ const Wallet = ({ name: SCREEN_KEY }) => { refetchQueries: () => ['getData'] }) + const { data: marketsData } = useQuery(GET_MARKETS) + const [saveAccount] = useMutation(SAVE_ACCOUNT, { onCompleted: () => setEditingSchema(null), refetchQueries: () => ['getData'] @@ -89,6 +97,10 @@ const Wallet = ({ name: SCREEN_KEY }) => { const cryptoCurrencies = data?.cryptoCurrencies ?? [] const accounts = data?.accounts ?? [] + const markets = marketsData?.getMarkets + + const schemas = _schemas(markets) + const onChange = (previous, current, setValue) => { if (!current) return setValue(current) diff --git a/new-lamassu-admin/src/routing/lamassu.routes.js b/new-lamassu-admin/src/routing/lamassu.routes.js index ec5d5325..59049b60 100644 --- a/new-lamassu-admin/src/routing/lamassu.routes.js +++ b/new-lamassu-admin/src/routing/lamassu.routes.js @@ -16,6 +16,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus' import Notifications from 'src/pages/Notifications/Notifications' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' +import MachineScreens from 'src/pages/OperatorInfo/MachineScreens' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' @@ -193,6 +194,13 @@ const getLamassuRoutes = () => [ route: '/settings/operator-info/terms-conditions', allowedRoles: [ROLES.USER, ROLES.SUPERUSER], component: TermsConditions + }, + { + key: 'machine-screens', + label: 'Machine screens', + route: '/settings/operator-info/machine-screens', + allowedRoles: [ROLES.USER, ROLES.SUPERUSER], + component: MachineScreens } ] } diff --git a/new-lamassu-admin/src/routing/pazuz.routes.js b/new-lamassu-admin/src/routing/pazuz.routes.js index 8551c123..53d22d9d 100644 --- a/new-lamassu-admin/src/routing/pazuz.routes.js +++ b/new-lamassu-admin/src/routing/pazuz.routes.js @@ -18,6 +18,7 @@ import MachineStatus from 'src/pages/Maintenance/MachineStatus' import Notifications from 'src/pages/Notifications/Notifications' import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar' import ContactInfo from 'src/pages/OperatorInfo/ContactInfo' +import MachineScreens from 'src/pages/OperatorInfo/MachineScreens' import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting' import SMSNotices from 'src/pages/OperatorInfo/SMSNotices/SMSNotices' import TermsConditions from 'src/pages/OperatorInfo/TermsConditions' @@ -172,6 +173,13 @@ const getPazuzRoutes = () => [ route: '/settings/operator-info/terms-conditions', allowedRoles: [ROLES.USER, ROLES.SUPERUSER], component: TermsConditions + }, + { + key: 'machine-screens', + label: 'Machine screens', + route: '/settings/operator-info/machine-screens', + allowedRoles: [ROLES.USER, ROLES.SUPERUSER], + component: MachineScreens } ] } diff --git a/new-lamassu-admin/src/styling/variables.js b/new-lamassu-admin/src/styling/variables.js index 2cb84f7f..63289223 100644 --- a/new-lamassu-admin/src/styling/variables.js +++ b/new-lamassu-admin/src/styling/variables.js @@ -32,6 +32,9 @@ const mistyRose = '#ffeceb' const pumpkin = '#ff7311' const linen = '#fbf3ec' +// Warning +const orangeYellow = '#ffcc00' + // Color Variables const primaryColor = zodiac @@ -136,6 +139,7 @@ export { java, neon, linen, + orangeYellow, // named colors primaryColor, secondaryColor, diff --git a/new-lamassu-admin/src/utils/config.js b/new-lamassu-admin/src/utils/config.js index a2f78582..0c282719 100644 --- a/new-lamassu-admin/src/utils/config.js +++ b/new-lamassu-admin/src/utils/config.js @@ -12,7 +12,8 @@ const namespaces = { RECEIPT: 'receipt', COIN_ATM_RADAR: 'coinAtmRadar', TERMS_CONDITIONS: 'termsConditions', - TRIGGERS: 'triggersConfig' + TRIGGERS: 'triggersConfig', + MACHINE_SCREENS: 'machineScreens' } const mapKeys = R.curry((fn, obj) =>