Merge branch 'dev' into fix/lam-298/custom_info_requests_flow

* dev: (40 commits)
  fix: remove unnecessary variable
  fix: check for starting supervisor state
  fix: Ethereum sends
  fix: force balance to big number
  fix: XMR sendCoins interface
  chore: update react
  fix: update only if node is stopped
  fix: convert bn.js to bignumber.js bignum
  fix: wallet file checking
  fix: show batching errors on admin fix: funding page should take into account batching transactions
  fix: add feeMultiplier parameter
  fix: BN functions on sendCoinsBatch
  fix: remove allowTransactionBatching from being required
  feat: disable transaction batching editing unless it's BTC
  feat: add transaction batching option to advanced wallet settings
  fix: pollingRoutes typo
  fix: button style and incomplete url
  fix: remove date width
  fix: l-c import on lamassu-send-coins script
  chore: built react files
  ...
This commit is contained in:
André Sá 2022-02-11 15:16:00 +00:00
commit b03901ddd4
56 changed files with 438 additions and 158 deletions

View file

@ -3,7 +3,7 @@
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')
const coinUtils = require('../lib/coin-utils') const { utils: coinUtils } = require('lamassu-coins')
const BN = require('../lib/bn') const BN = require('../lib/bn')
const inquirer = require('inquirer') const inquirer = require('inquirer')
const ticker = require('../lib/ticker') const ticker = require('../lib/ticker')

View file

@ -8,11 +8,12 @@ const cryptos = coinUtils.cryptoCurrencies()
const PLUGINS = { const PLUGINS = {
BTC: require('../lib/blockchain/bitcoin.js'), BTC: require('../lib/blockchain/bitcoin.js'),
LTC: require('../lib/blockchain/litecoin.js'), BCH: require('../lib/blockchain/bitcoincash.js'),
ETH: require('../lib/blockchain/ethereum.js'),
DASH: require('../lib/blockchain/dash.js'), DASH: require('../lib/blockchain/dash.js'),
ZEC: require('../lib/blockchain/zcash.js'), ETH: require('../lib/blockchain/ethereum.js'),
BCH: require('../lib/blockchain/bitcoincash.js') LTC: require('../lib/blockchain/litecoin.js'),
XMR: require('../lib/blockchain/monero.js'),
ZEC: require('../lib/blockchain/zcash.js')
} }
function plugin (crypto) { function plugin (crypto) {
@ -28,8 +29,7 @@ function run () {
const cryptoPlugin = plugin(crypto) const cryptoPlugin = plugin(crypto)
const status = common.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`).trim() const status = common.es(`sudo supervisorctl status ${crypto.code} | awk '{ print $2 }'`).trim()
if (status === 'RUNNING') cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), true) cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), _.includes(status, ['RUNNING', 'STARTING']))
if (status === 'STOPPED') cryptoPlugin.updateCore(common.getBinaries(crypto.cryptoCode), false)
}, cryptos) }, cryptos)
} }

View file

@ -48,5 +48,6 @@ addresstype=p2sh-segwit
changetype=bech32 changetype=bech32
walletrbf=1 walletrbf=1
bind=0.0.0.0:8332 bind=0.0.0.0:8332
rpcport=8333` rpcport=8333
listenonion=0`
} }

View file

@ -18,7 +18,7 @@ function setup (dataDir) {
function updateCore (coinRec, isCurrentlyRunning) { 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...')
if (isCurrentlyRunning) 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}`)
common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
@ -45,6 +45,6 @@ maxconnections=40
keypool=10000 keypool=10000
prune=4000 prune=4000
daemon=0 daemon=0
bind=0.0.0.0:8334 bind=0.0.0.0:8335
rpcport=8335` rpcport=8336`
} }

View file

@ -18,7 +18,7 @@ function setup (dataDir) {
function updateCore (coinRec, isCurrentlyRunning) { 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...')
if (isCurrentlyRunning) 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}`)
common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/dash.tar.gz -C /tmp/`)

View file

@ -6,7 +6,7 @@ module.exports = { setup, updateCore }
function updateCore (coinRec, isCurrentlyRunning) { 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...')
if (isCurrentlyRunning) 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}`)
common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/ethereum.tar.gz -C /tmp/`)

View file

