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 configManager = require('../lib/new-config-manager')
const wallet = require('../lib/wallet')
const coinUtils = require('../lib/coin-utils')
const { utils: coinUtils } = require('lamassu-coins')
const BN = require('../lib/bn')
const inquirer = require('inquirer')
const ticker = require('../lib/ticker')

View file

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

View file

@ -48,5 +48,6 @@ addresstype=p2sh-segwit
changetype=bech32
walletrbf=1
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) {
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(`tar -xzf /tmp/bitcoincash.tar.gz -C /tmp/`)
@ -45,6 +45,6 @@ maxconnections=40
keypool=10000
prune=4000
daemon=0
bind=0.0.0.0:8334
rpcport=8335`
bind=0.0.0.0:8335
rpcport=8336`
}

View file

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

View file

@ -6,7 +6,7 @@ module.exports = { setup, updateCore }
function updateCore (coinRec, isCurrentlyRunning) {
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(`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 options = require('../options')
const settingsLoader = require('../new-settings-loader')
const wallet = require('../wallet')
const common = require('./common')
const doVolume = require('./do-volume')
@ -112,6 +114,35 @@ function plugin (crypto) {
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 () {
const choices = _.flow([
_.filter(c => c.type !== 'erc-20'),
@ -129,13 +160,40 @@ function run () {
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({
type: 'checkbox',
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
})
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) {
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(`tar -xzf /tmp/litecoin.tar.gz -C /tmp/`)

View file

@ -4,7 +4,7 @@ const { utils } = require('lamassu-coins')
const common = require('./common')
module.exports = {setup}
module.exports = { setup, updateCore }
const coinRec = utils.getCryptoCurrency('XMR')
@ -13,11 +13,31 @@ function setup (dataDir) {
const auth = `lamassuserver:${common.randomPass()}`
const config = buildConfig(auth)
common.writeFile(path.resolve(dataDir, coinRec.configFile), config)
const cmd = `/usr/local/bin/${coinRec.daemon} --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 cmd = `/usr/local/bin/${coinRec.daemon} --no-zmq --data-dir ${dataDir} --config-file ${dataDir}/${coinRec.configFile}`
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)
}
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) {
return `rpc-login=${auth}
stagenet=0

View file

@ -11,7 +11,7 @@ const logger = common.logger
function updateCore (coinRec, isCurrentlyRunning) {
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(`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',
'notified', 'redeem', 'phone', 'error', 'swept', 'publishedAt', 'confirmedAt', 'errorCode',
'receivedCryptoAtoms' ]
'receivedCryptoAtoms', 'walletScore' ]
module.exports = {upsert, update, insert}

View file

