Merge branch 'release-8.1' into fix/ct-ui-score-error-color

This commit is contained in:
Rafael Taranto 2022-11-21 11:18:13 +00:00 committed by GitHub
commit 01e0c57655
40 changed files with 332 additions and 160 deletions

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const argv = require('minimist')(process.argv.slice(2)) const argv = require('minimist')(process.argv.slice(2))
const _ = require('lodash') const _ = require('lodash')
const db = require('../lib/db') const db = require('../lib/db')

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const install = require('../lib/blockchain/install') const install = require('../lib/blockchain/install')
install.run() install.run()

View file

@ -1,3 +1,5 @@
require('../lib/environment-helper')
const settingsLoader = require('../lib/new-settings-loader') const settingsLoader = require('../lib/new-settings-loader')
const pp = require('../lib/pp') const pp = require('../lib/pp')

View file

@ -1,4 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const hdkey = require('ethereumjs-wallet/hdkey') const hdkey = require('ethereumjs-wallet/hdkey')
const hkdf = require('futoin-hkdf') const hkdf = require('futoin-hkdf')
const crypto = require('crypto') const crypto = require('crypto')

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const migrate = require('../lib/migrate-options') const migrate = require('../lib/migrate-options')
migrate.run() migrate.run()

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const ofac = require('../lib/ofac/update') const ofac = require('../lib/ofac/update')
console.log('Updating OFAC databases.') console.log('Updating OFAC databases.')

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const settingsLoader = require('../lib/new-settings-loader') const settingsLoader = require('../lib/new-settings-loader')
const configManager = require('../lib/new-config-manager') const configManager = require('../lib/new-config-manager')
const wallet = require('../lib/wallet') const wallet = require('../lib/wallet')

View file

@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const _ = require('lodash') const _ = require('lodash')
const db = require('../lib/db') const db = require('../lib/db')

View file

@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
require('../lib/environment-helper')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const common = require('../lib/blockchain/common') const common = require('../lib/blockchain/common')
const { utils: coinUtils } = require('@lamassu/coins') const { utils: coinUtils } = require('@lamassu/coins')

36
build/Dockerfile Normal file
View file

@ -0,0 +1,36 @@
FROM ubuntu:20.04 as base
ARG VERSION
ENV SERVER_VERSION=$VERSION
ARG DEBIAN_FRONTEND=noninteractive
ENV TZ=Europe/Lisbon
RUN apt-get update
RUN apt-get install -y -q curl \
sudo \
git \
python2-minimal \
build-essential \
libpq-dev \
net-tools \
tar
RUN curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
RUN apt-get install nodejs -y -q
FROM base as l-s-build
WORKDIR /lamassu
RUN git clone https://github.com/lamassu/lamassu-server -b ${SERVER_VERSION}
RUN rm -rf /lamassu/lamassu-server/public/*
RUN cd lamassu-server && npm install --production
RUN cd lamassu-server/new-lamassu-admin && npm install && npm run build
RUN cp -r /lamassu/lamassu-server/new-lamassu-admin/build/* /lamassu/lamassu-server/public
RUN rm -rf /lamassu/lamassu-server/new-lamassu-admin/node_modules
RUN tar -zcvf lamassu-server-$SERVER_VERSION.tar.gz lamassu-server/
ENTRYPOINT [ "/bin/bash" ]

13
build/build.sh Executable file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env bash
if [ $# -eq 0 ]; then
echo "Error: no arguments specified"
echo "Usage: ./build.sh <SERVER_VERSION_TAG>"
exit 1
fi
docker build --rm --build-arg VERSION=$1 --tag l-s-prepackage:$1 --file Dockerfile .
id=$(docker create l-s-prepackage:$1)
docker cp $id:/lamassu/lamassu-server-$1.tar.gz ./lamassu-server-$1.tar.gz
docker rm -v $id

View file

@ -33,12 +33,12 @@ const BINARIES = {
dir: 'geth-linux-amd64-1.10.25-69568c55' dir: 'geth-linux-amd64-1.10.25-69568c55'
}, },
ZEC: { ZEC: {
url: 'https://z.cash/downloads/zcash-5.2.0-linux64-debian-bullseye.tar.gz', url: 'https://z.cash/downloads/zcash-5.3.0-linux64-debian-bullseye.tar.gz',
dir: 'zcash-5.2.0/bin' dir: 'zcash-5.3.0/bin'
}, },
DASH: { DASH: {
url: 'https://github.com/dashpay/dash/releases/download/v18.0.1/dashcore-18.0.1-x86_64-linux-gnu.tar.gz', url: 'https://github.com/dashpay/dash/releases/download/v18.1.0/dashcore-18.1.0-x86_64-linux-gnu.tar.gz',
dir: 'dashcore-18.0.1/bin' dir: 'dashcore-18.1.0/bin'
}, },
LTC: { LTC: {
defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz', defaultUrl: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',

View file

@ -168,13 +168,21 @@ function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
function doesTxReuseAddress (tx) { function doesTxReuseAddress (tx) {
if (!tx.fiat || tx.fiat.isZero()) { if (!tx.fiat || tx.fiat.isZero()) {
const sql = `SELECT EXISTS (SELECT DISTINCT to_address FROM cash_in_txs WHERE to_address = $1)` const sql = `
return db.any(sql, [tx.toAddress]) SELECT EXISTS (
SELECT DISTINCT to_address FROM (
SELECT to_address FROM cash_in_txs WHERE id != $1
) AS x WHERE to_address = $2
)`
return db.one(sql, [tx.id, tx.toAddress]).then(({ exists }) => exists)
} }
return Promise.resolve(false) return Promise.resolve(false)
} }
function getWalletScore (tx, pi) { function getWalletScore (tx, pi) {
pi.isWalletScoringEnabled(tx)
.then(isEnabled => {
if(!isEnabled) return null
if (!tx.fiat || tx.fiat.isZero()) { if (!tx.fiat || tx.fiat.isZero()) {
return pi.rateWallet(tx.cryptoCode, tx.toAddress) return pi.rateWallet(tx.cryptoCode, tx.toAddress)
} }
@ -185,6 +193,7 @@ function getWalletScore (tx, pi) {
score: tx.walletScore, score: tx.walletScore,
isValid isValid
})) }))
})
} }
function monitorPending (settings) { function monitorPending (settings) {

View file

@ -36,7 +36,8 @@ function selfPost (tx, pi) {
} }
function post (tx, pi, fromClient = true) { function post (tx, pi, fromClient = true) {
logger.silly('Updating cashout tx:', tx) logger.silly('Updating cashout -- tx:', JSON.stringify(tx))
logger.silly('Updating cashout -- fromClient:', JSON.stringify(fromClient))
return cashOutAtomic.atomic(tx, pi, fromClient) return cashOutAtomic.atomic(tx, pi, fromClient)
.then(txVector => { .then(txVector => {
const [, newTx, justAuthorized] = txVector const [, newTx, justAuthorized] = txVector
@ -63,7 +64,7 @@ function postProcess (txVector, justAuthorized, pi) {
fiat: newTx.fiat fiat: newTx.fiat
}) })
const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat) const bills = billMath.makeChange(cassettes.cassettes, newTx.fiat)
logger.silly('Bills to dispense:', bills) logger.silly('Bills to dispense:', JSON.stringify(bills))
if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE) if (!bills) throw httpError('Out of bills', INSUFFICIENT_FUNDS_CODE)
return bills return bills
@ -121,6 +122,9 @@ function getWalletScore (tx, pi) {
return tx return tx
// Transaction shows up on the blockchain, we can request the sender address // Transaction shows up on the blockchain, we can request the sender address
return pi.isWalletScoringEnabled(tx)
.then(isEnabled => {
if (!isEnabled) return tx
return pi.getTransactionHash(tx) return pi.getTransactionHash(tx)
.then(rejectEmpty("No transaction hashes")) .then(rejectEmpty("No transaction hashes"))
.then(txHashes => pi.getInputAddresses(tx, txHashes)) .then(txHashes => pi.getInputAddresses(tx, txHashes))
@ -145,6 +149,7 @@ function getWalletScore (tx, pi) {
errorCode: 'ciphertraceError', errorCode: 'ciphertraceError',
dispense: true dispense: true
})) }))
})
} }
function monitorLiveIncoming (settings) { function monitorLiveIncoming (settings) {

View file

@ -262,7 +262,7 @@ function deleteEditedData (id, data) {
*/ */
async function updateEditedPhoto (id, photo, photoType) { async function updateEditedPhoto (id, photo, photoType) {
const newPatch = {} const newPatch = {}
const baseDir = photoType === 'frontCamera' ? frontCameraBaseDir : idPhotoCardBasedir const baseDir = photoType === 'frontCamera' ? FRONT_CAMERA_DIR : ID_PHOTO_CARD_DIR
const { createReadStream, filename } = photo const { createReadStream, filename } = photo
const stream = createReadStream() const stream = createReadStream()

View file

@ -198,7 +198,10 @@ const dynamicConfig = ({ deviceId, operatorId, pid, pq, settings, }) => {
_.toPairs, _.toPairs,
/* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */ /* [[cryptoCode, { balance, ask, bid, cashIn, cashOut }], ...] => [{ cryptoCode, balance, ask, bid, cashIn, cashOut }, ...] */
_.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)) _.map(([cryptoCode, obj]) => _.set('cryptoCode', cryptoCode, obj)),
/* Only send coins which have all information needed by the machine. This prevents the machine going down if there's an issue with the coin node */
_.filter(coin => ['ask', 'bid', 'balance', 'cashIn', 'cashOut', 'cryptoCode'].every(it => it in coin))
)(_.concat(balances, coins)) )(_.concat(balances, coins))
}), }),
@ -232,6 +235,7 @@ const configs = (parent, { currentConfigVersion }, { deviceId, deviceName, opera
const massageTerms = terms => (terms.active && terms.text) ? ({ const massageTerms = terms => (terms.active && terms.text) ? ({
tcPhoto: Boolean(terms.tcPhoto),
delay: Boolean(terms.delay), delay: Boolean(terms.delay),
title: terms.title, title: terms.title,
text: nmd(terms.text), text: nmd(terms.text),

View file

@ -58,6 +58,32 @@ type TriggersAutomation {
usSsn: Boolean! usSsn: Boolean!
} }
type CustomScreen {
text: String!
title: String!
}
type CustomInput {
type: String!
constraintType: String!
label1: String
label2: String
choiceList: [String]
}
type CustomRequest {
name: String!
input: CustomInput!
screen1: CustomScreen!
screen2: CustomScreen!
}
type CustomInfoRequest {
id: String!
enabled: Boolean!
customRequest: CustomRequest!
}
type Trigger { type Trigger {
id: String! id: String!
customInfoRequestId: String! customInfoRequestId: String!
@ -68,9 +94,11 @@ type Trigger {
suspensionDays: Float suspensionDays: Float
threshold: Int threshold: Int
thresholdDays: Int thresholdDays: Int
customInfoRequest: CustomInfoRequest
} }
type TermsDetails { type TermsDetails {
tcPhoto: Boolean!
delay: Boolean! delay: Boolean!
title: String! title: String!
accept: String! accept: String!

View file

@ -55,30 +55,54 @@ const populateSettings = function (req, res, next) {
} }
try { try {
const operatorSettings = settingsCache.get(operatorId) // Priority of configs to retrieve
if (!versionId && (!operatorSettings || !!needsSettingsReload[operatorId])) { // 1. Machine is in the middle of a transaction and has the config-version header set, fetch that config from cache or database, depending on whether it exists in cache
return newSettingsLoader.loadLatest() // 2. The operator settings changed, so we must update the cache
// 3. There's a cached config, send the cached value
// 4. There's no cached config, cache and send the latest config
if (versionId) {
const cachedVersionedSettings = settingsCache.get(`${operatorId}-v${versionId}`)
if (!cachedVersionedSettings) {
logger.debug('Fetching a specific config version cached value')
return newSettingsLoader.load(versionId)
.then(settings => { .then(settings => {
settingsCache.set(operatorId, settings) settingsCache.set(`${operatorId}-v${versionId}`, settings)
delete needsSettingsReload[operatorId]
req.settings = settings req.settings = settings
}) })
.then(() => next()) .then(() => next())
.catch(next) .catch(next)
} }
if (!versionId && operatorSettings) { logger.debug('Fetching and caching a specific config version')
req.settings = operatorSettings req.settings = cachedVersionedSettings
return next() return next()
} }
} catch (e) {
logger.error(e)
}
newSettingsLoader.load(versionId) const operatorSettings = settingsCache.get(`${operatorId}-latest`)
.then(settings => { req.settings = settings })
if (!!needsSettingsReload[operatorId] || !operatorSettings) {
!!needsSettingsReload[operatorId]
? logger.debug('Fetching and caching a new latest config value, as a reload was requested')
: logger.debug('Fetching the latest config version because there\'s no cached value')
return newSettingsLoader.loadLatest()
.then(settings => {
settingsCache.set(`${operatorId}-latest`, settings)
if (!!needsSettingsReload[operatorId]) delete needsSettingsReload[operatorId]
req.settings = settings
})
.then(() => next()) .then(() => next())
.catch(next) .catch(next)
} }
logger.debug('Fetching the latest config value from cache')
req.settings = operatorSettings
return next()
} catch (e) {
logger.error(e)
}
}
module.exports = populateSettings module.exports = populateSettings

View file

@ -145,13 +145,13 @@ const getTriggersAutomation = (customInfoRequests, config) => {
const splitGetFirst = _.compose(_.head, _.split('_')) const splitGetFirst = _.compose(_.head, _.split('_'))
const getCryptosFromWalletNamespace = config => { const getCryptosFromWalletNamespace =
return _.uniq(_.map(splitGetFirst, _.keys(fromNamespace('wallets', config)))) _.compose(_.without(['advanced']), _.uniq, _.map(splitGetFirst), _.keys, fromNamespace('wallets'))
}
const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config) const getCashInSettings = config => fromNamespace(namespaces.CASH_IN)(config)
const getCryptoUnits = (crypto, config) => getWalletSettings(crypto, config).cryptoUnits const getCryptoUnits = (crypto, config) =>
getWalletSettings(crypto, config).cryptoUnits ?? 'full'
const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS) const setTermsConditions = toNamespace(namespaces.TERMS_CONDITIONS)

View file

@ -850,6 +850,10 @@ function plugins (settings, deviceId) {
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes) return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
} }
function isWalletScoringEnabled (tx) {
return walletScoring.isWalletScoringEnabled(settings, tx.cryptoCode)
}
return { return {
getRates, getRates,
recordPing, recordPing,
@ -882,7 +886,8 @@ function plugins (settings, deviceId) {
rateWallet, rateWallet,
isValidWalletScore, isValidWalletScore,
getTransactionHash, getTransactionHash,
getInputAddresses getInputAddresses,
isWalletScoringEnabled
} }
} }

View file

@ -28,18 +28,18 @@ function rateWallet (account, cryptoCode, address) {
const { apiVersion, authHeader } = client const { apiVersion, authHeader } = client
const threshold = account.scoreThreshold const threshold = account.scoreThreshold
console.log(`** DEBUG ** rateWallet ENDPOINT: https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`) logger.info(`** DEBUG ** rateWallet ENDPOINT: https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`)
return axios.get(`https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`, { return axios.get(`https://rest.ciphertrace.com/aml/${apiVersion}/${_.toLower(cryptoCode)}/risk?address=${address}`, {
headers: authHeader headers: authHeader
}) })
.then(res => ({ address, score: res.data.risk, isValid: res.data.risk < threshold })) .then(res => ({ address, score: res.data.risk, isValid: res.data.risk < threshold }))
.then(result => { .then(result => {
console.log('** DEBUG ** rateWallet RETURN:', result) logger.info(`** DEBUG ** rateWallet RETURN: ${result}`)
return result return result
}) })
.catch(err => { .catch(err => {
console.log(`** DEBUG ** rateWallet ERROR: ${err.message}`) logger.error(`** DEBUG ** rateWallet ERROR: ${err.message}`)
throw err throw err
}) })
} }
@ -54,7 +54,8 @@ function isValidWalletScore (account, score) {
function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wallet) { function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wallet) {
const { apiVersion, authHeader } = client const { apiVersion, authHeader } = client
console.log(`** DEBUG ** getTransactionHash ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`)
logger.info(`** DEBUG ** getTransactionHash ENDPOINT: https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`)
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`, { return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}&mempool=true`, {
headers: authHeader headers: authHeader
@ -64,8 +65,8 @@ function getAddressTransactionsHashes (receivingAddress, cryptoCode, client, wal
_.map(_.get(['txHash'])) _.map(_.get(['txHash']))
)) ))
.catch(err => { .catch(err => {
console.log(`** DEBUG ** getTransactionHash ERROR: ${err}`) logger.error(`** DEBUG ** getTransactionHash ERROR: ${err}`)
console.log(`** DEBUG ** Fetching transactions hashes via wallet node...`) logger.error(`** DEBUG ** Fetching transactions hashes via wallet node...`)
return wallet.getTxHashesByAddress(cryptoCode, receivingAddress) return wallet.getTxHashesByAddress(cryptoCode, receivingAddress)
}) })
} }
@ -78,11 +79,11 @@ function getTransactionHash (account, cryptoCode, receivingAddress, wallet) {
if (_.size(txHashes) > 1) { if (_.size(txHashes) > 1) {
logger.warn('An address generated by this wallet was used in more than one transaction') logger.warn('An address generated by this wallet was used in more than one transaction')
} }
console.log('** DEBUG ** getTransactionHash RETURN: ', _.join(', ', txHashes)) logger.info('** DEBUG ** getTransactionHash RETURN: ', _.join(', ', txHashes))
return txHashes return txHashes
}) })
.catch(err => { .catch(err => {
console.log('** DEBUG ** getTransactionHash from wallet node ERROR: ', err) logger.error('** DEBUG ** getTransactionHash from wallet node ERROR: ', err)
throw err throw err
}) })
} }
@ -116,20 +117,29 @@ function getInputAddresses (account, cryptoCode, txHashes) {
const transactionInputs = _.flatMap(it => it.inputs, data.transactions) const transactionInputs = _.flatMap(it => it.inputs, data.transactions)
const inputAddresses = _.map(it => it.address, transactionInputs) const inputAddresses = _.map(it => it.address, transactionInputs)
console.log(`** DEBUG ** getInputAddresses RETURN: ${inputAddresses}`) logger.info(`** DEBUG ** getInputAddresses RETURN: ${inputAddresses}`)
return inputAddresses return inputAddresses
}) })
.catch(err => { .catch(err => {
console.log(`** DEBUG ** getInputAddresses ERROR: ${err.message}`) logger.error(`** DEBUG ** getInputAddresses ERROR: ${err.message}`)
throw err throw err
}) })
} }
function isWalletScoringEnabled (account, cryptoCode) {
if (!SUPPORTED_COINS.includes(cryptoCode)) {
return Promise.reject(new Error('Unsupported crypto: ' + cryptoCode))
}
return Promise.resolve(!_.isNil(account) && account.enabled)
}
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore, isValidWalletScore,
getTransactionHash, getTransactionHash,
getInputAddresses getInputAddresses,
isWalletScoringEnabled
} }

View file

@ -36,10 +36,20 @@ function getInputAddresses (account, cryptoCode, txHashes) {
}) })
} }
function isWalletScoringEnabled (account, cryptoCode) {
return new Promise((resolve, _) => {
setTimeout(() => {
return resolve(true)
}, 100)
})
}
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore, isValidWalletScore,
getTransactionHash, getTransactionHash,
getInputAddresses getInputAddresses,
isWalletScoringEnabled
} }

View file

@ -47,9 +47,19 @@ function getInputAddresses (settings, cryptoCode, txHashes) {
}) })
} }
function isWalletScoringEnabled (settings, cryptoCode) {
return Promise.resolve()
.then(() => {
const { plugin, account } = loadWalletScoring(settings)
return plugin.isWalletScoringEnabled(account, cryptoCode)
})
}
module.exports = { module.exports = {
rateWallet, rateWallet,
isValidWalletScore, isValidWalletScore,
getTransactionHash, getTransactionHash,
getInputAddresses getInputAddresses,
isWalletScoringEnabled
} }

View file

@ -1,6 +1,7 @@
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import classnames from 'classnames' import classnames from 'classnames'
import { compareAsc, differenceInDays, set } from 'date-fns/fp' import { compareAsc, differenceInDays, set } from 'date-fns/fp'
import * as R from 'ramda'
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import Calendar from './Calendar' import Calendar from './Calendar'
@ -37,7 +38,12 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day) set({ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 }, day)
) )
} else { } else {
setTo(from) setTo(
set(
{ hours: 23, minutes: 59, seconds: 59, milliseconds: 999 },
R.clone(from)
)
)
setFrom(day) setFrom(day)
} }
return return

View file

@ -70,7 +70,7 @@ const Header = () => {
} }
const mapElement = ( const mapElement = (
{ name, width = DEFAULT_COL_SIZE, header, textAlign }, { name, display, width = DEFAULT_COL_SIZE, header, textAlign },
idx idx
) => { ) => {
const orderClasses = classnames({ const orderClasses = classnames({
@ -99,7 +99,7 @@ const Header = () => {
<>{attachOrderedByToComplexHeader(header) ?? header}</> <>{attachOrderedByToComplexHeader(header) ?? header}</>
) : ( ) : (
<span className={orderClasses}> <span className={orderClasses}>
{startCase(name)}{' '} {!R.isNil(display) ? display : startCase(name)}{' '}
{!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'} {!R.isNil(orderedBy) && R.equals(name, orderedBy.code) && '-'}
</span> </span>
)} )}

View file

@ -277,7 +277,7 @@ const Analytics = () => {
case 'topMachines': case 'topMachines':
return ( return (
<TopMachinesWrapper <TopMachinesWrapper
title="Transactions over time" title="Top 5 Machines"
representing={representing} representing={representing}
period={period} period={period}
data={R.map(convertFiatToLocale)(filteredData(period.code).current)} data={R.map(convertFiatToLocale)(filteredData(period.code).current)}

View file

@ -55,6 +55,7 @@ const GET_TRANSACTIONS = gql`
customerId customerId
isAnonymous isAnonymous
rawTickerPrice rawTickerPrice
profit
} }
} }
` `

View file

@ -27,9 +27,9 @@ const CASSETTE_LIST = [
] ]
const widthsByNumberOfCassettes = { const widthsByNumberOfCassettes = {
2: { machine: 230, cassette: 250 }, 2: { machine: 230, cashbox: 150, cassette: 250 },
3: { machine: 216, cassette: 270 }, 3: { machine: 216, cashbox: 150, cassette: 270 },
4: { machine: 210, cassette: 204 } 4: { machine: 210, cashbox: 150, cassette: 204 }
} }
const FiatBalanceOverrides = ({ config, section }) => { const FiatBalanceOverrides = ({ config, section }) => {
@ -44,19 +44,17 @@ const FiatBalanceOverrides = ({ config, section }) => {
const setupValues = data?.fiatBalanceOverrides ?? [] const setupValues = data?.fiatBalanceOverrides ?? []
const innerSetEditing = it => setEditing(NAME, it) const innerSetEditing = it => setEditing(NAME, it)
const cashoutConfig = it => fromNamespace(it)(config) const cashoutConfig = it => fromNamespace(it)(config)
const overriddenMachines = R.map(override => override.machine, setupValues) const overriddenMachines = R.map(override => override.machine, setupValues)
const suggestionFilter = R.filter( const suggestions = R.differenceWith(
it => (it, m) => it.deviceId === m,
!R.includes(it.deviceId, overriddenMachines) && machines,
cashoutConfig(it.deviceId).active overriddenMachines
) )
const suggestions = suggestionFilter(machines)
const findSuggestion = it => { const findSuggestion = it => {
const coin = R.compose(R.find(R.propEq('deviceId', it?.machine)))(machines) const coin = R.find(R.propEq('deviceId', it?.machine), machines)
return coin ? [coin] : [] return coin ? [coin] : []
} }
@ -83,7 +81,6 @@ const FiatBalanceOverrides = ({ config, section }) => {
.shape({ .shape({
[MACHINE_KEY]: Yup.string() [MACHINE_KEY]: Yup.string()
.label('Machine') .label('Machine')
.nullable()
.required(), .required(),
[CASHBOX_KEY]: Yup.number() [CASHBOX_KEY]: Yup.number()
.label('Cash box') .label('Cash box')
@ -121,23 +118,24 @@ const FiatBalanceOverrides = ({ config, section }) => {
.max(percentMax) .max(percentMax)
.nullable() .nullable()
}) })
.test((values, context) => { .test((values, context) =>
const picked = R.pick(CASSETTE_LIST, values) R.any(key => !R.isNil(values[key]), R.prepend(CASHBOX_KEY, CASSETTE_LIST))
? undefined
if (CASSETTE_LIST.some(it => !R.isNil(picked[it]))) return : context.createError({
path: CASHBOX_KEY,
return context.createError({ message:
path: CASSETTE_1_KEY, 'The cash box or at least one of the cassettes must have a value'
message: 'At least one of the cassettes must have a value'
})
}) })
)
const viewMachine = it => const viewMachine = it =>
R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines) R.compose(R.path(['name']), R.find(R.propEq('deviceId', it)))(machines)
const elements = [ const elements = R.concat(
[
{ {
name: MACHINE_KEY, name: MACHINE_KEY,
display: 'Machine',
width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine, width: widthsByNumberOfCassettes[maxNumberOfCassettes].machine,
size: 'sm', size: 'sm',
view: viewMachine, view: viewMachine,
@ -151,7 +149,7 @@ const FiatBalanceOverrides = ({ config, section }) => {
{ {
name: CASHBOX_KEY, name: CASHBOX_KEY,
display: 'Cash box', display: 'Cash box',
width: 155, width: widthsByNumberOfCassettes[maxNumberOfCassettes].cashbox,
textAlign: 'right', textAlign: 'right',
bold: true, bold: true,
input: NumberInput, input: NumberInput,
@ -160,12 +158,9 @@ const FiatBalanceOverrides = ({ config, section }) => {
decimalPlaces: 0 decimalPlaces: 0
} }
} }
] ],
R.map(
R.until( it => ({
R.gt(R.__, maxNumberOfCassettes),
it => {
elements.push({
name: `fillingPercentageCassette${it}`, name: `fillingPercentageCassette${it}`,
display: `Cash cassette ${it}`, display: `Cash cassette ${it}`,
width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette, width: widthsByNumberOfCassettes[maxNumberOfCassettes].cassette,
@ -177,15 +172,18 @@ const FiatBalanceOverrides = ({ config, section }) => {
inputProps: { inputProps: {
decimalPlaces: 0 decimalPlaces: 0
}, },
view: it => it?.toString() ?? '—', view: el => el?.toString() ?? '—',
isHidden: value => isHidden: value =>
!cashoutConfig(value.machine).active ||
it > it >
R.defaultTo(
0,
machines.find(({ deviceId }) => deviceId === value.machine) machines.find(({ deviceId }) => deviceId === value.machine)
?.numberOfCassettes ?.numberOfCassettes
}) )
return R.add(1, it) }),
}, R.range(1, maxNumberOfCassettes + 1)
1 )
) )
return ( return (

View file

@ -107,7 +107,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
const zip = new JSZip() const zip = new JSZip()
const [fetchSummary] = useLazyQuery(TX_SUMMARY, { const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
onCompleted: data => createCsv(data) onCompleted: data => createCsv(R.filter(it => !R.isEmpty(it), data))
}) })
const [cancelTransaction] = useMutation( const [cancelTransaction] = useMutation(

View file

@ -1,5 +1,4 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { utils as coinUtils } from '@lamassu/coins'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
@ -54,13 +53,9 @@ const AllSet = ({ data: currentData, doContinue }) => {
const cryptoCurrencies = data?.cryptoCurrencies ?? [] const cryptoCurrencies = data?.cryptoCurrencies ?? []
const save = () => { const save = () => {
const defaultCryptoUnit = R.head(
R.keys(coinUtils.getCryptoCurrency(coin).units)
)
const adjustedData = { const adjustedData = {
zeroConfLimit: 0, zeroConfLimit: 0,
...currentData, ...currentData
cryptoUnits: defaultCryptoUnit
} }
if (!WalletSchema.isValidSync(adjustedData)) return setError(true) if (!WalletSchema.isValidSync(adjustedData)) return setError(true)

View file

@ -123,7 +123,7 @@
"test": "mocha --recursive tests", "test": "mocha --recursive tests",
"jtest": "jest --detectOpenHandles", "jtest": "jest --detectOpenHandles",
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu", "build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
"server": "nodemon bin/lamassu-server --mockSms --logLevel silly", "server": "nodemon bin/lamassu-server --mockSms --mockScoring --logLevel silly",
"admin-server": "nodemon bin/lamassu-admin-server --dev --logLevel silly", "admin-server": "nodemon bin/lamassu-admin-server --dev --logLevel silly",
"graphql-server": "nodemon bin/new-graphql-dev-insecure", "graphql-server": "nodemon bin/new-graphql-dev-insecure",
"watch": "concurrently \"npm:server\" \"npm:admin-server\" \"npm:graphql-server\"", "watch": "concurrently \"npm:server\" \"npm:admin-server\" \"npm:graphql-server\"",

View file

@ -1,13 +1,13 @@
{ {
"files": { "files": {
"main.js": "/static/js/main.589d2bd0.chunk.js", "main.js": "/static/js/main.aa68bc4d.chunk.js",
"main.js.map": "/static/js/main.589d2bd0.chunk.js.map", "main.js.map": "/static/js/main.aa68bc4d.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.5b925903.js", "runtime-main.js": "/static/js/runtime-main.5b925903.js",
"runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map", "runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map",
"static/js/2.56a90c80.chunk.js": "/static/js/2.56a90c80.chunk.js", "static/js/2.4b3df17b.chunk.js": "/static/js/2.4b3df17b.chunk.js",
"static/js/2.56a90c80.chunk.js.map": "/static/js/2.56a90c80.chunk.js.map", "static/js/2.4b3df17b.chunk.js.map": "/static/js/2.4b3df17b.chunk.js.map",
"index.html": "/index.html", "index.html": "/index.html",
"static/js/2.56a90c80.chunk.js.LICENSE.txt": "/static/js/2.56a90c80.chunk.js.LICENSE.txt", "static/js/2.4b3df17b.chunk.js.LICENSE.txt": "/static/js/2.4b3df17b.chunk.js.LICENSE.txt",
"static/media/3-cassettes-open-1-left.d6d9aa73.svg": "/static/media/3-cassettes-open-1-left.d6d9aa73.svg", "static/media/3-cassettes-open-1-left.d6d9aa73.svg": "/static/media/3-cassettes-open-1-left.d6d9aa73.svg",
"static/media/3-cassettes-open-2-left.a9ee8d4c.svg": "/static/media/3-cassettes-open-2-left.a9ee8d4c.svg", "static/media/3-cassettes-open-2-left.a9ee8d4c.svg": "/static/media/3-cassettes-open-2-left.a9ee8d4c.svg",
"static/media/3-cassettes-open-3-left.08fed660.svg": "/static/media/3-cassettes-open-3-left.08fed660.svg", "static/media/3-cassettes-open-3-left.08fed660.svg": "/static/media/3-cassettes-open-3-left.08fed660.svg",
@ -153,7 +153,7 @@
}, },
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.5b925903.js", "static/js/runtime-main.5b925903.js",
"static/js/2.56a90c80.chunk.js", "static/js/2.4b3df17b.chunk.js",
"static/js/main.589d2bd0.chunk.js" "static/js/main.aa68bc4d.chunk.js"
] ]
} }

View file

@ -1 +1 @@
<!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>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.56a90c80.chunk.js"></script><script src="/static/js/main.589d2bd0.chunk.js"></script></body></html> <!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>!function(e){function r(r){for(var n,a,l=r[0],i=r[1],f=r[2],c=0,s=[];c<l.length;c++)a=l[c],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&s.push(o[a][0]),o[a]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,f||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,l=1;l<t.length;l++){var i=t[l];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=a(a.s=t[0]))}return e}var n={},o={1:0},u=[];function a(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,a),t.l=!0,t.exports}a.m=e,a.c=n,a.d=function(e,r,t){a.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,r){if(1&r&&(e=a(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(a.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)a.d(t,n,function(r){return e[r]}.bind(null,n));return t},a.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(r,"a",r),r},a.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},a.p="/";var l=this["webpackJsonplamassu-admin"]=this["webpackJsonplamassu-admin"]||[],i=l.push.bind(l);l.push=r,l=l.slice();for(var f=0;f<l.length;f++)r(l[f]);var p=i;t()}([])</script><script src="/static/js/2.4b3df17b.chunk.js"></script><script src="/static/js/main.aa68bc4d.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long