Merge pull request #1327 from chaotixkilla/release-8.0-cherry-picked

backport ETH commits for v8.0.1
This commit is contained in:
Rafael Taranto 2022-07-29 21:54:17 +01:00 committed by GitHub
commit d6fb19ed4a
22 changed files with 179 additions and 75 deletions

View file

@ -25,7 +25,6 @@ module.exports = {
const STALE_INCOMING_TX_AGE = T.day const STALE_INCOMING_TX_AGE = T.day
const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes const STALE_LIVE_INCOMING_TX_AGE = 10 * T.minutes
const STALE_LIVE_INCOMING_TX_AGE_FILTER = 5 * T.minutes
const MAX_NOTIFY_AGE = T.day const MAX_NOTIFY_AGE = T.day
const MIN_NOTIFY_AGE = 5 * T.minutes const MIN_NOTIFY_AGE = 5 * T.minutes
const INSUFFICIENT_FUNDS_CODE = 570 const INSUFFICIENT_FUNDS_CODE = 570
@ -91,21 +90,17 @@ function postProcess (txVector, justAuthorized, pi) {
return Promise.resolve({}) return Promise.resolve({})
} }
function fetchOpenTxs (statuses, fromAge, toAge, applyFilter, coinFilter) { function fetchOpenTxs (statuses, fromAge, toAge) {
const notClause = applyFilter ? '' : 'not'
const sql = `select * const sql = `select *
from cash_out_txs from cash_out_txs
where ((extract(epoch from (now() - created))) * 1000)>$1 where ((extract(epoch from (now() - created))) * 1000)>$1
and ((extract(epoch from (now() - created))) * 1000)<$2 and ((extract(epoch from (now() - created))) * 1000)<$2
${_.isEmpty(coinFilter) and status in ($3^)
? `` and error is distinct from 'Operator cancel'`
: `and crypto_code ${notClause} in ($3^)`}
and status in ($4^)`
const coinClause = _.map(pgp.as.text, coinFilter).join(',')
const statusClause = _.map(pgp.as.text, statuses).join(',') const statusClause = _.map(pgp.as.text, statuses).join(',')
return db.any(sql, [fromAge, toAge, coinClause, statusClause]) return db.any(sql, [fromAge, toAge, statusClause])
.then(rows => rows.map(toObj)) .then(rows => rows.map(toObj))
} }
@ -164,22 +159,18 @@ function getWalletScore (tx, pi) {
return tx return tx
} }
function monitorLiveIncoming (settings, applyFilter, coinFilter) { function monitorLiveIncoming (settings) {
const statuses = ['notSeen', 'published', 'insufficientFunds'] const statuses = ['notSeen', 'published', 'insufficientFunds']
const toAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE return monitorIncoming(settings, statuses, 0, STALE_LIVE_INCOMING_TX_AGE)
return monitorIncoming(settings, statuses, 0, toAge, applyFilter, coinFilter)
} }
function monitorStaleIncoming (settings, applyFilter, coinFilter) { function monitorStaleIncoming (settings) {
const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds'] const statuses = ['notSeen', 'published', 'authorized', 'instant', 'rejected', 'insufficientFunds']
const fromAge = applyFilter ? STALE_LIVE_INCOMING_TX_AGE_FILTER : STALE_LIVE_INCOMING_TX_AGE return monitorIncoming(settings, statuses, STALE_LIVE_INCOMING_TX_AGE, STALE_INCOMING_TX_AGE)
return monitorIncoming(settings, statuses, fromAge, STALE_INCOMING_TX_AGE, applyFilter, coinFilter)
} }
function monitorIncoming (settings, statuses, fromAge, toAge, applyFilter, coinFilter) { function monitorIncoming (settings, statuses, fromAge, toAge) {
return fetchOpenTxs(statuses, fromAge, toAge, applyFilter, coinFilter) return fetchOpenTxs(statuses, fromAge, toAge)
.then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings))) .then(txs => pEachSeries(txs, tx => processTxStatus(tx, settings)))
.catch(err => { .catch(err => {
if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) { if (err.code === dbErrorCodes.SERIALIZATION_FAILURE) {

View file

@ -24,6 +24,11 @@ const RECEIPT = 'sms_receipt'
const WALLET_SCORE_THRESHOLD = 9 const WALLET_SCORE_THRESHOLD = 9
const BALANCE_FETCH_SPEED_MULTIPLIER = {
NORMAL: 1,
SLOW: 3
}
module.exports = { module.exports = {
anonymousCustomer, anonymousCustomer,
CASSETTE_MAX_CAPACITY, CASSETTE_MAX_CAPACITY,
@ -39,5 +44,6 @@ module.exports = {
CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES, CASH_OUT_MINIMUM_AMOUNT_OF_CASSETTES,
CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES, CASH_OUT_MAXIMUM_AMOUNT_OF_CASSETTES,
WALLET_SCORE_THRESHOLD, WALLET_SCORE_THRESHOLD,
RECEIPT RECEIPT,
BALANCE_FETCH_SPEED_MULTIPLIER
} }

View file

@ -21,7 +21,8 @@ function transaction () {
SELECT 'address' AS type, to_address AS value FROM cash_in_txs UNION SELECT 'address' AS type, to_address AS value FROM cash_in_txs UNION
SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION SELECT 'address' AS type, to_address AS value FROM cash_out_txs UNION
SELECT 'status' AS type, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION SELECT 'status' AS type, ${cashInTx.TRANSACTION_STATES} AS value FROM cash_in_txs UNION
SELECT 'status' AS type, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs SELECT 'status' AS type, ${CASH_OUT_TRANSACTION_STATES} AS value FROM cash_out_txs UNION
SELECT 'sweep status' AS type, CASE WHEN swept THEN 'Swept' WHEN NOT swept THEN 'Unswept' END AS value FROM cash_out_txs
) f` ) f`
return db.any(sql) return db.any(sql)

View file

@ -19,11 +19,11 @@ const resolvers = {
isAnonymous: parent => (parent.customerId === anonymous.uuid) isAnonymous: parent => (parent.customerId === anonymous.uuid)
}, },
Query: { Query: {
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers }]) => transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers }]) =>
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers), transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers),
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, excludeTestingCustomers, simplified }]) => transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, timezone, excludeTestingCustomers, simplified }]) =>
transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, excludeTestingCustomers, simplified) transactions.batch(from, until, limit, offset, null, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept, excludeTestingCustomers, simplified)
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']))), .then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime', 'publishedAt']))),
transactionCsv: (...[, { id, txClass, timezone }]) => transactionCsv: (...[, { id, txClass, timezone }]) =>
transactions.getTx(id, txClass).then(data => transactions.getTx(id, txClass).then(data =>
parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime'])) parseAsync(logDateFormat(timezone, [data], ['created', 'sendTime']))

View file

@ -50,6 +50,7 @@ const typeDef = gql`
batchError: String batchError: String
walletScore: Int walletScore: Int
profit: String profit: String
swept: Boolean
} }
type Filter { type Filter {
@ -58,8 +59,8 @@ const typeDef = gql`
} }
type Query { type Query {
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, excludeTestingCustomers: Boolean): [Transaction] @auth transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, excludeTestingCustomers: Boolean): [Transaction] @auth
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String, swept: Boolean, timezone: String, excludeTestingCustomers: Boolean, simplified: Boolean): String @auth
transactionCsv(id: ID, txClass: String, timezone: String): String @auth transactionCsv(id: ID, txClass: String, timezone: String): String @auth
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
transactionFilters: [Filter] @auth transactionFilters: [Filter] @auth

View file

@ -46,6 +46,7 @@ function batch (
cryptoCode = null, cryptoCode = null,
toAddress = null, toAddress = null,
status = null, status = null,
swept = null,
excludeTestingCustomers = false, excludeTestingCustomers = false,
simplified simplified
) { ) {
@ -109,14 +110,33 @@ function batch (
AND ($11 is null or txs.crypto_code = $11) AND ($11 is null or txs.crypto_code = $11)
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)
AND ($14 is null or txs.swept = $14)
${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``} ${excludeTestingCustomers ? `AND c.is_test_customer is false` : ``}
AND (fiat > 0) AND (fiat > 0)
ORDER BY created DESC limit $4 offset $5` ORDER BY created DESC limit $4 offset $5`
return Promise.all([ // The swept filter is cash-out only, so omit the cash-in query entirely
const hasCashInOnlyFilters = false
const hasCashOutOnlyFilters = !_.isNil(swept)
let promises
if (hasCashInOnlyFilters && hasCashOutOnlyFilters) {
throw new Error('Trying to filter transactions with mutually exclusive filters')
}
if (hasCashInOnlyFilters) {
promises = [db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])]
} else if (hasCashOutOnlyFilters) {
promises = [db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])]
} else {
promises = [
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]), db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]) db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, swept])
]) ]
}
return Promise.all(promises)
.then(packager) .then(packager)
.then(res => { .then(res => {
if (simplified) return simplifiedBatch(res) if (simplified) return simplifiedBatch(res)

View file

@ -806,8 +806,8 @@ function plugins (settings, deviceId) {
} }
function sweepHd () { function sweepHd () {
const sql = `select id, crypto_code, hd_index from cash_out_txs const sql = `SELECT id, crypto_code, hd_index FROM cash_out_txs
where hd_index is not null and not swept and status in ('confirmed', 'instant')` WHERE hd_index IS NOT NULL AND NOT swept AND status IN ('confirmed', 'instant') AND created > now() - interval '1 week'`
return db.any(sql) return db.any(sql)
.then(rows => Promise.all(rows.map(sweepHdRow))) .then(rows => Promise.all(rows.map(sweepHdRow)))

View file

@ -9,6 +9,8 @@ const { default: Common, Chain, Hardfork } = require('@ethereumjs/common')
const Tx = require('ethereumjs-tx') const Tx = require('ethereumjs-tx')
const util = require('ethereumjs-util') const util = require('ethereumjs-util')
const coins = require('@lamassu/coins') const coins = require('@lamassu/coins')
const { default: PQueue } = require('p-queue')
const _pify = require('pify') const _pify = require('pify')
const BN = require('../../../bn') const BN = require('../../../bn')
const ABI = require('../../tokens') const ABI = require('../../tokens')
@ -48,6 +50,11 @@ const logInfuraCall = call => {
logger.info(`Calling web3 method ${call} via Infura. Current count for this session: ${JSON.stringify(infuraCalls)}`) logger.info(`Calling web3 method ${call} via Infura. Current count for this session: ${JSON.stringify(infuraCalls)}`)
} }
const SWEEP_QUEUE = new PQueue({
concurrency: 3,
interval: 250,
})
function connect (url) { function connect (url) {
web3.setProvider(new web3.providers.HttpProvider(url)) web3.setProvider(new web3.providers.HttpProvider(url))
} }
@ -236,13 +243,14 @@ function sweep (account, cryptoCode, hdIndex, settings, operatorId) {
const wallet = paymentHdNode(account).deriveChild(hdIndex).getWallet() const wallet = paymentHdNode(account).deriveChild(hdIndex).getWallet()
const fromAddress = wallet.getChecksumAddressString() const fromAddress = wallet.getChecksumAddressString()
return confirmedBalance(fromAddress, cryptoCode) return SWEEP_QUEUE.add(() => confirmedBalance(fromAddress, cryptoCode)
.then(r => { .then(r => {
if (r.eq(0)) return if (r.eq(0)) return
return generateTx(defaultAddress(account), wallet, r, true, cryptoCode) return generateTx(defaultAddress(account), wallet, r, true, cryptoCode)
.then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx)) .then(signedTx => pify(web3.eth.sendSignedTransaction)(signedTx))
}) })
)
} }
function newAddress (account, info, tx, settings, operatorId) { function newAddress (account, info, tx, settings, operatorId) {

View file

@ -1,5 +1,10 @@
const _ = require('lodash/fp') const _ = require('lodash/fp')
const NodeCache = require('node-cache')
const base = require('../geth/base') const base = require('../geth/base')
const T = require('../../../time')
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('../../../constants')
const REGULAR_TX_POLLING = 5 * T.seconds
const NAME = 'infura' const NAME = 'infura'
@ -12,4 +17,54 @@ function run (account) {
base.connect(endpoint) base.connect(endpoint)
} }
module.exports = _.merge(base, { NAME, run }) const txsCache = new NodeCache({
stdTTL: T.hour / 1000,
checkperiod: T.minute / 1000,
deleteOnExpire: true
})
function shouldGetStatus (tx) {
const timePassedSinceTx = Date.now() - new Date(tx.created)
const timePassedSinceReq = Date.now() - new Date(txsCache.get(tx.id).lastReqTime)
// Allow for infura to gradually lower the amount of requests based on the time passed since the transaction
// Until first 5 minutes - 1/2 regular polling speed
// Until first 10 minutes - 1/4 regular polling speed
// Until first hour - 1/8 polling speed
// Until first two hours - 1/12 polling speed
// Until first four hours - 1/16 polling speed
// Until first day - 1/24 polling speed
// After first day - 1/32 polling speed
if (timePassedSinceTx < 5 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 2 * REGULAR_TX_POLLING
if (timePassedSinceTx < 10 * T.minutes) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 4 * REGULAR_TX_POLLING
if (timePassedSinceTx < 1 * T.hour) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 8 * REGULAR_TX_POLLING
if (timePassedSinceTx < 2 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 12 * REGULAR_TX_POLLING
if (timePassedSinceTx < 4 * T.hours) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 16 * REGULAR_TX_POLLING
if (timePassedSinceTx < 1 * T.day) return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 24 * REGULAR_TX_POLLING
return _.isNil(txsCache.get(tx.id).res) || timePassedSinceReq > 32 * REGULAR_TX_POLLING
}
// Override geth's getStatus function to allow for different polling timing
function getStatus (account, tx, requested, settings, operatorId) {
if (_.isNil(txsCache.get(tx.id))) {
txsCache.set(tx.id, { lastReqTime: Date.now() })
}
// return last available response
if (!shouldGetStatus(tx)) {
return Promise.resolve(txsCache.get(tx.id).res)
}
return base.getStatus(account, tx, requested, settings, operatorId)
.then(res => {
if (res.status === 'confirmed') {
txsCache.del(tx.id) // Transaction reached final status, can trim it from the caching obj
} else {
txsCache.set(tx.id, { lastReqTime: Date.now(), res })
txsCache.ttl(tx.id, T.hour / 1000)
}
return res
})
}
module.exports = _.merge(base, { NAME, run, getStatus, fetchSpeed: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW })

View file

@ -21,10 +21,8 @@ const processBatches = require('./tx-batching-processing')
const INCOMING_TX_INTERVAL = 30 * T.seconds const INCOMING_TX_INTERVAL = 30 * T.seconds
const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds const LIVE_INCOMING_TX_INTERVAL = 5 * T.seconds
const INCOMING_TX_INTERVAL_FILTER = 1 * T.minute
const LIVE_INCOMING_TX_INTERVAL_FILTER = 10 * T.seconds
const UNNOTIFIED_INTERVAL = 10 * T.seconds const UNNOTIFIED_INTERVAL = 10 * T.seconds
const SWEEP_HD_INTERVAL = T.minute const SWEEP_HD_INTERVAL = 5 * T.minute
const TRADE_INTERVAL = 60 * T.seconds const TRADE_INTERVAL = 60 * T.seconds
const PONG_INTERVAL = 10 * T.seconds const PONG_INTERVAL = 10 * T.seconds
const LOGS_CLEAR_INTERVAL = 1 * T.day const LOGS_CLEAR_INTERVAL = 1 * T.day
@ -60,7 +58,6 @@ const QUEUE = {
SLOW: SLOW_QUEUE SLOW: SLOW_QUEUE
} }
const coinFilter = ['ETH']
const schemaCallbacks = new Map() const schemaCallbacks = new Map()
const cachedVariables = new NodeCache({ const cachedVariables = new NodeCache({
@ -167,24 +164,16 @@ function doPolling (schema) {
pi().executeTrades() pi().executeTrades()
pi().pong() pi().pong()
pi().clearOldLogs() pi().clearOldLogs()
cashOutTx.monitorLiveIncoming(settings(), false, coinFilter) cashOutTx.monitorLiveIncoming(settings())
cashOutTx.monitorStaleIncoming(settings(), false, coinFilter) cashOutTx.monitorStaleIncoming(settings())
if (!_.isEmpty(coinFilter)) {
cashOutTx.monitorLiveIncoming(settings(), true, coinFilter)
cashOutTx.monitorStaleIncoming(settings(), true, coinFilter)
}
cashOutTx.monitorUnnotified(settings()) cashOutTx.monitorUnnotified(settings())
pi().sweepHd() pi().sweepHd()
notifier.checkNotification(pi()) notifier.checkNotification(pi())
updateCoinAtmRadar() updateCoinAtmRadar()
addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST) addToQueue(pi().executeTrades, TRADE_INTERVAL, schema, QUEUE.FAST)
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings, false, coinFilter) addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL, schema, QUEUE.FAST, settings)
if (!_.isEmpty(coinFilter)) {
addToQueue(cashOutTx.monitorLiveIncoming, LIVE_INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter)
addToQueue(cashOutTx.monitorStaleIncoming, INCOMING_TX_INTERVAL_FILTER, schema, QUEUE.FAST, settings, true, coinFilter)
}
addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings) addToQueue(cashOutTx.monitorUnnotified, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings)
addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings) addToQueue(cashInTx.monitorPending, PENDING_INTERVAL, schema, QUEUE.FAST, settings)
addToQueue(processBatches, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE) addToQueue(processBatches, UNNOTIFIED_INTERVAL, schema, QUEUE.FAST, settings, TRANSACTION_BATCH_LIFECYCLE)

View file

@ -14,6 +14,7 @@ const httpError = require('./route-helpers').httpError
const logger = require('./logger') const logger = require('./logger')
const { getOpenBatchCryptoValue } = require('./tx-batching') const { getOpenBatchCryptoValue } = require('./tx-batching')
const BN = require('./bn') const BN = require('./bn')
const { BALANCE_FETCH_SPEED_MULTIPLIER } = require('./constants')
const FETCH_INTERVAL = 5000 const FETCH_INTERVAL = 5000
const INSUFFICIENT_FUNDS_CODE = 570 const INSUFFICIENT_FUNDS_CODE = 570
@ -254,20 +255,28 @@ function checkBlockchainStatus (settings, cryptoCode) {
.then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode)) .then(({ checkBlockchainStatus }) => checkBlockchainStatus(cryptoCode))
} }
const coinFilter = ['ETH']
const balance = (settings, cryptoCode) => { const balance = (settings, cryptoCode) => {
if (_.includes(coinFilter, cryptoCode)) return balanceFiltered(settings, cryptoCode) return fetchWallet(settings, cryptoCode)
return balanceUnfiltered(settings, cryptoCode) .then(r => r.wallet.fetchSpeed ?? BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL)
.then(multiplier => {
switch (multiplier) {
case BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL:
return balanceNormal(settings, cryptoCode)
case BALANCE_FETCH_SPEED_MULTIPLIER.SLOW:
return balanceSlow(settings, cryptoCode)
default:
throw new Error()
}
})
} }
const balanceUnfiltered = mem(_balance, { const balanceNormal = mem(_balance, {
maxAge: FETCH_INTERVAL, maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.NORMAL * FETCH_INTERVAL,
cacheKey: (settings, cryptoCode) => cryptoCode cacheKey: (settings, cryptoCode) => cryptoCode
}) })
const balanceFiltered = mem(_balance, { const balanceSlow = mem(_balance, {
maxAge: 3 * FETCH_INTERVAL, maxAge: BALANCE_FETCH_SPEED_MULTIPLIER.SLOW * FETCH_INTERVAL,
cacheKey: (settings, cryptoCode) => cryptoCode cacheKey: (settings, cryptoCode) => cryptoCode
}) })