@ -107,9 +107,56 @@ function processTxStatus (tx, settings) {
return pi.getStatus(tx)
.then(res => _.assign(tx, { receivedCryptoAtoms: res.receivedCryptoAtoms, status: res.status }))
.then(_tx => getWalletScore(_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) {
const statuses = ['notSeen', 'published', 'insufficientFunds']
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
batched: Boolean
batchTime: Date
batchError: String
walletScore: Int
}

View file

@ -4,6 +4,7 @@ const settingsLoader = require('../../new-settings-loader')
const configManager = require('../../new-config-manager')
const wallet = require('../../wallet')
const ticker = require('../../ticker')
const txBatching = require('../../tx-batching')
const { utils: coinUtils } = require('lamassu-coins')
function computeCrypto (cryptoCode, _balance) {
@ -23,16 +24,17 @@ function computeFiat (rate, cryptoCode, _balance) {
function getSingleCoinFunding (settings, fiatCode, cryptoCode) {
const promises = [
wallet.newFunding(settings, cryptoCode),
ticker.getRates(settings, fiatCode, cryptoCode)
ticker.getRates(settings, fiatCode, cryptoCode),
txBatching.getOpenBatchCryptoValue(cryptoCode)
]
return Promise.all(promises)
.then(([fundingRec, ratesRec]) => {
.then(([fundingRec, ratesRec, batchRec]) => {
const rates = ratesRec.rates
const rate = (rates.ask.plus(rates.bid)).div(2)
const fundingConfirmedBalance = fundingRec.fundingConfirmedBalance
const fiatConfirmedBalance = computeFiat(rate, cryptoCode, fundingConfirmedBalance)
const pending = fundingRec.fundingPendingBalance
const pending = fundingRec.fundingPendingBalance.minus(batchRec)
const fiatPending = computeFiat(rate, cryptoCode, pending)
const fundingAddress = fundingRec.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,
txs.tx_customer_photo_at AS tx_customer_photo_at,
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
LEFT OUTER JOIN customers c ON txs.customer_id = c.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 ${
id !== null ? `AND txs.device_id = $6` : ``
}
@ -69,7 +71,7 @@ function batch (
AND ($12 is null or txs.to_address = $12)
AND ($13 is null or txs.txStatus = $13)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (error IS NOT null OR 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`
const cashOutSql = `SELECT 'cashOut' AS tx_class,
@ -151,6 +153,7 @@ const getCashOutStatus = it => {
const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error'
if (it.batchError) return 'Error'
if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired'
return 'Pending'
@ -176,9 +179,11 @@ function getCustomerTransactionsBatch (ids) {
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_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
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^)
ORDER BY created DESC limit $3`
@ -220,9 +225,11 @@ function single (txId) {
c.name AS customer_name,
c.front_camera_path AS customer_front_camera_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
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`
const cashOutSql = `SELECT 'cashOut' AS tx_class,

View file

@ -836,6 +836,14 @@ function plugins (settings, deviceId) {
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 {
getRates,
buildRates,
@ -865,7 +873,9 @@ function plugins (settings, deviceId) {
fetchCurrentConfigVersion,
pruneMachinesHeartbeat,
rateWallet,
isValidWalletScore
isValidWalletScore,
getTransactionHash,
getInputAddresses
}
}

View file

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

View file

@ -1,6 +1,8 @@
const axios = require('axios')
const _ = require('lodash/fp')
const logger = require('../../../logger')
const NAME = 'CipherTrace'
const SUPPORTED_COINS = ['BTC', 'ETH', 'BCH', 'LTC', 'BNB', 'RSK']
@ -37,11 +39,53 @@ function isValidWalletScore(account, score) {
if (_.isNil(client)) return Promise.resolve(true)
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 = {
NAME,
rateWallet,
isValidWalletScore
isValidWalletScore,
getTransactionHash,
getInputAddresses
}

View file

@ -6,7 +6,7 @@ function rateWallet (account, cryptoCode, address) {
return new Promise((resolve, _) => {
setTimeout(() => {
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 }))
}, 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 = {
NAME,
rateWallet,
isValidWalletScore
isValidWalletScore,
getTransactionHash,
getInputAddresses
}

View file

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

View file

@ -9,7 +9,6 @@ const { utils: coinUtils } = require('lamassu-coins')
const cryptoRec = coinUtils.getCryptoCurrency('BTC')
const unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = true
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
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)
.then(() => calculateFeeDiscount(feeMultiplier))
.then(newFee => fetch('settxfee', [newFee]))
.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))])
})
.then(([obj]) => fetch('sendmany', ['', obj]))
.then((txId) => fetch('gettransaction', [txId]))
.then((res) => _.pick(['fee', 'txid'], res))
.then((pickedObj) => ({
fee: BN(pickedObj.fee).abs().shift(unitScale).round(),
fee: new BN(pickedObj.fee).abs().shiftedBy(unitScale).decimalPlaces(0),
txid: pickedObj.txid
}))
.catch(err => {
@ -171,9 +170,10 @@ function fetchRBF (txId) {
})
}
function supportsBatching (cryptoCode) {
function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING)
.then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
}
module.exports = {
@ -186,5 +186,5 @@ module.exports = {
fetchRBF,
estimateFee,
sendCoinsBatch,
supportsBatching
checkBlockchainStatus
}

View file

@ -14,8 +14,6 @@ const NAME = 'BitGo'
const SUPPORTED_COINS = ['BTC', 'ZEC', 'LTC', 'BCH', 'DASH']
const BCH_CODES = ['BCH', 'TBCH']
const SUPPORTS_BATCHING = false
function buildBitgo (account) {
const env = account.environment === 'test' ? 'test' : 'prod'
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')
}
function supportsBatching (cryptoCode) {
function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING)
.then(() => Promise.resolve('ready'))
}
module.exports = {
@ -172,5 +170,5 @@ module.exports = {
getStatus,
newFunding,
cryptoNetwork,
supportsBatching
checkBlockchainStatus
}

View file

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

View file

@ -17,8 +17,6 @@ const paymentPrefixPath = "m/44'/60'/0'/0'"
const defaultPrefixPath = "m/44'/60'/1'/0'"
let lastUsedNonces = {}
const SUPPORTS_BATCHING = false
module.exports = {
NAME,
balance,
@ -32,7 +30,7 @@ module.exports = {
privateKey,
isStrictAddress,
connect,
supportsBatching
checkBlockchainStatus
}
function connect (url) {
@ -92,6 +90,8 @@ function _balance (includePending, address, cryptoCode) {
}
const block = includePending ? 'pending' : undefined
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) {
@ -103,7 +103,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
const txTemplate = {
from: fromAddress,
to: toAddress,
value: amount
value: amount.toString()
}
const promises = [
@ -226,7 +226,8 @@ function newFunding (account, cryptoCode, settings, operatorId) {
})
}
function supportsBatching (cryptoCode) {
function checkBlockchainStatus (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 unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
const rpcConfig = jsonRpc.rpcConfig(cryptoRec)
function fetch (method, params) {
@ -113,9 +112,10 @@ function newFunding (account, cryptoCode, settings, operatorId) {
}))
}
function supportsBatching (cryptoCode) {
function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => SUPPORTS_BATCHING)
.then(() => fetch('getblockchaininfo'))
.then(res => !!res['initialblockdownload'] ? 'syncing' : 'ready')
}
module.exports = {
@ -124,5 +124,5 @@ module.exports = {
newAddress,
getStatus,
newFunding,
supportsBatching
checkBlockchainStatus
}

View file

@ -5,7 +5,6 @@ const E = require('../../../error')
const { utils: coinUtils } = require('lamassu-coins')
const NAME = 'FakeWallet'
const BATCHABLE_COINS = ['BTC']
const SECONDS = 1000
const PUBLISH_TIME = 3 * SECONDS
@ -111,8 +110,9 @@ function getStatus (account, tx, requested, settings, operatorId) {
return Promise.resolve({status: 'confirmed'})
}
function supportsBatching (cryptoCode) {
return Promise.resolve(_.includes(cryptoCode, BATCHABLE_COINS))
function checkBlockchainStatus (cryptoCode) {
return checkCryptoCode(cryptoCode)
.then(() => Promise.resolve('ready'))
}
module.exports = {
@ -123,5 +123,5 @@ module.exports = {
newAddress,
getStatus,
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 unitScale = cryptoRec.unitScale
const SUPPORTS_BATCHING = false
function rpcConfig () {
try {
const config = jsonRpc.parseConf(configPath)
@ -42,8 +40,7 @@ function handleError (error) {
{
if (
fs.existsSync(path.resolve(walletDir, 'Wallet')) &&
fs.existsSync(path.resolve(walletDir, 'Wallet.keys')) &&
fs.existsSync(path.resolve(walletDir, 'Wallet.address.txt'))
fs.existsSync(path.resolve(walletDir, 'Wallet.keys'))
) {
logger.debug('Found wallet! Opening wallet...')
return openWallet()
@ -92,7 +89,7 @@ function accountBalance (cryptoCode) {
.then(() => refreshWallet())
.then(() => fetch('get_balance', { account_index: 0, address_indices: [0] }))
.then(res => {
return BN(res.unlocked_balance).shift(unitScale).round()
return BN(res.unlocked_balance).shiftedBy(unitScale).decimalPlaces(0)
})
.catch(err => handleError(err))
}
@ -101,11 +98,12 @@ function balance (account, 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)
.then(() => refreshWallet())
.then(() => fetch('transfer_split', {
destinations: [{ amount: cryptoAtoms, address }],
destinations: [{ amount: cryptoAtoms, address: toAddress }],
account_index: 0,
subaddr_indices: [],
priority: 0,
@ -177,7 +175,7 @@ function newFunding (account, cryptoCode) {
fetch('create_address', { account_index: 0 })
]))
.then(([balanceRes, addressRes]) => ({
fundingPendingBalance: BN(balanceRes.balance).sub(balanceRes.unlocked_balance),
fundingPendingBalance: BN(balanceRes.balance).minus(balanceRes.unlocked_balance),
fundingConfirmedBalance: BN(balanceRes.unlocked_balance),
fundingAddress: addressRes.address
}))
@ -200,9 +198,28 @@ function cryptoNetwork (account, cryptoCode) {
})
}
function supportsBatching (cryptoCode) {
function checkBlockchainStatus (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 = {
@ -212,5 +229,5 @@ module.exports = {
getStatus,
newFunding,
cryptoNetwork,
supportsBatching
checkBlockchainStatus
}

View file

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

View file

@ -139,8 +139,8 @@ function poll (req, res, next) {
response.idCardDataVerificationThreshold = compatTriggers.idCardData
response.idCardPhotoVerificationActive = !!compatTriggers.idCardPhoto
response.idCardPhotoVerificationThreshold = compatTriggers.idCardPhoto
response.sanctionsVerificationActive = !!compatTriggers.sancations
response.sanctionsVerificationThreshold = compatTriggers.sancations
response.sanctionsVerificationActive = !!compatTriggers.sanctions
response.sanctionsVerificationThreshold = compatTriggers.sanctions
response.frontCameraVerificationActive = !!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 = {
rateWallet,
isValidWalletScore
isValidWalletScore,
getTransactionHash,
getInputAddresses
}

View file

@ -48,9 +48,8 @@ const lastBalance = {}
function _balance (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.balance(r.account, cryptoCode, settings, r.operatorId))
.then(balance => Promise.all([balance, supportsBatching(settings, cryptoCode)]))
.then(([balance, supportsBatching]) => Promise.all([balance, supportsBatching ? getOpenBatchCryptoValue(cryptoCode) : Promise.resolve(BN(0))]))
.then(([balance, reservedBalance]) => ({ balance: balance.minus(reservedBalance), reservedBalance, timestamp: Date.now() }))
.then(balance => Promise.all([balance, getOpenBatchCryptoValue(cryptoCode)]))
.then(([balance, reservedBalance]) => ({ balance: BN(balance).minus(reservedBalance), reservedBalance, timestamp: Date.now() }))
.then(r => {
lastBalance[cryptoCode] = r
return r
@ -82,7 +81,8 @@ function sendCoins (settings, tx) {
function sendCoinsBatch (settings, txs, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.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 => {
mem.clear(module.exports.balance)
return res
@ -233,8 +233,12 @@ function isStrictAddress (settings, cryptoCode, toAddress) {
}
function supportsBatching (settings, cryptoCode) {
return Promise.resolve(!!configManager.getWalletSettings(cryptoCode, settings.config).allowTransactionBatching)
}
function checkBlockchainStatus (settings, cryptoCode) {
return fetchWallet(settings, cryptoCode)
.then(r => r.wallet.supportsBatching(cryptoCode))
.then(r => r.wallet.checkBlockchainStatus(cryptoCode))
}
const coinFilter = ['ETH']
@ -265,5 +269,6 @@ module.exports = {
isHd,
newFunding,
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: {
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
width: 550
display: 'flex'
},
imgInner: {
objectFit: 'cover',
objectPosition: 'center',
width: 550,
width: 500,
marginBottom: 40
}
})
@ -33,17 +32,16 @@ export const Carousel = memo(({ photosData, slidePhoto }) => {
style: {
backgroundColor: 'transparent',
borderRadius: 0,
width: 50,
color: 'transparent',
opacity: 1
}
}}
// navButtonsWrapperProps={{
// style: {
// background: 'linear-gradient(to right, black 10%, transparent 80%)',
// opacity: '0.4'
// }
// }}
navButtonsWrapperProps={{
style: {
marginLeft: -22,
marginRight: -22
}
}}
autoPlay={false}
indicators={false}
navButtonsAlwaysVisible={true}

View file

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

View file

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

View file

@ -36,12 +36,7 @@ const DenominationsSchema = Yup.object().shape({
.min(1)
.max(CURRENCY_MAX)
.nullable()
.transform(transformNumber),
zeroConfLimit: Yup.number()
.label('0-conf Limit')
.required()
.min(0)
.max(CURRENCY_MAX)
.transform(transformNumber)
})
const getElements = (machines, locale = {}, classes) => {
@ -99,20 +94,6 @@ const getElements = (machines, locale = {}, classes) => {
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
}

View file

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

View file

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

View file

@ -27,9 +27,11 @@ export default {
settings: {
field: 'wallets_BTC_wallet',
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',
requirement: 'bitcoind'
requirement: 'bitcoind',
rightSideLabel: true
},
face: true
}

View file

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

View file

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

View file

@ -1,3 +1,5 @@
import * as R from 'ramda'
const getCashOutStatus = it => {
if (it.hasError === 'Operator cancel') return 'Cancelled'
if (it.hasError) return 'Error'
@ -8,7 +10,7 @@ const getCashOutStatus = it => {
const getCashInStatus = it => {
if (it.operatorCompleted) return 'Cancelled'
if (it.hasError) return 'Error'
if (it.hasError || it.batchError) return 'Error'
if (it.sendConfirmed) return 'Sent'
if (it.expired) return 'Expired'
if (it.batched) return 'Batched'
@ -23,11 +25,14 @@ const getStatus = 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 => ({
hasError: status === 'Error' || null,
batchError: status === 'Error' || null,
dispense: status === 'Success' || null,
expired: status === 'Expired' || null,
operatorCompleted: status === 'Cancelled' || null,

View file

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

View file

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

View file

@ -1,8 +1,11 @@
import * as R from 'ramda'
import * as Yup from 'yup'
import { NumberInput } from 'src/components/inputs/formik'
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
import {
Autocomplete,
Checkbox,
NumberInput
} from 'src/components/inputs/formik'
import { disabledColor } from 'src/styling/variables'
import { CURRENCY_MAX } from 'src/utils/constants'
import { transformNumber } from 'src/utils/number'
@ -29,10 +32,11 @@ const WalletSchema = 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 =>
R.compose(
R.prop(['display']),
@ -66,6 +70,19 @@ const getAdvancedWalletElements = (cryptoCurrencies, coinUtils) => {
valueProp: 'code',
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"?>
<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">
<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>
</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"?>
<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="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>
</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}`

View file

@ -1,7 +1,7 @@
{
"files": {
"main.js": "/static/js/main.6ef30c04.chunk.js",
"main.js.map": "/static/js/main.6ef30c04.chunk.js.map",
"main.js": "/static/js/main.8cb55c42.chunk.js",
"main.js.map": "/static/js/main.8cb55c42.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.5b925903.js",
"runtime-main.js.map": "/static/js/runtime-main.5b925903.js.map",
"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/acceptor-left.f37bcb1a.svg": "/static/media/acceptor-left.f37bcb1a.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-right-arrow.4748b93d.svg": "/static/media/carousel-right-arrow.4748b93d.svg",
"static/media/carousel-left-arrow.c6575d9d.svg": "/static/media/carousel-left-arrow.c6575d9d.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-out.f029ae96.svg": "/static/media/cash-out.f029ae96.svg",
"static/media/cashbox-empty.828bd3b9.svg": "/static/media/cashbox-empty.828bd3b9.svg",
@ -151,6 +151,6 @@
"entrypoints": [
"static/js/runtime-main.5b925903.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"?>
<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">
<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>
</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"?>
<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="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>
</g>
</g>

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 485 B

Before After
Before After