Merge branch 'dev' into releases/v10.1
This commit is contained in:
commit
320e76fdae
393 changed files with 11183 additions and 31834 deletions
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodejs 14
|
||||
|
|
@ -25,8 +25,8 @@ EXPOSE 3000
|
|||
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
|
||||
|
||||
|
||||
FROM alpine:3.14 AS build-ui
|
||||
RUN apk add --no-cache nodejs npm git curl build-base python3
|
||||
FROM node:22-alpine AS build-ui
|
||||
RUN apk add --no-cache npm git curl build-base python3
|
||||
|
||||
COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +1,61 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const { addressDetector } = require('@lamassu/coins')
|
||||
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 isValidAddress = address => {
|
||||
try {
|
||||
return !_.isEmpty(addressDetector.getSupportedCoinsForAddress(address).matches)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const insertIntoBlacklist = (cryptoCode, address) => {
|
||||
const insertIntoBlacklist = address => {
|
||||
if (!isValidAddress(address)) {
|
||||
return Promise.reject(new Error('Invalid address'))
|
||||
}
|
||||
return db
|
||||
.none(
|
||||
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);',
|
||||
[cryptoCode, address]
|
||||
'INSERT INTO blacklist (address) VALUES ($1);',
|
||||
[address]
|
||||
)
|
||||
}
|
||||
|
||||
function blocked (address, cryptoCode) {
|
||||
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2`
|
||||
return db.any(sql, [address, cryptoCode])
|
||||
function blocked (address) {
|
||||
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
|
||||
return db.oneOrNone(sql, [address])
|
||||
}
|
||||
|
||||
function addToUsedAddresses (address, cryptoCode) {
|
||||
// ETH reuses addresses
|
||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||
function getMessages () {
|
||||
const sql = `SELECT * FROM blacklist_messages`
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)`
|
||||
return db.oneOrNone(sql, [cryptoCode, address])
|
||||
function editBlacklistMessage (id, content) {
|
||||
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
||||
return db.oneOrNone(sql, [content, id])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
blocked,
|
||||
addToUsedAddresses,
|
||||
getBlacklist,
|
||||
deleteFromBlacklist,
|
||||
insertIntoBlacklist
|
||||
insertIntoBlacklist,
|
||||
getMessages,
|
||||
editBlacklistMessage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Bitcoin Core. This may take a minute...')
|
||||
!isDevMode() && common.es(`sudo supervisorctl stop bitcoin`)
|
||||
common.es(`curl -#o /tmp/bitcoin.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/bitcoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Bitcoin Core: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/bitcoin.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
@ -55,6 +59,20 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.es(`echo "\nlistenonion=0" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (common.es(`grep "fallbackfee=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||
common.logger.info(`fallbackfee already defined, skipping...`)
|
||||
} else {
|
||||
common.logger.info(`Setting 'fallbackfee=0.00005' in config file...`)
|
||||
common.es(`echo "\nfallbackfee=0.00005" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (common.es(`grep "rpcworkqueue=" ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf || true`)) {
|
||||
common.logger.info(`rpcworkqueue already defined, skipping...`)
|
||||
} else {
|
||||
common.logger.info(`Setting 'rpcworkqueue=2000' in config file...`)
|
||||
common.es(`echo "\nrpcworkqueue=2000" >> ${BLOCKCHAIN_DIR}/bitcoin/bitcoin.conf`)
|
||||
}
|
||||
|
||||
if (isCurrentlyRunning && !isDevMode()) {
|
||||
common.logger.info('Starting wallet...')
|
||||
common.es(`sudo supervisorctl start bitcoin`)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Bitcoin Cash. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop bitcoincash`)
|
||||
common.es(`curl -#Lo /tmp/bitcoincash.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/bitcoincash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Bitcoin Cash: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -29,39 +29,49 @@ 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-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-28.0/bin'
|
||||
dir: 'bitcoin-28.0/bin',
|
||||
urlHash: '7fe294b02b25b51acb8e8e0a0eb5af6bbafa7cd0c5b0e5fcbb61263104a82fbc',
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.12-293a300d.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.14.12-293a300d'
|
||||
dir: 'geth-linux-amd64-1.14.12-293a300d',
|
||||
urlHash: 'e56216b9d179a66a8f71d3dee13ad554da5544d3d29dba33f64c9c0eda5a2237',
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://download.z.cash/downloads/zcash-6.0.0-linux64-debian-bullseye.tar.gz',
|
||||
dir: 'zcash-6.0.0/bin'
|
||||
dir: 'zcash-6.0.0/bin',
|
||||
urlHash: '3cb82f490e9c8e88007a0216b5261b33ef0fda962b9258441b2def59cb272a4d',
|
||||
},
|
||||
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.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-21.1.1/bin'
|
||||
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
|
||||
},
|
||||
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.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'litecoin-0.21.4/bin'
|
||||
dir: 'litecoin-0.21.4/bin',
|
||||
urlHash: '857fc41091f2bae65c3bf0fd4d388fca915fc93a03f16dd2578ac3cc92898390',
|
||||
},
|
||||
BCH: {
|
||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v28.0.0/bitcoin-cash-node-28.0.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-cash-node-28.0.0/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']],
|
||||
urlHash: 'ba735cd3b70fab35ac1496e38596cec1f8d34989924376de001d4a86198f7158',
|
||||
},
|
||||
XMR: {
|
||||
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2',
|
||||
dir: 'monero-x86_64-linux-gnu-v0.18.3.4',
|
||||
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']]
|
||||
files: [['monerod', 'monerod'], ['monero-wallet-rpc', 'monero-wallet-rpc']],
|
||||
urlHash: '51ba03928d189c1c11b5379cab17dd9ae8d2230056dc05c872d0f8dba4a87f1d',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -133,10 +143,15 @@ function fetchAndInstall (coinRec) {
|
|||
if (!binaries) throw new Error(`No such coin: ${coinRec.code}`)
|
||||
|
||||
const url = requiresUpdate ? binaries.defaultUrl : binaries.url
|
||||
const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash
|
||||
const downloadFile = path.basename(url)
|
||||
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
|
||||
|
||||
es(`wget -q ${url}`)
|
||||
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
|
||||
logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`)
|
||||
return
|
||||
}
|
||||
es(`tar -xf ${downloadFile}`)
|
||||
|
||||
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Dash Core. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop dash`)
|
||||
common.es(`curl -#Lo /tmp/dash.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/dash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Dash Core: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating the Geth Ethereum wallet. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop ethereum`)
|
||||
common.es(`curl -#o /tmp/ethereum.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/ethereum.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Geth: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -20,6 +20,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Litecoin Core. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop litecoin`)
|
||||
common.es(`curl -#o /tmp/litecoin.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/litecoin.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Litecoin Core: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating Monero. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop monero monero-wallet`)
|
||||
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/monero.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Monero: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ function updateCore (coinRec, isCurrentlyRunning) {
|
|||
common.logger.info('Updating your Zcash wallet. This may take a minute...')
|
||||
common.es(`sudo supervisorctl stop zcash`)
|
||||
common.es(`curl -#Lo /tmp/zcash.tar.gz ${coinRec.url}`)
|
||||
if (common.es(`sha256sum /tmp/zcash.tar.gz | awk '{print $1}'`).trim() !== coinRec.urlHash) {
|
||||
common.logger.info('Failed to update Zcash: Package signature do not match!')
|
||||
return
|
||||
}
|
||||
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)
|
||||
|
||||
common.logger.info('Updating wallet...')
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const E = require('../error')
|
|||
|
||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
||||
|
||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||
|
||||
const massage = _.flow(_.omit(massageFields),
|
||||
|
|
|
|||
|
|
@ -32,38 +32,41 @@ function post (machineTx, pi) {
|
|||
return cashInAtomic.atomic(machineTx, pi)
|
||||
.then(r => {
|
||||
const updatedTx = r.tx
|
||||
let blacklisted = false
|
||||
let addressReuse = false
|
||||
let walletScore = {}
|
||||
|
||||
const promises = [settingsLoader.loadLatestConfig()]
|
||||
|
||||
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||
if (isFirstPost) {
|
||||
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi))
|
||||
promises.push(
|
||||
checkForBlacklisted(updatedTx),
|
||||
doesTxReuseAddress(updatedTx),
|
||||
getWalletScore(updatedTx, pi)
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
|
||||
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
||||
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
||||
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||
const isBlacklisted = !!blacklisted
|
||||
|
||||
walletScore = fetchedWalletScore
|
||||
|
||||
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
|
||||
blacklisted = true
|
||||
if (isBlacklisted) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
||||
} else if (isReusedAddress && rejectAddressReuse) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
||||
addressReuse = true
|
||||
}
|
||||
return postProcess(r, pi, blacklisted, addressReuse, walletScore)
|
||||
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||
.then(_.flow(
|
||||
_.set('bills', machineTx.bills),
|
||||
_.set('blacklisted', isBlacklisted),
|
||||
_.set('blacklistMessage', blacklisted?.content),
|
||||
_.set('addressReuse', addressReuse),
|
||||
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
|
||||
))
|
||||
})
|
||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||
.then(tx => _.set('bills', machineTx.bills, tx))
|
||||
.then(tx => _.set('blacklisted', blacklisted, tx))
|
||||
.then(tx => _.set('addressReuse', addressReuse, tx))
|
||||
.then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) {
|
|||
}
|
||||
|
||||
function checkForBlacklisted (tx) {
|
||||
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
||||
return blacklist.blocked(tx.toAddress)
|
||||
}
|
||||
|
||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||
|
|
@ -148,18 +151,17 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
|||
}
|
||||
})
|
||||
.catch(err => {
|
||||
// Important: We don't know what kind of error this is
|
||||
// so not safe to assume that funds weren't sent.
|
||||
// Therefore, don't set sendPending to false except for
|
||||
// errors (like InsufficientFundsError) that are guaranteed
|
||||
// not to send.
|
||||
const sendPending = err.name !== 'InsufficientFundsError'
|
||||
// Important: We don't know what kind of error this is
|
||||
// so not safe to assume that funds weren't sent.
|
||||
|
||||
// Setting sendPending to true ensures that the transaction gets
|
||||
// silently terminated and no retries are done
|
||||
|
||||
return {
|
||||
sendTime: 'now()^',
|
||||
error: err.message,
|
||||
errorCode: err.name,
|
||||
sendPending
|
||||
sendPending: true
|
||||
}
|
||||
})
|
||||
.then(sendRec => {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
|
|||
|
||||
function convertBigNumFields (obj) {
|
||||
const convert = (value, key) => {
|
||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat' ])) {
|
||||
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) {
|
||||
return value.toString()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
|||
const cashInFee = showCommissions ? commissions.cashIn / 100 : null
|
||||
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
|
||||
const cashInFixedFee = showCommissions ? commissions.fixedFee : null
|
||||
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
|
||||
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
|
||||
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
|
|||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInFixedFee,
|
||||
cashOutFixedFee,
|
||||
cashInRate,
|
||||
cashOutRate
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function getWithEmail (email) {
|
|||
*
|
||||
* @param {string} id Customer's id
|
||||
* @param {object} data Fields to update
|
||||
* @param {string} Acting user's token
|
||||
* @param {string} userToken Acting user's token
|
||||
*
|
||||
* @returns {Promise} Newly updated Customer
|
||||
*/
|
||||
|
|
@ -114,6 +114,7 @@ function update (id, data, userToken) {
|
|||
async function updateCustomer (id, data, userToken) {
|
||||
const formattedData = _.pick(
|
||||
[
|
||||
'sanctions',
|
||||
'authorized_override',
|
||||
'id_card_photo_override',
|
||||
'id_card_data_override',
|
||||
|
|
@ -229,7 +230,7 @@ function enhanceEditedPhotos (fields) {
|
|||
/**
|
||||
* Remove the edited data from the db record
|
||||
*
|
||||
* @name enhanceOverrideFields
|
||||
* @name deleteEditedData
|
||||
* @function
|
||||
*
|
||||
* @param {string} id Customer's id
|
||||
|
|
|
|||
|
|
@ -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,33 @@ function active (settings, cryptoCode) {
|
|||
return !!lookupExchange(settings, cryptoCode)
|
||||
}
|
||||
|
||||
function getMarkets () {
|
||||
const filterExchanges = _.filter(it => it.class === 'exchange' && !it.dev && it.code !== 'no-exchange')
|
||||
const availableExchanges = _.map(it => it.code, filterExchanges(accounts.ACCOUNT_LIST))
|
||||
|
||||
const fetchMarketForExchange = exchange =>
|
||||
ccxt.getMarkets(exchange, ALL_CRYPTOS)
|
||||
.then(markets => ({ exchange, markets }))
|
||||
.catch(error => ({
|
||||
exchange,
|
||||
markets: [],
|
||||
error: error.message
|
||||
}))
|
||||
|
||||
const transformToObject = _.reduce((acc, { exchange, markets }) => ({
|
||||
...acc,
|
||||
[exchange]: markets
|
||||
}), {})
|
||||
|
||||
const promises = _.map(fetchMarketForExchange, availableExchanges)
|
||||
return Promise.all(promises)
|
||||
.then(transformToObject)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchExchange,
|
||||
buy,
|
||||
sell,
|
||||
active
|
||||
active,
|
||||
getMarkets
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ const addReceiptInfo = receiptInfo => ret => {
|
|||
if (!receiptInfo) return ret
|
||||
|
||||
const fields = [
|
||||
'automaticPrint',
|
||||
'paper',
|
||||
'sms',
|
||||
'operatorWebsite',
|
||||
|
|
@ -61,6 +62,18 @@ const addReceiptInfo = receiptInfo => ret => {
|
|||
}
|
||||
|
||||
|
||||
const addMachineScreenOpts = smth => _.update(
|
||||
'screenOptions',
|
||||
_.flow(
|
||||
addSmthInfo(
|
||||
'rates',
|
||||
[
|
||||
'active'
|
||||
]
|
||||
)(smth.rates)
|
||||
)
|
||||
)
|
||||
|
||||
/* TODO: Simplify this. */
|
||||
const buildTriggers = allTriggers => {
|
||||
const normalTriggers = []
|
||||
|
|
@ -89,6 +102,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
'cashInCommission',
|
||||
'cashInFee',
|
||||
'cashOutCommission',
|
||||
'cashOutFee',
|
||||
'cryptoCode',
|
||||
'cryptoCodeDisplay',
|
||||
'cryptoNetwork',
|
||||
|
|
@ -102,7 +116,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
_.pick([
|
||||
'coins',
|
||||
'configVersion',
|
||||
'timezone'
|
||||
'timezone',
|
||||
'screenOptions'
|
||||
]),
|
||||
_.update('coins', massageCoins),
|
||||
_.set('serverVersion', VERSION),
|
||||
|
|
@ -116,6 +131,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)
|
||||
|
|
@ -128,6 +144,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
localeInfo,
|
||||
operatorInfo,
|
||||
receiptInfo,
|
||||
machineScreenOpts,
|
||||
twoWayMode,
|
||||
{ numberOfCassettes, numberOfRecyclers },
|
||||
customerAuthentication,
|
||||
|
|
@ -152,7 +169,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
|
|||
urlsToPing,
|
||||
}),
|
||||
addOperatorInfo(operatorInfo),
|
||||
addReceiptInfo(receiptInfo)
|
||||
addReceiptInfo(receiptInfo),
|
||||
addMachineScreenOpts(machineScreenOpts)
|
||||
)(staticConf))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type Coin {
|
|||
display: String!
|
||||
minimumTx: String!
|
||||
cashInFee: String!
|
||||
cashOutFee: String!
|
||||
cashInCommission: String!
|
||||
cashOutCommission: String!
|
||||
cryptoNetwork: String!
|
||||
|
|
@ -37,6 +38,7 @@ type MachineInfo {
|
|||
|
||||
type ReceiptInfo {
|
||||
paper: Boolean!
|
||||
automaticPrint: Boolean!
|
||||
sms: Boolean!
|
||||
operatorWebsite: Boolean!
|
||||
operatorEmail: Boolean!
|
||||
|
|
@ -48,6 +50,14 @@ type ReceiptInfo {
|
|||
addressQRCode: Boolean!
|
||||
}
|
||||
|
||||
type MachineScreenOptions {
|
||||
rates: RateScreenOptions!
|
||||
}
|
||||
|
||||
type RateScreenOptions {
|
||||
active: Boolean!
|
||||
}
|
||||
|
||||
type SpeedtestFile {
|
||||
url: String!
|
||||
size: Int!
|
||||
|
|
@ -146,6 +156,7 @@ type StaticConfig {
|
|||
operatorInfo: OperatorInfo
|
||||
machineInfo: MachineInfo!
|
||||
receiptInfo: ReceiptInfo
|
||||
screenOptions: MachineScreenOptions
|
||||
|
||||
speedtestFiles: [SpeedtestFile!]!
|
||||
urlsToPing: [String!]!
|
||||
|
|
|
|||
15
lib/middlewares/addRWBytes.js
Normal file
15
lib/middlewares/addRWBytes.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
const addRWBytes = () => (req, res, next) => {
|
||||
const handle = () => {
|
||||
res.removeListener('finish', handle)
|
||||
res.removeListener('close', handle)
|
||||
res.bytesRead = req.connection.bytesRead
|
||||
res.bytesWritten = req.connection.bytesWritten
|
||||
}
|
||||
|
||||
res.on('finish', handle)
|
||||
res.on('close', handle)
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = addRWBytes
|
||||
|
|
@ -14,6 +14,7 @@ const { ApolloServer } = require('apollo-server-express')
|
|||
require('../environment-helper')
|
||||
const { asyncLocalStorage, defaultStore } = require('../async-storage')
|
||||
const logger = require('../logger')
|
||||
const exchange = require('../exchange')
|
||||
|
||||
const { AuthDirective } = require('./graphql/directives')
|
||||
const { typeDefs, resolvers } = require('./graphql/schema')
|
||||
|
|
@ -98,6 +99,9 @@ function run () {
|
|||
|
||||
const serverLog = `lamassu-admin-server listening on port ${serverPort}`
|
||||
|
||||
// cache markets on startup
|
||||
exchange.getMarkets().catch(console.error)
|
||||
|
||||
const webServer = https.createServer(certOptions, app)
|
||||
webServer.listen(serverPort, () => logger.info(serverLog))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
blacklist: () => blacklist.getBlacklist()
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
blacklistMessages: () => blacklist.getMessages()
|
||||
},
|
||||
Mutation: {
|
||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address)
|
||||
deleteBlacklistRow: (...[, { address }]) =>
|
||||
blacklist.deleteFromBlacklist(address),
|
||||
insertBlacklistRow: (...[, { address }]) =>
|
||||
blacklist.insertIntoBlacklist(address),
|
||||
editBlacklistMessage: (...[, { id, content }]) =>
|
||||
blacklist.editBlacklistMessage(id, content)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ 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')
|
||||
const sanctions = require('./sanctions.resolver')
|
||||
const scalar = require('./scalar.resolver')
|
||||
const settings = require('./settings.resolver')
|
||||
const sms = require('./sms.resolver')
|
||||
|
|
@ -34,9 +36,11 @@ const resolvers = [
|
|||
log,
|
||||
loyalty,
|
||||
machine,
|
||||
market,
|
||||
notification,
|
||||
pairing,
|
||||
rates,
|
||||
sanctions,
|
||||
scalar,
|
||||
settings,
|
||||
sms,
|
||||
|
|
|
|||
9
lib/new-admin/graphql/resolvers/market.resolver.js
Normal file
9
lib/new-admin/graphql/resolvers/market.resolver.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const exchange = require('../../../exchange')
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
getMarkets: () => exchange.getMarkets()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
13
lib/new-admin/graphql/resolvers/sanctions.resolver.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const sanctions = require('../../../sanctions')
|
||||
const authentication = require('../modules/userManagement')
|
||||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
checkAgainstSanctions: (...[, { customerId }, context]) => {
|
||||
const token = authentication.getToken(context)
|
||||
return sanctions.checkByUser(customerId, token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = resolvers
|
||||
|
|
@ -2,17 +2,26 @@ const { gql } = require('apollo-server-express')
|
|||
|
||||
const typeDef = gql`
|
||||
type Blacklist {
|
||||
cryptoCode: String!
|
||||
address: String!
|
||||
blacklistMessage: BlacklistMessage!
|
||||
}
|
||||
|
||||
type BlacklistMessage {
|
||||
id: ID
|
||||
label: String
|
||||
content: String
|
||||
allowToggle: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
blacklist: [Blacklist] @auth
|
||||
blacklistMessages: [BlacklistMessage] @auth
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
deleteBlacklistRow(address: String!): Blacklist @auth
|
||||
insertBlacklistRow(address: String!): Blacklist @auth
|
||||
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -11,9 +11,11 @@ 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')
|
||||
const sanctions = require('./sanctions.type')
|
||||
const scalar = require('./scalar.type')
|
||||
const settings = require('./settings.type')
|
||||
const sms = require('./sms.type')
|
||||
|
|
@ -34,9 +36,11 @@ const types = [
|
|||
log,
|
||||
loyalty,
|
||||
machine,
|
||||
market,
|
||||
notification,
|
||||
pairing,
|
||||
rates,
|
||||
sanctions,
|
||||
scalar,
|
||||
settings,
|
||||
sms,
|
||||
|
|
|
|||
9
lib/new-admin/graphql/types/market.type.js
Normal file
9
lib/new-admin/graphql/types/market.type.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type Query {
|
||||
getMarkets: JSONObject @auth
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = typeDef
|
||||
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
13
lib/new-admin/graphql/types/sanctions.type.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const { gql } = require('apollo-server-express')
|
||||
|
||||
const typeDef = gql`
|
||||
type SanctionMatches {
|
||||
ofacSanctioned: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
checkAgainstSanctions(customerId: ID): SanctionMatches @auth
|
||||
}
|
||||
`
|
||||
|
||||
module.exports = typeDef
|
||||
|
|
@ -23,7 +23,7 @@ const typeDef = gql`
|
|||
errorCode: String
|
||||
operatorCompleted: Boolean
|
||||
sendPending: Boolean
|
||||
cashInFee: String
|
||||
fixedFee: String
|
||||
minimumTx: Float
|
||||
customerId: ID
|
||||
isAnonymous: Boolean
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ const buildApolloContext = async ({ req, res }) => {
|
|||
req.session.user.role = user.role
|
||||
|
||||
res.set('lamassu_role', user.role)
|
||||
res.cookie('pazuz_operatoridentifier', base64.encode(user.username))
|
||||
res.set('Access-Control-Expose-Headers', 'lamassu_role')
|
||||
|
||||
return { req, res }
|
||||
|
|
|
|||
|
|
@ -50,7 +50,20 @@ function batch (
|
|||
excludeTestingCustomers = false,
|
||||
simplified
|
||||
) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addProfits, addNames)
|
||||
const isCsvExport = _.isBoolean(simplified)
|
||||
const packager = _.flow(
|
||||
_.flatten,
|
||||
_.orderBy(_.property('created'), ['desc']),
|
||||
_.map(_.flow(
|
||||
camelize,
|
||||
_.mapKeys(k =>
|
||||
k == 'cashInFee' ? 'fixedFee' :
|
||||
k
|
||||
)
|
||||
)),
|
||||
addProfits,
|
||||
addNames
|
||||
)
|
||||
|
||||
const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
|
||||
c.phone AS customer_phone,
|
||||
|
|
@ -80,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,
|
||||
|
|
@ -114,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
|
||||
|
|
@ -140,20 +153,20 @@ 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) {
|
||||
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
|
||||
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error',
|
||||
'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee',
|
||||
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
|
||||
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
|
||||
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
|
||||
|
|
@ -169,7 +182,9 @@ function advancedBatch (data) {
|
|||
...it,
|
||||
status: getStatus(it),
|
||||
fiatProfit: getProfit(it).toString(),
|
||||
cryptoAmount: getCryptoAmount(it).toString()
|
||||
cryptoAmount: getCryptoAmount(it).toString(),
|
||||
fixedFee: it.fixedFee ?? null,
|
||||
fee: it.fee ?? null,
|
||||
}))
|
||||
|
||||
return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,12 @@ const namespaces = {
|
|||
TERMS_CONDITIONS: 'termsConditions',
|
||||
CASH_OUT: 'cashOut',
|
||||
CASH_IN: 'cashIn',
|
||||
COMPLIANCE: 'compliance'
|
||||
COMPLIANCE: 'compliance',
|
||||
MACHINE_SCREENS: 'machineScreens'
|
||||
}
|
||||
|
||||
const machineScreens = {
|
||||
RATES: 'rates'
|
||||
}
|
||||
|
||||
const stripl = _.curry((q, str) => _.startsWith(q, str) ? str.slice(q.length) : str)
|
||||
|
|
@ -72,6 +77,8 @@ const getCoinAtmRadar = fromNamespace(namespaces.COIN_ATM_RADAR)
|
|||
const getTermsConditions = fromNamespace(namespaces.TERMS_CONDITIONS)
|
||||
const getReceipt = fromNamespace(namespaces.RECEIPT)
|
||||
const getCompliance = fromNamespace(namespaces.COMPLIANCE)
|
||||
const getMachineScreenOpts = (screenName, config) => _.compose(fromNamespace(screenName), fromNamespace(namespaces.MACHINE_SCREENS))(config)
|
||||
const getAllMachineScreenOpts = config => _.reduce((acc, value) => ({ ...acc, [value]: getMachineScreenOpts(value, config) }), {}, _.values(machineScreens))
|
||||
|
||||
const getAllCryptoCurrencies = (config) => {
|
||||
const locale = fromNamespace(namespaces.LOCALE)(config)
|
||||
|
|
@ -180,6 +187,8 @@ module.exports = {
|
|||
getWalletSettings,
|
||||
getCashInSettings,
|
||||
getOperatorInfo,
|
||||
getMachineScreenOpts,
|
||||
getAllMachineScreenOpts,
|
||||
getNotifications,
|
||||
getGlobalNotifications,
|
||||
getLocale,
|
||||
|
|
|
|||
|
|
@ -100,16 +100,19 @@ function loadAccounts (schemaVersion) {
|
|||
.then(_.compose(_.defaultTo({}), _.get('data.accounts')))
|
||||
}
|
||||
|
||||
function hideSecretFields (accounts) {
|
||||
return _.flow(
|
||||
_.filter(path => !_.isEmpty(_.get(path, accounts))),
|
||||
_.reduce(
|
||||
(accounts, path) => _.assoc(path, PASSWORD_FILLED, accounts),
|
||||
accounts
|
||||
)
|
||||
)(SECRET_FIELDS)
|
||||
}
|
||||
|
||||
function showAccounts (schemaVersion) {
|
||||
return loadAccounts(schemaVersion)
|
||||
.then(accounts => {
|
||||
const filledSecretPaths = _.compact(_.map(path => {
|
||||
if (!_.isEmpty(_.get(path, accounts))) {
|
||||
return path
|
||||
}
|
||||
}, SECRET_FIELDS))
|
||||
return _.compose(_.map(path => _.assoc(path, PASSWORD_FILLED), filledSecretPaths))(accounts)
|
||||
})
|
||||
.then(hideSecretFields)
|
||||
}
|
||||
|
||||
const insertConfigRow = (dbOrTx, data) =>
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ function plugins (settings, deviceId) {
|
|||
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
|
||||
const minimumTx = new BN(commissions.minimumTx)
|
||||
const cashInFee = new BN(commissions.fixedFee)
|
||||
const cashOutFee = new BN(commissions.cashOutFixedFee)
|
||||
const cashInCommission = new BN(commissions.cashIn)
|
||||
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
|
|
@ -261,6 +262,7 @@ function plugins (settings, deviceId) {
|
|||
isCashInOnly: Boolean(cryptoRec.isCashinOnly),
|
||||
minimumTx: BN.max(minimumTx, cashInFee),
|
||||
cashInFee,
|
||||
cashOutFee,
|
||||
cashInCommission,
|
||||
cashOutCommission,
|
||||
cryptoNetwork,
|
||||
|
|
@ -276,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))
|
||||
|
|
@ -325,7 +328,8 @@ function plugins (settings, deviceId) {
|
|||
coins,
|
||||
configVersion,
|
||||
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
|
||||
timezone
|
||||
timezone,
|
||||
screenOptions: machineScreenOpts
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -473,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) {
|
||||
|
|
@ -548,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ const ALL = {
|
|||
bitpay: bitpay,
|
||||
coinbase: {
|
||||
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX, LN],
|
||||
FIAT: 'ALL_CURRENCIES'
|
||||
FIAT: 'ALL_CURRENCIES',
|
||||
DEFAULT_FIAT_MARKET: 'EUR'
|
||||
},
|
||||
binance: binance,
|
||||
bitfinex: bitfinex
|
||||
|
|
@ -33,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
|
||||
}
|
||||
|
||||
|
|
@ -51,4 +49,8 @@ function isConfigValid (config, fields) {
|
|||
return _.every(it => it || it === 0)(values)
|
||||
}
|
||||
|
||||
module.exports = { buildMarket, ALL, verifyFiatSupport, isConfigValid }
|
||||
function defaultFiatMarket (serviceName) {
|
||||
return ALL[serviceName].DEFAULT_FIAT_MARKET
|
||||
}
|
||||
|
||||
module.exports = { buildMarket, ALL, verifyFiatSupport, isConfigValid, defaultFiatMarket }
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@ const { ORDER_TYPES } = require('./consts')
|
|||
const ORDER_TYPE = ORDER_TYPES.MARKET
|
||||
const { BTC, BCH, XMR, ETH, LTC, ZEC, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, ZEC, BCH, XMR, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const FIAT = ['EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
|||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
|||
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, USDT_TRON, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, LN]
|
||||
const FIAT = ['USD']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const DEFAULT_FIAT_MARKET = 'USD'
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
|||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
|||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 8
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret']
|
||||
|
||||
|
|
@ -18,4 +19,4 @@ const loadConfig = (account) => {
|
|||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, DEFAULT_FIAT_MARKET, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
|||
const { BTC, ETH, LTC, BCH, USDT, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, BCH, USDT, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 8
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId']
|
||||
const REQUIRED_CONFIG_FIELDS = ['key', 'secret', 'clientId', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -19,4 +20,4 @@ const loadConfig = (account) => {
|
|||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
const { utils: coinUtils } = require('@lamassu/coins')
|
||||
const _ = require('lodash/fp')
|
||||
const ccxt = require('ccxt')
|
||||
const mem = require('mem')
|
||||
|
||||
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
|
||||
const { ORDER_TYPES } = require('./consts')
|
||||
const logger = require('../../logger')
|
||||
const { currencies } = require('../../new-admin/config')
|
||||
const T = require('../../time')
|
||||
|
||||
const DEFAULT_PRICE_PRECISION = 2
|
||||
const DEFAULT_AMOUNT_PRECISION = 8
|
||||
|
|
@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) {
|
|||
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
|
||||
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
|
||||
|
||||
const symbol = buildMarket(fiatCode, cryptoCode, exchangeName)
|
||||
const selectedFiatMarket = account.currencyMarket
|
||||
const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName)
|
||||
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
|
||||
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
|
||||
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
|
||||
|
|
@ -50,4 +55,38 @@ function calculatePrice (side, amount, orderBook) {
|
|||
throw new Error('Insufficient market depth')
|
||||
}
|
||||
|
||||
module.exports = { trade }
|
||||
function _getMarkets (exchangeName, availableCryptos) {
|
||||
const prunedCryptos = _.compose(_.uniq, _.map(coinUtils.getEquivalentCode))(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, prunedCryptos) && _.includes(value.quote, currencyCodes)) {
|
||||
if (value.quote === value.base) return acc
|
||||
|
||||
if (_.isNil(acc[value.quote])) {
|
||||
return { ...acc, [value.quote]: [value.base] }
|
||||
}
|
||||
|
||||
acc[value.quote].push(value.base)
|
||||
}
|
||||
return acc
|
||||
}, {}, res)
|
||||
)
|
||||
} catch (e) {
|
||||
logger.debug(`No CCXT exchange found for ${exchangeName}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getMarkets = mem(_getMarkets, {
|
||||
maxAge: T.week,
|
||||
cacheKey: (exchangeName, availableCryptos) => exchangeName
|
||||
})
|
||||
|
||||
module.exports = { trade, getMarkets }
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
|||
const { BTC, BCH, DASH, ETH, LTC, USDT, TRX, USDT_TRON, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, BCH, USDT, TRX, USDT_TRON, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -17,4 +18,4 @@ const loadConfig = (account) => {
|
|||
return { ...mapped, timeout: 3000 }
|
||||
}
|
||||
|
||||
module.exports = { loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
module.exports = { loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE }
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.LIMIT
|
|||
const { BTC, ETH, USDT, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, USDT, LN]
|
||||
const FIAT = ['USD']
|
||||
const DEFAULT_FIAT_MARKET = 'USD'
|
||||
const AMOUNT_PRECISION = 4
|
||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId']
|
||||
const REQUIRED_CONFIG_FIELDS = ['clientKey', 'clientSecret', 'userId', 'walletId', 'currencyMarket']
|
||||
|
||||
const loadConfig = (account) => {
|
||||
const mapper = {
|
||||
|
|
@ -21,4 +22,4 @@ const loadConfig = (account) => {
|
|||
}
|
||||
const loadOptions = ({ walletId }) => ({ walletId })
|
||||
|
||||
module.exports = { loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
module.exports = { loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ const ORDER_TYPE = ORDER_TYPES.MARKET
|
|||
const { BTC, BCH, DASH, ETH, LTC, ZEC, XMR, USDT, TRX, USDT_TRON, LN } = COINS
|
||||
const CRYPTO = [BTC, ETH, LTC, DASH, ZEC, BCH, XMR, USDT, TRX, USDT_TRON, LN]
|
||||
const FIAT = ['USD', 'EUR']
|
||||
const DEFAULT_FIAT_MARKET = 'EUR'
|
||||
const AMOUNT_PRECISION = 6
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey']
|
||||
const REQUIRED_CONFIG_FIELDS = ['apiKey', 'privateKey', 'currencyMarket']
|
||||
const USER_REF = 'userref'
|
||||
|
||||
const loadConfig = (account) => {
|
||||
|
|
@ -26,4 +27,4 @@ const loadConfig = (account) => {
|
|||
|
||||
const loadOptions = () => ({ expiretm: '+60' })
|
||||
|
||||
module.exports = { USER_REF, loadOptions, loadConfig, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
module.exports = { USER_REF, loadOptions, loadConfig, DEFAULT_FIAT_MARKET, REQUIRED_CONFIG_FIELDS, CRYPTO, FIAT, ORDER_TYPE, AMOUNT_PRECISION }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const ccxt = require('ccxt')
|
||||
|
||||
const BN = require('../../bn')
|
||||
const { buildMarket, verifyFiatSupport } = require('../common/ccxt')
|
||||
const { buildMarket, verifyFiatSupport, defaultFiatMarket } = require('../common/ccxt')
|
||||
const { getRate } = require('../../../lib/forex')
|
||||
|
||||
const RETRIES = 2
|
||||
|
|
@ -33,7 +33,7 @@ function ticker (fiatCode, cryptoCode, tickerName) {
|
|||
return getRate(RETRIES, fiatCode)
|
||||
.then(({ fxRate }) => {
|
||||
try {
|
||||
return getCurrencyRates(ticker, 'USD', cryptoCode)
|
||||
return getCurrencyRates(ticker, defaultFiatMarket(tickerName), cryptoCode)
|
||||
.then(res => ({
|
||||
rates: {
|
||||
ask: res.rates.ask.times(fxRate),
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ const nocache = require('nocache')
|
|||
|
||||
const logger = require('./logger')
|
||||
|
||||
const addRWBytes = require('./middlewares/addRWBytes')
|
||||
const authorize = require('./middlewares/authorize')
|
||||
const computeSchema = require('./middlewares/compute-schema')
|
||||
const errorHandler = require('./middlewares/errorHandler')
|
||||
const filterOldRequests = require('./middlewares/filterOldRequests')
|
||||
const computeSchema = require('./middlewares/compute-schema')
|
||||
const findOperatorId = require('./middlewares/operatorId')
|
||||
const populateDeviceId = require('./middlewares/populateDeviceId')
|
||||
const populateSettings = require('./middlewares/populateSettings')
|
||||
|
|
@ -50,11 +51,24 @@ const configRequiredRoutes = [
|
|||
]
|
||||
|
||||
// middleware setup
|
||||
app.use(addRWBytes())
|
||||
app.use(compression({ threshold: 500 }))
|
||||
app.use(helmet())
|
||||
app.use(nocache())
|
||||
app.use(express.json({ limit: '2mb' }))
|
||||
app.use(morgan(':method :url :status :response-time ms -- :req[content-length]/:res[content-length] b', { stream: logger.stream }))
|
||||
|
||||
morgan.token('bytesRead', (_req, res) => res.bytesRead)
|
||||
morgan.token('bytesWritten', (_req, res) => res.bytesWritten)
|
||||
app.use(morgan(':method :url :status :response-time ms -- :bytesRead/:bytesWritten B', { stream: logger.stream }))
|
||||
|
||||
app.use('/robots.txt', (req, res) => {
|
||||
res.type('text/plain')
|
||||
res.send("User-agent: *\nDisallow: /")
|
||||
})
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.sendStatus(404)
|
||||
})
|
||||
|
||||
// app /pair and /ca routes
|
||||
app.use('/', pairingRoutes)
|
||||
|
|
|
|||
|
|
@ -1,40 +1,41 @@
|
|||
const express = require('express')
|
||||
const _ = require('lodash/fp')
|
||||
const router = express.Router()
|
||||
|
||||
const cashbox = require('../cashbox-batches')
|
||||
const notifier = require('../notifier')
|
||||
const { getMachine, setMachine } = require('../machine-loader')
|
||||
const { getMachine, setMachine, getMachineName } = require('../machine-loader')
|
||||
const { loadLatestConfig } = require('../new-settings-loader')
|
||||
const { getCashInSettings } = require('../new-config-manager')
|
||||
const { AUTOMATIC } = require('../constants')
|
||||
const logger = require('../logger')
|
||||
|
||||
function notifyCashboxRemoval (req, res, next) {
|
||||
|
||||
function cashboxRemoval (req, res, next) {
|
||||
const operatorId = res.locals.operatorId
|
||||
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Received a cashbox opening request from device ${req.deviceId}`)
|
||||
notifier.cashboxNotify(req.deviceId).catch(logger.error)
|
||||
|
||||
return notifier.cashboxNotify(req.deviceId)
|
||||
.then(() => Promise.all([getMachine(req.deviceId), loadLatestConfig()]))
|
||||
return Promise.all([getMachine(req.deviceId), loadLatestConfig()])
|
||||
.then(([machine, config]) => {
|
||||
logger.info('** DEBUG ** - Cashbox removal - Retrieving system options for cash-in')
|
||||
const cashInSettings = getCashInSettings(config)
|
||||
if (cashInSettings.cashboxReset !== AUTOMATIC) {
|
||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to manual. A cashbox batch will NOT be created')
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||
return res.status(200).send({ status: 'OK' })
|
||||
return Promise.all([
|
||||
cashbox.getMachineUnbatchedBills(req.deviceId),
|
||||
getMachineName(req.deviceId)
|
||||
])
|
||||
}
|
||||
logger.info('** DEBUG ** - Cashbox removal - Cashbox reset is set to automatic. A cashbox batch WILL be created')
|
||||
logger.info('** DEBUG ** - Cashbox removal - Creating new batch...')
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashUnits.cashbox)
|
||||
.then(() => {
|
||||
logger.info(`** DEBUG ** - Cashbox removal - Process finished`)
|
||||
return res.status(200).send({ status: 'OK' })
|
||||
})
|
||||
return cashbox.createCashboxBatch(req.deviceId, machine.cashbox)
|
||||
.then(batch => Promise.all([
|
||||
cashbox.getBatchById(batch.id),
|
||||
getMachineName(batch.device_id),
|
||||
setMachine({ deviceId: req.deviceId, action: 'emptyCashInBills' }, operatorId)
|
||||
]))
|
||||
})
|
||||
.then(([batch, machineName]) => res.status(200).send({ batch: _.merge(batch, { machineName }), status: 'OK' }))
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
router.post('/removal', notifyCashboxRemoval)
|
||||
router.post('/removal', cashboxRemoval)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ function updateCustomer (req, res, next) {
|
|||
.then(_.merge(patch))
|
||||
.then(newPatch => customers.updatePhotoCard(id, newPatch))
|
||||
.then(newPatch => customers.updateFrontCamera(id, newPatch))
|
||||
.then(newPatch => customers.update(id, newPatch, null, txId))
|
||||
.then(newPatch => customers.update(id, newPatch, null))
|
||||
.then(customer => {
|
||||
createPendingManualComplianceNotifs(settings, customer, deviceId)
|
||||
respond(req, res, { customer })
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ function poll (req, res, next) {
|
|||
locale,
|
||||
version,
|
||||
receiptPrintingActive: receipt.active,
|
||||
automaticReceiptPrint: receipt.automaticPrint,
|
||||
smsReceiptActive: receipt.sms,
|
||||
enablePaperWalletOnly,
|
||||
twoWayMode: cashOutConfig.active,
|
||||
|
|
|
|||
44
lib/sanctions.js
Normal file
44
lib/sanctions.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const _ = require('lodash/fp')
|
||||
const ofac = require('./ofac')
|
||||
const T = require('./time')
|
||||
const logger = require('./logger')
|
||||
const customers = require('./customers')
|
||||
|
||||
const sanctionStatus = {
|
||||
loaded: false,
|
||||
timestamp: null
|
||||
}
|
||||
|
||||
const loadOrUpdateSanctions = () => {
|
||||
if (!sanctionStatus.loaded || (sanctionStatus.timestamp && Date.now() > sanctionStatus.timestamp + T.day)) {
|
||||
logger.info('No sanction lists loaded. Loading sanctions...')
|
||||
return ofac.load()
|
||||
.then(() => {
|
||||
logger.info('OFAC sanction list loaded!')
|
||||
sanctionStatus.loaded = true
|
||||
sanctionStatus.timestamp = Date.now()
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Couldn\'t load OFAC sanction list!')
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const checkByUser = (customerId, userToken) => {
|
||||
return Promise.all([loadOrUpdateSanctions(), customers.getCustomerById(customerId)])
|
||||
.then(([, customer]) => {
|
||||
const { firstName, lastName, dateOfBirth } = customer?.idCardData
|
||||
const birthdate = _.replace(/-/g, '')(dateOfBirth)
|
||||
const ofacMatches = ofac.match({ firstName, lastName }, birthdate, { threshold: 0.85, fullNameThreshold: 0.95, debug: false })
|
||||
const isOfacSanctioned = _.size(ofacMatches) > 0
|
||||
customers.updateCustomer(customerId, { sanctions: !isOfacSanctioned }, userToken)
|
||||
|
||||
return { ofacSanctioned: isOfacSanctioned }
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkByUser
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ function massage (tx, pi) {
|
|||
: {
|
||||
cryptoAtoms: new BN(r.cryptoAtoms),
|
||||
fiat: new BN(r.fiat),
|
||||
fixedFee: r.cashOutFee ? new BN(r.cashOutFee) : null,
|
||||
rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null,
|
||||
commissionPercentage: new BN(r.commissionPercentage)
|
||||
}
|
||||
|
|
@ -50,7 +51,9 @@ function massage (tx, pi) {
|
|||
const mapper = _.flow(
|
||||
transformDates,
|
||||
mapBN,
|
||||
_.unset('dirty'))
|
||||
_.unset('dirty'),
|
||||
_.unset('cashOutFee')
|
||||
)
|
||||
|
||||
return mapper(tx)
|
||||
}
|
||||
|
|
@ -69,7 +72,7 @@ function cancel (txId) {
|
|||
}
|
||||
|
||||
function customerHistory (customerId, thresholdDays) {
|
||||
const sql = `SELECT * FROM (
|
||||
const sql = `SELECT ch.id, ch.created, ch.fiat, ch.direction FROM (
|
||||
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
||||
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
||||
FROM cash_in_txs txIn
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = next => db.multi([
|
||||
'ALTER TABLE cash_out_txs ADD COLUMN fixed_fee numeric(14, 5) NOT NULL DEFAULT 0;'
|
||||
], next)
|
||||
|
||||
exports.down = next => next()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const { saveConfig } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = next => saveConfig({ 'commissions_cashOutFixedFee': 0 })
|
||||
.then(next)
|
||||
.catch(next)
|
||||
|
||||
exports.down = next => next()
|
||||
30
migrations/1732874039534-market-currency.js
Normal file
30
migrations/1732874039534-market-currency.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { loadLatest, saveAccounts } = require('../lib/new-settings-loader')
|
||||
const { ACCOUNT_LIST } = require('../lib/new-admin/config/accounts')
|
||||
const { ALL } = require('../lib/plugins/common/ccxt')
|
||||
|
||||
exports.up = function (next) {
|
||||
return loadLatest()
|
||||
.then(({ accounts }) => {
|
||||
const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST))
|
||||
const configuredExchanges = _.intersection(allExchanges, _.keys(accounts))
|
||||
|
||||
const newAccounts = _.reduce(
|
||||
(acc, value) => {
|
||||
if (!_.isNil(accounts[value].currencyMarket)) return acc
|
||||
if (_.includes('EUR', ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: 'EUR' } }
|
||||
return { ...acc, [value]: { currencyMarket: ALL[value].DEFAULT_FIAT_CURRENCY } }
|
||||
},
|
||||
{},
|
||||
configuredExchanges
|
||||
)
|
||||
|
||||
return saveAccounts(newAccounts)
|
||||
})
|
||||
.then(next)
|
||||
.catch(next)
|
||||
}
|
||||
|
||||
module.exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`CREATE TABLE blacklist_temp (
|
||||
address TEXT NOT NULL UNIQUE
|
||||
)`,
|
||||
`INSERT INTO blacklist_temp (address) SELECT DISTINCT address FROM blacklist`,
|
||||
`DROP TABLE blacklist`,
|
||||
`ALTER TABLE blacklist_temp RENAME TO blacklist`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const uuid = require('uuid')
|
||||
|
||||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
const defaultMessageId = uuid.v4()
|
||||
|
||||
var sql = [
|
||||
`CREATE TABLE blacklist_messages (
|
||||
id UUID PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
allow_toggle BOOLEAN NOT NULL DEFAULT true
|
||||
)`,
|
||||
`INSERT INTO blacklist_messages (id, label, content, allow_toggle) VALUES ('${defaultMessageId}', 'Suspicious address', 'This address may be associated with a deceptive offer or a prohibited group. Please make sure you''re using an address from your own wallet.', false)`,
|
||||
`ALTER TABLE blacklist ADD COLUMN blacklist_message_id UUID REFERENCES blacklist_messages(id) NOT NULL DEFAULT '${defaultMessageId}'`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
20
migrations/1732881659436-rates-screen.js
Normal file
20
migrations/1732881659436-rates-screen.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const _ = require('lodash/fp')
|
||||
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
|
||||
|
||||
exports.up = function (next) {
|
||||
const newConfig = {}
|
||||
return loadLatest()
|
||||
.then(({ config }) => {
|
||||
if (!_.isNil(config.machineScreens_rates_active)) return
|
||||
newConfig[`machineScreens_rates_active`] = true
|
||||
return saveConfig(newConfig)
|
||||
})
|
||||
.then(next)
|
||||
.catch(err => {
|
||||
return next(err)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -2,4 +2,3 @@ SKIP_PREFLIGHT_CHECK=true
|
|||
HTTPS=true
|
||||
REACT_APP_TYPE_CHECK_SANCTUARY=false
|
||||
PORT=3001
|
||||
REACT_APP_BUILD_TARGET=LAMASSU
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
module.exports = {
|
||||
extends: ['react-app', 'prettier-standard', 'prettier/react'],
|
||||
plugins: ['import'],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
alias: [['src', './src']]
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': [2, { allowObject: true }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index'
|
||||
],
|
||||
alphabetize: {
|
||||
order: 'asc'
|
||||
},
|
||||
'newlines-between': 'always'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
8
new-lamassu-admin/.prettierrc
Normal file
8
new-lamassu-admin/.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"bracketSameLine": true
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
stories: ['../src/stories/index.js'],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-knobs',
|
||||
'@storybook/addon-backgrounds',
|
||||
'@storybook/preset-create-react-app'
|
||||
]
|
||||
}
|
||||
1
new-lamassu-admin/.tool-versions
Normal file
1
new-lamassu-admin/.tool-versions
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodejs 22
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Dev Environment
|
||||
|
||||
### formatting
|
||||
|
|
@ -10,16 +8,6 @@ The configuration for vscode is already on the repo, all you need to do is insta
|
|||
This project has a husky pre commit hook to format the staged changes using our styleguide.
|
||||
To take advantage of that make sure to run `git commit` from within this folder.
|
||||
|
||||
### Sanctuary
|
||||
|
||||
Sanctuary has a runtime typechecker that can make be quite slow, but its turned off by default.
|
||||
|
||||
To turn it on add the following line to a `.env.local` file.
|
||||
|
||||
```
|
||||
REACT_APP_TYPE_CHECK_SANCTUARY=true
|
||||
```
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
|
@ -36,10 +24,6 @@ You will also see any lint errors in the console.
|
|||
|
||||
Runs eslint --fix on the src folder
|
||||
|
||||
### `npm storybook`
|
||||
|
||||
Runs the storybook server
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br>
|
||||
|
|
|
|||
39
new-lamassu-admin/eslint.config.js
Normal file
39
new-lamassu-admin/eslint.config.js
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import globals from 'globals'
|
||||
import pluginJs from '@eslint/js'
|
||||
import pluginReact from 'eslint-plugin-react'
|
||||
import reactCompiler from 'eslint-plugin-react-compiler'
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,jsx}'],
|
||||
languageOptions: {
|
||||
...pluginReact.configs.flat.recommended.languageOptions,
|
||||
globals: {
|
||||
...globals.browser,
|
||||
process: 'readonly'
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: '16'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
'react-compiler': reactCompiler
|
||||
}
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
{
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react-compiler/react-compiler': 'warn'
|
||||
}
|
||||
},
|
||||
eslintConfigPrettier
|
||||
]
|
||||
20
new-lamassu-admin/index.html
Normal file
20
new-lamassu-admin/index.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="robots" content="noindex"/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>Lamassu Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
33530
new-lamassu-admin/package-lock.json
generated
33530
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,13 @@
|
|||
"name": "lamassu-admin",
|
||||
"version": "0.2.1",
|
||||
"license": "../LICENSE",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@apollo/react-hooks": "^3.1.3",
|
||||
"@lamassu/coins": "v1.4.0-beta.4",
|
||||
"@material-ui/core": "4.11.0",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.56",
|
||||
"@lamassu/coins": "v1.5.3",
|
||||
"@material-ui/core": "4.12.4",
|
||||
"@material-ui/icons": "4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@simplewebauthn/browser": "^3.0.0",
|
||||
"@use-hooks/axios": "1.3.0",
|
||||
"apollo-cache-inmemory": "^1.6.6",
|
||||
|
|
@ -27,12 +28,11 @@
|
|||
"downshift": "3.3.4",
|
||||
"file-saver": "2.0.2",
|
||||
"formik": "2.2.0",
|
||||
"google-libphonenumber": "^3.2.22",
|
||||
"graphql": "^14.5.8",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jss-plugin-extend": "^10.0.0",
|
||||
"jszip": "^3.6.0",
|
||||
"libphonenumber-js": "^1.7.50",
|
||||
"libphonenumber-js": "^1.11.15",
|
||||
"match-sorter": "^4.2.0",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"qrcode.react": "0.9.3",
|
||||
|
|
@ -41,69 +41,34 @@
|
|||
"react-copy-to-clipboard": "^5.0.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"react-dropzone": "^11.4.2",
|
||||
"react-material-ui-carousel": "^2.2.7",
|
||||
"react-material-ui-carousel": "^2.3.11",
|
||||
"react-number-format": "^4.4.1",
|
||||
"react-otp-input": "^2.3.0",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-use": "15.3.2",
|
||||
"react-virtualized": "^9.21.2",
|
||||
"sanctuary": "^2.0.1",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "0.32.9"
|
||||
"yup": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "6.0.26",
|
||||
"@storybook/addon-backgrounds": "6.0.26",
|
||||
"@storybook/addon-knobs": "6.0.26",
|
||||
"@storybook/addon-links": "6.0.26",
|
||||
"@storybook/addons": "6.0.26",
|
||||
"@storybook/preset-create-react-app": "^3.1.4",
|
||||
"@storybook/react": "6.0.26",
|
||||
"@welldone-software/why-did-you-render": "^3.3.9",
|
||||
"eslint": "^7.19.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-config-prettier-standard": "^3.0.1",
|
||||
"eslint-config-standard": "^14.1.0",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-node": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"husky": "^3.1.0",
|
||||
"lint-staged": "^9.5.0",
|
||||
"patch-package": "^6.2.2",
|
||||
"prettier": "1.19.1",
|
||||
"prettier-config-standard": "^1.0.1",
|
||||
"react-scripts": "4.0.0",
|
||||
"serve": "^11.3.2",
|
||||
"source-map-explorer": "^2.4.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@vitejs/plugin-react-swc": "^3.7.2",
|
||||
"esbuild-plugin-react-virtualized": "^1.0.4",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||
"globals": "^15.13.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"prettier": "3.4.1",
|
||||
"vite": "^6.0.1",
|
||||
"vite-plugin-svgr": "^4.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"fix": "eslint --fix --ext .js,.md,.json src/",
|
||||
"build": "react-scripts build",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"storybook": "start-storybook -p 9009 -s public",
|
||||
"postinstall": "patch-package",
|
||||
"build-storybook": "build-storybook -s public",
|
||||
"lamassu": "REACT_APP_BUILD_TARGET=LAMASSU react-scripts start",
|
||||
"pazuz": "REACT_APP_BUILD_TARGET=PAZUZ react-scripts start"
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
@ -116,5 +81,9 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,md,json}": "eslint --cache --fix",
|
||||
"*.{js,jsx,css,md,json}": "prettier --write"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="robots" content="noindex"/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Lamassu Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root" class="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
2
new-lamassu-admin/public/robots.txt
Normal file
2
new-lamassu-admin/public/robots.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
User-agent: *
|
||||
Disallow: /
|
||||
|
|
@ -17,21 +17,16 @@ import {
|
|||
useHistory,
|
||||
BrowserRouter as Router
|
||||
} from 'react-router-dom'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import Header from 'src/components/layout/Header'
|
||||
import Sidebar from 'src/components/layout/Sidebar'
|
||||
import TitleSection from 'src/components/layout/TitleSection'
|
||||
import { tree, hasSidebar, Routes, getParent } from 'src/routing/routes'
|
||||
import ApolloProvider from 'src/utils/apollo'
|
||||
|
||||
import AppContext from 'src/AppContext'
|
||||
import global from 'src/styling/global'
|
||||
import theme from 'src/styling/theme'
|
||||
import { backgroundColor, mainWidth } from 'src/styling/variables'
|
||||
import ApolloProvider from 'src/utils/apollo'
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render')
|
||||
whyDidYouRender(React)
|
||||
}
|
||||
|
||||
const jss = create({
|
||||
plugins: [extendJss(), ...jssPreset().plugins]
|
||||
|
|
@ -120,17 +115,11 @@ const Main = () => {
|
|||
)}
|
||||
<main className={classes.wrapper}>
|
||||
{sidebar && !is404 && wizardTested && (
|
||||
<Slide
|
||||
direction="left"
|
||||
in={true}
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
children={
|
||||
<div>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Slide direction="left" in={true} mountOnEnter unmountOnExit>
|
||||
<div>
|
||||
<TitleSection title={parent.title}></TitleSection>
|
||||
</div>
|
||||
</Slide>
|
||||
)}
|
||||
|
||||
<Grid container className={classes.grid}>
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React, { memo } from 'react'
|
||||
import ReactCarousel from 'react-material-ui-carousel'
|
||||
|
||||
import { ReactComponent as LeftArrow } from 'src/styling/icons/arrow/carousel-left-arrow.svg'
|
||||
import { ReactComponent as RightArrow } from 'src/styling/icons/arrow/carousel-right-arrow.svg'
|
||||
import { URI } from 'src/utils/apollo'
|
||||
import LeftArrow from 'src/styling/icons/arrow/carousel-left-arrow.svg?react'
|
||||
import RightArrow from 'src/styling/icons/arrow/carousel-right-arrow.svg?react'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
imgWrapper: {
|
||||
|
|
@ -13,9 +11,10 @@ const useStyles = makeStyles({
|
|||
display: 'flex'
|
||||
},
|
||||
imgInner: {
|
||||
objectFit: 'cover',
|
||||
objectFit: 'contain',
|
||||
objectPosition: 'center',
|
||||
width: 500,
|
||||
height: 400,
|
||||
marginBottom: 40
|
||||
}
|
||||
})
|
||||
|
|
@ -48,11 +47,11 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
|
|||
next={activeIndex => slidePhoto(activeIndex)}
|
||||
prev={activeIndex => slidePhoto(activeIndex)}>
|
||||
{photosData.map((item, i) => (
|
||||
<div>
|
||||
<div key={i}>
|
||||
<div className={classes.imgWrapper}>
|
||||
<img
|
||||
className={classes.imgInner}
|
||||
src={`${URI}/${item?.photoDir}/${item?.path}`}
|
||||
src={`/${item?.photoDir}/${item?.path}`}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -6,11 +6,11 @@ import {
|
|||
InputLabel
|
||||
} from '@material-ui/core'
|
||||
import React, { memo, useState } from 'react'
|
||||
import { H4, P } from 'src/components/typography'
|
||||
import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { Button, IconButton } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs'
|
||||
import { H4, P } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { spacer } from 'src/styling/variables'
|
||||
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
|
|
@ -1,98 +1,98 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
makeStyles
|
||||
} from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import { Button, IconButton } from 'src/components/buttons'
|
||||
import { H4, P } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { spacer } from 'src/styling/variables'
|
||||
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
width: 434,
|
||||
padding: spacer * 2,
|
||||
paddingRight: spacer * 3.5
|
||||
},
|
||||
titleSection: {
|
||||
padding: spacer * 2,
|
||||
paddingRight: spacer * 1.5,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: 0
|
||||
},
|
||||
actions: {
|
||||
padding: spacer * 4,
|
||||
paddingTop: spacer * 2
|
||||
},
|
||||
title: {
|
||||
margin: 0
|
||||
},
|
||||
closeButton: {
|
||||
padding: 0,
|
||||
marginTop: -(spacer / 2)
|
||||
}
|
||||
})
|
||||
|
||||
export const DialogTitle = ({ children, close }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classes.titleSection}>
|
||||
{children}
|
||||
{close && (
|
||||
<IconButton
|
||||
size={16}
|
||||
aria-label="close"
|
||||
onClick={close}
|
||||
className={classes.closeButton}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DeleteDialog = ({
|
||||
title = 'Confirm Delete',
|
||||
open = false,
|
||||
onConfirmed,
|
||||
onDismissed,
|
||||
item = 'item',
|
||||
confirmationMessage = `Are you sure you want to delete this ${item}?`,
|
||||
extraMessage,
|
||||
errorMessage = ''
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Dialog open={open} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle close={() => onDismissed()}>
|
||||
<H4 className={classes.title}>{title}</H4>
|
||||
</DialogTitle>
|
||||
{errorMessage && (
|
||||
<DialogTitle>
|
||||
<ErrorMessage>
|
||||
{errorMessage.split(':').map(error => (
|
||||
<>
|
||||
{error}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</ErrorMessage>
|
||||
</DialogTitle>
|
||||
)}
|
||||
<DialogContent className={classes.content}>
|
||||
{confirmationMessage && <P>{confirmationMessage}</P>}
|
||||
{extraMessage}
|
||||
</DialogContent>
|
||||
<DialogActions className={classes.actions}>
|
||||
<Button onClick={onConfirmed}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
makeStyles
|
||||
} from '@material-ui/core'
|
||||
import React from 'react'
|
||||
import { H4, P } from 'src/components/typography'
|
||||
import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { Button, IconButton } from 'src/components/buttons'
|
||||
import { spacer } from 'src/styling/variables'
|
||||
|
||||
import ErrorMessage from './ErrorMessage'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
content: {
|
||||
width: 434,
|
||||
padding: spacer * 2,
|
||||
paddingRight: spacer * 3.5
|
||||
},
|
||||
titleSection: {
|
||||
padding: spacer * 2,
|
||||
paddingRight: spacer * 1.5,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
margin: 0
|
||||
},
|
||||
actions: {
|
||||
padding: spacer * 4,
|
||||
paddingTop: spacer * 2
|
||||
},
|
||||
title: {
|
||||
margin: 0
|
||||
},
|
||||
closeButton: {
|
||||
padding: 0,
|
||||
marginTop: -(spacer / 2)
|
||||
}
|
||||
})
|
||||
|
||||
export const DialogTitle = ({ children, close }) => {
|
||||
const classes = useStyles()
|
||||
return (
|
||||
<div className={classes.titleSection}>
|
||||
{children}
|
||||
{close && (
|
||||
<IconButton
|
||||
size={16}
|
||||
aria-label="close"
|
||||
onClick={close}
|
||||
className={classes.closeButton}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const DeleteDialog = ({
|
||||
title = 'Confirm Delete',
|
||||
open = false,
|
||||
onConfirmed,
|
||||
onDismissed,
|
||||
item = 'item',
|
||||
confirmationMessage = `Are you sure you want to delete this ${item}?`,
|
||||
extraMessage,
|
||||
errorMessage = ''
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return (
|
||||
<Dialog open={open} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle close={() => onDismissed()}>
|
||||
<H4 className={classes.title}>{title}</H4>
|
||||
</DialogTitle>
|
||||
{errorMessage && (
|
||||
<DialogTitle>
|
||||
<ErrorMessage>
|
||||
{errorMessage.split(':').map(error => (
|
||||
<>
|
||||
{error}
|
||||
<br />
|
||||
</>
|
||||
))}
|
||||
</ErrorMessage>
|
||||
</DialogTitle>
|
||||
)}
|
||||
<DialogContent className={classes.content}>
|
||||
{confirmationMessage && <P>{confirmationMessage}</P>}
|
||||
{extraMessage}
|
||||
</DialogContent>
|
||||
<DialogActions className={classes.actions}>
|
||||
<Button onClick={onConfirmed}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import ErrorIcon from 'src/styling/icons/warning-icon/tomato.svg?react'
|
||||
|
||||
import { ReactComponent as ErrorIcon } from 'src/styling/icons/warning-icon/tomato.svg'
|
||||
import { errorColor } from 'src/styling/variables'
|
||||
|
||||
import { Info3 } from './typography'
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import { makeStyles, ClickAwayListener } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
|
||||
import Popper from 'src/components/Popper'
|
||||
import ZoomIconInverse from 'src/styling/icons/circle buttons/search/white.svg?react'
|
||||
import ZoomIcon from 'src/styling/icons/circle buttons/search/zodiac.svg?react'
|
||||
|
||||
import { FeatureButton } from 'src/components/buttons'
|
||||
import { ReactComponent as ZoomIconInverse } from 'src/styling/icons/circle buttons/search/white.svg'
|
||||
import { ReactComponent as ZoomIcon } from 'src/styling/icons/circle buttons/search/zodiac.svg'
|
||||
|
||||
import imagePopperStyles from './ImagePopper.styles'
|
||||
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { Box, makeStyles } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import { Label1 } from 'src/components/typography'
|
||||
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/comet.svg'
|
||||
import WarningIcon from 'src/styling/icons/warning-icon/comet.svg?react'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
message: ({ width }) => ({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { Dialog, DialogContent, makeStyles } from '@material-ui/core'
|
||||
import React, { memo } from 'react'
|
||||
import { H1 } from 'src/components/typography'
|
||||
import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { H1 } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { spacer } from 'src/styling/variables'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
|
|
@ -5,11 +5,11 @@ import { format, set } from 'date-fns/fp'
|
|||
import FileSaver from 'file-saver'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useCallback } from 'react'
|
||||
import Arrow from 'src/styling/icons/arrow/download_logs.svg?react'
|
||||
import DownloadInverseIcon from 'src/styling/icons/button/download/white.svg?react'
|
||||
import Download from 'src/styling/icons/button/download/zodiac.svg?react'
|
||||
|
||||
import { FeatureButton, Link } from 'src/components/buttons'
|
||||
import { ReactComponent as Arrow } from 'src/styling/icons/arrow/download_logs.svg'
|
||||
import { ReactComponent as DownloadInverseIcon } from 'src/styling/icons/button/download/white.svg'
|
||||
import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg'
|
||||
import { primaryColor, offColor, zircon } from 'src/styling/variables'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { makeStyles, Modal as MaterialModal, Paper } from '@material-ui/core'
|
||||
import classnames from 'classnames'
|
||||
import React from 'react'
|
||||
import { H1, H4 } from 'src/components/typography'
|
||||
import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||
|
||||
import { IconButton } from 'src/components/buttons'
|
||||
import { H1, H4 } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
|
||||
const styles = {
|
||||
modal: {
|
||||
|
|
@ -55,8 +55,8 @@ const styles = {
|
|||
margin: xl
|
||||
? [[0, 0, 'auto', 'auto']]
|
||||
: small
|
||||
? [[12, 12, 'auto', 'auto']]
|
||||
: [[16, 16, 'auto', 'auto']]
|
||||
? [[12, 12, 'auto', 'auto']]
|
||||
: [[16, 16, 'auto', 'auto']]
|
||||
}),
|
||||
header: {
|
||||
display: 'flex'
|
||||
|
|
@ -3,13 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { H5 } from 'src/components/typography'
|
||||
import { ReactComponent as NotificationIconZodiac } from 'src/styling/icons/menu/notification-zodiac.svg'
|
||||
import { ReactComponent as ClearAllIconInverse } from 'src/styling/icons/stage/spring/empty.svg'
|
||||
import { ReactComponent as ClearAllIcon } from 'src/styling/icons/stage/zodiac/empty.svg'
|
||||
import { ReactComponent as ShowUnreadIcon } from 'src/styling/icons/stage/zodiac/full.svg'
|
||||
import NotificationIconZodiac from 'src/styling/icons/menu/notification-zodiac.svg?react'
|
||||
import ClearAllIconInverse from 'src/styling/icons/stage/spring/empty.svg?react'
|
||||
import ClearAllIcon from 'src/styling/icons/stage/zodiac/empty.svg?react'
|
||||
import ShowUnreadIcon from 'src/styling/icons/stage/zodiac/full.svg?react'
|
||||
|
||||
import styles from './NotificationCenter.styles'
|
||||
import NotificationRow from './NotificationRow'
|
||||
|
|
@ -3,11 +3,10 @@ import classnames from 'classnames'
|
|||
import prettyMs from 'pretty-ms'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
|
||||
import { Label1, Label2, TL2 } from 'src/components/typography'
|
||||
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg'
|
||||
import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg'
|
||||
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg'
|
||||
import Wrench from 'src/styling/icons/action/wrench/zodiac.svg?react'
|
||||
import Transaction from 'src/styling/icons/arrow/transaction.svg?react'
|
||||
import WarningIcon from 'src/styling/icons/warning-icon/tomato.svg?react'
|
||||
|
||||
import styles from './NotificationCenter.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
|
@ -61,8 +60,8 @@ const NotificationRow = ({
|
|||
typeDisplay && deviceName
|
||||
? `${typeDisplay} - ${deviceName}`
|
||||
: !typeDisplay && deviceName
|
||||
? `${deviceName}`
|
||||
: `${typeDisplay}`
|
||||
? `${deviceName}`
|
||||
: `${typeDisplay}`
|
||||
|
||||
const iconClass = {
|
||||
[classes.readIcon]: read,
|
||||
|
|
@ -105,20 +105,29 @@ const Popover = ({
|
|||
|
||||
const classes = useStyles()
|
||||
|
||||
const arrowClasses = {
|
||||
const getArrowClasses = placement => ({
|
||||
[classes.arrow]: true,
|
||||
[classes.arrowBottom]: props.placement === 'bottom',
|
||||
[classes.arrowTop]: props.placement === 'top',
|
||||
[classes.arrowRight]: props.placement === 'right',
|
||||
[classes.arrowLeft]: props.placement === 'left'
|
||||
[classes.arrowBottom]: placement === 'bottom',
|
||||
[classes.arrowTop]: placement === 'top',
|
||||
[classes.arrowRight]: placement === 'right',
|
||||
[classes.arrowLeft]: placement === 'left'
|
||||
})
|
||||
|
||||
const flipPlacements = {
|
||||
top: ['bottom'],
|
||||
bottom: ['top'],
|
||||
left: ['right'],
|
||||
right: ['left']
|
||||
}
|
||||
|
||||
const modifiers = R.merge(props.modifiers, {
|
||||
const modifiers = R.mergeDeepLeft(props.modifiers, {
|
||||
flip: {
|
||||
enabled: false
|
||||
enabled: R.defaultTo(false, props.flip),
|
||||
allowedAutoPlacements: flipPlacements[props.placement],
|
||||
boundary: 'clippingParents'
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
enabled: R.defaultTo(true, props.preventOverflow),
|
||||
boundariesElement: 'scrollParent'
|
||||
},
|
||||
offset: {
|
||||
|
|
@ -126,7 +135,7 @@ const Popover = ({
|
|||
offset: '0, 10'
|
||||
},
|
||||
arrow: {
|
||||
enabled: true,
|
||||
enabled: R.defaultTo(true, props.showArrow),
|
||||
element: arrowRef
|
||||
},
|
||||
computeStyle: {
|
||||
|
|
@ -134,6 +143,12 @@ const Popover = ({
|
|||
}
|
||||
})
|
||||
|
||||
if (props.preventOverflow === false) {
|
||||
modifiers.hide = {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MaterialPopper
|
||||
|
|
@ -141,10 +156,15 @@ const Popover = ({
|
|||
modifiers={modifiers}
|
||||
className={classes.popover}
|
||||
{...props}>
|
||||
<Paper className={classnames(classes.root, className)}>
|
||||
<span className={classnames(arrowClasses)} ref={setArrowRef} />
|
||||
{children}
|
||||
</Paper>
|
||||
{({ placement }) => (
|
||||
<Paper className={classnames(classes.root, className)}>
|
||||
<span
|
||||
className={classnames(getArrowClasses(placement))}
|
||||
ref={setArrowRef}
|
||||
/>
|
||||
{children}
|
||||
</Paper>
|
||||
)}
|
||||
</MaterialPopper>
|
||||
</>
|
||||
)
|
||||
|
|
@ -4,9 +4,8 @@ import { makeStyles } from '@material-ui/core/styles'
|
|||
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState } from 'react'
|
||||
|
||||
import { P } from 'src/components/typography'
|
||||
import { ReactComponent as SearchIcon } from 'src/styling/icons/circle buttons/search/zodiac.svg'
|
||||
import SearchIcon from 'src/styling/icons/circle buttons/search/zodiac.svg?react'
|
||||
|
||||
import styles from './SearchBox.styles'
|
||||
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { makeStyles } from '@material-ui/core'
|
||||
import React from 'react'
|
||||
|
||||
import Chip from 'src/components/Chip'
|
||||
import { ActionButton } from 'src/components/buttons'
|
||||
import { P, Label3 } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg'
|
||||
import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg'
|
||||
import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
|
||||
import FilterIcon from 'src/styling/icons/button/filter/white.svg?react'
|
||||
import ReverseFilterIcon from 'src/styling/icons/button/filter/zodiac.svg?react'
|
||||
|
||||
import { ActionButton } from 'src/components/buttons'
|
||||
import { onlyFirstToUpper, singularOrPlural } from 'src/utils/string'
|
||||
|
||||
import { chipStyles, styles } from './SearchFilter.styles'
|
||||
|
|
@ -2,13 +2,13 @@ import { makeStyles } from '@material-ui/core'
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
import CompleteStageIconSpring from 'src/styling/icons/stage/spring/complete.svg?react'
|
||||
import CurrentStageIconSpring from 'src/styling/icons/stage/spring/current.svg?react'
|
||||
import EmptyStageIconSpring from 'src/styling/icons/stage/spring/empty.svg?react'
|
||||
import CompleteStageIconZodiac from 'src/styling/icons/stage/zodiac/complete.svg?react'
|
||||
import CurrentStageIconZodiac from 'src/styling/icons/stage/zodiac/current.svg?react'
|
||||
import EmptyStageIconZodiac from 'src/styling/icons/stage/zodiac/empty.svg?react'
|
||||
|
||||
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconSpring } from 'src/styling/icons/stage/spring/current.svg'
|
||||
import { ReactComponent as EmptyStageIconSpring } from 'src/styling/icons/stage/spring/empty.svg'
|
||||
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg'
|
||||
import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg'
|
||||
import {
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
|
|
@ -2,13 +2,13 @@ import { makeStyles } from '@material-ui/core'
|
|||
import classnames from 'classnames'
|
||||
import * as R from 'ramda'
|
||||
import React, { memo } from 'react'
|
||||
import CompleteStageIconSpring from 'src/styling/icons/stage/spring/complete.svg?react'
|
||||
import CurrentStageIconSpring from 'src/styling/icons/stage/spring/current.svg?react'
|
||||
import EmptyStageIconSpring from 'src/styling/icons/stage/spring/empty.svg?react'
|
||||
import CompleteStageIconZodiac from 'src/styling/icons/stage/zodiac/complete.svg?react'
|
||||
import CurrentStageIconZodiac from 'src/styling/icons/stage/zodiac/current.svg?react'
|
||||
import EmptyStageIconZodiac from 'src/styling/icons/stage/zodiac/empty.svg?react'
|
||||
|
||||
import { ReactComponent as CompleteStageIconSpring } from 'src/styling/icons/stage/spring/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconSpring } from 'src/styling/icons/stage/spring/current.svg'
|
||||
import { ReactComponent as EmptyStageIconSpring } from 'src/styling/icons/stage/spring/empty.svg'
|
||||
import { ReactComponent as CompleteStageIconZodiac } from 'src/styling/icons/stage/zodiac/complete.svg'
|
||||
import { ReactComponent as CurrentStageIconZodiac } from 'src/styling/icons/stage/zodiac/current.svg'
|
||||
import { ReactComponent as EmptyStageIconZodiac } from 'src/styling/icons/stage/zodiac/empty.svg'
|
||||
import {
|
||||
primaryColor,
|
||||
secondaryColor,
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import { makeStyles, ClickAwayListener } from '@material-ui/core'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, memo } from 'react'
|
||||
|
||||
import Popper from 'src/components/Popper'
|
||||
import { ReactComponent as HelpIcon } from 'src/styling/icons/action/help/zodiac.svg'
|
||||
import HelpIcon from 'src/styling/icons/action/help/zodiac.svg?react'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
transparentButton: {
|
||||
|
|
@ -13,6 +12,16 @@ const useStyles = makeStyles({
|
|||
cursor: 'pointer',
|
||||
marginTop: 4
|
||||
},
|
||||
relativelyPositioned: {
|
||||
position: 'relative'
|
||||
},
|
||||
safeSpace: {
|
||||
position: 'absolute',
|
||||
backgroundColor: '#0000',
|
||||
height: 40,
|
||||
left: '-50%',
|
||||
width: '200%'
|
||||
},
|
||||
popoverContent: ({ width }) => ({
|
||||
width,
|
||||
padding: [[10, 15]]
|
||||
|
|
@ -27,6 +36,10 @@ const usePopperHandler = width => {
|
|||
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
|
||||
}
|
||||
|
||||
const openHelpPopper = event => {
|
||||
setHelpPopperAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleCloseHelpPopper = () => {
|
||||
setHelpPopperAnchorEl(null)
|
||||
}
|
||||
|
|
@ -38,25 +51,32 @@ const usePopperHandler = width => {
|
|||
helpPopperAnchorEl,
|
||||
helpPopperOpen,
|
||||
handleOpenHelpPopper,
|
||||
openHelpPopper,
|
||||
handleCloseHelpPopper
|
||||
}
|
||||
}
|
||||
|
||||
const Tooltip = memo(({ children, width, Icon = HelpIcon }) => {
|
||||
const HelpTooltip = memo(({ children, width }) => {
|
||||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div>
|
||||
<div
|
||||
className={handler.classes.relativelyPositioned}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}>
|
||||
{handler.helpPopperOpen && (
|
||||
<div className={handler.classes.safeSpace}></div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={handler.classes.transparentButton}
|
||||
onClick={handler.handleOpenHelpPopper}>
|
||||
<Icon />
|
||||
onMouseEnter={handler.openHelpPopper}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
arrowEnabled={true}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
|
|
@ -69,31 +89,33 @@ const HoverableTooltip = memo(({ parentElements, children, width }) => {
|
|||
const handler = usePopperHandler(width)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!R.isNil(parentElements) && (
|
||||
<div
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}>
|
||||
{parentElements}
|
||||
</div>
|
||||
)}
|
||||
{R.isNil(parentElements) && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
className={handler.classes.transparentButton}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
)}
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
</div>
|
||||
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
|
||||
<div>
|
||||
{!R.isNil(parentElements) && (
|
||||
<div
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
onMouseEnter={handler.handleOpenHelpPopper}>
|
||||
{parentElements}
|
||||
</div>
|
||||
)}
|
||||
{R.isNil(parentElements) && (
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={handler.handleOpenHelpPopper}
|
||||
onMouseLeave={handler.handleCloseHelpPopper}
|
||||
className={handler.classes.transparentButton}>
|
||||
<HelpIcon />
|
||||
</button>
|
||||
)}
|
||||
<Popper
|
||||
open={handler.helpPopperOpen}
|
||||
anchorEl={handler.helpPopperAnchorEl}
|
||||
placement="bottom">
|
||||
<div className={handler.classes.popoverContent}>{children}</div>
|
||||
</Popper>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
)
|
||||
})
|
||||
|
||||
export { Tooltip, HoverableTooltip }
|
||||
export { HoverableTooltip, HelpTooltip }
|
||||
|
|
@ -3,17 +3,17 @@ import classnames from 'classnames'
|
|||
import { useFormikContext, Form, Formik, Field as FormikField } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, memo } from 'react'
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import EditIconDisabled from 'src/styling/icons/action/edit/disabled.svg?react'
|
||||
import EditIcon from 'src/styling/icons/action/edit/enabled.svg?react'
|
||||
import FalseIcon from 'src/styling/icons/table/false.svg?react'
|
||||
import TrueIcon from 'src/styling/icons/table/true.svg?react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import PromptWhenDirty from 'src/components/PromptWhenDirty'
|
||||
import { Link, IconButton } from 'src/components/buttons'
|
||||
import { RadioGroup } from 'src/components/inputs/formik'
|
||||
import { Table, TableBody, TableRow, TableCell } from 'src/components/table'
|
||||
import { H4 } from 'src/components/typography'
|
||||
import { ReactComponent as EditIconDisabled } from 'src/styling/icons/action/edit/disabled.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as FalseIcon } from 'src/styling/icons/table/false.svg'
|
||||
import { ReactComponent as TrueIcon } from 'src/styling/icons/table/true.svg'
|
||||
|
||||
import { booleanPropertiesTableStyles } from './BooleanPropertiesTable.styles'
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo } from 'react'
|
||||
import AddIcon from 'src/styling/icons/button/add/zodiac.svg?react'
|
||||
|
||||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
|
||||
import { zircon, zircon2, comet, fontColor, white } from 'src/styling/variables'
|
||||
|
||||
const { p } = typographyStyles
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue