Merge branch 'dev' into releases/v10.1

This commit is contained in:
Rafael Taranto 2025-01-07 14:47:10 +00:00 committed by GitHub
commit 320e76fdae
393 changed files with 11183 additions and 31834 deletions

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
nodejs 14

View file

@ -25,8 +25,8 @@ EXPOSE 3000
ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ] ENTRYPOINT [ "/lamassu-server/bin/lamassu-server-entrypoint.sh" ]
FROM alpine:3.14 AS build-ui FROM node:22-alpine AS build-ui
RUN apk add --no-cache nodejs npm git curl build-base python3 RUN apk add --no-cache npm git curl build-base python3
COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"] COPY ["new-lamassu-admin/package.json", "new-lamassu-admin/package-lock.json", "./"]

View file

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

View file

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

View file

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

View file

@ -29,39 +29,49 @@ module.exports = {
const BINARIES = { const BINARIES = {
BTC: { BTC: {
defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://bitcoincore.org/bin/bitcoin-core-0.20.1/bitcoin-0.20.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: '376194f06596ecfa40331167c39bc70c355f960280bd2a645fdbf18f66527397',
defaultDir: 'bitcoin-0.20.1/bin', defaultDir: 'bitcoin-0.20.1/bin',
url: 'https://bitcoincore.org/bin/bitcoin-core-28.0/bitcoin-28.0-x86_64-linux-gnu.tar.gz', 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: { ETH: {
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.14.12-293a300d.tar.gz', 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: { ZEC: {
url: 'https://download.z.cash/downloads/zcash-6.0.0-linux64-debian-bullseye.tar.gz', 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: { DASH: {
defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'd89c2afd78183f3ee815adcccdff02098be0c982633889e7b1e9c9656fbef219',
defaultDir: 'dashcore-18.1.0/bin', defaultDir: 'dashcore-18.1.0/bin',
url: 'https://github.com/dashpay/dash/releases/download/v21.1.1/dashcore-21.1.1-x86_64-linux-gnu.tar.gz', 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' dir: 'dashcore-21.1.1/bin'
urlHash: 'c3157d4a82a3cb7c904a68e827bd1e629854fefcc0dcaf1de4343a810a190bf5',
}, },
LTC: { LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
defaultUrlHash: 'ca50936299e2c5a66b954c266dcaaeef9e91b2f5307069b9894048acf3eb5751',
defaultDir: 'litecoin-0.18.1/bin', defaultDir: 'litecoin-0.18.1/bin',
url: 'https://download.litecoin.org/litecoin-0.21.4/linux/litecoin-0.21.4-x86_64-linux-gnu.tar.gz', 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: { 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', 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', 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: { XMR: {
url: 'https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.3.4.tar.bz2', 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', 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}`) if (!binaries) throw new Error(`No such coin: ${coinRec.code}`)
const url = requiresUpdate ? binaries.defaultUrl : binaries.url const url = requiresUpdate ? binaries.defaultUrl : binaries.url
const hash = requiresUpdate ? binaries.defaultUrlHash : binaries.urlHash
const downloadFile = path.basename(url) const downloadFile = path.basename(url)
const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir const binDir = requiresUpdate ? binaries.defaultDir : binaries.dir
es(`wget -q ${url}`) es(`wget -q ${url}`)
if (es(`sha256sum ${downloadFile} | awk '{print $1}'`).trim() !== hash) {
logger.info(`Failed to install ${coinRec.code}: Package signature do not match!`)
return
}
es(`tar -xf ${downloadFile}`) es(`tar -xf ${downloadFile}`)
const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin' const usrBinDir = isDevMode() ? path.resolve(BLOCKCHAIN_DIR, 'bin') : '/usr/local/bin'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,38 +32,41 @@ function post (machineTx, pi) {
return cashInAtomic.atomic(machineTx, pi) return cashInAtomic.atomic(machineTx, pi)
.then(r => { .then(r => {
const updatedTx = r.tx const updatedTx = r.tx
let blacklisted = false
let addressReuse = false let addressReuse = false
let walletScore = {}
const promises = [settingsLoader.loadLatestConfig()] const promises = [settingsLoader.loadLatestConfig()]
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero() const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
if (isFirstPost) { if (isFirstPost) {
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi)) promises.push(
checkForBlacklisted(updatedTx),
doesTxReuseAddress(updatedTx),
getWalletScore(updatedTx, pi)
)
} }
return Promise.all(promises) return Promise.all(promises)
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => { .then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse const { rejectAddressReuse } = configManager.getCompliance(config)
const isBlacklisted = !!blacklisted
walletScore = fetchedWalletScore if (isBlacklisted) {
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
blacklisted = true
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false) notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
} else if (isReusedAddress && rejectAddressReuse) { } else if (isReusedAddress && rejectAddressReuse) {
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true) notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
addressReuse = true addressReuse = true
} }
return postProcess(r, pi, blacklisted, addressReuse, walletScore) return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(_.flow(
_.set('bills', machineTx.bills),
_.set('blacklisted', isBlacklisted),
_.set('blacklistMessage', blacklisted?.content),
_.set('addressReuse', addressReuse),
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
))
}) })
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
.then(changes => cashInLow.update(db, updatedTx, changes))
.then(tx => _.set('bills', machineTx.bills, tx))
.then(tx => _.set('blacklisted', blacklisted, tx))
.then(tx => _.set('addressReuse', addressReuse, tx))
.then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx))
}) })
} }
@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) {
} }
function checkForBlacklisted (tx) { function checkForBlacklisted (tx) {
return blacklist.blocked(tx.toAddress, tx.cryptoCode) return blacklist.blocked(tx.toAddress)
} }
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
@ -148,18 +151,17 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
} }
}) })
.catch(err => { .catch(err => {
// Important: We don't know what kind of error this is // Important: We don't know what kind of error this is
// so not safe to assume that funds weren't sent. // 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 // Setting sendPending to true ensures that the transaction gets
// not to send. // silently terminated and no retries are done
const sendPending = err.name !== 'InsufficientFundsError'
return { return {
sendTime: 'now()^', sendTime: 'now()^',
error: err.message, error: err.message,
errorCode: err.name, errorCode: err.name,
sendPending sendPending: true
} }
}) })
.then(sendRec => { .then(sendRec => {

View file

@ -51,7 +51,7 @@ const mapValuesWithKey = _.mapValues.convert({cap: false})
function convertBigNumFields (obj) { function convertBigNumFields (obj) {
const convert = (value, key) => { const convert = (value, key) => {
if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat' ])) { if (_.includes(key, [ 'cryptoAtoms', 'receivedCryptoAtoms', 'fiat', 'fixedFee' ])) {
return value.toString() return value.toString()
} }

View file

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

View file

@ -29,6 +29,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
const cashInFee = showCommissions ? commissions.cashIn / 100 : null const cashInFee = showCommissions ? commissions.cashIn / 100 : null
const cashOutFee = showCommissions ? commissions.cashOut / 100 : null const cashOutFee = showCommissions ? commissions.cashOut / 100 : null
const cashInFixedFee = showCommissions ? commissions.fixedFee : null const cashInFixedFee = showCommissions ? commissions.fixedFee : null
const cashOutFixedFee = showCommissions ? commissions.cashOutFixedFee : null
const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null const cashInRate = showCommissions ? _.invoke('cashIn.toNumber', buildedRates) : null
const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null const cashOutRate = showCommissions ? _.invoke('cashOut.toNumber', buildedRates) : null
@ -37,6 +38,7 @@ function mapCoin (rates, deviceId, settings, cryptoCode) {
cashInFee, cashInFee,
cashOutFee, cashOutFee,
cashInFixedFee, cashInFixedFee,
cashOutFixedFee,
cashInRate, cashInRate,
cashOutRate cashOutRate
} }

View file

@ -80,7 +80,7 @@ function getWithEmail (email) {
* *
* @param {string} id Customer's id * @param {string} id Customer's id
* @param {object} data Fields to update * @param {object} data Fields to update
* @param {string} Acting user's token * @param {string} userToken Acting user's token
* *
* @returns {Promise} Newly updated Customer * @returns {Promise} Newly updated Customer
*/ */
@ -114,6 +114,7 @@ function update (id, data, userToken) {
async function updateCustomer (id, data, userToken) { async function updateCustomer (id, data, userToken) {
const formattedData = _.pick( const formattedData = _.pick(
[ [
'sanctions',
'authorized_override', 'authorized_override',
'id_card_photo_override', 'id_card_photo_override',
'id_card_data_override', 'id_card_data_override',
@ -229,7 +230,7 @@ function enhanceEditedPhotos (fields) {
/** /**
* Remove the edited data from the db record * Remove the edited data from the db record
* *
* @name enhanceOverrideFields * @name deleteEditedData
* @function * @function
* *
* @param {string} id Customer's id * @param {string} id Customer's id

View file

@ -1,6 +1,10 @@
const _ = require('lodash/fp')
const { ALL_CRYPTOS } = require('@lamassu/coins')
const configManager = require('./new-config-manager') const configManager = require('./new-config-manager')
const ccxt = require('./plugins/exchange/ccxt') const ccxt = require('./plugins/exchange/ccxt')
const mockExchange = require('./plugins/exchange/mock-exchange') const mockExchange = require('./plugins/exchange/mock-exchange')
const accounts = require('./new-admin/config/accounts')
function lookupExchange (settings, cryptoCode) { function lookupExchange (settings, cryptoCode) {
const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange const exchange = configManager.getWalletSettings(cryptoCode, settings.config).exchange
@ -45,8 +49,33 @@ function active (settings, cryptoCode) {
return !!lookupExchange(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 = { module.exports = {
fetchExchange,
buy, buy,
sell, sell,
active active,
getMarkets
} }

View file

@ -36,6 +36,7 @@ const addReceiptInfo = receiptInfo => ret => {
if (!receiptInfo) return ret if (!receiptInfo) return ret
const fields = [ const fields = [
'automaticPrint',
'paper', 'paper',
'sms', 'sms',
'operatorWebsite', 'operatorWebsite',
@ -61,6 +62,18 @@ const addReceiptInfo = receiptInfo => ret => {
} }
const addMachineScreenOpts = smth => _.update(
'screenOptions',
_.flow(
addSmthInfo(
'rates',
[
'active'
]
)(smth.rates)
)
)
/* TODO: Simplify this. */ /* TODO: Simplify this. */
const buildTriggers = allTriggers => { const buildTriggers = allTriggers => {
const normalTriggers = [] const normalTriggers = []
@ -89,6 +102,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
'cashInCommission', 'cashInCommission',
'cashInFee', 'cashInFee',
'cashOutCommission', 'cashOutCommission',
'cashOutFee',
'cryptoCode', 'cryptoCode',
'cryptoCodeDisplay', 'cryptoCodeDisplay',
'cryptoNetwork', 'cryptoNetwork',
@ -102,7 +116,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
_.pick([ _.pick([
'coins', 'coins',
'configVersion', 'configVersion',
'timezone' 'timezone',
'screenOptions'
]), ]),
_.update('coins', massageCoins), _.update('coins', massageCoins),
_.set('serverVersion', VERSION), _.set('serverVersion', VERSION),
@ -116,6 +131,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
configManager.getLocale(deviceId, settings.config), configManager.getLocale(deviceId, settings.config),
configManager.getOperatorInfo(settings.config), configManager.getOperatorInfo(settings.config),
configManager.getReceipt(settings.config), configManager.getReceipt(settings.config),
configManager.getAllMachineScreenOpts(settings.config),
!!configManager.getCashOut(deviceId, settings.config).active, !!configManager.getCashOut(deviceId, settings.config).active,
getMachine(deviceId, currentConfigVersion), getMachine(deviceId, currentConfigVersion),
configManager.getCustomerAuthenticationMethod(settings.config) configManager.getCustomerAuthenticationMethod(settings.config)
@ -128,6 +144,7 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
localeInfo, localeInfo,
operatorInfo, operatorInfo,
receiptInfo, receiptInfo,
machineScreenOpts,
twoWayMode, twoWayMode,
{ numberOfCassettes, numberOfRecyclers }, { numberOfCassettes, numberOfRecyclers },
customerAuthentication, customerAuthentication,
@ -152,7 +169,8 @@ const staticConfig = ({ currentConfigVersion, deviceId, deviceName, pq, settings
urlsToPing, urlsToPing,
}), }),
addOperatorInfo(operatorInfo), addOperatorInfo(operatorInfo),
addReceiptInfo(receiptInfo) addReceiptInfo(receiptInfo),
addMachineScreenOpts(machineScreenOpts)
)(staticConf)) )(staticConf))
} }

View file

@ -6,6 +6,7 @@ type Coin {
display: String! display: String!
minimumTx: String! minimumTx: String!
cashInFee: String! cashInFee: String!
cashOutFee: String!
cashInCommission: String! cashInCommission: String!
cashOutCommission: String! cashOutCommission: String!
cryptoNetwork: String! cryptoNetwork: String!
@ -37,6 +38,7 @@ type MachineInfo {
type ReceiptInfo { type ReceiptInfo {
paper: Boolean! paper: Boolean!
automaticPrint: Boolean!
sms: Boolean! sms: Boolean!
operatorWebsite: Boolean! operatorWebsite: Boolean!
operatorEmail: Boolean! operatorEmail: Boolean!
@ -48,6 +50,14 @@ type ReceiptInfo {
addressQRCode: Boolean! addressQRCode: Boolean!
} }
type MachineScreenOptions {
rates: RateScreenOptions!
}
type RateScreenOptions {
active: Boolean!
}
type SpeedtestFile { type SpeedtestFile {
url: String! url: String!
size: Int! size: Int!
@ -146,6 +156,7 @@ type StaticConfig {
operatorInfo: OperatorInfo operatorInfo: OperatorInfo
machineInfo: MachineInfo! machineInfo: MachineInfo!
receiptInfo: ReceiptInfo receiptInfo: ReceiptInfo
screenOptions: MachineScreenOptions
speedtestFiles: [SpeedtestFile!]! speedtestFiles: [SpeedtestFile!]!
urlsToPing: [String!]! urlsToPing: [String!]!

View file

@ -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

View file

@ -14,6 +14,7 @@ const { ApolloServer } = require('apollo-server-express')
require('../environment-helper') require('../environment-helper')
const { asyncLocalStorage, defaultStore } = require('../async-storage') const { asyncLocalStorage, defaultStore } = require('../async-storage')
const logger = require('../logger') const logger = require('../logger')
const exchange = require('../exchange')
const { AuthDirective } = require('./graphql/directives') const { AuthDirective } = require('./graphql/directives')
const { typeDefs, resolvers } = require('./graphql/schema') const { typeDefs, resolvers } = require('./graphql/schema')
@ -98,6 +99,9 @@ function run () {
const serverLog = `lamassu-admin-server listening on port ${serverPort}` const serverLog = `lamassu-admin-server listening on port ${serverPort}`
// cache markets on startup
exchange.getMarkets().catch(console.error)
const webServer = https.createServer(certOptions, app) const webServer = https.createServer(certOptions, app)
webServer.listen(serverPort, () => logger.info(serverLog)) webServer.listen(serverPort, () => logger.info(serverLog))
}) })

View file

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

View file

@ -11,9 +11,11 @@ const funding = require('./funding.resolver')
const log = require('./log.resolver') const log = require('./log.resolver')
const loyalty = require('./loyalty.resolver') const loyalty = require('./loyalty.resolver')
const machine = require('./machine.resolver') const machine = require('./machine.resolver')
const market = require('./market.resolver')
const notification = require('./notification.resolver') const notification = require('./notification.resolver')
const pairing = require('./pairing.resolver') const pairing = require('./pairing.resolver')
const rates = require('./rates.resolver') const rates = require('./rates.resolver')
const sanctions = require('./sanctions.resolver')
const scalar = require('./scalar.resolver') const scalar = require('./scalar.resolver')
const settings = require('./settings.resolver') const settings = require('./settings.resolver')
const sms = require('./sms.resolver') const sms = require('./sms.resolver')
@ -34,9 +36,11 @@ const resolvers = [
log, log,
loyalty, loyalty,
machine, machine,
market,
notification, notification,
pairing, pairing,
rates, rates,
sanctions,
scalar, scalar,
settings, settings,
sms, sms,

View file

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

View 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

View file

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

View file

@ -11,9 +11,11 @@ const funding = require('./funding.type')
const log = require('./log.type') const log = require('./log.type')
const loyalty = require('./loyalty.type') const loyalty = require('./loyalty.type')
const machine = require('./machine.type') const machine = require('./machine.type')
const market = require('./market.type')
const notification = require('./notification.type') const notification = require('./notification.type')
const pairing = require('./pairing.type') const pairing = require('./pairing.type')
const rates = require('./rates.type') const rates = require('./rates.type')
const sanctions = require('./sanctions.type')
const scalar = require('./scalar.type') const scalar = require('./scalar.type')
const settings = require('./settings.type') const settings = require('./settings.type')
const sms = require('./sms.type') const sms = require('./sms.type')
@ -34,9 +36,11 @@ const types = [
log, log,
loyalty, loyalty,
machine, machine,
market,
notification, notification,
pairing, pairing,
rates, rates,
sanctions,
scalar, scalar,
settings, settings,
sms, sms,

View file

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

View file

@ -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

View file

@ -23,7 +23,7 @@ const typeDef = gql`
errorCode: String errorCode: String
operatorCompleted: Boolean operatorCompleted: Boolean
sendPending: Boolean sendPending: Boolean
cashInFee: String fixedFee: String
minimumTx: Float minimumTx: Float
customerId: ID customerId: ID
isAnonymous: Boolean isAnonymous: Boolean

View file

@ -20,7 +20,6 @@ const buildApolloContext = async ({ req, res }) => {
req.session.user.role = user.role req.session.user.role = user.role
res.set('lamassu_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') res.set('Access-Control-Expose-Headers', 'lamassu_role')
return { req, res } return { req, res }

View file

@ -50,7 +50,20 @@ function batch (
excludeTestingCustomers = false, excludeTestingCustomers = false,
simplified 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.*, const cashInSql = `SELECT 'cashIn' AS tx_class, txs.*,
c.phone AS customer_phone, c.phone AS customer_phone,
@ -80,7 +93,7 @@ function batch (
AND ($12 is null or txs.to_address = $12) AND ($12 is null or txs.to_address = $12)
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0) ${isCsvExport && !simplified ? '' : 'AND (error IS NOT null OR tb.error_message IS NOT null OR fiat > 0)'}
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
const cashOutSql = `SELECT 'cashOut' AS tx_class, const cashOutSql = `SELECT 'cashOut' AS tx_class,
@ -114,7 +127,7 @@ function batch (
AND ($13 is null or txs.txStatus = $13) AND ($13 is null or txs.txStatus = $13)
AND ($14 is null or txs.swept = $14) AND ($14 is null or txs.swept = $14)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (fiat > 0) ${isCsvExport ? '' : 'AND fiat > 0'}
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
// The swept filter is cash-out only, so omit the cash-in query entirely // The swept filter is cash-out only, so omit the cash-in query entirely
@ -140,20 +153,20 @@ function batch (
return Promise.all(promises) return Promise.all(promises)
.then(packager) .then(packager)
.then(res => { .then(res =>
if (simplified) return simplifiedBatch(res) !isCsvExport ? res :
// GQL transactions and transactionsCsv both use this function and // GQL transactions and transactionsCsv both use this function and
// if we don't check for the correct simplified value, the Transactions page polling // if we don't check for the correct simplified value, the Transactions page polling
// will continuously build a csv in the background // will continuously build a csv in the background
else if (simplified === false) return advancedBatch(res) simplified ? simplifiedBatch(res) :
return res advancedBatch(res)
}) )
} }
function advancedBatch (data) { function advancedBatch (data) {
const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms', const fields = ['txClass', 'id', 'deviceId', 'toAddress', 'cryptoAtoms',
'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount', 'cryptoCode', 'fiat', 'fiatCode', 'fee', 'status', 'fiatProfit', 'cryptoAmount',
'dispense', 'notified', 'redeem', 'phone', 'error', 'dispense', 'notified', 'redeem', 'phone', 'error', 'fixedFee',
'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout', 'created', 'confirmedAt', 'hdIndex', 'swept', 'timedout',
'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4', 'dispenseConfirmed', 'provisioned1', 'provisioned2', 'provisioned3', 'provisioned4',
'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6', 'provisionedRecycler1', 'provisionedRecycler2', 'provisionedRecycler3', 'provisionedRecycler4', 'provisionedRecycler5', 'provisionedRecycler6',
@ -169,7 +182,9 @@ function advancedBatch (data) {
...it, ...it,
status: getStatus(it), status: getStatus(it),
fiatProfit: getProfit(it).toString(), 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) return _.compose(_.map(_.pick(fields)), addAdvancedFields)(data)

View file

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

View file

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

View file

@ -249,6 +249,7 @@ function plugins (settings, deviceId) {
const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config) const commissions = configManager.getCommissions(cryptoCode, deviceId, settings.config)
const minimumTx = new BN(commissions.minimumTx) const minimumTx = new BN(commissions.minimumTx)
const cashInFee = new BN(commissions.fixedFee) const cashInFee = new BN(commissions.fixedFee)
const cashOutFee = new BN(commissions.cashOutFixedFee)
const cashInCommission = new BN(commissions.cashIn) const cashInCommission = new BN(commissions.cashIn)
const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null const cashOutCommission = _.isNumber(commissions.cashOut) ? new BN(commissions.cashOut) : null
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode) const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
@ -261,6 +262,7 @@ function plugins (settings, deviceId) {
isCashInOnly: Boolean(cryptoRec.isCashinOnly), isCashInOnly: Boolean(cryptoRec.isCashinOnly),
minimumTx: BN.max(minimumTx, cashInFee), minimumTx: BN.max(minimumTx, cashInFee),
cashInFee, cashInFee,
cashOutFee,
cashInCommission, cashInCommission,
cashOutCommission, cashOutCommission,
cryptoNetwork, cryptoNetwork,
@ -276,6 +278,7 @@ function plugins (settings, deviceId) {
const localeConfig = configManager.getLocale(deviceId, settings.config) const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies const cryptoCodes = localeConfig.cryptoCurrencies
const machineScreenOpts = configManager.getAllMachineScreenOpts(settings.config)
const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c)) const tickerPromises = cryptoCodes.map(c => getTickerRates(fiatCode, c))
const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c)) const balancePromises = cryptoCodes.map(c => fiatBalance(fiatCode, c))
@ -325,7 +328,8 @@ function plugins (settings, deviceId) {
coins, coins,
configVersion, configVersion,
areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0, areThereAvailablePromoCodes: numberOfAvailablePromoCodes > 0,
timezone timezone,
screenOptions: machineScreenOpts
} }
}) })
} }
@ -473,25 +477,28 @@ function plugins (settings, deviceId) {
function buyAndSell (rec, doBuy, tx) { function buyAndSell (rec, doBuy, tx) {
const cryptoCode = rec.cryptoCode const cryptoCode = rec.cryptoCode
const fiatCode = rec.fiatCode return exchange.fetchExchange(settings, cryptoCode)
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated() .then(_exchange => {
const fiatCode = _exchange.account.currencyMarket
const cryptoAtoms = doBuy ? commissionMath.fiatToCrypto(tx, rec, deviceId, settings.config) : rec.cryptoAtoms.negated()
const market = [fiatCode, cryptoCode].join('') const market = [fiatCode, cryptoCode].join('')
if (!exchange.active(settings, cryptoCode)) return if (!exchange.active(settings, cryptoCode)) return
const direction = doBuy ? 'cashIn' : 'cashOut' const direction = doBuy ? 'cashIn' : 'cashOut'
const internalTxId = tx ? tx.id : rec.id const internalTxId = tx ? tx.id : rec.id
logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms) logger.debug('[%s] Pushing trade: %d', market, cryptoAtoms)
if (!tradesQueues[market]) tradesQueues[market] = [] if (!tradesQueues[market]) tradesQueues[market] = []
tradesQueues[market].push({ tradesQueues[market].push({
direction, direction,
internalTxId, internalTxId,
fiatCode, fiatCode,
cryptoAtoms, cryptoAtoms,
cryptoCode, cryptoCode,
timestamp: Date.now() timestamp: Date.now()
}) })
})
} }
function consolidateTrades (cryptoCode, fiatCode) { function consolidateTrades (cryptoCode, fiatCode) {
@ -548,19 +555,22 @@ function plugins (settings, deviceId) {
const deviceIds = devices.map(device => device.deviceId) const deviceIds = devices.map(device => device.deviceId)
const lists = deviceIds.map(deviceId => { const lists = deviceIds.map(deviceId => {
const localeConfig = configManager.getLocale(deviceId, settings.config) const localeConfig = configManager.getLocale(deviceId, settings.config)
const fiatCode = localeConfig.fiatCurrency
const cryptoCodes = localeConfig.cryptoCurrencies const cryptoCodes = localeConfig.cryptoCurrencies
return cryptoCodes.map(cryptoCode => ({ return Promise.all(cryptoCodes.map(cryptoCode => {
fiatCode, return exchange.fetchExchange(settings, cryptoCode)
cryptoCode .then(exchange => ({
fiatCode: exchange.account.currencyMarket,
cryptoCode
}))
})) }))
}) })
const tradesPromises = _.uniq(_.flatten(lists)) return Promise.all(lists)
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)) })
.then(lists => {
return Promise.all(tradesPromises) return Promise.all(_.uniq(_.flatten(lists))
.map(r => executeTradesForMarket(settings, r.fiatCode, r.cryptoCode)))
}) })
.catch(logger.error) .catch(logger.error)
} }

View file

@ -23,7 +23,8 @@ const ALL = {
bitpay: bitpay, bitpay: bitpay,
coinbase: { coinbase: {
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX, LN], CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, USDT_TRON, TRX, LN],
FIAT: 'ALL_CURRENCIES' FIAT: 'ALL_CURRENCIES',
DEFAULT_FIAT_MARKET: 'EUR'
}, },
binance: binance, binance: binance,
bitfinex: bitfinex bitfinex: bitfinex
@ -33,11 +34,8 @@ function buildMarket (fiatCode, cryptoCode, serviceName) {
if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) { if (!_.includes(cryptoCode, ALL[serviceName].CRYPTO)) {
throw new Error('Unsupported crypto: ' + cryptoCode) throw new Error('Unsupported crypto: ' + cryptoCode)
} }
const fiatSupported = ALL[serviceName].FIAT
if (fiatSupported !== 'ALL_CURRENCIES' && !_.includes(fiatCode, fiatSupported)) { if (_.isNil(fiatCode)) throw new Error('Market pair building failed: Missing fiat code')
logger.info('Building a market for an unsupported fiat. Defaulting to EUR market')
return cryptoCode + '/' + 'EUR'
}
return cryptoCode + '/' + fiatCode return cryptoCode + '/' + fiatCode
} }
@ -51,4 +49,8 @@ function isConfigValid (config, fields) {
return _.every(it => it || it === 0)(values) 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 }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,9 +1,13 @@
const { utils: coinUtils } = require('@lamassu/coins') const { utils: coinUtils } = require('@lamassu/coins')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const ccxt = require('ccxt') const ccxt = require('ccxt')
const mem = require('mem')
const { buildMarket, ALL, isConfigValid } = require('../common/ccxt') const { buildMarket, ALL, isConfigValid } = require('../common/ccxt')
const { ORDER_TYPES } = require('./consts') const { ORDER_TYPES } = require('./consts')
const logger = require('../../logger')
const { currencies } = require('../../new-admin/config')
const T = require('../../time')
const DEFAULT_PRICE_PRECISION = 2 const DEFAULT_PRICE_PRECISION = 2
const DEFAULT_AMOUNT_PRECISION = 8 const DEFAULT_AMOUNT_PRECISION = 8
@ -18,7 +22,8 @@ function trade (side, account, tradeEntry, exchangeName) {
const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig const { USER_REF, loadOptions, loadConfig = _.noop, REQUIRED_CONFIG_FIELDS, ORDER_TYPE, AMOUNT_PRECISION } = exchangeConfig
if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config') if (!isConfigValid(account, REQUIRED_CONFIG_FIELDS)) throw Error('Invalid config')
const symbol = buildMarket(fiatCode, cryptoCode, exchangeName) const selectedFiatMarket = account.currencyMarket
const symbol = buildMarket(selectedFiatMarket, cryptoCode, exchangeName)
const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION) const precision = _.defaultTo(DEFAULT_AMOUNT_PRECISION, AMOUNT_PRECISION)
const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision) const amount = coinUtils.toUnit(cryptoAtoms, cryptoCode).toFixed(precision)
const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {} const accountOptions = _.isFunction(loadOptions) ? loadOptions(account) : {}
@ -50,4 +55,38 @@ function calculatePrice (side, amount, orderBook) {
throw new Error('Insufficient market depth') 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 }

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
const ccxt = require('ccxt') const ccxt = require('ccxt')
const BN = require('../../bn') const BN = require('../../bn')
const { buildMarket, verifyFiatSupport } = require('../common/ccxt') const { buildMarket, verifyFiatSupport, defaultFiatMarket } = require('../common/ccxt')
const { getRate } = require('../../../lib/forex') const { getRate } = require('../../../lib/forex')
const RETRIES = 2 const RETRIES = 2
@ -33,7 +33,7 @@ function ticker (fiatCode, cryptoCode, tickerName) {
return getRate(RETRIES, fiatCode) return getRate(RETRIES, fiatCode)
.then(({ fxRate }) => { .then(({ fxRate }) => {
try { try {
return getCurrencyRates(ticker, 'USD', cryptoCode) return getCurrencyRates(ticker, defaultFiatMarket(tickerName), cryptoCode)
.then(res => ({ .then(res => ({
rates: { rates: {
ask: res.rates.ask.times(fxRate), ask: res.rates.ask.times(fxRate),

View file

@ -7,10 +7,11 @@ const nocache = require('nocache')
const logger = require('./logger') const logger = require('./logger')
const addRWBytes = require('./middlewares/addRWBytes')
const authorize = require('./middlewares/authorize') const authorize = require('./middlewares/authorize')
const computeSchema = require('./middlewares/compute-schema')
const errorHandler = require('./middlewares/errorHandler') const errorHandler = require('./middlewares/errorHandler')
const filterOldRequests = require('./middlewares/filterOldRequests') const filterOldRequests = require('./middlewares/filterOldRequests')
const computeSchema = require('./middlewares/compute-schema')
const findOperatorId = require('./middlewares/operatorId') const findOperatorId = require('./middlewares/operatorId')
const populateDeviceId = require('./middlewares/populateDeviceId') const populateDeviceId = require('./middlewares/populateDeviceId')
const populateSettings = require('./middlewares/populateSettings') const populateSettings = require('./middlewares/populateSettings')
@ -50,11 +51,24 @@ const configRequiredRoutes = [
] ]
// middleware setup // middleware setup
app.use(addRWBytes())
app.use(compression({ threshold: 500 })) app.use(compression({ threshold: 500 }))
app.use(helmet()) app.use(helmet())
app.use(nocache()) app.use(nocache())
app.use(express.json({ limit: '2mb' })) 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 /pair and /ca routes
app.use('/', pairingRoutes) app.use('/', pairingRoutes)

View file

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

View file

@ -108,7 +108,7 @@ function updateCustomer (req, res, next) {
.then(_.merge(patch)) .then(_.merge(patch))
.then(newPatch => customers.updatePhotoCard(id, newPatch)) .then(newPatch => customers.updatePhotoCard(id, newPatch))
.then(newPatch => customers.updateFrontCamera(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 => { .then(customer => {
createPendingManualComplianceNotifs(settings, customer, deviceId) createPendingManualComplianceNotifs(settings, customer, deviceId)
respond(req, res, { customer }) respond(req, res, { customer })

View file

@ -114,6 +114,7 @@ function poll (req, res, next) {
locale, locale,
version, version,
receiptPrintingActive: receipt.active, receiptPrintingActive: receipt.active,
automaticReceiptPrint: receipt.automaticPrint,
smsReceiptActive: receipt.sms, smsReceiptActive: receipt.sms,
enablePaperWalletOnly, enablePaperWalletOnly,
twoWayMode: cashOutConfig.active, twoWayMode: cashOutConfig.active,

44
lib/sanctions.js Normal file
View 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
}

View file

@ -40,6 +40,7 @@ function massage (tx, pi) {
: { : {
cryptoAtoms: new BN(r.cryptoAtoms), cryptoAtoms: new BN(r.cryptoAtoms),
fiat: new BN(r.fiat), fiat: new BN(r.fiat),
fixedFee: r.cashOutFee ? new BN(r.cashOutFee) : null,
rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null, rawTickerPrice: r.rawTickerPrice ? new BN(r.rawTickerPrice) : null,
commissionPercentage: new BN(r.commissionPercentage) commissionPercentage: new BN(r.commissionPercentage)
} }
@ -50,7 +51,9 @@ function massage (tx, pi) {
const mapper = _.flow( const mapper = _.flow(
transformDates, transformDates,
mapBN, mapBN,
_.unset('dirty')) _.unset('dirty'),
_.unset('cashOutFee')
)
return mapper(tx) return mapper(tx)
} }
@ -69,7 +72,7 @@ function cancel (txId) {
} }
function customerHistory (customerId, thresholdDays) { 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, SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired ((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
FROM cash_in_txs txIn FROM cash_in_txs txIn

View file

@ -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()

View file

@ -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()

View file

@ -0,0 +1,30 @@
const _ = require('lodash/fp')
const { loadLatest, saveAccounts } = require('../lib/new-settings-loader')
const { ACCOUNT_LIST } = require('../lib/new-admin/config/accounts')
const { ALL } = require('../lib/plugins/common/ccxt')
exports.up = function (next) {
return loadLatest()
.then(({ accounts }) => {
const allExchanges = _.map(it => it.code)(_.filter(it => it.class === 'exchange', ACCOUNT_LIST))
const configuredExchanges = _.intersection(allExchanges, _.keys(accounts))
const newAccounts = _.reduce(
(acc, value) => {
if (!_.isNil(accounts[value].currencyMarket)) return acc
if (_.includes('EUR', ALL[value].FIAT)) return { ...acc, [value]: { currencyMarket: 'EUR' } }
return { ...acc, [value]: { currencyMarket: ALL[value].DEFAULT_FIAT_CURRENCY } }
},
{},
configuredExchanges
)
return saveAccounts(newAccounts)
})
.then(next)
.catch(next)
}
module.exports.down = function (next) {
next()
}

View file

@ -0,0 +1,18 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
`CREATE TABLE blacklist_temp (
address TEXT NOT NULL UNIQUE
)`,
`INSERT INTO blacklist_temp (address) SELECT DISTINCT address FROM blacklist`,
`DROP TABLE blacklist`,
`ALTER TABLE blacklist_temp RENAME TO blacklist`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,24 @@
const uuid = require('uuid')
var db = require('./db')
exports.up = function (next) {
const defaultMessageId = uuid.v4()
var sql = [
`CREATE TABLE blacklist_messages (
id UUID PRIMARY KEY,
label TEXT NOT NULL,
content TEXT NOT NULL,
allow_toggle BOOLEAN NOT NULL DEFAULT true
)`,
`INSERT INTO blacklist_messages (id, label, content, allow_toggle) VALUES ('${defaultMessageId}', 'Suspicious address', 'This address may be associated with a deceptive offer or a prohibited group. Please make sure you''re using an address from your own wallet.', false)`,
`ALTER TABLE blacklist ADD COLUMN blacklist_message_id UUID REFERENCES blacklist_messages(id) NOT NULL DEFAULT '${defaultMessageId}'`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,20 @@
const _ = require('lodash/fp')
const { saveConfig, loadLatest } = require('../lib/new-settings-loader')
exports.up = function (next) {
const newConfig = {}
return loadLatest()
.then(({ config }) => {
if (!_.isNil(config.machineScreens_rates_active)) return
newConfig[`machineScreens_rates_active`] = true
return saveConfig(newConfig)
})
.then(next)
.catch(err => {
return next(err)
})
}
module.exports.down = function (next) {
next()
}

View file

@ -2,4 +2,3 @@ SKIP_PREFLIGHT_CHECK=true
HTTPS=true HTTPS=true
REACT_APP_TYPE_CHECK_SANCTUARY=false REACT_APP_TYPE_CHECK_SANCTUARY=false
PORT=3001 PORT=3001
REACT_APP_BUILD_TARGET=LAMASSU

View file

@ -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'
}
]
}
}

View file

@ -0,0 +1,8 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"bracketSameLine": true
}

View file

@ -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'
]
}

View file

@ -0,0 +1 @@
nodejs 22

View file

@ -1,5 +1,3 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Dev Environment ## Dev Environment
### formatting ### 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. 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. 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 ## Available Scripts
In the project directory, you can run: 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 Runs eslint --fix on the src folder
### `npm storybook`
Runs the storybook server
### `npm test` ### `npm test`
Launches the test runner in the interactive watch mode.<br> Launches the test runner in the interactive watch mode.<br>

View 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
]

View 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>

File diff suppressed because it is too large Load diff

View file

@ -2,12 +2,13 @@
"name": "lamassu-admin", "name": "lamassu-admin",
"version": "0.2.1", "version": "0.2.1",
"license": "../LICENSE", "license": "../LICENSE",
"type": "module",
"dependencies": { "dependencies": {
"@apollo/react-hooks": "^3.1.3", "@apollo/react-hooks": "^3.1.3",
"@lamassu/coins": "v1.4.0-beta.4", "@lamassu/coins": "v1.5.3",
"@material-ui/core": "4.11.0", "@material-ui/core": "4.12.4",
"@material-ui/icons": "4.9.1", "@material-ui/icons": "4.11.2",
"@material-ui/lab": "^4.0.0-alpha.56", "@material-ui/lab": "^4.0.0-alpha.61",
"@simplewebauthn/browser": "^3.0.0", "@simplewebauthn/browser": "^3.0.0",
"@use-hooks/axios": "1.3.0", "@use-hooks/axios": "1.3.0",
"apollo-cache-inmemory": "^1.6.6", "apollo-cache-inmemory": "^1.6.6",
@ -27,12 +28,11 @@
"downshift": "3.3.4", "downshift": "3.3.4",
"file-saver": "2.0.2", "file-saver": "2.0.2",
"formik": "2.2.0", "formik": "2.2.0",
"google-libphonenumber": "^3.2.22",
"graphql": "^14.5.8", "graphql": "^14.5.8",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.12.6",
"jss-plugin-extend": "^10.0.0", "jss-plugin-extend": "^10.0.0",
"jszip": "^3.6.0", "jszip": "^3.6.0",
"libphonenumber-js": "^1.7.50", "libphonenumber-js": "^1.11.15",
"match-sorter": "^4.2.0", "match-sorter": "^4.2.0",
"pretty-ms": "^2.1.0", "pretty-ms": "^2.1.0",
"qrcode.react": "0.9.3", "qrcode.react": "0.9.3",
@ -41,69 +41,34 @@
"react-copy-to-clipboard": "^5.0.2", "react-copy-to-clipboard": "^5.0.2",
"react-dom": "^16.10.2", "react-dom": "^16.10.2",
"react-dropzone": "^11.4.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-number-format": "^4.4.1",
"react-otp-input": "^2.3.0", "react-otp-input": "^2.3.0",
"react-router-dom": "5.1.2", "react-router-dom": "5.1.2",
"react-use": "15.3.2", "react-use": "15.3.2",
"react-virtualized": "^9.21.2", "react-virtualized": "^9.21.2",
"sanctuary": "^2.0.1",
"ua-parser-js": "^1.0.2", "ua-parser-js": "^1.0.2",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"yup": "0.32.9" "yup": "1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-actions": "6.0.26", "@eslint/js": "^9.16.0",
"@storybook/addon-backgrounds": "6.0.26", "@vitejs/plugin-react-swc": "^3.7.2",
"@storybook/addon-knobs": "6.0.26", "esbuild-plugin-react-virtualized": "^1.0.4",
"@storybook/addon-links": "6.0.26", "eslint": "^9.16.0",
"@storybook/addons": "6.0.26", "eslint-config-prettier": "^9.1.0",
"@storybook/preset-create-react-app": "^3.1.4", "eslint-plugin-react": "^7.37.2",
"@storybook/react": "6.0.26", "eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
"@welldone-software/why-did-you-render": "^3.3.9", "globals": "^15.13.0",
"eslint": "^7.19.0", "lint-staged": "^15.2.10",
"eslint-config-prettier": "^6.7.0", "prettier": "3.4.1",
"eslint-config-prettier-standard": "^3.0.1", "vite": "^6.0.1",
"eslint-config-standard": "^14.1.0", "vite-plugin-svgr": "^4.3.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"
]
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "vite",
"fix": "eslint --fix --ext .js,.md,.json src/", "build": "vite build",
"build": "react-scripts build", "preview": "vite preview"
"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"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@ -116,5 +81,9 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"lint-staged": {
"*.{js,jsx,md,json}": "eslint --cache --fix",
"*.{js,jsx,css,md,json}": "prettier --write"
} }
} }

View file

@ -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>

View file

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

View file

@ -17,21 +17,16 @@ import {
useHistory, useHistory,
BrowserRouter as Router BrowserRouter as Router
} from 'react-router-dom' } from 'react-router-dom'
import AppContext from 'src/AppContext'
import Header from 'src/components/layout/Header' import Header from 'src/components/layout/Header'
import Sidebar from 'src/components/layout/Sidebar' import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { tree, hasSidebar, Routes, getParent } from 'src/routing/routes' 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 global from 'src/styling/global'
import theme from 'src/styling/theme' import theme from 'src/styling/theme'
import { backgroundColor, mainWidth } from 'src/styling/variables' 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({ const jss = create({
plugins: [extendJss(), ...jssPreset().plugins] plugins: [extendJss(), ...jssPreset().plugins]
@ -120,17 +115,11 @@ const Main = () => {
)} )}
<main className={classes.wrapper}> <main className={classes.wrapper}>
{sidebar && !is404 && wizardTested && ( {sidebar && !is404 && wizardTested && (
<Slide <Slide direction="left" in={true} mountOnEnter unmountOnExit>
direction="left" <div>
in={true} <TitleSection title={parent.title}></TitleSection>
mountOnEnter </div>
unmountOnExit </Slide>
children={
<div>
<TitleSection title={parent.title}></TitleSection>
</div>
}
/>
)} )}
<Grid container className={classes.grid}> <Grid container className={classes.grid}>

View file

@ -1,10 +1,8 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import React, { memo } from 'react' import React, { memo } from 'react'
import ReactCarousel from 'react-material-ui-carousel' import ReactCarousel from 'react-material-ui-carousel'
import LeftArrow from 'src/styling/icons/arrow/carousel-left-arrow.svg?react'
import { ReactComponent as LeftArrow } from 'src/styling/icons/arrow/carousel-left-arrow.svg' import RightArrow from 'src/styling/icons/arrow/carousel-right-arrow.svg?react'
import { ReactComponent as RightArrow } from 'src/styling/icons/arrow/carousel-right-arrow.svg'
import { URI } from 'src/utils/apollo'
const useStyles = makeStyles({ const useStyles = makeStyles({
imgWrapper: { imgWrapper: {
@ -13,9 +11,10 @@ const useStyles = makeStyles({
display: 'flex' display: 'flex'
}, },
imgInner: { imgInner: {
objectFit: 'cover', objectFit: 'contain',
objectPosition: 'center', objectPosition: 'center',
width: 500, width: 500,
height: 400,
marginBottom: 40 marginBottom: 40
} }
}) })
@ -48,11 +47,11 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
next={activeIndex => slidePhoto(activeIndex)} next={activeIndex => slidePhoto(activeIndex)}
prev={activeIndex => slidePhoto(activeIndex)}> prev={activeIndex => slidePhoto(activeIndex)}>
{photosData.map((item, i) => ( {photosData.map((item, i) => (
<div> <div key={i}>
<div className={classes.imgWrapper}> <div className={classes.imgWrapper}>
<img <img
className={classes.imgInner} className={classes.imgInner}
src={`${URI}/${item?.photoDir}/${item?.path}`} src={`/${item?.photoDir}/${item?.path}`}
alt="" alt=""
/> />
</div> </div>

View file

@ -6,11 +6,11 @@ import {
InputLabel InputLabel
} from '@material-ui/core' } from '@material-ui/core'
import React, { memo, useState } from 'react' 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 { Button, IconButton } from 'src/components/buttons'
import { TextInput } from 'src/components/inputs' 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 { spacer } from 'src/styling/variables'
import ErrorMessage from './ErrorMessage' import ErrorMessage from './ErrorMessage'

View file

@ -1,98 +1,98 @@
import { import {
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
makeStyles makeStyles
} from '@material-ui/core' } from '@material-ui/core'
import React from 'react' import React from 'react'
import { H4, P } from 'src/components/typography'
import { Button, IconButton } from 'src/components/buttons' import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
import { H4, P } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { Button, IconButton } from 'src/components/buttons'
import { spacer } from 'src/styling/variables' import { spacer } from 'src/styling/variables'
import ErrorMessage from './ErrorMessage' import ErrorMessage from './ErrorMessage'
const useStyles = makeStyles({ const useStyles = makeStyles({
content: { content: {
width: 434, width: 434,
padding: spacer * 2, padding: spacer * 2,
paddingRight: spacer * 3.5 paddingRight: spacer * 3.5
}, },
titleSection: { titleSection: {
padding: spacer * 2, padding: spacer * 2,
paddingRight: spacer * 1.5, paddingRight: spacer * 1.5,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
margin: 0 margin: 0
}, },
actions: { actions: {
padding: spacer * 4, padding: spacer * 4,
paddingTop: spacer * 2 paddingTop: spacer * 2
}, },
title: { title: {
margin: 0 margin: 0
}, },
closeButton: { closeButton: {
padding: 0, padding: 0,
marginTop: -(spacer / 2) marginTop: -(spacer / 2)
} }
}) })
export const DialogTitle = ({ children, close }) => { export const DialogTitle = ({ children, close }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<div className={classes.titleSection}> <div className={classes.titleSection}>
{children} {children}
{close && ( {close && (
<IconButton <IconButton
size={16} size={16}
aria-label="close" aria-label="close"
onClick={close} onClick={close}
className={classes.closeButton}> className={classes.closeButton}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
)} )}
</div> </div>
) )
} }
export const DeleteDialog = ({ export const DeleteDialog = ({
title = 'Confirm Delete', title = 'Confirm Delete',
open = false, open = false,
onConfirmed, onConfirmed,
onDismissed, onDismissed,
item = 'item', item = 'item',
confirmationMessage = `Are you sure you want to delete this ${item}?`, confirmationMessage = `Are you sure you want to delete this ${item}?`,
extraMessage, extraMessage,
errorMessage = '' errorMessage = ''
}) => { }) => {
const classes = useStyles() const classes = useStyles()
return ( return (
<Dialog open={open} aria-labelledby="form-dialog-title"> <Dialog open={open} aria-labelledby="form-dialog-title">
<DialogTitle close={() => onDismissed()}> <DialogTitle close={() => onDismissed()}>
<H4 className={classes.title}>{title}</H4> <H4 className={classes.title}>{title}</H4>
</DialogTitle> </DialogTitle>
{errorMessage && ( {errorMessage && (
<DialogTitle> <DialogTitle>
<ErrorMessage> <ErrorMessage>
{errorMessage.split(':').map(error => ( {errorMessage.split(':').map(error => (
<> <>
{error} {error}
<br /> <br />
</> </>
))} ))}
</ErrorMessage> </ErrorMessage>
</DialogTitle> </DialogTitle>
)} )}
<DialogContent className={classes.content}> <DialogContent className={classes.content}>
{confirmationMessage && <P>{confirmationMessage}</P>} {confirmationMessage && <P>{confirmationMessage}</P>}
{extraMessage} {extraMessage}
</DialogContent> </DialogContent>
<DialogActions className={classes.actions}> <DialogActions className={classes.actions}>
<Button onClick={onConfirmed}>Confirm</Button> <Button onClick={onConfirmed}>Confirm</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) )
} }

View file

@ -1,8 +1,8 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import React from 'react' 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 { errorColor } from 'src/styling/variables'
import { Info3 } from './typography' import { Info3 } from './typography'

View file

@ -1,11 +1,11 @@
import { makeStyles, ClickAwayListener } from '@material-ui/core' import { makeStyles, ClickAwayListener } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import Popper from 'src/components/Popper' 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 { 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' import imagePopperStyles from './ImagePopper.styles'

View file

@ -1,8 +1,7 @@
import { Box, makeStyles } from '@material-ui/core' import { Box, makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import { Label1 } from 'src/components/typography' 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({ const useStyles = makeStyles({
message: ({ width }) => ({ message: ({ width }) => ({

View file

@ -1,9 +1,9 @@
import { Dialog, DialogContent, makeStyles } from '@material-ui/core' import { Dialog, DialogContent, makeStyles } from '@material-ui/core'
import React, { memo } from 'react' 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 { 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' import { spacer } from 'src/styling/variables'
const useStyles = makeStyles({ const useStyles = makeStyles({

View file

@ -5,11 +5,11 @@ import { format, set } from 'date-fns/fp'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, useCallback } from 'react' 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 { 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 { primaryColor, offColor, zircon } from 'src/styling/variables'
import { formatDate } from 'src/utils/timezones' import { formatDate } from 'src/utils/timezones'

View file

@ -1,10 +1,10 @@
import { makeStyles, Modal as MaterialModal, Paper } from '@material-ui/core' import { makeStyles, Modal as MaterialModal, Paper } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import React from 'react' 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 { 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 = { const styles = {
modal: { modal: {
@ -55,8 +55,8 @@ const styles = {
margin: xl margin: xl
? [[0, 0, 'auto', 'auto']] ? [[0, 0, 'auto', 'auto']]
: small : small
? [[12, 12, 'auto', 'auto']] ? [[12, 12, 'auto', 'auto']]
: [[16, 16, 'auto', 'auto']] : [[16, 16, 'auto', 'auto']]
}), }),
header: { header: {
display: 'flex' display: 'flex'

View file

@ -3,13 +3,12 @@ import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import ActionButton from 'src/components/buttons/ActionButton' import ActionButton from 'src/components/buttons/ActionButton'
import { H5 } from 'src/components/typography' import { H5 } from 'src/components/typography'
import { ReactComponent as NotificationIconZodiac } from 'src/styling/icons/menu/notification-zodiac.svg' import NotificationIconZodiac from 'src/styling/icons/menu/notification-zodiac.svg?react'
import { ReactComponent as ClearAllIconInverse } from 'src/styling/icons/stage/spring/empty.svg' import ClearAllIconInverse from 'src/styling/icons/stage/spring/empty.svg?react'
import { ReactComponent as ClearAllIcon } from 'src/styling/icons/stage/zodiac/empty.svg' import ClearAllIcon from 'src/styling/icons/stage/zodiac/empty.svg?react'
import { ReactComponent as ShowUnreadIcon } from 'src/styling/icons/stage/zodiac/full.svg' import ShowUnreadIcon from 'src/styling/icons/stage/zodiac/full.svg?react'
import styles from './NotificationCenter.styles' import styles from './NotificationCenter.styles'
import NotificationRow from './NotificationRow' import NotificationRow from './NotificationRow'

View file

@ -3,11 +3,10 @@ import classnames from 'classnames'
import prettyMs from 'pretty-ms' import prettyMs from 'pretty-ms'
import * as R from 'ramda' import * as R from 'ramda'
import React from 'react' import React from 'react'
import { Label1, Label2, TL2 } from 'src/components/typography' import { Label1, Label2, TL2 } from 'src/components/typography'
import { ReactComponent as Wrench } from 'src/styling/icons/action/wrench/zodiac.svg' import Wrench from 'src/styling/icons/action/wrench/zodiac.svg?react'
import { ReactComponent as Transaction } from 'src/styling/icons/arrow/transaction.svg' import Transaction from 'src/styling/icons/arrow/transaction.svg?react'
import { ReactComponent as WarningIcon } from 'src/styling/icons/warning-icon/tomato.svg' import WarningIcon from 'src/styling/icons/warning-icon/tomato.svg?react'
import styles from './NotificationCenter.styles' import styles from './NotificationCenter.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
@ -61,8 +60,8 @@ const NotificationRow = ({
typeDisplay && deviceName typeDisplay && deviceName
? `${typeDisplay} - ${deviceName}` ? `${typeDisplay} - ${deviceName}`
: !typeDisplay && deviceName : !typeDisplay && deviceName
? `${deviceName}` ? `${deviceName}`
: `${typeDisplay}` : `${typeDisplay}`
const iconClass = { const iconClass = {
[classes.readIcon]: read, [classes.readIcon]: read,

View file

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

View file

@ -4,9 +4,8 @@ import { makeStyles } from '@material-ui/core/styles'
import MAutocomplete from '@material-ui/lab/Autocomplete' import MAutocomplete from '@material-ui/lab/Autocomplete'
import classnames from 'classnames' import classnames from 'classnames'
import React, { memo, useState } from 'react' import React, { memo, useState } from 'react'
import { P } from 'src/components/typography' 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' import styles from './SearchBox.styles'

View file

@ -1,12 +1,12 @@
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import Chip from 'src/components/Chip' import Chip from 'src/components/Chip'
import { ActionButton } from 'src/components/buttons'
import { P, Label3 } from 'src/components/typography' import { P, Label3 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import CloseIcon from 'src/styling/icons/action/close/zodiac.svg?react'
import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg' import FilterIcon from 'src/styling/icons/button/filter/white.svg?react'
import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg' 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 { onlyFirstToUpper, singularOrPlural } from 'src/utils/string'
import { chipStyles, styles } from './SearchFilter.styles' import { chipStyles, styles } from './SearchFilter.styles'

View file

@ -2,13 +2,13 @@ import { makeStyles } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import * as R from 'ramda' import * as R from 'ramda'
import React, { memo } from 'react' 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 { import {
primaryColor, primaryColor,
secondaryColor, secondaryColor,

View file

@ -2,13 +2,13 @@ import { makeStyles } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import * as R from 'ramda' import * as R from 'ramda'
import React, { memo } from 'react' 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 { import {
primaryColor, primaryColor,
secondaryColor, secondaryColor,

View file

@ -1,9 +1,8 @@
import { makeStyles, ClickAwayListener } from '@material-ui/core' import { makeStyles, ClickAwayListener } from '@material-ui/core'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, memo } from 'react' import React, { useState, memo } from 'react'
import Popper from 'src/components/Popper' 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({ const useStyles = makeStyles({
transparentButton: { transparentButton: {
@ -13,6 +12,16 @@ const useStyles = makeStyles({
cursor: 'pointer', cursor: 'pointer',
marginTop: 4 marginTop: 4
}, },
relativelyPositioned: {
position: 'relative'
},
safeSpace: {
position: 'absolute',
backgroundColor: '#0000',
height: 40,
left: '-50%',
width: '200%'
},
popoverContent: ({ width }) => ({ popoverContent: ({ width }) => ({
width, width,
padding: [[10, 15]] padding: [[10, 15]]
@ -27,6 +36,10 @@ const usePopperHandler = width => {
setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget) setHelpPopperAnchorEl(helpPopperAnchorEl ? null : event.currentTarget)
} }
const openHelpPopper = event => {
setHelpPopperAnchorEl(event.currentTarget)
}
const handleCloseHelpPopper = () => { const handleCloseHelpPopper = () => {
setHelpPopperAnchorEl(null) setHelpPopperAnchorEl(null)
} }
@ -38,25 +51,32 @@ const usePopperHandler = width => {
helpPopperAnchorEl, helpPopperAnchorEl,
helpPopperOpen, helpPopperOpen,
handleOpenHelpPopper, handleOpenHelpPopper,
openHelpPopper,
handleCloseHelpPopper handleCloseHelpPopper
} }
} }
const Tooltip = memo(({ children, width, Icon = HelpIcon }) => { const HelpTooltip = memo(({ children, width }) => {
const handler = usePopperHandler(width) const handler = usePopperHandler(width)
return ( return (
<ClickAwayListener onClickAway={handler.handleCloseHelpPopper}> <ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
<div> <div
className={handler.classes.relativelyPositioned}
onMouseLeave={handler.handleCloseHelpPopper}>
{handler.helpPopperOpen && (
<div className={handler.classes.safeSpace}></div>
)}
<button <button
type="button" type="button"
className={handler.classes.transparentButton} className={handler.classes.transparentButton}
onClick={handler.handleOpenHelpPopper}> onMouseEnter={handler.openHelpPopper}>
<Icon /> <HelpIcon />
</button> </button>
<Popper <Popper
open={handler.helpPopperOpen} open={handler.helpPopperOpen}
anchorEl={handler.helpPopperAnchorEl} anchorEl={handler.helpPopperAnchorEl}
arrowEnabled={true}
placement="bottom"> placement="bottom">
<div className={handler.classes.popoverContent}>{children}</div> <div className={handler.classes.popoverContent}>{children}</div>
</Popper> </Popper>
@ -69,31 +89,33 @@ const HoverableTooltip = memo(({ parentElements, children, width }) => {
const handler = usePopperHandler(width) const handler = usePopperHandler(width)
return ( return (
<div> <ClickAwayListener onClickAway={handler.handleCloseHelpPopper}>
{!R.isNil(parentElements) && ( <div>
<div {!R.isNil(parentElements) && (
onMouseEnter={handler.handleOpenHelpPopper} <div
onMouseLeave={handler.handleCloseHelpPopper}> onMouseLeave={handler.handleCloseHelpPopper}
{parentElements} onMouseEnter={handler.handleOpenHelpPopper}>
</div> {parentElements}
)} </div>
{R.isNil(parentElements) && ( )}
<button {R.isNil(parentElements) && (
type="button" <button
onMouseEnter={handler.handleOpenHelpPopper} type="button"
onMouseLeave={handler.handleCloseHelpPopper} onMouseEnter={handler.handleOpenHelpPopper}
className={handler.classes.transparentButton}> onMouseLeave={handler.handleCloseHelpPopper}
<HelpIcon /> className={handler.classes.transparentButton}>
</button> <HelpIcon />
)} </button>
<Popper )}
open={handler.helpPopperOpen} <Popper
anchorEl={handler.helpPopperAnchorEl} open={handler.helpPopperOpen}
placement="bottom"> anchorEl={handler.helpPopperAnchorEl}
<div className={handler.classes.popoverContent}>{children}</div> placement="bottom">
</Popper> <div className={handler.classes.popoverContent}>{children}</div>
</div> </Popper>
</div>
</ClickAwayListener>
) )
}) })
export { Tooltip, HoverableTooltip } export { HoverableTooltip, HelpTooltip }

View file

@ -3,17 +3,17 @@ import classnames from 'classnames'
import { useFormikContext, Form, Formik, Field as FormikField } from 'formik' import { useFormikContext, Form, Formik, Field as FormikField } from 'formik'
import * as R from 'ramda' import * as R from 'ramda'
import React, { useState, memo } from 'react' 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 * as Yup from 'yup'
import PromptWhenDirty from 'src/components/PromptWhenDirty'
import { Link, IconButton } from 'src/components/buttons' import { Link, IconButton } from 'src/components/buttons'
import { RadioGroup } from 'src/components/inputs/formik' import { RadioGroup } from 'src/components/inputs/formik'
import { Table, TableBody, TableRow, TableCell } from 'src/components/table' 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' import { booleanPropertiesTableStyles } from './BooleanPropertiesTable.styles'

View file

@ -1,9 +1,9 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import React, { memo } from 'react' import React, { memo } from 'react'
import AddIcon from 'src/styling/icons/button/add/zodiac.svg?react'
import typographyStyles from 'src/components/typography/styles' 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' import { zircon, zircon2, comet, fontColor, white } from 'src/styling/variables'
const { p } = typographyStyles const { p } = typographyStyles

Some files were not shown because too many files have changed in this diff Show more