@ -9,6 +9,8 @@ const _ = require('lodash/fp')
const { utils: coinUtils } = require('lamassu-coins') const { utils: coinUtils } = require('lamassu-coins')
const options = require('../options') const options = require('../options')
const settingsLoader = require('../new-settings-loader')
const wallet = require('../wallet')
const common = require('./common') const common = require('./common')
const doVolume = require('./do-volume') const doVolume = require('./do-volume')
@ -112,6 +114,35 @@ function plugin (crypto) {
return plugin return plugin
} }
function getBlockchainSyncStatus (cryptoList) {
const installedCryptos = _.reduce((acc, value) => ({ ...acc, [value.cryptoCode]: isInstalledSoftware(value) && isInstalledVolume(value) }), {}, cryptoList)
return settingsLoader.loadLatest()
.then(settings => {
const installedButNotConfigured = []
const blockchainStatuses = _.reduce((acc, value) => {
const processStatus = common.es(`sudo supervisorctl status ${value.code} | awk '{ print $2 }'`).trim()
return acc.then(a => {
return wallet.checkBlockchainStatus(settings, value.cryptoCode)
.then(res => _.includes(value.cryptoCode, _.keys(installedCryptos)) ? Promise.resolve({ ...a, [value.cryptoCode]: res }) : Promise.resolve({ ...a }))
.catch(() => {
if (processStatus === 'RUNNING') {
installedButNotConfigured.push(value.cryptoCode)
return Promise.resolve({ ...a, [value.cryptoCode]: 'syncing' })
}
return Promise.resolve({ ...a })
})
})
},
Promise.resolve({}),
cryptoList
)
return Promise.all([blockchainStatuses, installedButNotConfigured])
})
.then(([blockchainStatuses, installedButNotConfigured]) => ({ blockchainStatuses, installedButNotConfigured }))
}
function run () { function run () {
const choices = _.flow([ const choices = _.flow([
_.filter(c => c.type !== 'erc-20'), _.filter(c => c.type !== 'erc-20'),
@ -129,13 +160,40 @@ function run () {
const questions = [] const questions = []
const validateAnswers = async (answers) => {
if (_.size(answers) > 2) return { message: `Please insert a maximum of two coins to install.`, isValid: false }
return getBlockchainSyncStatus(cryptos)
.then(({ blockchainStatuses, installedButNotConfigured }) => {
if (!_.isEmpty(installedButNotConfigured)) {
logger.warn(`Detected ${_.join(' and ', installedButNotConfigured)} installed on this machine, but couldn't establish connection. ${_.size(installedButNotConfigured) === 1 ? `Is this plugin` : `Are these plugins`} configured via admin?`)
}
const result = _.reduce((acc, value) => ({ ...acc, [value]: _.isNil(acc[value]) ? 1 : acc[value] + 1 }), {}, _.values(blockchainStatuses))
if (_.size(answers) + result.syncing > 2) {
return { message: `Installing these coins would pass the 2 parallel blockchain synchronization limit. Please try again with fewer coins or try again later.`, isValid: false }
}
if (result.syncing > 2) {
return { message: `There are currently more than 2 blockchains in their initial synchronization. Please try again later.`, isValid: false }
}
return { message: null, isValid: true }
})
}
questions.push({ questions.push({
type: 'checkbox', type: 'checkbox',
name: 'crypto', name: 'crypto',
message: 'Which cryptocurrencies would you like to install?', message: 'Which cryptocurrencies would you like to install?\nTo prevent server resource overloading, only TWO coins should be syncing simultaneously.\nMore coins can be installed after this process is over.',
choices choices
}) })
inquirer.prompt(questions) inquirer.prompt(questions)
.then(answers => processCryptos(answers.crypto)) .then(answers => Promise.all([validateAnswers(answers.crypto), answers]))
.then(([res, answers]) => {
if (res.isValid) {
return processCryptos(answers.crypto)
}
logger.error(res.message)
})
} }

View file

@ -18,7 +18,7 @@ function setup (dataDir) {
function updateCore (coinRec, isCurrentlyRunning) { 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...')
if (isCurrentlyRunning) 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}`)
common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)

View file

@ -4,7 +4,7 @@ const { utils } = require('lamassu-coins')
const common = require('./common') const common = require('./common')
module.exports = {setup} module.exports = { setup, updateCore }
const coinRec = utils.getCryptoCurrency('XMR') const coinRec = utils.getCryptoCurrency('XMR')
@ -13,11 +13,31 @@ function setup (dataDir) {
const auth = `lamassuserver:${common.randomPass()}` const auth = `lamassuserver:${common.randomPass()}`
const config = buildConfig(auth) const config = buildConfig(auth)
common.writeFile(path.resolve(dataDir, coinRec.configFile), config) common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
const cmd = `/usr/local/bin/${coinRec.daemon} --data-dir ${dataDir} --config-file ${dataDir}/${coinRec.configFile}` const cmd = `/usr/local/bin/${coinRec.daemon} --no-zmq --data-dir ${dataDir} --config-file ${dataDir}/${coinRec.configFile}`
const walletCmd = `/usr/local/bin/${coinRec.wallet} --stagenet --rpc-login ${auth} --daemon-host 127.0.0.1 --daemon-port 18081 --trusted-daemon --daemon-login ${auth} --rpc-bind-port 18082 --wallet-dir ${dataDir}/wallets` const walletCmd = `/usr/local/bin/${coinRec.wallet} --rpc-login ${auth} --daemon-host 127.0.0.1 --daemon-port 18081 --trusted-daemon --daemon-login ${auth} --rpc-bind-port 18082 --wallet-dir ${dataDir}/wallets`
common.writeSupervisorConfig(coinRec, cmd, walletCmd) common.writeSupervisorConfig(coinRec, cmd, walletCmd)
} }
function updateCore (coinRec, isCurrentlyRunning) {
common.logger.info('Updating Monero. This may take a minute...')
common.es(`sudo supervisorctl stop monero monero-wallet`)
common.es(`curl -#o /tmp/monero.tar.gz ${coinRec.url}`)
common.es(`tar -xf /tmp/monero.tar.gz -C /tmp/`)
common.logger.info('Updating wallet...')
common.es(`cp /tmp/${coinRec.dir}/monerod /usr/local/bin/monerod`)
common.es(`cp /tmp/${coinRec.dir}/monero-wallet-rpc /usr/local/bin/monero-wallet-rpc`)
common.es(`rm -r /tmp/${coinRec.dir.replace('/bin', '')}`)
common.es(`rm /tmp/monero.tar.gz`)
if (isCurrentlyRunning) {
common.logger.info('Starting wallet...')
common.es(`sudo supervisorctl start monero monero-wallet`)
}
common.logger.info('Monero is updated!')
}
function buildConfig (auth) { function buildConfig (auth) {
return `rpc-login=${auth} return `rpc-login=${auth}
stagenet=0 stagenet=0

View file

@ -11,7 +11,7 @@ const logger = common.logger
function updateCore (coinRec, isCurrentlyRunning) { 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...')
if (isCurrentlyRunning) 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}`)
common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`) common.es(`tar -xzf /tmp/zcash.tar.gz -C /tmp/`)

View file

@ -8,7 +8,7 @@ const toObj = helper.toObj
const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed', const UPDATEABLE_FIELDS = ['txHash', 'txVersion', 'status', 'dispense', 'dispenseConfirmed',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode', 'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
'receivedCryptoAtoms' ] 'receivedCryptoAtoms', 'walletScore' ]
module.exports = {upsert, update, insert} module.exports = {upsert, update, insert}

View file

@ -107,9 +107,56 @@ function processTxStatus (tx, settings) {
return pi.getStatus(tx) return pi.getStatus(tx)
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status })) .then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
.then(_tx => getWalletScore(_tx, pi))
.then(_tx => selfPost(_tx, pi)) .then(_tx => selfPost(_tx, pi))
} }
function getWalletScore (tx, pi) {
const statuses = ['published', 'authorized', 'rejected', 'insufficientFunds']
if (_.includes(tx.status, statuses) && _.isNil(tx.walletScore)) {
// Transaction shows up on the blockchain, we can request the sender address
return pi.getTransactionHash(tx)
.then(txHashes => pi.getInputAddresses(tx, txHashes))
.then(addresses => {
const addressesPromise = []
_.forEach(it => addressesPromise.push(pi.rateWallet(tx.cryptoCode, it)), addresses)
return Promise.all(addressesPromise)
})
.then(scores => {
if (_.isNil(scores) || _.isEmpty(scores)) return tx
const highestScore = _.maxBy(it => it.score, scores)
// Conservatively assign the highest risk of all input addresses to the risk of this transaction
return highestScore.isValid
? _.assign(tx, { walletScore: highestScore.score })
: _.assign(tx, {
walletScore: highestScore.score,
error: 'Ciphertrace score is above defined threshold',
errorCode: 'operatorCancel',
dispense: true
})
})
.catch(() => _.assign(tx, {
walletScore: 10,
error: 'Ciphertrace services not available',
errorCode: 'operatorCancel',
dispense: true
}))
}
if (_.includes(tx.status, statuses) && !_.isNil(tx.walletScore)) {
return pi.isValidWalletScore(tx.walletScore)
.then(isValid => isValid ? tx : _.assign(tx, {
error: 'Ciphertrace score is above defined threshold',
errorCode: 'operatorCancel',
dispense: true
}))
}
return tx
}
function monitorLiveIncoming (settings, applyFilter, coinFilter) { function monitorLiveIncoming (settings, applyFilter, coinFilter) {
const statuses = ['notSeen', 'published', 'insufficientFunds'] const statuses = ['notSeen', 'published', 'insufficientFunds']
const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE

View file

@ -47,6 +47,7 @@ const typeDef = gql`
txCustomerPhotoAt: Date txCustomerPhotoAt: Date
batched: Boolean batched: Boolean
batchTime: Date batchTime: Date
batchError: String
walletScore: Int walletScore: Int
} }

View file

@ -4,6 +4,7 @@ const settingsLoader = require('../../new-settings-loader')
const configManager = require('../../new-config-manager') const configManager = require('../../new-config-manager')
const wallet = require('../../wallet') const wallet = require('../../wallet')
const ticker = require('../../ticker') const ticker = require('../../ticker')
const txBatching = require('../../tx-batching')
const { utils: coinUtils } = require('lamassu-coins') const { utils: coinUtils } = require('lamassu-coins')
function computeCrypto (cryptoCode, _balance) { function computeCrypto (cryptoCode, _balance) {
@ -23,16 +24,17 @@ function computeFiat (rate, cryptoCode, _balance) {
function getSingleCoinFunding (settings, fiatCode, cryptoCode) { function getSingleCoinFunding (settings, fiatCode, cryptoCode) {
const promises = [ const promises = [
wallet.newFunding(settings, cryptoCode), wallet.newFunding(settings, cryptoCode),
ticker.getRates(settings, fiatCode, cryptoCode) ticker.getRates(settings, fiatCode, cryptoCode),
txBatching.getOpenBatchCryptoValue(cryptoCode)
] ]
return Promise.all(promises) return Promise.all(promises)
.then(([fundingRec, ratesRec]) => { .then(([fundingRec, ratesRec, batchRec]) => {
const rates = ratesRec.rates const rates = ratesRec.rates
const rate = (rates.ask.plus(rates.bid)).div(2) const rate = (rates.ask.plus(rates.bid)).div(2)
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance) const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
const pending = fundingRec.fundingPendingBalance const pending = fundingRec.fundingPendingBalance.minus(batchRec)
const fiatPending = computeFiat(rate, cryptoCode, pending) const fiatPending = computeFiat(rate, cryptoCode, pending)
const fundingAddress = fundingRec.fundingAddress const fundingAddress = fundingRec.fundingAddress
const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress) const fundingAddressUrl = coinUtils.buildUrl(cryptoCode, fundingAddress)

View file

@ -54,10 +54,12 @@ function batch (
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
txs.tx_customer_photo_at AS tx_customer_photo_at, txs.tx_customer_photo_at AS tx_customer_photo_at,
txs.tx_customer_photo_path AS tx_customer_photo_path, txs.tx_customer_photo_path AS tx_customer_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired ((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
tb.error_message AS batch_error
FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs FROM (SELECT *, ${cashInTx.TRANSACTION_STATES} AS txStatus FROM cash_in_txs) AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id LEFT OUTER JOIN customers c ON txs.customer_id = c.id
LEFT JOIN devices d ON txs.device_id = d.device_id LEFT JOIN devices d ON txs.device_id = d.device_id
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE txs.created >= $2 AND txs.created <= $3 ${ WHERE txs.created >= $2 AND txs.created <= $3 ${
id !== null ? `AND txs.device_id = $6` : `` id !== null ? `AND txs.device_id = $6` : ``
} }
@ -69,7 +71,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 fiat > 0) 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,
@ -151,6 +153,7 @@ const getCashOutStatus = it => {
const getCashInStatus = it => { const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled' if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error' if (it.hasError) return 'Error'
if (it.batchError) return 'Error'
if (it.sendConfirmed) return 'Sent' if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired' if (it.expired) return 'Expired'
return 'Pending' return 'Pending'
@ -176,9 +179,11 @@ function getCustomerTransactionsBatch (ids) {
c.name AS customer_name, c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path, c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $2)) AS expired ((NOT txs.send_confirmed) AND (txs.created <= now() - interval $2)) AS expired,
tb.error_message AS batch_error
FROM cash_in_txs AS txs FROM cash_in_txs AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id LEFT OUTER JOIN customers c ON txs.customer_id = c.id
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE c.id IN ($1^) WHERE c.id IN ($1^)
ORDER BY created DESC limit $3` ORDER BY created DESC limit $3`
@ -220,9 +225,11 @@ function single (txId) {
c.name AS customer_name, c.name AS customer_name,
c.front_camera_path AS customer_front_camera_path, c.front_camera_path AS customer_front_camera_path,
c.id_card_photo_path AS customer_id_card_photo_path, c.id_card_photo_path AS customer_id_card_photo_path,
((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired ((NOT txs.send_confirmed) AND (txs.created <= now() - interval $1)) AS expired,
tb.error_message AS batch_error
FROM cash_in_txs AS txs FROM cash_in_txs AS txs
LEFT OUTER JOIN customers c ON txs.customer_id = c.id LEFT OUTER JOIN customers c ON txs.customer_id = c.id
LEFT OUTER JOIN transaction_batches tb ON txs.batch_id = tb.id
WHERE id=$2` WHERE id=$2`
const cashOutSql = `SELECT 'cashOut' AS tx_class, const cashOutSql = `SELECT 'cashOut' AS tx_class,

View file

@ -836,6 +836,14 @@ function plugins (settings, deviceId) {
return walletScoring.isValidWalletScore(settings, score) return walletScoring.isValidWalletScore(settings, score)
} }
function getTransactionHash (tx) {
return walletScoring.getTransactionHash(settings, tx.cryptoCode, tx.toAddress)
}
function getInputAddresses (tx, txHashes) {
return walletScoring.getInputAddresses(settings, tx.cryptoCode, txHashes)
}
return { return {
getRates, getRates,
buildRates, buildRates,
@ -865,7 +873,9 @@ function plugins (settings, deviceId) {
fetchCurrentConfigVersion, fetchCurrentConfigVersion,
pruneMachinesHeartbeat, pruneMachinesHeartbeat,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }
} }

View file

@ -8,7 +8,7 @@ const binanceus = require('../exchange/binanceus')
const cex = require('../exchange/cex') const cex = require('../exchange/cex')
const ftx = require('../exchange/ftx') const ftx = require('../exchange/ftx')
const bitpay = require('../ticker/bitpay') const bitpay = require('../ticker/bitpay')
const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT, XMR } = COINS const { BTC, BCH, DASH, ETH, LTC, ZEC, USDT } = COINS
const ALL = { const ALL = {
cex: cex, cex: cex,
@ -19,7 +19,7 @@ const ALL = {
itbit: itbit, itbit: itbit,
bitpay: bitpay, bitpay: bitpay,
coinbase: { coinbase: {
CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT, XMR], CRYPTO: [BTC, ETH, LTC, DASH, ZEC, BCH, USDT],
FIAT: 'ALL_CURRENCIES' FIAT: 'ALL_CURRENCIES'
} }
} }

View file

@ -1,6 +1,8 @@
const axios = require('axios') const axios = require('axios')
const _ = require('lodash/fp') const _ = require('lodash/fp')
const logger = require('../../../logger')
const NAME = 'CipherTrace' const NAME = 'CipherTrace'
const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC', 'BNB', 'RSK'] const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC', 'BNB', 'RSK']
@ -37,11 +39,53 @@ function isValidWalletScore(account, score) {
if (_.isNil(client)) return Promise.resolve(true) if (_.isNil(client)) return Promise.resolve(true)
const threshold = account.scoreThreshold const threshold = account.scoreThreshold
return Promise.resolve(score < threshold) return _.isNil(account) ? Promise.resolve(true) : Promise.resolve(score < threshold)
}
function getTransactionHash(account, cryptoCode, receivingAddress) {
const client = getClient(account)
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
const { apiVersion, authHeader } = client
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}address/search?features=tx&address=${receivingAddress}`, {
headers: authHeader
})
.then(res => {
const data = res.data
if (_.size(data.txHistory) > 1) {
logger.warn('An address generated by this wallet was used in more than one transaction')
}
return _.join(', ', _.map(it => it.txHash, data.txHistory))
})
}
function getInputAddresses(account, cryptoCode, txHashes) {
const client = getClient(account)
if (!_.includes(_.toUpper(cryptoCode), SUPPORTED_COINS) || _.isNil(client)) return Promise.resolve(null)
const { apiVersion, authHeader } = client
return axios.get(`https://rest.ciphertrace.com/api/${apiVersion}/${_.toLower(cryptoCode) !== 'btc' ? `${_.toLower(cryptoCode)}_` : ``}tx?txhashes=${txHashes}`, {
headers: authHeader
})
.then(res => {
const data = res.data
if (_.size(data.transactions) > 1) {
logger.warn('An address generated by this wallet was used in more than one transaction')
}
const transactionInputs = _.flatMap(it => it.inputs, data.transactions)
const inputAddresses = _.map(it => it.address, transactionInputs)
return inputAddresses
})
} }
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -6,7 +6,7 @@ function rateWallet (account, cryptoCode, address) {
return new Promise((resolve, _) => { return new Promise((resolve, _) => {
setTimeout(() => { setTimeout(() => {
console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address) console.log('[WALLET-SCORING] DEBUG: Mock scoring rating wallet address %s', address)
return Promise.resolve(7) return Promise.resolve(2)
.then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD })) .then(score => resolve({ address, score, isValid: score < WALLET_SCORE_THRESHOLD }))
}, 100) }, 100)
}) })
@ -20,8 +20,26 @@ function isValidWalletScore (account, score) {
}) })
} }
function getTransactionHash (account, cryptoCode, receivingAddress) {
return new Promise((resolve, _) => {
setTimeout(() => {
return resolve('<Fake transaction hash>')
}, 100)
})
}
function getInputAddresses (account, cryptoCode, txHashes) {
return new Promise((resolve, _) => {
setTimeout(() => {
return resolve(['<Fake input address hash>'])
}, 100)
})
}
module.exports = { module.exports = {
NAME, NAME,
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -8,8 +8,6 @@ const { utils: coinUtils } = require('lamassu-coins')
const cryptoRec = coinUtils.getCryptoCurrency('BCH') const cryptoRec = coinUtils.getCryptoCurrency('BCH')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function fetch (method, params) { function fetch (method, params) {
@ -118,9 +116,10 @@ function cryptoNetwork (account, cryptoCode, settings, operatorId) {
.then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main') .then(() => parseInt(rpcConfig.port, 10) === 18332 ? 'test' : 'main')
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
} }
module.exports = { module.exports = {
@ -130,5 +129,5 @@ module.exports = {
getStatus, getStatus,
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
supportsBatching checkBlockchainStatus
} }

View file

@ -9,7 +9,6 @@ const { utils: coinUtils } = require('lamassu-coins')
const cryptoRec = coinUtils.getCryptoCurrency('BTC') const cryptoRec = coinUtils.getCryptoCurrency('BTC')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = true
const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function fetch (method, params) { function fetch (method, params) {
@ -80,19 +79,19 @@ function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
}) })
} }
function sendCoinsBatch (account, txs, cryptoCode) { function sendCoinsBatch (account, txs, cryptoCode, feeMultiplier) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => calculateFeeDiscount(feeMultiplier)) .then(() => calculateFeeDiscount(feeMultiplier))
.then(newFee => fetch('settxfee', [newFee])) .then(newFee => fetch('settxfee', [newFee]))
.then(() => { .then(() => {
const txAddressAmountPairs = _.map(tx => [tx.address, tx.cryptoAtoms.shift(-unitScale).toFixed(8)], txs) const txAddressAmountPairs = _.map(tx => [tx.address, tx.cryptoAtoms.shiftedBy(-unitScale).toFixed(8)], txs)
return Promise.all([JSON.stringify(_.fromPairs(txAddressAmountPairs))]) return Promise.all([JSON.stringify(_.fromPairs(txAddressAmountPairs))])
}) })
.then(([obj]) => fetch('sendmany', ['', obj])) .then(([obj]) => fetch('sendmany', ['', obj]))
.then((txId) => fetch('gettransaction', [txId])) .then((txId) => fetch('gettransaction', [txId]))
.then((res) => _.pick(['fee', 'txid'], res)) .then((res) => _.pick(['fee', 'txid'], res))
.then((pickedObj) => ({ .then((pickedObj) => ({
fee: BN(pickedObj.fee).abs().shift(unitScale).round(), fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
txid: pickedObj.txid txid: pickedObj.txid
})) }))
.catch(err => { .catch(err => {
@ -171,9 +170,10 @@ function fetchRBF (txId) {
}) })
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
} }
module.exports = { module.exports = {
@ -186,5 +186,5 @@ module.exports = {
fetchRBF, fetchRBF,
estimateFee, estimateFee,
sendCoinsBatch, sendCoinsBatch,
supportsBatching checkBlockchainStatus
} }

View file

@ -14,8 +14,6 @@ const NAME = 'BitGo'
const SUPPORTED_COINS = ['BTC', 'ZEC', 'LTC', 'BCH', 'DASH'] const SUPPORTED_COINS = ['BTC', 'ZEC', 'LTC', 'BCH', 'DASH']
const BCH_CODES = ['BCH', 'TBCH'] const BCH_CODES = ['BCH', 'TBCH']
const SUPPORTS_BATCHING = false
function buildBitgo (account) { function buildBitgo (account) {
const env = account.environment === 'test' ? 'test' : 'prod' const env = account.environment === 'test' ? 'test' : 'prod'
return new BitGo.BitGo({ accessToken: account.token.trim(), env, userAgent: userAgent }) return new BitGo.BitGo({ accessToken: account.token.trim(), env, userAgent: userAgent })
@ -159,9 +157,9 @@ function cryptoNetwork (account, cryptoCode, settings, operatorId) {
.then(() => account.environment === 'test' ? 'test' : 'main') .then(() => account.environment === 'test' ? 'test' : 'main')
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => Promise.resolve('ready'))
} }
module.exports = { module.exports = {
@ -172,5 +170,5 @@ module.exports = {
getStatus, getStatus,
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
supportsBatching checkBlockchainStatus
} }

View file

@ -9,7 +9,6 @@ const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('DASH') const cryptoRec = coinUtils.getCryptoCurrency('DASH')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function fetch (method, params) { function fetch (method, params) {
@ -113,9 +112,10 @@ function newFunding (account, cryptoCode, settings, operatorId) {
})) }))
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
} }
module.exports = { module.exports = {
@ -124,5 +124,5 @@ module.exports = {
newAddress, newAddress,
getStatus, getStatus,
newFunding, newFunding,
supportsBatching checkBlockchainStatus
} }

View file

@ -17,8 +17,6 @@ const paymentPrefixPath = "m/44'/60'/0'/0'"
const defaultPrefixPath = "m/44'/60'/1'/0'" const defaultPrefixPath = "m/44'/60'/1'/0'"
let lastUsedNonces = {} let lastUsedNonces = {}
const SUPPORTS_BATCHING = false
module.exports = { module.exports = {
NAME, NAME,
balance, balance,
@ -32,7 +30,7 @@ module.exports = {
privateKey, privateKey,
isStrictAddress, isStrictAddress,
connect, connect,
supportsBatching checkBlockchainStatus
} }
function connect (url) { function connect (url) {
@ -92,6 +90,8 @@ function _balance (includePending, address, cryptoCode) {
} }
const block = includePending ? 'pending' : undefined const block = includePending ? 'pending' : undefined
return pify(web3.eth.getBalance)(address.toLowerCase(), block) return pify(web3.eth.getBalance)(address.toLowerCase(), block)
/* NOTE: Convert bn.js bignum to bignumber.js bignum */
.then(balance => balance ? BN(balance.toString(16), 16) : BN(0))
} }
function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) { function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
@ -103,7 +103,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
const txTemplate = { const txTemplate = {
from: fromAddress, from: fromAddress,
to: toAddress, to: toAddress,
value: amount value: amount.toString()
} }
const promises = [ const promises = [
@ -226,7 +226,8 @@ function newFunding (account, cryptoCode, settings, operatorId) {
}) })
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(pify(web3.eth.isSyncing))
.then(res => _.isObject(res) ? 'syncing' : 'ready')
} }

View file

@ -9,7 +9,6 @@ const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('LTC') const cryptoRec = coinUtils.getCryptoCurrency('LTC')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function fetch (method, params) { function fetch (method, params) {
@ -113,9 +112,10 @@ function newFunding (account, cryptoCode, settings, operatorId) {
})) }))
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
} }
module.exports = { module.exports = {
@ -124,5 +124,5 @@ module.exports = {
newAddress, newAddress,
getStatus, getStatus,
newFunding, newFunding,
supportsBatching checkBlockchainStatus
} }

View file

@ -5,7 +5,6 @@ const E = require('../../../error')
const { utils: coinUtils } = require('lamassu-coins') const { utils: coinUtils } = require('lamassu-coins')
const NAME = 'FakeWallet' const NAME = 'FakeWallet'
const BATCHABLE_COINS = ['BTC']
const SECONDS = 1000 const SECONDS = 1000
const PUBLISH_TIME = 3 * SECONDS const PUBLISH_TIME = 3 * SECONDS
@ -111,8 +110,9 @@ function getStatus (account, tx, requested, settings, operatorId) {
return Promise.resolve({status: 'confirmed'}) return Promise.resolve({status: 'confirmed'})
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return Promise.resolve(_.includes(cryptoCode, BATCHABLE_COINS)) return checkCryptoCode(cryptoCode)
.then(() => Promise.resolve('ready'))
} }
module.exports = { module.exports = {
@ -123,5 +123,5 @@ module.exports = {
newAddress, newAddress,
getStatus, getStatus,
newFunding, newFunding,
supportsBatching checkBlockchainStatus
} }

View file

@ -17,8 +17,6 @@ const configPath = utils.configPath(cryptoRec, blockchainDir)
const walletDir = path.resolve(utils.cryptoDir(cryptoRec, blockchainDir), 'wallets') const walletDir = path.resolve(utils.cryptoDir(cryptoRec, blockchainDir), 'wallets')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
function rpcConfig () { function rpcConfig () {
try { try {
const config = jsonRpc.parseConf(configPath) const config = jsonRpc.parseConf(configPath)
@ -42,8 +40,7 @@ function handleError (error) {
{ {
if ( if (
fs.existsSync(path.resolve(walletDir, 'Wallet')) && fs.existsSync(path.resolve(walletDir, 'Wallet')) &&
fs.existsSync(path.resolve(walletDir, 'Wallet.keys')) && fs.existsSync(path.resolve(walletDir, 'Wallet.keys'))
fs.existsSync(path.resolve(walletDir, 'Wallet.address.txt'))
) { ) {
logger.debug('Found wallet! Opening wallet...') logger.debug('Found wallet! Opening wallet...')
return openWallet() return openWallet()
@ -92,7 +89,7 @@ function accountBalance (cryptoCode) {
.then(() => refreshWallet()) .then(() => refreshWallet())
.then(() => fetch('get_balance', { account_index: 0, address_indices: [0] })) .then(() => fetch('get_balance', { account_index: 0, address_indices: [0] }))
.then(res => { .then(res => {
return BN(res.unlocked_balance).shift(unitScale).round() return BN(res.unlocked_balance).shiftedBy(unitScale).decimalPlaces(0)
}) })
.catch(err => handleError(err)) .catch(err => handleError(err))
} }
@ -101,11 +98,12 @@ function balance (account, cryptoCode) {
return accountBalance(cryptoCode) return accountBalance(cryptoCode)
} }
function sendCoins (account, address, cryptoAtoms, cryptoCode) { function sendCoins (account, tx, settings, operatorId, feeMultiplier) {
const { toAddress, cryptoAtoms, cryptoCode } = tx
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => refreshWallet()) .then(() => refreshWallet())
.then(() => fetch('transfer_split', { .then(() => fetch('transfer_split', {
destinations: [{ amount: cryptoAtoms, address }], destinations: [{ amount: cryptoAtoms, address: toAddress }],
account_index: 0, account_index: 0,
subaddr_indices: [], subaddr_indices: [],
priority: 0, priority: 0,
@ -177,7 +175,7 @@ function newFunding (account, cryptoCode) {
fetch('create_address', { account_index: 0 }) fetch('create_address', { account_index: 0 })
])) ]))
.then(([balanceRes, addressRes]) => ({ .then(([balanceRes, addressRes]) => ({
fundingPendingBalance: BN(balanceRes.balance).sub(balanceRes.unlocked_balance), fundingPendingBalance: BN(balanceRes.balance).minus(balanceRes.unlocked_balance),
fundingConfirmedBalance: BN(balanceRes.unlocked_balance), fundingConfirmedBalance: BN(balanceRes.unlocked_balance),
fundingAddress: addressRes.address fundingAddress: addressRes.address
})) }))
@ -200,9 +198,28 @@ function cryptoNetwork (account, cryptoCode) {
}) })
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => {
try {
const config = jsonRpc.parseConf(configPath)
// Daemon uses a different connection of the wallet
const rpcConfig = {
username: config['rpc-login'].split(':')[0],
password: config['rpc-login'].split(':')[1],
port: cryptoRec.defaultPort
}
return jsonRpc.fetchDigest(rpcConfig, 'get_info')
.then(res => {
console.log('res XMR', res)
return !!res.synchronized ? 'ready' : 'syncing'
})
} catch (err) {
throw new Error('XMR daemon is currently not installed')
}
})
} }
module.exports = { module.exports = {
@ -212,5 +229,5 @@ module.exports = {
getStatus, getStatus,
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
supportsBatching checkBlockchainStatus
} }

View file

@ -9,7 +9,6 @@ const E = require('../../../error')
const cryptoRec = coinUtils.getCryptoCurrency('ZEC') const cryptoRec = coinUtils.getCryptoCurrency('ZEC')
const unitScale = cryptoRec.unitScale const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
const rpcConfig = jsonRpc.rpcConfig(cryptoRec) const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
@ -139,9 +138,10 @@ function newFunding (account, cryptoCode, settings, operatorId) {
})) }))
} }
function supportsBatching (cryptoCode) { function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode) return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING) .then(() => fetch('getblockchaininfo'))
.then(res => !!res['initial_block_download_complete'] ? 'ready' : 'syncing')
} }
module.exports = { module.exports = {
@ -150,5 +150,5 @@ module.exports = {
newAddress, newAddress,
getStatus, getStatus,
newFunding, newFunding,
supportsBatching checkBlockchainStatus
} }

View file

@ -139,8 +139,8 @@ function poll (req, res, next) {
response.idCardDataVerificationThreshold = compatTriggers.idCardData response.idCardDataVerificationThreshold = compatTriggers.idCardData
response.idCardPhotoVerificationActive = !!compatTriggers.idCardPhoto response.idCardPhotoVerificationActive = !!compatTriggers.idCardPhoto
response.idCardPhotoVerificationThreshold = compatTriggers.idCardPhoto response.idCardPhotoVerificationThreshold = compatTriggers.idCardPhoto
response.sanctionsVerificationActive = !!compatTriggers.sancations response.sanctionsVerificationActive = !!compatTriggers.sanctions
response.sanctionsVerificationThreshold = compatTriggers.sancations response.sanctionsVerificationThreshold = compatTriggers.sanctions
response.frontCameraVerificationActive = !!compatTriggers.facephoto response.frontCameraVerificationActive = !!compatTriggers.facephoto
response.frontCameraVerificationThreshold = compatTriggers.facephoto response.frontCameraVerificationThreshold = compatTriggers.facephoto
} }