View file

@ -7,7 +7,7 @@ import { P, Label3 } from 'src/components/typography'
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg' import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg' import { ReactComponent as FilterIcon } from 'src/styling/icons/button/filter/white.svg'
import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg' import { ReactComponent as ReverseFilterIcon } from 'src/styling/icons/button/filter/zodiac.svg'
import { onlyFirstToUpper } from 'src/utils/string' import { onlyFirstToUpper, singularOrPlural } from 'src/utils/string'
import { chipStyles, styles } from './SearchFilter.styles' import { chipStyles, styles } from './SearchFilter.styles'
@ -18,7 +18,7 @@ const SearchFilter = ({
filters, filters,
onFilterDelete, onFilterDelete,
deleteAllFilters, deleteAllFilters,
entries entries = 0
}) => { }) => {
const chipClasses = useChipStyles() const chipClasses = useChipStyles()
const classes = useStyles() const classes = useStyles()
@ -40,8 +40,11 @@ const SearchFilter = ({
</div> </div>
<div className={classes.deleteWrapper}> <div className={classes.deleteWrapper}>
{ {
<Label3 className={classes.entries}>{`${entries ?? <Label3 className={classes.entries}>{`${entries} ${singularOrPlural(
0} entries`}</Label3> entries,
`entry`,
`entries`
)}`}</Label3>
} }
<ActionButton <ActionButton
color="secondary" color="secondary"

View file

@ -33,6 +33,7 @@ import {
offErrorColor offErrorColor
} from 'src/styling/variables' } from 'src/styling/variables'
import { URI } from 'src/utils/apollo' import { URI } from 'src/utils/apollo'
import { SWEEPABLE_CRYPTOS } from 'src/utils/constants'
import * as Customer from 'src/utils/customer' import * as Customer from 'src/utils/customer'
import CopyToClipboard from './CopyToClipboard' import CopyToClipboard from './CopyToClipboard'
@ -405,6 +406,14 @@ const DetailsRow = ({ it: tx, timezone }) => {
</ActionButton> </ActionButton>
)} )}
</div> </div>
{!R.isNil(tx.swept) && R.includes(tx.cryptoCode, SWEEPABLE_CRYPTOS) && (
<div className={classes.swept}>
<Label>Sweep status</Label>
<span className={classes.bold}>
{tx.swept ? `Swept` : `Unswept`}
</span>
</div>
)}
<div> <div>
<Label>Other actions</Label> <Label>Other actions</Label>
<div className={classes.otherActionsGroup}> <div className={classes.otherActionsGroup}>

View file

@ -131,5 +131,8 @@ export default {
}, },
error: { error: {
color: tomato color: tomato
},
swept: {
width: 250
} }
} }

View file

@ -75,6 +75,7 @@ const GET_TRANSACTIONS = gql`
$cryptoCode: String $cryptoCode: String
$toAddress: String $toAddress: String
$status: String $status: String
$swept: Boolean
) { ) {
transactions( transactions(
limit: $limit limit: $limit
@ -87,6 +88,7 @@ const GET_TRANSACTIONS = gql`
cryptoCode: $cryptoCode cryptoCode: $cryptoCode
toAddress: $toAddress toAddress: $toAddress
status: $status status: $status
swept: $swept
) { ) {
id id
txClass txClass
@ -121,6 +123,7 @@ const GET_TRANSACTIONS = gql`
rawTickerPrice rawTickerPrice
batchError batchError
walletScore walletScore
swept
} }
} }
` `
@ -246,7 +249,8 @@ const Transactions = () => {
fiatCode: filtersObject.fiat, fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto, cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address, toAddress: filtersObject.address,
status: filtersObject.status status: filtersObject.status,
swept: filtersObject.swept === 'Swept'
}) })
refetch && refetch() refetch && refetch()
@ -269,7 +273,8 @@ const Transactions = () => {
fiatCode: filtersObject.fiat, fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto, cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address, toAddress: filtersObject.address,
status: filtersObject.status status: filtersObject.status,
swept: filtersObject.swept === 'Swept'
}) })
refetch && refetch() refetch && refetch()
@ -287,7 +292,8 @@ const Transactions = () => {
fiatCode: filtersObject.fiat, fiatCode: filtersObject.fiat,
cryptoCode: filtersObject.crypto, cryptoCode: filtersObject.crypto,
toAddress: filtersObject.address, toAddress: filtersObject.address,
status: filtersObject.status status: filtersObject.status,
swept: filtersObject.swept === 'Swept'
}) })
refetch && refetch() refetch && refetch()

View file

@ -8,6 +8,8 @@ const MANUAL = 'manual'
const IP_CHECK_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ const IP_CHECK_REGEX = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
const SWEEPABLE_CRYPTOS = ['ETH']
export { export {
CURRENCY_MAX, CURRENCY_MAX,
MIN_NUMBER_OF_CASSETTES, MIN_NUMBER_OF_CASSETTES,
@ -15,5 +17,6 @@ export {
AUTOMATIC, AUTOMATIC,
MANUAL, MANUAL,
WALLET_SCORING_DEFAULT_THRESHOLD, WALLET_SCORING_DEFAULT_THRESHOLD,
IP_CHECK_REGEX IP_CHECK_REGEX,
SWEEPABLE_CRYPTOS
} }

View file

@ -1,7 +1,7 @@
{ {
"files": { "files": {
"main.js": "/static/js/main.be5427dd.chunk.js", "main.js": "/static/js/main.cbb37932.chunk.js",
"main.js.map": "/static/js/main.be5427dd.chunk.js.map", "main.js.map": "/static/js/main.cbb37932.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.9bb48edb.chunk.js": "/static/js/2.9bb48edb.chunk.js", "static/js/2.9bb48edb.chunk.js": "/static/js/2.9bb48edb.chunk.js",
@ -154,6 +154,6 @@
"entrypoints": [ "entrypoints": [
"static/js/runtime-main.5b925903.js", "static/js/runtime-main.5b925903.js",
"static/js/2.9bb48edb.chunk.js", "static/js/2.9bb48edb.chunk.js",
"static/js/main.be5427dd.chunk.js" "static/js/main.cbb37932.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.9bb48edb.chunk.js"></script><script src="/static/js/main.be5427dd.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.9bb48edb.chunk.js"></script><script src="/static/js/main.cbb37932.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