View file

@ -28,7 +28,27 @@ function isValidWalletScore (settings, score) {
}) })
} }
function getTransactionHash (settings, cryptoCode, receivingAddress) {
return Promise.resolve()
.then(() => {
const { plugin, account } = loadWalletScoring(settings)
return plugin.getTransactionHash(account, cryptoCode, receivingAddress)
})
}
function getInputAddresses (settings, cryptoCode, txHashes) {
return Promise.resolve()
.then(() => {
const { plugin, account } = loadWalletScoring(settings)
return plugin.getInputAddresses(account, cryptoCode, txHashes)
})
}
module.exports = { module.exports = {
rateWallet, rateWallet,
isValidWalletScore isValidWalletScore,
getTransactionHash,
getInputAddresses
} }

View file

@ -48,9 +48,8 @@ const lastBalance = {}
function _balance (settings, cryptoCode) { function _balance (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode) return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.balance(r.account, cryptoCode, settings, r.operatorId)) .then(r => r.wallet.balance(r.account, cryptoCode, settings, r.operatorId))
.then(balance => Promise.all([balance, supportsBatching(settings, cryptoCode)])) .then(balance => Promise.all([balance, getOpenBatchCryptoValue(cryptoCode)]))
.then(([balance, supportsBatching]) => Promise.all([balance, supportsBatching ? getOpenBatchCryptoValue(cryptoCode) : Promise.resolve(BN(0))])) .then(([balance, reservedBalance]) => ({ balance: BN(balance).minus(reservedBalance), reservedBalance, timestamp: Date.now() }))
.then(([balance, reservedBalance]) => ({ balance: balance.minus(reservedBalance), reservedBalance, timestamp: Date.now() }))
.then(r => { .then(r => {
lastBalance[cryptoCode] = r lastBalance[cryptoCode] = r
return r return r
@ -82,7 +81,8 @@ function sendCoins (settings, tx) {
function sendCoinsBatch (settings, txs, cryptoCode) { function sendCoinsBatch (settings, txs, cryptoCode) {
return fetchWallet(settings, cryptoCode) return fetchWallet(settings, cryptoCode)
.then(r => { .then(r => {
return r.wallet.sendCoinsBatch(r.account, txs, cryptoCode) const feeMultiplier = settings[`wallets_${cryptoCode}_feeMultiplier`]
return r.wallet.sendCoinsBatch(r.account, txs, cryptoCode, feeMultiplier)
.then(res => { .then(res => {
mem.clear(module.exports.balance) mem.clear(module.exports.balance)
return res return res
@ -233,8 +233,12 @@ function isStrictAddress (settings, cryptoCode, toAddress) {
} }
function supportsBatching (settings, cryptoCode) { function supportsBatching (settings, cryptoCode) {
return Promise.resolve(!!configManager.getWalletSettings(cryptoCode, settings.config).allowTransactionBatching)
}
function checkBlockchainStatus (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode) return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.supportsBatching(cryptoCode)) .then(r => r.wallet.checkBlockchainStatus(cryptoCode))
} }
const coinFilter = ['ETH'] const coinFilter = ['ETH']
@ -265,5 +269,6 @@ module.exports = {
isHd, isHd,
newFunding, newFunding,
cryptoNetwork, cryptoNetwork,
supportsBatching supportsBatching,
checkBlockchainStatus
} }

View file

@ -0,0 +1,13 @@
var db = require('./db')
exports.up = function (next) {
var sql = [
`ALTER TABLE cash_out_txs ADD COLUMN wallet_score SMALLINT`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -10,13 +10,12 @@ const useStyles = makeStyles({
imgWrapper: { imgWrapper: {
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
display: 'flex', display: 'flex'
width: 550
}, },
imgInner: { imgInner: {
objectFit: 'cover', objectFit: 'cover',
objectPosition: 'center', objectPosition: 'center',
width: 550, width: 500,
marginBottom: 40 marginBottom: 40
} }
}) })
@ -33,17 +32,16 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
style: { style: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
borderRadius: 0, borderRadius: 0,
width: 50,
color: 'transparent', color: 'transparent',
opacity: 1 opacity: 1
} }
}} }}
// navButtonsWrapperProps={{ navButtonsWrapperProps={{
// style: { style: {
// background: 'linear-gradient(to right, black 10%, transparent 80%)', marginLeft: -22,
// opacity: '0.4' marginRight: -22
// } }
// }} }}
autoPlay={false} autoPlay={false}
indicators={false} indicators={false}
navButtonsAlwaysVisible={true} navButtonsAlwaysVisible={true}

View file

@ -41,14 +41,14 @@ const useStyles = makeStyles({
}) })
const CheckboxInput = ({ name, onChange, value, settings, ...props }) => { const CheckboxInput = ({ name, onChange, value, settings, ...props }) => {
const { enabled, label, disabledMessage } = settings const { enabled, label, disabledMessage, rightSideLabel } = settings
const classes = useStyles() const classes = useStyles()
return ( return (
<> <>
{enabled ? ( {enabled ? (
<div className={classes.checkBoxLabel}> <div className={classes.checkBoxLabel}>
<Label2>{label}</Label2> {!rightSideLabel && <Label2>{label}</Label2>}
<Checkbox <Checkbox
id={name} id={name}
classes={{ classes={{
@ -67,6 +67,7 @@ const CheckboxInput = ({ name, onChange, value, settings, ...props }) => {
disableRipple disableRipple
{...props} {...props}
/> />
{rightSideLabel && <Label2>{label}</Label2>}
</div> </div>
) : ( ) : (
<div className={classes.wrapper}> <div className={classes.wrapper}>

View file

@ -61,9 +61,9 @@ const Row = ({
expandable && expandRow(id, data) expandable && expandRow(id, data)
onClick && onClick(data) onClick && onClick(data)
}} }}
error={data.error || data.hasError} error={data.error || data.hasError || data.batchError}
shouldShowError={false} shouldShowError={false}
errorMessage={data.errorMessage || data.hasError}> errorMessage={data.errorMessage || data.hasError || data.batchError}>
{elements.map(({ view = it => it?.toString(), ...props }, idx) => ( {elements.map(({ view = it => it?.toString(), ...props }, idx) => (
<Td key={idx} {...props}> <Td key={idx} {...props}>
{view(data)} {view(data)}

View file

@ -36,12 +36,7 @@ const DenominationsSchema = Yup.object().shape({
.min(1) .min(1)
.max(CURRENCY_MAX) .max(CURRENCY_MAX)
.nullable() .nullable()
.transform(transformNumber), .transform(transformNumber)
zeroConfLimit: Yup.number()
.label('0-conf Limit')
.required()
.min(0)
.max(CURRENCY_MAX)
}) })
const getElements = (machines, locale = {}, classes) => { const getElements = (machines, locale = {}, classes) => {
@ -99,20 +94,6 @@ const getElements = (machines, locale = {}, classes) => {
1 1
) )
elements.push({
name: 'zeroConfLimit',
header: '0-conf Limit',
size: 'sm',
stripe: true,
textAlign: 'right',
width: widthsByNumberOfCassettes[maxNumberOfCassettes].zeroConf,
input: NumberInput,
inputProps: {
decimalPlaces: 0
},
suffix: fiatCurrency
})
return elements return elements
} }

View file

@ -293,7 +293,8 @@ const CustomerData = ({
name: it.customInfoRequest.id, name: it.customInfoRequest.id,
label: it.customInfoRequest.customRequest.name, label: it.customInfoRequest.customRequest.name,
value: it.customerData.data ?? '', value: it.customerData.data ?? '',
component: TextInput component: TextInput,
editable: true
} }
], ],
title: it.customInfoRequest.customRequest.name, title: it.customInfoRequest.customRequest.name,
@ -344,7 +345,8 @@ const CustomerData = ({
name: it.label, name: it.label,
label: it.label, label: it.label,
value: it.value ?? '', value: it.value ?? '',
component: TextInput component: TextInput,
editable: true
} }
], ],
title: it.label, title: it.label,

View file

@ -21,7 +21,6 @@ export default {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
'& > div': { '& > div': {
width: 144,
height: 37, height: 37,
marginBottom: 15, marginBottom: 15,
marginRight: 55 marginRight: 55

View file

@ -27,9 +27,11 @@ export default {
settings: { settings: {
field: 'wallets_BTC_wallet', field: 'wallets_BTC_wallet',
enabled: true, enabled: true,
disabledMessage: 'RBF verification not available', disabledMessage:
'Lower the confidence of RBF transactions (Available when using bitcoind.)',
label: 'Lower the confidence of RBF transactions', label: 'Lower the confidence of RBF transactions',
requirement: 'bitcoind' requirement: 'bitcoind',
rightSideLabel: true
}, },
face: true face: true
} }

View file

@ -146,8 +146,8 @@ const DetailsRow = ({ it: tx, timezone }) => {
'' ''
} }
const from = sub({ minutes: MINUTES_OFFSET }, tx.created) const from = sub({ minutes: MINUTES_OFFSET }, new Date(tx.created))
const until = add({ minutes: MINUTES_OFFSET }, tx.created) const until = add({ minutes: MINUTES_OFFSET }, new Date(tx.created))
const downloadRawLogs = ({ id: txId, deviceId, txClass }, timezone) => { const downloadRawLogs = ({ id: txId, deviceId, txClass }, timezone) => {
fetchSummary({ fetchSummary({
@ -419,5 +419,6 @@ export default memo(
(prev, next) => (prev, next) =>
prev.it.id === next.it.id && prev.it.id === next.it.id &&
prev.it.hasError === next.it.hasError && prev.it.hasError === next.it.hasError &&
prev.it.batchError === next.it.batchError &&
getStatus(prev.it) === getStatus(next.it) getStatus(prev.it) === getStatus(next.it)
) )

View file

@ -116,6 +116,7 @@ const GET_TRANSACTIONS = gql`
isAnonymous isAnonymous
batched batched
batchTime batchTime
batchError
walletScore walletScore
} }
} }
@ -190,7 +191,7 @@ const Transactions = () => {
<div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div> <div className={classes.overflowTd}>{getCustomerDisplayName(it)}</div>
{!it.isAnonymous && ( {!it.isAnonymous && (
<div onClick={() => redirect(it.customerId)}> <div onClick={() => redirect(it.customerId)}>
{it.hasError ? ( {it.hasError || it.batchError ? (
<CustomerLinkWhiteIcon className={classes.customerLinkIcon} /> <CustomerLinkWhiteIcon className={classes.customerLinkIcon} />
) : ( ) : (
<CustomerLinkIcon className={classes.customerLinkIcon} /> <CustomerLinkIcon className={classes.customerLinkIcon} />

View file

@ -1,3 +1,5 @@
import * as R from 'ramda'
const getCashOutStatus = it => { const getCashOutStatus = it => {
if (it.hasError === 'Operator cancel') return 'Cancelled' if (it.hasError === 'Operator cancel') return 'Cancelled'
if (it.hasError) return 'Error' if (it.hasError) return 'Error'
@ -8,7 +10,7 @@ const getCashOutStatus = it => {
const getCashInStatus = it => { const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled' if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error' if (it.hasError || it.batchError) return 'Error'
if (it.sendConfirmed) return 'Sent' if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired' if (it.expired) return 'Expired'
if (it.batched) return 'Batched' if (it.batched) return 'Batched'
@ -23,11 +25,14 @@ const getStatus = it => {
} }
const getStatusDetails = it => { const getStatusDetails = it => {
return it.hasError ? it.hasError : null if (!R.isNil(it.hasError)) return it.hasError
if (!R.isNil(it.batchError)) return `Batch error: ${it.batchError}`
return null
} }
const getStatusProperties = status => ({ const getStatusProperties = status => ({
hasError: status === 'Error' || null, hasError: status === 'Error' || null,
batchError: status === 'Error' || null,
dispense: status === 'Success' || null, dispense: status === 'Success' || null,
expired: status === 'Expired' || null, expired: status === 'Expired' || null,
operatorCompleted: status === 'Cancelled' || null, operatorCompleted: status === 'Cancelled' || null,

View file

@ -118,6 +118,9 @@ const styles = {
actionButtonWrapper: { actionButtonWrapper: {
display: 'flex', display: 'flex',
gap: 12 gap: 12
},
enterButton: {
display: 'none'
} }
} }

View file

@ -7,7 +7,11 @@ import React from 'react'
import { NamespacedTable as EditableTable } from 'src/components/editableTable' import { NamespacedTable as EditableTable } from 'src/components/editableTable'
import { fromNamespace, toNamespace, namespaces } from 'src/utils/config' import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
import { AdvancedWalletSchema, getAdvancedWalletElements } from './helper' import {
WalletSchema,
AdvancedWalletSchema,
getAdvancedWalletElements
} from './helper'
const SAVE_CONFIG = gql` const SAVE_CONFIG = gql`
mutation Save($config: JSONObject, $accounts: JSONObject) { mutation Save($config: JSONObject, $accounts: JSONObject) {
@ -51,8 +55,9 @@ const AdvancedWallet = () => {
enableEdit enableEdit
editWidth={174} editWidth={174}
save={save} save={save}
stripeWhen={it => !WalletSchema.isValidSync(it)}
validationSchema={AdvancedWalletSchema} validationSchema={AdvancedWalletSchema}
elements={getAdvancedWalletElements(cryptoCurrencies, coinUtils)} elements={getAdvancedWalletElements(cryptoCurrencies, coinUtils, config)}
/> />
) )
} }

View file

@ -1,8 +1,11 @@
import * as R from 'ramda' import * as R from 'ramda'
import * as Yup from 'yup' import * as Yup from 'yup'
import { NumberInput } from 'src/components/inputs/formik' import {
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js' Autocomplete,
Checkbox,
NumberInput
} from 'src/components/inputs/formik'
import { disabledColor } from 'src/styling/variables' import { disabledColor } from 'src/styling/variables'
import { CURRENCY_MAX } from 'src/utils/constants' import { CURRENCY_MAX } from 'src/utils/constants'
import { transformNumber } from 'src/utils/number' import { transformNumber } from 'src/utils/number'
@ -29,10 +32,11 @@ const WalletSchema = Yup.object().shape({
}) })
const AdvancedWalletSchema = Yup.object().shape({ const AdvancedWalletSchema = Yup.object().shape({
cryptoUnits: Yup.string().required() cryptoUnits: Yup.string().required(),
allowTransactionBatching: Yup.boolean()
}) })
const getAdvancedWalletElements = (cryptoCurrencies, coinUtils) => { const getAdvancedWalletElements = (cryptoCurrencies, coinUtils, config) => {
const viewCryptoCurrency = it => const viewCryptoCurrency = it =>
R.compose( R.compose(
R.prop(['display']), R.prop(['display']),
@ -66,6 +70,19 @@ const getAdvancedWalletElements = (cryptoCurrencies, coinUtils) => {
valueProp: 'code', valueProp: 'code',
labelProp: 'display' labelProp: 'display'
} }
},
{
name: 'allowTransactionBatching',
size: 'sm',
stripe: true,
width: 250,
view: (_, ite) => {
if (ite.id !== 'BTC')
return <span style={classes.editDisabled}>{`No`}</span>
return config[`${ite.id}_allowTransactionBatching`] ? 'Yes' : 'No'
},
input: Checkbox,
editable: it => it.id === 'BTC'
} }
] ]
} }

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="33px" viewBox="0 0 13 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="13px" height="33px" viewBox="0 0 13 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="Simple-Arrow-White" fill="#FFFFFF" fill-rule="nonzero" points="12.1912718 1.56064837 10.8306233 0.395663059 0.196798664 16.2200463 10.8250965 32.3956631 12.1967987 31.2473125 2.33241023 16.233075"></polygon> <polygon id="Simple-Arrow-White" fill="#1b2559" fill-rule="nonzero" points="12.1912718 1.56064837 10.8306233 0.395663059 0.196798664 16.2200463 10.8250965 32.3956631 12.1967987 31.2473125 2.33241023 16.233075"></polygon>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

Before After
Before After

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="15px" height="34px" viewBox="0 0 15 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="15px" height="34px" viewBox="0 0 15 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2-Copy" transform="translate(1.000000, 1.000000)" stroke="#FFFFFF" stroke-width="2"> <g id="Group-2-Copy" transform="translate(1.000000, 1.000000)" stroke="#1b2559" stroke-width="2">
<polyline id="Path-4-Copy" points="0 0 12 15.8202247 0 32"></polyline> <polyline id="Path-4-Copy" points="0 0 12 15.8202247 0 32"></polyline>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

Before After
Before After

View file

@ -1,4 +1,8 @@
const url = `https://${window.location.hostname}` const url = `https://${
process.env.NODE_ENV === 'development'
? window.location.host
: window.location.hostname
}`
const urlResolver = content => `${url}${content}` const urlResolver = content => `${url}${content}`

View file

@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.js": "/static/js/main.6ef30c04.chunk.js", "main.js": "/static/js/main.8cb55c42.chunk.js",
"main.js.map": "/static/js/main.6ef30c04.chunk.js.map", "main.js.map": "/static/js/main.8cb55c42.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.c4e7abab.chunk.js": "/static/js/2.c4e7abab.chunk.js", "static/js/2.c4e7abab.chunk.js": "/static/js/2.c4e7abab.chunk.js",
@ -17,8 +17,8 @@
"static/media/4-cassettes-open-4-left.bc1a9829.svg": "/static/media/4-cassettes-open-4-left.bc1a9829.svg", "static/media/4-cassettes-open-4-left.bc1a9829.svg": "/static/media/4-cassettes-open-4-left.bc1a9829.svg",
"static/media/acceptor-left.f37bcb1a.svg": "/static/media/acceptor-left.f37bcb1a.svg", "static/media/acceptor-left.f37bcb1a.svg": "/static/media/acceptor-left.f37bcb1a.svg",
"static/media/both-filled.7af80d5f.svg": "/static/media/both-filled.7af80d5f.svg", "static/media/both-filled.7af80d5f.svg": "/static/media/both-filled.7af80d5f.svg",
"static/media/carousel-left-arrow.04e38344.svg": "/static/media/carousel-left-arrow.04e38344.svg", "static/media/carousel-left-arrow.c6575d9d.svg": "/static/media/carousel-left-arrow.c6575d9d.svg",
"static/media/carousel-right-arrow.4748b93d.svg": "/static/media/carousel-right-arrow.4748b93d.svg", "static/media/carousel-right-arrow.1d5e04d1.svg": "/static/media/carousel-right-arrow.1d5e04d1.svg",
"static/media/cash-in.c06970a7.svg": "/static/media/cash-in.c06970a7.svg", "static/media/cash-in.c06970a7.svg": "/static/media/cash-in.c06970a7.svg",
"static/media/cash-out.f029ae96.svg": "/static/media/cash-out.f029ae96.svg", "static/media/cash-out.f029ae96.svg": "/static/media/cash-out.f029ae96.svg",
"static/media/cashbox-empty.828bd3b9.svg": "/static/media/cashbox-empty.828bd3b9.svg", "static/media/cashbox-empty.828bd3b9.svg": "/static/media/cashbox-empty.828bd3b9.svg",
@ -151,6 +151,6 @@
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.5b925903.js", "static/js/runtime-main.5b925903.js",
"static/js/2.c4e7abab.chunk.js", "static/js/2.c4e7abab.chunk.js",
"static/js/main.6ef30c04.chunk.js" "static/js/main.8cb55c42.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.c4e7abab.chunk.js"></script><script src="/static/js/main.6ef30c04.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.c4e7abab.chunk.js"></script><script src="/static/js/main.8cb55c42.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

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="33px" viewBox="0 0 13 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="13px" height="33px" viewBox="0 0 13 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="Simple-Arrow-White" fill="#FFFFFF" fill-rule="nonzero" points="12.1912718 1.56064837 10.8306233 0.395663059 0.196798664 16.2200463 10.8250965 32.3956631 12.1967987 31.2473125 2.33241023 16.233075"></polygon> <polygon id="Simple-Arrow-White" fill="#1b2559" fill-rule="nonzero" points="12.1912718 1.56064837 10.8306233 0.395663059 0.196798664 16.2200463 10.8250965 32.3956631 12.1967987 31.2473125 2.33241023 16.233075"></polygon>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 512 B

After

Width:  |  Height:  |  Size: 512 B

Before After
Before After

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<svg width="15px" height="34px" viewBox="0 0 15 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="15px" height="34px" viewBox="0 0 15 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group-2-Copy" transform="translate(1.000000, 1.000000)" stroke="#FFFFFF" stroke-width="2"> <g id="Group-2-Copy" transform="translate(1.000000, 1.000000)" stroke="#1b2559" stroke-width="2">
<polyline id="Path-4-Copy" points="0 0 12 15.8202247 0 32"></polyline> <polyline id="Path-4-Copy" points="0 0 12 15.8202247 0 32"></polyline>
</g> </g>
</g> </g>

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

Before After
Before After