Merge branch 'dev' into update_install-nix
This commit is contained in:
commit
3817ee63fe
77 changed files with 1163 additions and 817 deletions
|
|
@ -33,7 +33,7 @@ npm install
|
|||
## Generate certificates
|
||||
|
||||
```
|
||||
bash bin/cert-gen.sh
|
||||
bash tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ npm install
|
|||
## Generate certificates
|
||||
|
||||
```
|
||||
bash bin/cert-gen.sh
|
||||
bash tools/cert-gen.sh
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
|
|
|||
|
|
@ -26,5 +26,7 @@ keypool=10000
|
|||
prune=4000
|
||||
daemon=0
|
||||
addresstype=p2sh-segwit
|
||||
walletrbf=1`
|
||||
walletrbf=1
|
||||
bind=0.0.0.0:8332
|
||||
rpcport=8333`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,5 @@ keypool=10000
|
|||
prune=4000
|
||||
daemon=0
|
||||
bind=0.0.0.0:8334
|
||||
rpcport=8335
|
||||
`
|
||||
rpcport=8335`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,24 +25,24 @@ const BINARIES = {
|
|||
dir: 'bitcoin-0.20.1/bin'
|
||||
},
|
||||
ETH: {
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.25-e7872729.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.9.25-e7872729'
|
||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.10.8-26675454.tar.gz',
|
||||
dir: 'geth-linux-amd64-1.10.8-26675454'
|
||||
},
|
||||
ZEC: {
|
||||
url: 'https://z.cash/downloads/zcash-4.3.0-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.3.0/bin'
|
||||
url: 'https://z.cash/downloads/zcash-4.4.1-linux64-debian-stretch.tar.gz',
|
||||
dir: 'zcash-4.4.1/bin'
|
||||
},
|
||||
DASH: {
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.16.1.1/dashcore-0.16.1.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.16.1/bin'
|
||||
url: 'https://github.com/dashpay/dash/releases/download/v0.17.0.3/dashcore-0.17.0.3-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'dashcore-0.17.0/bin'
|
||||
},
|
||||
LTC: {
|
||||
url: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'litecoin-0.18.1/bin'
|
||||
},
|
||||
BCH: {
|
||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.2.0/bitcoin-cash-node-22.2.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-cash-node-22.2.0/bin',
|
||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v23.1.0/bitcoin-cash-node-23.1.0-x86_64-linux-gnu.tar.gz',
|
||||
dir: 'bitcoin-cash-node-23.1.0/bin',
|
||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ function buildConfig () {
|
|||
rpcpassword=${common.randomPass()}
|
||||
dbcache=500
|
||||
keypool=10000
|
||||
litemode=1
|
||||
disablegovernance=1
|
||||
prune=4000
|
||||
txindex=0
|
||||
enableprivatesend=1
|
||||
privatesendautostart=1`
|
||||
enablecoinjoin=1
|
||||
coinjoinautostart=1`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,6 @@ module.exports = {setup}
|
|||
function setup (dataDir) {
|
||||
const coinRec = coinUtils.getCryptoCurrency('ETH')
|
||||
common.firewall([coinRec.defaultPort])
|
||||
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="fast" --cache 2048 --maxpeers 40 --rpc`
|
||||
const cmd = `/usr/local/bin/${coinRec.daemon} --datadir "${dataDir}" --syncmode="light" --cache 2048 --maxpeers 40 --rpc`
|
||||
common.writeSupervisorConfig(coinRec, cmd)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ const logger = common.logger
|
|||
|
||||
const PLUGINS = {
|
||||
BTC: require('./bitcoin.js'),
|
||||
LTC: require('./litecoin.js'),
|
||||
ETH: require('./ethereum.js'),
|
||||
BCH: require('./bitcoincash.js'),
|
||||
DASH: require('./dash.js'),
|
||||
ZEC: require('./zcash.js'),
|
||||
BCH: require('./bitcoincash.js')
|
||||
ETH: require('./ethereum.js'),
|
||||
LTC: require('./litecoin.js'),
|
||||
ZEC: require('./zcash.js')
|
||||
}
|
||||
|
||||
module.exports = {run}
|
||||
|
|
@ -57,7 +57,8 @@ function processCryptos (codes) {
|
|||
|
||||
const selectedCryptos = _.map(code => _.find(['code', code], cryptos), codes)
|
||||
_.forEach(setupCrypto, selectedCryptos)
|
||||
common.es('sudo service supervisor restart')
|
||||
common.es('sudo supervisorctl reread')
|
||||
common.es('sudo supervisorctl update')
|
||||
|
||||
const blockchainDir = options.blockchainDir
|
||||
const backupDir = path.resolve(os.homedir(), 'backups')
|
||||
|
|
@ -104,9 +105,7 @@ function run () {
|
|||
name: c.display,
|
||||
value: c.code,
|
||||
checked,
|
||||
disabled: c.cryptoCode === 'ETH'
|
||||
? 'Use admin\'s Infura plugin'
|
||||
: checked && 'Installed'
|
||||
disabled: checked && 'Installed'
|
||||
}
|
||||
}, cryptos)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ function atomic (tx, pi, fromClient) {
|
|||
const isolationLevel = pgp.txMode.isolationLevel
|
||||
const mode = new TransactionMode({ tiLevel: isolationLevel.serializable })
|
||||
function transaction (t) {
|
||||
const sql = 'select * from cash_out_txs where id=$1'
|
||||
const sql = 'SELECT * FROM cash_out_txs WHERE id=$1 FOR UPDATE'
|
||||
|
||||
return t.oneOrNone(sql, [tx.id])
|
||||
.then(toObj)
|
||||
.then(oldTx => {
|
||||
|
|
@ -72,7 +73,7 @@ function preProcess (t, oldTx, newTx, pi) {
|
|||
}
|
||||
|
||||
const hasError = !oldTx.error && newTx.error
|
||||
const hasDispenseOccurred = !dispenseOccurred(oldTx.bills) && dispenseOccurred(newTx.bills)
|
||||
const hasDispenseOccurred = !oldTx.dispenseConfirmed && dispenseOccurred(newTx.bills)
|
||||
|
||||
if (hasError || hasDispenseOccurred) {
|
||||
return cashOutActions.logDispense(t, updatedTx)
|
||||
|
|
|
|||
|
|
@ -81,6 +81,11 @@ function migrateCommissions (config) {
|
|||
}
|
||||
|
||||
const { global, scoped } = getConfigFields(_.keys(codes), config)
|
||||
const defaultCashOutCommissions = { code: 'cashOutCommission', value: 0, scope: global[0].scope }
|
||||
const isCashOutDisabled =
|
||||
_.isEmpty(_.filter(commissionElement => commissionElement.code === 'cashOutCommission', global))
|
||||
const globalWithDefaults =
|
||||
isCashOutDisabled ? _.concat(global, defaultCashOutCommissions) : global
|
||||
|
||||
const machineAndCryptoScoped = scoped.filter(
|
||||
f => f.scope.machine !== GLOBAL_SCOPE.machine && f.scope.crypto.length === 1
|
||||
|
|
@ -112,7 +117,7 @@ function migrateCommissions (config) {
|
|||
const allCommissionsOverrides = withCryptoScoped.concat(filteredMachineScoped)
|
||||
|
||||
return {
|
||||
..._.fromPairs(global.map(f => [`commissions_${codes[f.code]}`, f.value])),
|
||||
..._.fromPairs(globalWithDefaults.map(f => [`commissions_${codes[f.code]}`, f.value])),
|
||||
...(allCommissionsOverrides.length > 0 && {
|
||||
commissions_overrides: allCommissionsOverrides.map(s => ({
|
||||
..._.fromPairs(s.values.map(f => [codes[f.code], f.value])),
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ const NUM_RESULTS = 1000
|
|||
const idPhotoCardBasedir = _.get('idPhotoCardDir', options)
|
||||
const frontCameraBaseDir = _.get('frontCameraDir', options)
|
||||
const operatorDataDir = _.get('operatorDataDir', options)
|
||||
const sms = require('./sms')
|
||||
const settingsLoader = require('./new-settings-loader')
|
||||
|
||||
const TX_PASSTHROUGH_ERROR_CODES = ['operatorCancel']
|
||||
|
||||
/**
|
||||
* Add new customer
|
||||
|
|
@ -115,13 +119,21 @@ async function updateCustomer (id, data, userToken) {
|
|||
|
||||
const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
|
||||
const updateData = updateOverride(enhancedUpdateData)
|
||||
|
||||
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||
|
||||
if (!_.isEmpty(updateData)) {
|
||||
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||
' where id=$1'
|
||||
|
||||
await db.none(sql, [id])
|
||||
}
|
||||
|
||||
if (data.subscriberInfo) {
|
||||
Promise.all([getCustomerById(id), settingsLoader.loadLatest()])
|
||||
.then(([customer, config]) => sms.getLookup(config, customer.phone))
|
||||
.then(res => updateSubscriberData(id, res, userToken))
|
||||
.catch(console.error)
|
||||
}
|
||||
invalidateCustomerNotifications(id, formattedData)
|
||||
|
||||
await db.none(sql, [id])
|
||||
|
||||
return getCustomerById(id)
|
||||
}
|
||||
|
||||
|
|
@ -132,6 +144,11 @@ const invalidateCustomerNotifications = (id, data) => {
|
|||
return notifierQueries.invalidateNotification(detailB, 'compliance')
|
||||
}
|
||||
|
||||
const updateSubscriberData = (customerId, data, userToken) => {
|
||||
const sql = `UPDATE customers SET subscriber_info=$1, subscriber_info_at=now(), subscriber_info_by=$2 WHERE id=$3`
|
||||
return db.none(sql, [data, userToken, customerId])
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer by id
|
||||
*
|
||||
|
|
@ -264,16 +281,19 @@ function getComplianceTypes () {
|
|||
function updateOverride (fields) {
|
||||
const updateableFields = [
|
||||
'id_card_data',
|
||||
'id_card_photo',
|
||||
'front_camera',
|
||||
'id_card_photo_path',
|
||||
'front_camera_path',
|
||||
'authorized',
|
||||
'us_ssn'
|
||||
]
|
||||
|
||||
const updatedFields = _.intersection(updateableFields, _.keys(fields))
|
||||
const atFields = _.fromPairs(_.map(f => [`${f}_override`, 'automatic'], updatedFields))
|
||||
const removePathSuffix = _.map(_.replace('_path', ''))
|
||||
const getPairs = _.map(f => [`${f}_override`, 'automatic'])
|
||||
|
||||
return _.merge(fields, atFields)
|
||||
const updatedFields = _.intersection(updateableFields, _.keys(fields))
|
||||
const overrideFields = _.compose(_.fromPairs, getPairs, removePathSuffix)(updatedFields)
|
||||
|
||||
return _.merge(fields, overrideFields)
|
||||
}
|
||||
|
||||
function enhanceAtFields (fields) {
|
||||
|
|
@ -450,7 +470,10 @@ function batch () {
|
|||
*
|
||||
* @returns {array} Array of customers with it's transactions aggregations
|
||||
*/
|
||||
|
||||
function getCustomersList (phone = null, name = null, address = null, id = null) {
|
||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||
|
||||
const sql = `SELECT id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||
|
|
@ -465,21 +488,22 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||
row_number() OVER (partition by c.id order by t.created desc) AS rn,
|
||||
coalesce(sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id), 0) as total_spent
|
||||
sum(CASE WHEN t.id IS NOT NULL THEN 1 ELSE 0 END) OVER (partition by c.id) AS total_txs,
|
||||
coalesce(sum(t.fiat) OVER (partition by c.id), 0) AS total_spent
|
||||
FROM customers c LEFT OUTER JOIN (
|
||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id
|
||||
SELECT 'cashIn' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_in_txs WHERE send_confirmed = true UNION
|
||||
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id
|
||||
SELECT 'cashOut' AS tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
FROM cash_out_txs WHERE confirmed_at IS NOT NULL) t ON c.id = t.customer_id
|
||||
WHERE c.id != $1
|
||||
WHERE c.id != $2
|
||||
) AS cl WHERE rn = 1
|
||||
AND ($3 IS NULL OR phone = $3)
|
||||
AND ($4 IS NULL OR concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $4 OR id_card_data::json->>'firstName' = $4 OR id_card_data::json->>'lastName' = $4)
|
||||
AND ($5 IS NULL OR id_card_data::json->>'address' = $5)
|
||||
AND ($6 IS NULL OR id_card_data::json->>'documentNumber' = $6)
|
||||
limit $2`
|
||||
return db.any(sql, [ anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
||||
AND ($4 IS NULL OR phone = $4)
|
||||
AND ($5 IS NULL OR concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') = $5 OR id_card_data::json->>'firstName' = $5 OR id_card_data::json->>'lastName' = $5)
|
||||
AND ($6 IS NULL OR id_card_data::json->>'address' = $6)
|
||||
AND ($7 IS NULL OR id_card_data::json->>'documentNumber' = $7)
|
||||
limit $3`
|
||||
return db.any(sql, [ passableErrorCdoes, anonymous.uuid, NUM_RESULTS, phone, name, address, id ])
|
||||
.then(customers => Promise.all(_.map(customer => {
|
||||
return populateOverrideUsernames(customer)
|
||||
.then(camelize)
|
||||
|
|
@ -494,11 +518,12 @@ function getCustomersList (phone = null, name = null, address = null, id = null)
|
|||
* @returns {array} Array of customers with it's transactions aggregations
|
||||
*/
|
||||
function getCustomerById (id) {
|
||||
const passableErrorCodes = _.map(Pgp.as.text, TX_PASSTHROUGH_ERROR_CODES).join(',')
|
||||
const sql = `select id, authorized_override, days_suspended, is_suspended, front_camera_path, front_camera_override,
|
||||
phone, sms_override, id_card_data, id_card_data_override, id_card_data_expiration,
|
||||
id_card_photo_path, id_card_photo_override, us_ssn, us_ssn_override, sanctions, sanctions_at,
|
||||
sanctions_override, total_txs, total_spent, created as last_active, fiat as last_tx_fiat,
|
||||
fiat_code as last_tx_fiat_code, tx_class as last_tx_class
|
||||
fiat_code as last_tx_fiat_code, tx_class as last_tx_class, subscriber_info
|
||||
from (
|
||||
select c.id, c.authorized_override,
|
||||
greatest(0, date_part('day', c.suspended_until - now())) as days_suspended,
|
||||
|
|
@ -506,18 +531,18 @@ function getCustomerById (id) {
|
|||
c.front_camera_path, c.front_camera_override,
|
||||
c.phone, c.sms_override, c.id_card_data, c.id_card_data_override, c.id_card_data_expiration,
|
||||
c.id_card_photo_path, c.id_card_photo_override, c.us_ssn, c.us_ssn_override, c.sanctions,
|
||||
c.sanctions_at, c.sanctions_override, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||
c.sanctions_at, c.sanctions_override, c.subscriber_info, t.tx_class, t.fiat, t.fiat_code, t.created,
|
||||
row_number() over (partition by c.id order by t.created desc) as rn,
|
||||
sum(case when t.id is not null then 1 else 0 end) over (partition by c.id) as total_txs,
|
||||
sum(t.fiat) over (partition by c.id) as total_spent
|
||||
sum(case when error_code is null or error_code not in ($1^) then t.fiat else 0 end) over (partition by c.id) as total_spent
|
||||
from customers c left outer join (
|
||||
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id
|
||||
select 'cashIn' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
from cash_in_txs where send_confirmed = true union
|
||||
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id
|
||||
select 'cashOut' as tx_class, id, fiat, fiat_code, created, customer_id, error_code
|
||||
from cash_out_txs where confirmed_at is not null) t on c.id = t.customer_id
|
||||
where c.id = $1
|
||||
where c.id = $2
|
||||
) as cl where rn = 1`
|
||||
return db.oneOrNone(sql, [id])
|
||||
return db.oneOrNone(sql, [passableErrorCodes, id])
|
||||
.then(populateOverrideUsernames)
|
||||
.then(camelize)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ const stripDefaultDbFuncs = dbCtx => {
|
|||
manyOrNone: dbCtx.$manyOrNone,
|
||||
tx: dbCtx.$tx,
|
||||
task: dbCtx.$task,
|
||||
batch: dbCtx.batch
|
||||
batch: dbCtx.batch,
|
||||
multi: dbCtx.$multi
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +68,7 @@ const pgp = Pgp({
|
|||
obj.$one = (query, variables) => obj.__taskEx(t => t.one(query, variables))
|
||||
obj.$none = (query, variables) => obj.__taskEx(t => t.none(query, variables))
|
||||
obj.$any = (query, variables) => obj.__taskEx(t => t.any(query, variables))
|
||||
obj.$multi = (query, variables) => obj.__taskEx(t => t.multi(query, variables))
|
||||
// when opts is not defined "cb" occupies the "opts" spot of the arguments
|
||||
obj.$tx = (opts, cb) => typeof opts === 'function' ? _tx(obj, {}, opts) : _tx(obj, opts, cb)
|
||||
obj.$task = (opts, cb) => typeof opts === 'function' ? _task(obj, {}, opts) : _task(obj, opts, cb)
|
||||
|
|
|
|||
|
|
@ -60,10 +60,11 @@ function update (deviceId, logLines) {
|
|||
}
|
||||
|
||||
function clearOldLogs () {
|
||||
const sql = `delete from logs
|
||||
where timestamp < now() - interval '3 days'`
|
||||
|
||||
return db.none(sql)
|
||||
const sqls = `delete from logs
|
||||
where timestamp < now() - interval '3 days';
|
||||
delete from server_logs
|
||||
where timestamp < now() - interval '3 days';`
|
||||
return db.multi(sqls)
|
||||
}
|
||||
|
||||
function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ const settingsLoader = require('./new-settings-loader')
|
|||
const notifierUtils = require('./notifier/utils')
|
||||
const notifierQueries = require('./notifier/queries')
|
||||
|
||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||
|
||||
function getMachines () {
|
||||
return db.any('SELECT * FROM devices WHERE display=TRUE ORDER BY created')
|
||||
.then(rr => rr.map(r => ({
|
||||
|
|
@ -36,11 +40,32 @@ function getConfig (defaultConfig) {
|
|||
return settingsLoader.loadLatest().config
|
||||
}
|
||||
|
||||
function getMachineNames (config) {
|
||||
const fullyFunctionalStatus = { label: 'Fully functional', type: 'success' }
|
||||
const unresponsiveStatus = { label: 'Unresponsive', type: 'error' }
|
||||
const stuckStatus = { label: 'Stuck', type: 'error' }
|
||||
const getStatus = (ping, stuck) => {
|
||||
if (ping && ping.age) return unresponsiveStatus
|
||||
|
||||
if (stuck && stuck.age) return stuckStatus
|
||||
|
||||
return fullyFunctionalStatus
|
||||
}
|
||||
|
||||
function addName (pings, events, config) {
|
||||
return machine => {
|
||||
const cashOutConfig = configManager.getCashOut(machine.deviceId, config)
|
||||
|
||||
const cashOut = !!cashOutConfig.active
|
||||
|
||||
const statuses = [
|
||||
getStatus(
|
||||
_.first(pings[machine.deviceId]),
|
||||
_.first(checkStuckScreen(events, machine.name))
|
||||
)
|
||||
]
|
||||
|
||||
return _.assign(machine, { cashOut, statuses })
|
||||
}
|
||||
}
|
||||
|
||||
function getMachineNames (config) {
|
||||
return Promise.all([getMachines(), getConfig(config), getNetworkHeartbeat(), getNetworkPerformance()])
|
||||
.then(([rawMachines, config, heartbeat, performance]) => Promise.all(
|
||||
[rawMachines, checkPings(rawMachines), dbm.machineEvents(), config, heartbeat, performance]
|
||||
|
|
@ -91,9 +116,27 @@ function getMachineName (machineId) {
|
|||
.then(it => it.name)
|
||||
}
|
||||
|
||||
function getMachine (machineId) {
|
||||
function getMachine (machineId, config) {
|
||||
const sql = 'SELECT * FROM devices WHERE device_id=$1'
|
||||
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
|
||||
const queryMachine = db.oneOrNone(sql, [machineId]).then(r => ({
|
||||
deviceId: r.device_id,
|
||||
cashbox: r.cashbox,
|
||||
cassette1: r.cassette1,
|
||||
cassette2: r.cassette2,
|
||||
version: r.version,
|
||||
model: r.model,
|
||||
pairedAt: new Date(r.created),
|
||||
lastPing: new Date(r.last_online),
|
||||
name: r.name,
|
||||
paired: r.paired
|
||||
}))
|
||||
|
||||
return Promise.all([queryMachine, dbm.machineEvents(), config])
|
||||
.then(([machine, events, config]) => {
|
||||
const pings = checkPings([machine])
|
||||
|
||||
return [machine].map(addName(pings, events, config))[0]
|
||||
})
|
||||
}
|
||||
|
||||
function renameMachine (rec) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const ALL_ACCOUNTS = [
|
|||
{ code: 'bitcoind', display: 'bitcoind', class: WALLET, cryptos: [BTC] },
|
||||
{ code: 'no-layer2', display: 'No Layer 2', class: LAYER_2, cryptos: ALL_CRYPTOS },
|
||||
{ code: 'infura', display: 'Infura', class: WALLET, cryptos: [ETH, USDT] },
|
||||
{ code: 'geth', display: 'geth (DEPRECATED)', class: WALLET, cryptos: [ETH, USDT], deprecated: true },
|
||||
{ code: 'geth', display: 'geth', class: WALLET, cryptos: [ETH, USDT] },
|
||||
{ code: 'zcashd', display: 'zcashd', class: WALLET, cryptos: [ZEC] },
|
||||
{ code: 'litecoind', display: 'litecoind', class: WALLET, cryptos: [LTC] },
|
||||
{ code: 'dashd', display: 'dashd', class: WALLET, cryptos: [DASH] },
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ const resolvers = {
|
|||
Query: {
|
||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone }]) =>
|
||||
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status)
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, timezone, simplified }]) =>
|
||||
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status, simplified)
|
||||
.then(data => parseAsync(logDateFormat(timezone, data, ['created', 'sendTime']), { fields: txLogFields })),
|
||||
transactionCsv: (...[, { id, txClass, timezone }]) =>
|
||||
transactions.getTx(id, txClass).then(data =>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const typeDef = gql`
|
|||
lastTxFiatCode: String
|
||||
lastTxClass: String
|
||||
transactions: [Transaction]
|
||||
subscriberInfo: JSONObject
|
||||
}
|
||||
|
||||
input CustomerInput {
|
||||
|
|
@ -53,6 +54,7 @@ const typeDef = gql`
|
|||
lastTxFiatCode: String
|
||||
lastTxClass: String
|
||||
suspendedUntil: Date
|
||||
subscriberInfo: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const typeDef = gql`
|
|||
|
||||
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): [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): 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, timezone: String, simplified: Boolean): String @auth
|
||||
transactionCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
txAssociatedDataCsv(id: ID, txClass: String, timezone: String): String @auth
|
||||
transactionFilters: [Filter] @auth
|
||||
|
|
|
|||
|
|
@ -36,7 +36,8 @@ function batch (
|
|||
fiatCode = null,
|
||||
cryptoCode = null,
|
||||
toAddress = null,
|
||||
status = null
|
||||
status = null,
|
||||
simplified = false
|
||||
) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
|
||||
|
|
@ -99,6 +100,55 @@ function batch (
|
|||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
||||
])
|
||||
.then(packager)
|
||||
.then(res => {
|
||||
if (simplified) return simplifiedBatch(res)
|
||||
else return res
|
||||
})
|
||||
}
|
||||
|
||||
function simplifiedBatch (data) {
|
||||
const fields = ['txClass', 'id', 'created', 'machineName',
|
||||
'cryptoCode', 'fiat', 'fiatCode', 'phone', 'toAddress',
|
||||
'txHash', 'dispense', 'error', 'status', 'fiatProfit', 'cryptoAmount']
|
||||
|
||||
const addSimplifiedFields = _.map(it => ({
|
||||
...it,
|
||||
status: getStatus(it),
|
||||
fiatProfit: getProfit(it).toString(),
|
||||
cryptoAmount: getCryptoAmount(it)
|
||||
}))
|
||||
|
||||
return _.compose(_.map(_.pick(fields)), addSimplifiedFields)(data)
|
||||
}
|
||||
|
||||
const getCryptoAmount = it => coinUtils.toUnit(BN(it.cryptoAtoms), it.cryptoCode).toString()
|
||||
|
||||
const getProfit = it => {
|
||||
const getCommissionFee = it => BN(it.commissionPercentage).mul(BN(it.fiat))
|
||||
if (!it.cashInFee) return getCommissionFee(it)
|
||||
return getCommissionFee(it).add(BN(it.cashInFee))
|
||||
}
|
||||
|
||||
const getCashOutStatus = it => {
|
||||
if (it.hasError) return 'Error'
|
||||
if (it.dispense) return 'Success'
|
||||
if (it.expired) return 'Expired'
|
||||
return 'Pending'
|
||||
}
|
||||
|
||||
const getCashInStatus = it => {
|
||||
if (it.operatorCompleted) return 'Cancelled'
|
||||
if (it.hasError) return 'Error'
|
||||
if (it.sendConfirmed) return 'Sent'
|
||||
if (it.expired) return 'Expired'
|
||||
return 'Pending'
|
||||
}
|
||||
|
||||
const getStatus = it => {
|
||||
if (it.txClass === 'cashOut') {
|
||||
return getCashOutStatus(it)
|
||||
}
|
||||
return getCashInStatus(it)
|
||||
}
|
||||
|
||||
function getCustomerTransactionsBatch (ids) {
|
||||
|
|
|
|||
|
|
@ -105,6 +105,25 @@ const getGlobalNotifications = config => getNotifications(null, null, config)
|
|||
|
||||
const getTriggers = _.get('triggers')
|
||||
|
||||
const getTriggersAutomation = config => {
|
||||
const defaultAutomation = _.get('triggersConfig_automation')(config)
|
||||
const requirements = {
|
||||
sanctions: defaultAutomation,
|
||||
idCardPhoto: defaultAutomation,
|
||||
idCardData: defaultAutomation,
|
||||
facephoto: defaultAutomation,
|
||||
usSsn: defaultAutomation
|
||||
}
|
||||
|
||||
const overrides = _.get('triggersConfig_overrides')(config)
|
||||
|
||||
const requirementsOverrides = _.reduce((acc, override) => {
|
||||
return _.assign(acc, { [override.requirement]: override.automation })
|
||||
}, {}, overrides)
|
||||
|
||||
return _.assign(requirements, requirementsOverrides)
|
||||
}
|
||||
|
||||
const splitGetFirst = _.compose(_.head, _.split('_'))
|
||||
|
||||
const getCryptosFromWalletNamespace = config => {
|
||||
|
|
@ -128,6 +147,7 @@ module.exports = {
|
|||
getTermsConditions,
|
||||
getAllCryptoCurrencies,
|
||||
getTriggers,
|
||||
getTriggersAutomation,
|
||||
getCashOut,
|
||||
getCryptosFromWalletNamespace
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ function plugins (settings, deviceId) {
|
|||
|
||||
const rate = rawRate.div(cashInCommission)
|
||||
|
||||
const lowBalanceMargin = new BN(1.03)
|
||||
const lowBalanceMargin = new BN(1.05)
|
||||
|
||||
const cryptoRec = coinUtils.getCryptoCurrency(cryptoCode)
|
||||
const unitScale = cryptoRec.unitScale
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
exports.NAME = 'MockSMS'
|
||||
const NAME = 'MockSMS'
|
||||
|
||||
exports.sendMessage = function sendMessage (account, rec) {
|
||||
function getLookup (account, number) {
|
||||
console.log('Looking up number: %j', number)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_.endsWith('666', number)) {
|
||||
reject (new Error(`${exports.NAME} mocked error!`))
|
||||
} else {
|
||||
setTimeout(resolve, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function sendMessage (account, rec) {
|
||||
console.log('Sending SMS: %j', rec)
|
||||
return new Promise((resolve, reject) => {
|
||||
if (_.endsWith('666', _.getOr(false, 'sms.toNumber', rec))) {
|
||||
|
|
@ -12,3 +23,9 @@ exports.sendMessage = function sendMessage (account, rec) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,27 @@ function sendMessage (account, rec) {
|
|||
})
|
||||
}
|
||||
|
||||
function getLookup (account, number) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const client = twilio(account.accountSid, account.authToken)
|
||||
return client.lookups.v1.phoneNumbers(number)
|
||||
.fetch({ addOns: ['lamassu_ekata'] })
|
||||
})
|
||||
.then(info => info.addOns.results['lamassu_ekata'])
|
||||
.catch(err => {
|
||||
if (_.includes(err.code, BAD_NUMBER_CODES)) {
|
||||
const badNumberError = new Error(err.message)
|
||||
badNumberError.name = 'BadNumberError'
|
||||
throw badNumberError
|
||||
}
|
||||
|
||||
throw new Error(`Twilio error: ${err.message}`)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NAME,
|
||||
sendMessage
|
||||
sendMessage,
|
||||
getLookup
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ function balance (account, cryptoCode, settings, operatorId) {
|
|||
|
||||
const pendingBalance = (address, cryptoCode) => {
|
||||
const promises = [_balance(true, address, cryptoCode), _balance(false, address, cryptoCode)]
|
||||
return Promise.all(promises).then(([pending, confirmed]) => pending - confirmed)
|
||||
return Promise.all(promises).then(([pending, confirmed]) => pending.minus(confirmed))
|
||||
}
|
||||
const confirmedBalance = (address, cryptoCode) => _balance(false, address, cryptoCode)
|
||||
|
||||
|
|
@ -126,6 +126,7 @@ function generateTx (_toAddress, wallet, amount, includesFee, cryptoCode) {
|
|||
const contract = web3.eth.contract(ABI.ERC20).at(coins.utils.getErc20Token(cryptoCode).contractAddress)
|
||||
|
||||
const rawTx = {
|
||||
chainId: 1,
|
||||
nonce: txCount,
|
||||
gasPrice: hex(gasPrice),
|
||||
gasLimit: gas,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ function poll (req, res, next) {
|
|||
const hasLightning = checkHasLightning(settings)
|
||||
|
||||
const triggers = configManager.getTriggers(settings.config)
|
||||
const triggersAutomation = configManager.getTriggersAutomation(settings.config)
|
||||
|
||||
const operatorInfo = configManager.getOperatorInfo(settings.config)
|
||||
const machineInfo = { deviceId: req.deviceId, deviceName: req.deviceName }
|
||||
|
|
@ -82,7 +83,8 @@ function poll (req, res, next) {
|
|||
receipt,
|
||||
operatorInfo,
|
||||
machineInfo,
|
||||
triggers
|
||||
triggers,
|
||||
triggersAutomation
|
||||
}
|
||||
|
||||
// BACKWARDS_COMPATIBILITY 7.5
|
||||
|
|
|
|||
23
lib/sms.js
23
lib/sms.js
|
|
@ -1,15 +1,28 @@
|
|||
const ph = require('./plugin-helper')
|
||||
const argv = require('minimist')(process.argv.slice(2))
|
||||
|
||||
function getPlugin (settings) {
|
||||
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
||||
const plugin = ph.load(ph.SMS, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
return { plugin, account }
|
||||
}
|
||||
|
||||
function sendMessage (settings, rec) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const pluginCode = argv.mockSms ? 'mock-sms' : 'twilio'
|
||||
const plugin = ph.load(ph.SMS, pluginCode)
|
||||
const account = settings.accounts[pluginCode]
|
||||
|
||||
const { plugin, account } = getPlugin(settings)
|
||||
return plugin.sendMessage(account, rec)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {sendMessage}
|
||||
function getLookup (settings, number) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const { plugin, account } = getPlugin(settings)
|
||||
return plugin.getLookup(account, number)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { sendMessage, getLookup }
|
||||
|
|
|
|||
33
lib/tx.js
33
lib/tx.js
|
|
@ -3,6 +3,9 @@ const db = require('./db')
|
|||
const BN = require('./bn')
|
||||
const CashInTx = require('./cash-in/cash-in-tx')
|
||||
const CashOutTx = require('./cash-out/cash-out-tx')
|
||||
const T = require('./time')
|
||||
|
||||
const REDEEMABLE_AGE = T.day
|
||||
|
||||
function process (tx, pi) {
|
||||
const mtx = massage(tx, pi)
|
||||
|
|
@ -64,23 +67,27 @@ function cancel (txId) {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
function customerHistory (customerId, thresholdDays) {
|
||||
const sql = ` SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction
|
||||
FROM cash_in_txs txIn
|
||||
WHERE txIn.customer_id = $1
|
||||
AND txIn.created > now() - interval $2
|
||||
AND fiat > 0
|
||||
const sql = `SELECT * FROM (
|
||||
SELECT txIn.id, txIn.created, txIn.fiat, 'cashIn' AS direction,
|
||||
((NOT txIn.send_confirmed) AND (txIn.created <= now() - interval $3)) AS expired
|
||||
FROM cash_in_txs txIn
|
||||
WHERE txIn.customer_id = $1
|
||||
AND txIn.created > now() - interval $2
|
||||
AND fiat > 0
|
||||
UNION
|
||||
SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction
|
||||
FROM cash_out_txs txOut
|
||||
WHERE txOut.customer_id = $1
|
||||
AND txOut.created > now() - interval $2
|
||||
AND (timedout = true OR error_code != 'operatorCancel')
|
||||
AND fiat > 0
|
||||
ORDER BY created;`
|
||||
SELECT txOut.id, txOut.created, txOut.fiat, 'cashOut' AS direction,
|
||||
(extract(epoch FROM (now() - greatest(txOut.created, txOut.confirmed_at))) * 1000) >= $4 AS expired
|
||||
FROM cash_out_txs txOut
|
||||
WHERE txOut.customer_id = $1
|
||||
AND txOut.created > now() - interval $2
|
||||
AND error_code IS DISTINCT FROM 'operatorCancel'
|
||||
AND fiat > 0
|
||||
) ch WHERE NOT ch.expired ORDER BY ch.created`
|
||||
|
||||
const days = _.isNil(thresholdDays) ? 0 : thresholdDays
|
||||
return db.any(sql, [customerId, `${days} days`])
|
||||
return db.any(sql, [customerId, `${days} days`, '60 minutes', REDEEMABLE_AGE])
|
||||
}
|
||||
|
||||
module.exports = { post, cancel, customerHistory }
|
||||
|
|
|
|||
|
|
@ -1,35 +1,50 @@
|
|||
const db = require('./db')
|
||||
const settingsLoader = require('../lib/admin/settings-loader')
|
||||
const machineLoader = require('../lib/machine-loader')
|
||||
const { saveConfig, saveAccounts } = require('../lib/new-settings-loader')
|
||||
const { saveConfig, saveAccounts, loadLatest } = require('../lib/new-settings-loader')
|
||||
const { migrate } = require('../lib/config-migration')
|
||||
|
||||
const _ = require('lodash/fp')
|
||||
|
||||
const OLD_SETTINGS_LOADER_SCHEMA_VERSION = 1
|
||||
|
||||
module.exports.up = function (next) {
|
||||
function migrateConfig(settings) {
|
||||
function migrateConfig (settings) {
|
||||
const newSettings = migrate(settings.config, settings.accounts)
|
||||
return Promise.all([
|
||||
saveConfig(newSettings.config),
|
||||
saveAccounts(newSettings.accounts)
|
||||
])
|
||||
.then(() => next())
|
||||
.then(() => next())
|
||||
}
|
||||
|
||||
settingsLoader.loadLatest(false)
|
||||
.then(async settings => ({
|
||||
settings,
|
||||
machines: await machineLoader.getMachineNames(settings.config)
|
||||
}))
|
||||
loadLatest(OLD_SETTINGS_LOADER_SCHEMA_VERSION)
|
||||
.then(async settings => {
|
||||
if (_.isEmpty(settings.config)) {
|
||||
return {
|
||||
settings,
|
||||
machines: []
|
||||
}
|
||||
}
|
||||
return {
|
||||
settings,
|
||||
machines: await machineLoader.getMachineNames(settings.config)
|
||||
}
|
||||
})
|
||||
.then(({ settings, machines }) => {
|
||||
const sql = machines
|
||||
if (_.isEmpty(settings.config)) {
|
||||
return next()
|
||||
}
|
||||
const sql = machines
|
||||
? machines.map(m => `update devices set name = '${m.name}' where device_id = '${m.deviceId}'`)
|
||||
: []
|
||||
return db.multi(sql, () => migrateConfig(settings))
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.message = 'lamassu-server is not configured')
|
||||
next()
|
||||
|
||||
if (err.message === 'lamassu-server is not configured') {
|
||||
return next()
|
||||
}
|
||||
console.log(err.message)
|
||||
return next(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
const db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
const sql = [
|
||||
`ALTER TYPE compliance_type ADD VALUE 'us_ssn'`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
15
migrations/1628100660620-subscriber-info.js
Normal file
15
migrations/1628100660620-subscriber-info.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`ALTER TABLE customers ADD COLUMN subscriber_info JSON`,
|
||||
`ALTER TABLE customers ADD COLUMN subscriber_info_at TIMESTAMPTZ`,
|
||||
`ALTER TABLE customers ADD COLUMN subscriber_info_by UUID REFERENCES users(id)`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
34
new-lamassu-admin/package-lock.json
generated
34
new-lamassu-admin/package-lock.json
generated
|
|
@ -14909,6 +14909,11 @@
|
|||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
},
|
||||
"is-finite": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz",
|
||||
"integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
|
||||
|
|
@ -17152,7 +17157,7 @@
|
|||
"version": "git+https://github.com/lamassu/lamassu-coins.git#f80395e4bab0fccc860de166c97e981ca3ae66a6",
|
||||
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
||||
"requires": {
|
||||
"bech32": "^1.1.3",
|
||||
"bech32": "2.0.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bitcoinjs-lib": "4.0.3",
|
||||
"bs58check": "^2.0.2",
|
||||
|
|
@ -17160,6 +17165,13 @@
|
|||
"crypto-js": "^3.1.9-1",
|
||||
"ethereumjs-icap": "^0.3.1",
|
||||
"lodash": "^4.17.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"bech32": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"language-subtag-registry": {
|
||||
|
|
@ -19150,6 +19162,11 @@
|
|||
"json-parse-better-errors": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"parse-ms": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz",
|
||||
"integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0="
|
||||
},
|
||||
"parse5": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
|
|
@ -19431,6 +19448,11 @@
|
|||
"semver-compare": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"plur": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/plur/-/plur-1.0.0.tgz",
|
||||
"integrity": "sha1-24XGgU9eXlo7Se/CjWBP7GKXUVY="
|
||||
},
|
||||
"pnp-webpack-plugin": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/pnp-webpack-plugin/-/pnp-webpack-plugin-1.6.4.tgz",
|
||||
|
|
@ -20672,6 +20694,16 @@
|
|||
"integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-2.1.0.tgz",
|
||||
"integrity": "sha1-QlfCVt8/sLRR1q/6qwIYhBJpgdw=",
|
||||
"requires": {
|
||||
"is-finite": "^1.0.1",
|
||||
"parse-ms": "^1.0.0",
|
||||
"plur": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"prismjs": {
|
||||
"version": "1.23.0",
|
||||
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.23.0.tgz",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
"libphonenumber-js": "^1.7.50",
|
||||
"match-sorter": "^4.2.0",
|
||||
"moment": "2.24.0",
|
||||
"pretty-ms": "^2.1.0",
|
||||
"qrcode.react": "0.9.3",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^16.12.0",
|
||||
|
|
|
|||
|
|
@ -129,6 +129,8 @@ const styles = {
|
|||
const useStyles = makeStyles(styles)
|
||||
const ALL = 'all'
|
||||
const RANGE = 'range'
|
||||
const ADVANCED = 'advanced'
|
||||
const SIMPLIFIED = 'simplified'
|
||||
|
||||
const LogsDownloaderPopover = ({
|
||||
name,
|
||||
|
|
@ -136,9 +138,12 @@ const LogsDownloaderPopover = ({
|
|||
args,
|
||||
title,
|
||||
getLogs,
|
||||
timezone
|
||||
timezone,
|
||||
simplified
|
||||
}) => {
|
||||
const [selectedRadio, setSelectedRadio] = useState(ALL)
|
||||
const [selectedAdvancedRadio, setSelectedAdvancedRadio] = useState(ADVANCED)
|
||||
|
||||
const [range, setRange] = useState({ from: null, until: null })
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [fetchLogs] = useLazyQuery(query, {
|
||||
|
|
@ -158,6 +163,11 @@ const LogsDownloaderPopover = ({
|
|||
if (selectedRadio === ALL) setRange({ from: null, until: null })
|
||||
}
|
||||
|
||||
const handleAdvancedRadioButtons = evt => {
|
||||
const selectedAdvancedRadio = R.path(['target', 'value'])(evt)
|
||||
setSelectedAdvancedRadio(selectedAdvancedRadio)
|
||||
}
|
||||
|
||||
const handleRangeChange = useCallback(
|
||||
(from, until) => {
|
||||
setRange({ from, until })
|
||||
|
|
@ -165,11 +175,12 @@ const LogsDownloaderPopover = ({
|
|||
[setRange]
|
||||
)
|
||||
|
||||
const downloadLogs = (range, args, fetchLogs) => {
|
||||
const downloadLogs = (range, args) => {
|
||||
if (selectedRadio === ALL) {
|
||||
fetchLogs({
|
||||
variables: {
|
||||
...args
|
||||
...args,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -183,7 +194,8 @@ const LogsDownloaderPopover = ({
|
|||
variables: {
|
||||
...args,
|
||||
from: range.from,
|
||||
until: range.until
|
||||
until: range.until,
|
||||
simplified: selectedAdvancedRadio === SIMPLIFIED
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -221,6 +233,11 @@ const LogsDownloaderPopover = ({
|
|||
{ display: 'Date range', code: RANGE }
|
||||
]
|
||||
|
||||
const advancedRadioButtonOptions = [
|
||||
{ display: 'Advanced logs', code: ADVANCED },
|
||||
{ display: 'Simplified logs', code: SIMPLIFIED }
|
||||
]
|
||||
|
||||
const open = Boolean(anchorEl)
|
||||
const id = open ? 'date-range-popover' : undefined
|
||||
|
||||
|
|
@ -265,10 +282,20 @@ const LogsDownloaderPopover = ({
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
{simplified && (
|
||||
<div className={classes.radioButtonsContainer}>
|
||||
<RadioGroup
|
||||
name="simplified-tx-logs"
|
||||
value={selectedAdvancedRadio}
|
||||
options={advancedRadioButtonOptions}
|
||||
ariaLabel="simplified-tx-logs"
|
||||
onChange={handleAdvancedRadioButtons}
|
||||
className={classes.radioButtons}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.download}>
|
||||
<Link
|
||||
color="primary"
|
||||
onClick={() => downloadLogs(range, args, fetchLogs)}>
|
||||
<Link color="primary" onClick={() => downloadLogs(range, args)}>
|
||||
Download
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import classnames from 'classnames'
|
||||
import moment from 'moment'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import Calendar from './Calendar'
|
||||
|
|
@ -37,7 +38,7 @@ const DateRangePicker = ({ minDate, maxDate, className, onRangeChange }) => {
|
|||
}
|
||||
|
||||
if (from && !to && day.isSameOrAfter(from, 'day')) {
|
||||
setTo(day)
|
||||
setTo(moment(day.toDate().setHours(23, 59, 59, 999)))
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,219 @@
|
|||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { memo, useState } from 'react'
|
||||
|
||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||
import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg'
|
||||
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
||||
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
||||
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
||||
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
||||
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
||||
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
||||
|
||||
import { machineActionsStyles } from './MachineActions.styles'
|
||||
|
||||
const useStyles = makeStyles(machineActionsStyles)
|
||||
|
||||
const MACHINE_ACTION = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$newName: String
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
||||
deviceId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MACHINE = gql`
|
||||
query getMachine($deviceId: ID!) {
|
||||
machine(deviceId: $deviceId) {
|
||||
latestEvent {
|
||||
note
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const isStaticState = machineState => {
|
||||
if (!machineState) {
|
||||
return true
|
||||
}
|
||||
const staticStates = [
|
||||
'chooseCoin',
|
||||
'idle',
|
||||
'pendingIdle',
|
||||
'dualIdle',
|
||||
'networkDown',
|
||||
'unpaired',
|
||||
'maintenance',
|
||||
'virgin',
|
||||
'wifiList'
|
||||
]
|
||||
return staticStates.includes(machineState)
|
||||
}
|
||||
|
||||
const getState = machineEventsLazy =>
|
||||
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
||||
.state
|
||||
|
||||
const Label = ({ children }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
return <div className={classes.label}>{children}</div>
|
||||
}
|
||||
|
||||
const MachineActions = memo(({ machine, onActionSuccess }) => {
|
||||
const [action, setAction] = useState({ command: null })
|
||||
const [errorMessage, setErrorMessage] = useState(null)
|
||||
const classes = useStyles()
|
||||
|
||||
const warningMessage = (
|
||||
<span className={classes.warning}>
|
||||
A user may be in the middle of a transaction and they could lose their
|
||||
funds if you continue.
|
||||
</span>
|
||||
)
|
||||
|
||||
const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery(
|
||||
MACHINE,
|
||||
{
|
||||
variables: {
|
||||
deviceId: machine.deviceId
|
||||
},
|
||||
onCompleted: machineEventsLazy => {
|
||||
const message = !isStaticState(getState(machineEventsLazy))
|
||||
? warningMessage
|
||||
: null
|
||||
setAction(action => ({ ...action, message }))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
||||
onError: ({ message }) => {
|
||||
const errorMessage = message ?? 'An error ocurred'
|
||||
setErrorMessage(errorMessage)
|
||||
},
|
||||
onCompleted: () => {
|
||||
onActionSuccess && onActionSuccess()
|
||||
setAction({ command: null })
|
||||
}
|
||||
})
|
||||
|
||||
const confirmDialogOpen = Boolean(action.command)
|
||||
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label>Actions</Label>
|
||||
<div className={classes.stack}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={EditIcon}
|
||||
InverseIcon={EditReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'rename',
|
||||
display: 'Rename',
|
||||
confirmationMessage: 'Write the new name for this machine'
|
||||
})
|
||||
}>
|
||||
Rename
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={UnpairIcon}
|
||||
InverseIcon={UnpairReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'unpair',
|
||||
display: 'Unpair'
|
||||
})
|
||||
}>
|
||||
Unpair
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'reboot',
|
||||
display: 'Reboot'
|
||||
})
|
||||
}>
|
||||
Reboot
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={ShutdownIcon}
|
||||
InverseIcon={ShutdownReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'shutdown',
|
||||
display: 'Shutdown',
|
||||
message:
|
||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||
})
|
||||
}>
|
||||
Shutdown
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
fetchMachineEvents()
|
||||
setAction({
|
||||
command: 'restartServices',
|
||||
display: 'Restart services for'
|
||||
})
|
||||
}}>
|
||||
Restart Services
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
disabled={disabled}
|
||||
open={confirmDialogOpen}
|
||||
title={`${action?.display} this machine?`}
|
||||
errorMessage={errorMessage}
|
||||
toBeConfirmed={machine.name}
|
||||
message={action?.message}
|
||||
confirmationMessage={action?.confirmationMessage}
|
||||
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
||||
onConfirmed={value => {
|
||||
setErrorMessage(null)
|
||||
machineAction({
|
||||
variables: {
|
||||
deviceId: machine.deviceId,
|
||||
action: `${action?.command}`,
|
||||
...(action?.command === 'rename' && { newName: value })
|
||||
}
|
||||
})
|
||||
}}
|
||||
onDissmised={() => {
|
||||
setAction({ command: null })
|
||||
setErrorMessage(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default MachineActions
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import { offColor, spacer, errorColor } from 'src/styling/variables'
|
||||
|
||||
const { label1 } = typographyStyles
|
||||
|
||||
const machineActionsStyles = {
|
||||
label: {
|
||||
extend: label1,
|
||||
color: offColor,
|
||||
marginBottom: 4
|
||||
},
|
||||
inlineChip: {
|
||||
marginInlineEnd: '0.25em'
|
||||
},
|
||||
stack: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'start'
|
||||
},
|
||||
mr: {
|
||||
marginRight: spacer,
|
||||
marginBottom: spacer
|
||||
},
|
||||
warning: {
|
||||
color: errorColor
|
||||
}
|
||||
}
|
||||
|
||||
export { machineActionsStyles }
|
||||
|
|
@ -49,7 +49,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
|||
const classes = useStyles()
|
||||
const [showMachines, setShowMachines] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const { data } = useQuery(GET_DATA)
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||
refetchQueries: () => ['getData'],
|
||||
onError: error => setError(error)
|
||||
|
|
@ -118,7 +118,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
|||
iconClassName={classes.listViewButton}
|
||||
/>
|
||||
|
||||
{!showMachines && (
|
||||
{!showMachines && !loading && (
|
||||
<CommissionsDetails
|
||||
config={config}
|
||||
locale={localeConfig}
|
||||
|
|
@ -130,7 +130,7 @@ const Commissions = ({ name: SCREEN_KEY }) => {
|
|||
classes={classes}
|
||||
/>
|
||||
)}
|
||||
{showMachines && (
|
||||
{showMachines && !loading && (
|
||||
<CommissionsList
|
||||
config={config}
|
||||
localeConfig={localeConfig}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const CommissionsList = memo(
|
|||
const [coinFilter, setCoinFilter] = useState(SHOW_ALL)
|
||||
const [orderProp, setOrderProp] = useState(ORDER_OPTIONS[0])
|
||||
|
||||
const coins = R.prop('cryptoCurrencies', localeConfig)
|
||||
const coins = R.prop('cryptoCurrencies', localeConfig) ?? []
|
||||
|
||||
const getMachineCoins = deviceId => {
|
||||
const override = R.prop('overrides', localeConfig)?.find(
|
||||
|
|
|
|||
|
|
@ -395,7 +395,7 @@ const createCommissions = (cryptoCode, deviceId, isDefault, config) => {
|
|||
}
|
||||
|
||||
const getCommissions = (cryptoCode, deviceId, config) => {
|
||||
const overrides = R.prop('overrides', config)
|
||||
const overrides = R.prop('overrides', config) ?? []
|
||||
|
||||
if (!overrides && R.isEmpty(overrides)) {
|
||||
return createCommissions(cryptoCode, deviceId, true, config)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ const SET_CUSTOMER = gql`
|
|||
lastTxFiat
|
||||
lastTxFiatCode
|
||||
lastTxClass
|
||||
subscriberInfo
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -202,6 +203,24 @@ const CustomerProfile = memo(() => {
|
|||
}>
|
||||
{`${blocked ? 'Authorize' : 'Block'} customer`}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={blocked ? AuthorizeIcon : BlockIcon}
|
||||
InverseIcon={
|
||||
blocked ? AuthorizeReversedIcon : BlockReversedIcon
|
||||
}
|
||||
onClick={() =>
|
||||
setCustomer({
|
||||
variables: {
|
||||
customerId,
|
||||
customerInput: {
|
||||
subscriberInfo: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}>
|
||||
{`Retrieve information`}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const IdDataCard = memo(({ customerData, updateCustomer }) => {
|
|||
},
|
||||
{
|
||||
header: 'Gender',
|
||||
display: R.path(['gender'])(idData),
|
||||
display: R.path(['gender'])(idData) ?? R.path(['sex'])(idData),
|
||||
size: 80
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -31,27 +31,29 @@ BigNumber.config({ ROUNDING_MODE: BigNumber.ROUND_HALF_UP })
|
|||
|
||||
const useStyles = makeStyles(styles)
|
||||
const Footer = () => {
|
||||
const { data, loading } = useQuery(GET_DATA)
|
||||
const { data } = useQuery(GET_DATA)
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [delayedExpand, setDelayedExpand] = useState(null)
|
||||
|
||||
const withCommissions = R.path(['cryptoRates', 'withCommissions'])(data) ?? {}
|
||||
const classes = useStyles({
|
||||
bigFooter: R.keys(data?.cryptoRates?.withCommissions).length > 8,
|
||||
bigFooter: R.keys(withCommissions).length > 8,
|
||||
expanded
|
||||
})
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
const canExpand = R.keys(withCommissions).length > 4
|
||||
|
||||
const canExpand = R.keys(data?.cryptoRates.withCommissions ?? []).length > 4
|
||||
|
||||
const wallets = fromNamespace('wallets')(data?.config)
|
||||
const wallets = fromNamespace('wallets')(config)
|
||||
const cryptoCurrencies = R.path(['cryptoCurrencies'])(data) ?? []
|
||||
const accountsConfig = R.path(['accountsConfig'])(data) ?? []
|
||||
const localeFiatCurrency = R.path(['locale_fiatCurrency'])(config) ?? ''
|
||||
|
||||
const renderFooterItem = key => {
|
||||
const idx = R.findIndex(R.propEq('code', key))(data.cryptoCurrencies)
|
||||
const idx = R.findIndex(R.propEq('code', key))(cryptoCurrencies)
|
||||
const tickerCode = wallets[`${key}_ticker`]
|
||||
const tickerIdx = R.findIndex(R.propEq('code', tickerCode))(
|
||||
data.accountsConfig
|
||||
)
|
||||
const tickerIdx = R.findIndex(R.propEq('code', tickerCode))(accountsConfig)
|
||||
|
||||
const tickerName = data.accountsConfig[tickerIdx].display
|
||||
const tickerName = tickerIdx > -1 ? accountsConfig[tickerIdx].display : ''
|
||||
|
||||
const cashInNoCommission = parseFloat(
|
||||
R.path(['cryptoRates', 'withoutCommissions', key, 'cashIn'])(data)
|
||||
|
|
@ -74,12 +76,10 @@ const Footer = () => {
|
|||
)
|
||||
).toFormat(2)
|
||||
|
||||
const localeFiatCurrency = data.config.locale_fiatCurrency
|
||||
|
||||
return (
|
||||
<Grid key={key} item xs={3}>
|
||||
<Label2 className={classes.label}>
|
||||
{data.cryptoCurrencies[idx].display}
|
||||
{cryptoCurrencies[idx].display}
|
||||
</Label2>
|
||||
<div className={classes.headerLabels}>
|
||||
<div className={classes.headerLabel}>
|
||||
|
|
@ -116,15 +116,11 @@ const Footer = () => {
|
|||
onMouseEnter={handleMouseEnter}
|
||||
/>
|
||||
<div className={classes.content}>
|
||||
{!loading && data && (
|
||||
<Grid container spacing={1}>
|
||||
<Grid container className={classes.footerContainer}>
|
||||
{R.keys(data.cryptoRates.withCommissions).map(key =>
|
||||
renderFooterItem(key)
|
||||
)}
|
||||
</Grid>
|
||||
<Grid container spacing={1}>
|
||||
<Grid container className={classes.footerContainer}>
|
||||
{R.keys(withCommissions).map(key => renderFooterItem(key))}
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</div>
|
||||
<div className={classes.footer} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Status } from 'src/components/Status'
|
|||
import { Label2, TL2 } from 'src/components/typography'
|
||||
// import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { ReactComponent as MachineLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||
|
||||
import styles from './MachinesTable.styles'
|
||||
|
||||
|
|
@ -99,10 +100,19 @@ const MachinesTable = ({ machines, numToRender }) => {
|
|||
return (
|
||||
<TableRow
|
||||
onClick={() => redirect(machine)}
|
||||
className={classnames(classes.row, classes.clickableRow)}
|
||||
className={classnames(classes.row)}
|
||||
key={machine.deviceId + idx}>
|
||||
<StyledCell align="left">
|
||||
<StyledCell
|
||||
align="left"
|
||||
className={classes.machineNameWrapper}>
|
||||
<TL2>{machine.name}</TL2>
|
||||
<MachineLinkIcon
|
||||
className={classnames(
|
||||
classes.machineRedirectIcon,
|
||||
classes.clickableRow
|
||||
)}
|
||||
onClick={() => redirect(machine)}
|
||||
/>
|
||||
</StyledCell>
|
||||
<StyledCell>
|
||||
<Status status={machine.statuses[0]} />
|
||||
|
|
|
|||
|
|
@ -87,6 +87,14 @@ const styles = {
|
|||
marginBottom: 0,
|
||||
padding: 0,
|
||||
textAlign: 'center'
|
||||
},
|
||||
machineNameWrapper: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
},
|
||||
machineRedirectIcon: {
|
||||
marginLeft: 10
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
name: 'cashbox',
|
||||
header: 'Cashbox',
|
||||
width: 240,
|
||||
stripe: true,
|
||||
stripe: false,
|
||||
view: value => (
|
||||
<CashIn currency={{ code: fiatCurrency }} notes={value} total={0} />
|
||||
),
|
||||
|
|
@ -90,7 +90,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
view: (value, { deviceId }) => (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.bottom}
|
||||
denomination={getCashoutSettings(deviceId)?.top}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
|
|
@ -109,7 +109,7 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
return (
|
||||
<CashOut
|
||||
className={classes.cashbox}
|
||||
denomination={getCashoutSettings(deviceId)?.top}
|
||||
denomination={getCashoutSettings(deviceId)?.bottom}
|
||||
currency={{ code: fiatCurrency }}
|
||||
notes={value}
|
||||
/>
|
||||
|
|
@ -145,7 +145,6 @@ const CashCassettes = ({ machine, config, refetchData }) => {
|
|||
disableRowEdit={isCashOutDisabled}
|
||||
name="cashboxes"
|
||||
elements={elements}
|
||||
enableEdit
|
||||
data={[machine] || []}
|
||||
save={onSave}
|
||||
validationSchema={ValidationSchema}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,15 @@
|
|||
import { useMutation } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||
import { Status } from 'src/components/Status'
|
||||
import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import MachineActions from 'src/components/machineActions/MachineActions'
|
||||
import { H3, Label3, P } from 'src/components/typography'
|
||||
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
||||
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
||||
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
||||
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
||||
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
||||
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
||||
|
||||
import styles from '../Machines.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const MACHINE_ACTION = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$newName: String
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
||||
deviceId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const makeLastPing = lastPing => {
|
||||
if (!lastPing) return null
|
||||
const now = moment()
|
||||
|
|
@ -51,25 +30,8 @@ const makeLastPing = lastPing => {
|
|||
}
|
||||
|
||||
const Overview = ({ data, onActionSuccess }) => {
|
||||
const [action, setAction] = useState('')
|
||||
const [confirmActionDialogOpen, setConfirmActionDialogOpen] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState(null)
|
||||
const classes = useStyles()
|
||||
|
||||
const [machineAction] = useMutation(MACHINE_ACTION, {
|
||||
onError: ({ message }) => {
|
||||
const errorMessage = message ?? 'An error ocurred'
|
||||
setErrorMessage(errorMessage)
|
||||
},
|
||||
onCompleted: () => {
|
||||
onActionSuccess && onActionSuccess()
|
||||
setConfirmActionDialogOpen(false)
|
||||
}
|
||||
})
|
||||
|
||||
const confirmActionDialog = action =>
|
||||
setAction(action) || setConfirmActionDialogOpen(true)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.row}>
|
||||
|
|
@ -101,78 +63,10 @@ const Overview = ({ data, onActionSuccess }) => {
|
|||
</div>
|
||||
</div>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.rowItem}>
|
||||
<Label3 className={classes.label3}>Latency</Label3>
|
||||
<P>
|
||||
{data.responseTime
|
||||
? new BigNumber(data.responseTime).toFixed(3).toString() + ' ms'
|
||||
: 'unavailable'}
|
||||
</P>
|
||||
</div>
|
||||
<MachineActions
|
||||
machine={data}
|
||||
onActionSuccess={onActionSuccess}></MachineActions>
|
||||
</div>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.rowItem}>
|
||||
<Label3 className={classes.label3}>Packet Loss</Label3>
|
||||
<P>
|
||||
{data.packetLoss
|
||||
? new BigNumber(data.packetLoss).toFixed(3).toString() + ' %'
|
||||
: 'unavailable'}
|
||||
</P>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.row}>
|
||||
<div className={classes.rowItem}>
|
||||
{' '}
|
||||
<Label3 className={classes.label3}>Actions</Label3>
|
||||
{data.name && (
|
||||
<div className={classes.actionButtonsContainer}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.actionButton}
|
||||
Icon={UnpairIcon}
|
||||
InverseIcon={UnpairReversedIcon}
|
||||
onClick={() => confirmActionDialog('Unpair')}>
|
||||
Unpair
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.actionButton}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
onClick={() => confirmActionDialog('Reboot')}>
|
||||
Reboot
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className={classes.actionButton}
|
||||
color="primary"
|
||||
Icon={ShutdownIcon}
|
||||
InverseIcon={ShutdownReversedIcon}
|
||||
onClick={() => confirmActionDialog('Shutdown')}>
|
||||
Shutdown
|
||||
</ActionButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={confirmActionDialogOpen}
|
||||
title={`${action} this machine?`}
|
||||
errorMessage={errorMessage}
|
||||
toBeConfirmed={data.name}
|
||||
onConfirmed={() => {
|
||||
setErrorMessage(null)
|
||||
machineAction({
|
||||
variables: {
|
||||
deviceId: data.deviceId,
|
||||
action: `${action}`.toLowerCase()
|
||||
}
|
||||
})
|
||||
}}
|
||||
onDissmised={() => {
|
||||
setConfirmActionDialogOpen(false)
|
||||
setErrorMessage(null)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ const DataTable = ({
|
|||
useEffect(() => setExpanded(initialExpanded), [initialExpanded])
|
||||
|
||||
const coreWidth = R.compose(R.sum, R.map(R.prop('width')))(elements)
|
||||
const expWidth = 1000 - coreWidth
|
||||
const expWidth = 850 - coreWidth
|
||||
const width = coreWidth + (expandable ? expWidth : 0)
|
||||
|
||||
const classes = useStyles({ width })
|
||||
|
|
@ -166,7 +166,7 @@ const DataTable = ({
|
|||
{() => (
|
||||
<List
|
||||
// this has to be in a style because of how the component works
|
||||
style={{ overflow: 'inherit', outline: 'none' }}
|
||||
style={{ overflowX: 'inherit', outline: 'none' }}
|
||||
{...props}
|
||||
height={data.length * 62 + extraHeight}
|
||||
width={width}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ const GET_TRANSACTIONS = gql`
|
|||
customerIdCardPhotoPath
|
||||
customerFrontCameraPath
|
||||
customerPhone
|
||||
discount
|
||||
customerId
|
||||
isAnonymous
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
@ -95,13 +98,13 @@ const Transactions = ({ id }) => {
|
|||
const elements = [
|
||||
{
|
||||
header: '',
|
||||
width: 62,
|
||||
width: 0,
|
||||
size: 'sm',
|
||||
view: it => (it.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />)
|
||||
},
|
||||
{
|
||||
header: 'Customer',
|
||||
width: 162,
|
||||
width: 122,
|
||||
size: 'sm',
|
||||
view: getCustomerDisplayName
|
||||
},
|
||||
|
|
@ -128,20 +131,20 @@ const Transactions = ({ id }) => {
|
|||
className: classes.overflowTd,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
width: 170
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
header: 'Date (UTC)',
|
||||
view: it => moment.utc(it.created).format('YYYY-MM-DD'),
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
width: 150
|
||||
width: 140
|
||||
},
|
||||
{
|
||||
header: 'Status',
|
||||
view: it => getStatus(it),
|
||||
size: 'sm',
|
||||
width: 80
|
||||
width: 20
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -162,8 +165,7 @@ const Transactions = ({ id }) => {
|
|||
loading={loading || id === null}
|
||||
emptyText="No transactions so far"
|
||||
elements={elements}
|
||||
// need to splice because back end query could return double NUM_LOG_RESULTS because it doesnt merge the txIn and the txOut results before applying the limit
|
||||
data={R.path(['transactions'])(txResponse)} // .splice(0,NUM_LOG_RESULTS)}
|
||||
data={R.path(['transactions'])(txResponse)}
|
||||
Details={DetailsRow}
|
||||
expandable
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import NavigateNextIcon from '@material-ui/icons/NavigateNext'
|
|||
import classnames from 'classnames'
|
||||
import gql from 'graphql-tag'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import React from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
|
||||
import { TL1, TL2, Label3 } from 'src/components/typography'
|
||||
|
|
@ -19,11 +19,9 @@ import Transactions from './MachineComponents/Transactions'
|
|||
import styles from './Machines.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const getMachineInfo = R.compose(R.find, R.propEq('name'))
|
||||
|
||||
const GET_INFO = gql`
|
||||
query getInfo {
|
||||
machines {
|
||||
query getMachine($deviceId: ID!) {
|
||||
machine(deviceId: $deviceId) {
|
||||
name
|
||||
deviceId
|
||||
paired
|
||||
|
|
@ -41,33 +39,32 @@ const GET_INFO = gql`
|
|||
downloadSpeed
|
||||
responseTime
|
||||
packetLoss
|
||||
latestEvent {
|
||||
note
|
||||
}
|
||||
}
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
const getMachines = R.path(['machines'])
|
||||
const getMachineID = path => path.slice(path.lastIndexOf('/') + 1)
|
||||
|
||||
const Machines = () => {
|
||||
const { data, refetch, loading } = useQuery(GET_INFO)
|
||||
const location = useLocation()
|
||||
const [selectedMachine, setSelectedMachine] = useState('')
|
||||
const { data, refetch } = useQuery(GET_INFO, {
|
||||
variables: {
|
||||
deviceId: getMachineID(location.pathname)
|
||||
}
|
||||
})
|
||||
const classes = useStyles()
|
||||
|
||||
const machines = getMachines(data) ?? []
|
||||
const machineInfo = getMachineInfo(selectedMachine)(machines) ?? {}
|
||||
const timezone = R.path(['config', 'locale_timezone'], data) ?? {}
|
||||
|
||||
// pre-selects first machine from the list, if there is a machine configured.
|
||||
useEffect(() => {
|
||||
if (!loading && data && data.machines) {
|
||||
if (location.state && location.state.selectedMachine) {
|
||||
setSelectedMachine(location.state.selectedMachine)
|
||||
} else {
|
||||
setSelectedMachine(R.path(['machines', 0, 'name'])(data) ?? '')
|
||||
}
|
||||
}
|
||||
}, [loading, data, location.state])
|
||||
const machine = R.path(['machine'])(data) ?? {}
|
||||
const config = R.path(['config'])(data) ?? {}
|
||||
|
||||
const machineName = R.path(['name'])(machine) ?? null
|
||||
const machineID = R.path(['deviceId'])(machine) ?? null
|
||||
|
||||
return (
|
||||
<Grid container className={classes.grid}>
|
||||
|
|
@ -81,10 +78,10 @@ const Machines = () => {
|
|||
</Label3>
|
||||
</Link>
|
||||
<TL2 noMargin className={classes.subtitle}>
|
||||
{selectedMachine}
|
||||
{machineName}
|
||||
</TL2>
|
||||
</Breadcrumbs>
|
||||
<Overview data={machineInfo} onActionSuccess={refetch} />
|
||||
<Overview data={machine} onActionSuccess={refetch} />
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
|
@ -102,26 +99,23 @@ const Machines = () => {
|
|||
<div
|
||||
className={classnames(classes.detailItem, classes.detailsMargin)}>
|
||||
<TL1 className={classes.subtitle}>{'Details'}</TL1>
|
||||
<Details data={machineInfo} timezone={timezone} />
|
||||
<Details data={machine} timezone={timezone} />
|
||||
</div>
|
||||
<div className={classes.detailItem}>
|
||||
<TL1 className={classes.subtitle}>{'Cash cassettes'}</TL1>
|
||||
<Cassettes
|
||||
refetchData={refetch}
|
||||
machine={machineInfo}
|
||||
config={data?.config ?? false}
|
||||
machine={machine}
|
||||
config={config ?? false}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.transactionsItem}>
|
||||
<TL1 className={classes.subtitle}>{'Latest transactions'}</TL1>
|
||||
<Transactions id={machineInfo?.deviceId ?? null} />
|
||||
<Transactions id={machineID} />
|
||||
</div>
|
||||
<div className={classes.detailItem}>
|
||||
<TL1 className={classes.subtitle}>{'Commissions'}</TL1>
|
||||
<Commissions
|
||||
name={'commissions'}
|
||||
id={machineInfo?.deviceId ?? null}
|
||||
/>
|
||||
<Commissions name={'commissions'} id={machineID} />
|
||||
</div>
|
||||
</div>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import {
|
||||
spacer,
|
||||
fontPrimary,
|
||||
primaryColor,
|
||||
white,
|
||||
comet
|
||||
} from 'src/styling/variables'
|
||||
import { spacer, comet } from 'src/styling/variables'
|
||||
|
||||
const styles = {
|
||||
grid: {
|
||||
|
|
@ -18,16 +12,6 @@ const styles = {
|
|||
marginLeft: spacer * 6,
|
||||
maxWidth: 900
|
||||
},
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
},
|
||||
modalTitle: {
|
||||
lineHeight: '120%',
|
||||
color: primaryColor,
|
||||
fontSize: 14,
|
||||
fontFamily: fontPrimary,
|
||||
fontWeight: 900
|
||||
},
|
||||
subtitle: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
|
|
@ -39,15 +23,6 @@ const styles = {
|
|||
color: comet,
|
||||
marginTop: 0
|
||||
},
|
||||
white: {
|
||||
color: white
|
||||
},
|
||||
deleteButton: {
|
||||
paddingLeft: 13
|
||||
},
|
||||
addressRow: {
|
||||
marginLeft: 8
|
||||
},
|
||||
row: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
|
|
@ -60,16 +35,10 @@ const styles = {
|
|||
detailItem: {
|
||||
marginBottom: spacer * 4
|
||||
},
|
||||
transactionsItem: {
|
||||
marginBottom: -spacer * 4
|
||||
},
|
||||
actionButtonsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
actionButton: {
|
||||
marginRight: 8
|
||||
},
|
||||
breadcrumbsContainer: {
|
||||
marginTop: 32
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,10 +25,15 @@ const CashCassettesFooter = ({
|
|||
const classes = useStyles()
|
||||
const cashout = config && fromNamespace('cashOut')(config)
|
||||
const getCashoutSettings = id => fromNamespace(id)(cashout)
|
||||
const reducerFn = (acc, { cassette1, cassette2, id }) => [
|
||||
(acc[0] += cassette1 * getCashoutSettings(id).top),
|
||||
(acc[1] += cassette2 * getCashoutSettings(id).bottom)
|
||||
]
|
||||
const reducerFn = (acc, { cassette1, cassette2, id }) => {
|
||||
const topDenomination = getCashoutSettings(id).top ?? 0
|
||||
const bottomDenomination = getCashoutSettings(id).bottom ?? 0
|
||||
return [
|
||||
(acc[0] += cassette1 * topDenomination),
|
||||
(acc[1] += cassette2 * bottomDenomination)
|
||||
]
|
||||
}
|
||||
|
||||
const totalInCassettes = R.sum(R.reduce(reducerFn, [0, 0], machines))
|
||||
|
||||
/* const totalInCashBox = R.sum(
|
||||
|
|
|
|||
|
|
@ -1,48 +1,16 @@
|
|||
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||
import { Grid /*, Divider */ } from '@material-ui/core'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import gql from 'graphql-tag'
|
||||
import React, { useState } from 'react'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import React from 'react'
|
||||
|
||||
import { ConfirmDialog } from 'src/components/ConfirmDialog'
|
||||
// import { Status } from 'src/components/Status'
|
||||
import ActionButton from 'src/components/buttons/ActionButton'
|
||||
import { ReactComponent as EditReversedIcon } from 'src/styling/icons/button/edit/white.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/button/edit/zodiac.svg'
|
||||
// import { ReactComponent as LinkIcon } from 'src/styling/icons/button/link/zodiac.svg'
|
||||
import { ReactComponent as RebootReversedIcon } from 'src/styling/icons/button/reboot/white.svg'
|
||||
import { ReactComponent as RebootIcon } from 'src/styling/icons/button/reboot/zodiac.svg'
|
||||
import { ReactComponent as ShutdownReversedIcon } from 'src/styling/icons/button/shut down/white.svg'
|
||||
import { ReactComponent as ShutdownIcon } from 'src/styling/icons/button/shut down/zodiac.svg'
|
||||
import { ReactComponent as UnpairReversedIcon } from 'src/styling/icons/button/unpair/white.svg'
|
||||
import { ReactComponent as UnpairIcon } from 'src/styling/icons/button/unpair/zodiac.svg'
|
||||
import MachineActions from 'src/components/machineActions/MachineActions'
|
||||
import { modelPrettifier } from 'src/utils/machine'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
import { labelStyles, machineDetailsStyles } from './MachineDetailsCard.styles'
|
||||
|
||||
const MACHINE_ACTION = gql`
|
||||
mutation MachineAction(
|
||||
$deviceId: ID!
|
||||
$action: MachineAction!
|
||||
$newName: String
|
||||
) {
|
||||
machineAction(deviceId: $deviceId, action: $action, newName: $newName) {
|
||||
deviceId
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MACHINE = gql`
|
||||
query getMachine($deviceId: ID!) {
|
||||
machine(deviceId: $deviceId) {
|
||||
latestEvent {
|
||||
note
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// const supportArtices = [
|
||||
// {
|
||||
// // Default article for non-maped statuses
|
||||
|
|
@ -54,24 +22,6 @@ const MACHINE = gql`
|
|||
// // TODO add Stuck and Fully Functional statuses articles for the new-admins
|
||||
// ]
|
||||
|
||||
const isStaticState = machineState => {
|
||||
if (!machineState) {
|
||||
return true
|
||||
}
|
||||
const staticStates = [
|
||||
'chooseCoin',
|
||||
'idle',
|
||||
'pendingIdle',
|
||||
'dualIdle',
|
||||
'networkDown',
|
||||
'unpaired',
|
||||
'maintenance',
|
||||
'virgin',
|
||||
'wifiList'
|
||||
]
|
||||
return staticStates.includes(machineState)
|
||||
}
|
||||
|
||||
// const article = ({ code: status }) =>
|
||||
// supportArtices.find(({ code: article }) => article === status)
|
||||
|
||||
|
|
@ -97,51 +47,9 @@ const Item = ({ children, ...props }) => (
|
|||
</Grid>
|
||||
)
|
||||
|
||||
const getState = machineEventsLazy =>
|
||||
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
||||
.state
|
||||
|
||||
const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
||||
const [action, setAction] = useState({ command: null })
|
||||
const [errorMessage, setErrorMessage] = useState(null)
|
||||
const classes = useMDStyles()
|
||||
|
||||
const warningMessage = (
|
||||
<span className={classes.warning}>
|
||||
A user may be in the middle of a transaction and they could lose their
|
||||
funds if you continue.
|
||||
</span>
|
||||
)
|
||||
|
||||
const [fetchMachineEvents, { loading: loadingEvents }] = useLazyQuery(
|
||||
MACHINE,
|
||||
{
|
||||
variables: {
|
||||
deviceId: machine.deviceId
|
||||
},
|
||||
onCompleted: machineEventsLazy => {
|
||||
const message = !isStaticState(getState(machineEventsLazy))
|
||||
? warningMessage
|
||||
: null
|
||||
setAction(action => ({ ...action, message }))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
||||
onError: ({ message }) => {
|
||||
const errorMessage = message ?? 'An error ocurred'
|
||||
setErrorMessage(errorMessage)
|
||||
},
|
||||
onCompleted: () => {
|
||||
onActionSuccess && onActionSuccess()
|
||||
setAction({ command: null })
|
||||
}
|
||||
})
|
||||
|
||||
const confirmDialogOpen = Boolean(action.command)
|
||||
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
||||
|
||||
return (
|
||||
<Container className={classes.wrapper}>
|
||||
{/* <Item xs={5}>
|
||||
|
|
@ -181,30 +89,6 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
|||
flexItem
|
||||
className={classes.separator}
|
||||
/> */}
|
||||
<ConfirmDialog
|
||||
disabled={disabled}
|
||||
open={confirmDialogOpen}
|
||||
title={`${action?.display} this machine?`}
|
||||
errorMessage={errorMessage}
|
||||
toBeConfirmed={machine.name}
|
||||
message={action?.message}
|
||||
confirmationMessage={action?.confirmationMessage}
|
||||
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
||||
onConfirmed={value => {
|
||||
setErrorMessage(null)
|
||||
machineAction({
|
||||
variables: {
|
||||
deviceId: machine.deviceId,
|
||||
action: `${action?.command}`,
|
||||
...(action?.command === 'rename' && { newName: value })
|
||||
}
|
||||
})
|
||||
}}
|
||||
onDissmised={() => {
|
||||
setAction({ command: null })
|
||||
setErrorMessage(null)
|
||||
}}
|
||||
/>
|
||||
<Item xs>
|
||||
<Container className={classes.row}>
|
||||
<Item xs={2}>
|
||||
|
|
@ -219,166 +103,38 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess, timezone }) => {
|
|||
</span>
|
||||
</Item>
|
||||
<Item xs={6}>
|
||||
<Label>Actions</Label>
|
||||
<div className={classes.stack}>
|
||||
<ActionButton
|
||||
className={classes.mr}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
Icon={EditIcon}
|
||||
InverseIcon={EditReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'rename',
|
||||
display: 'Rename',
|
||||
confirmationMessage: 'Write the new name for this machine'
|
||||
})
|
||||
}>
|
||||
Rename
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={UnpairIcon}
|
||||
InverseIcon={UnpairReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'unpair',
|
||||
display: 'Unpair'
|
||||
})
|
||||
}>
|
||||
Unpair
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'reboot',
|
||||
display: 'Reboot'
|
||||
})
|
||||
}>
|
||||
Reboot
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className={classes.mr}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
Icon={ShutdownIcon}
|
||||
InverseIcon={ShutdownReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'shutdown',
|
||||
display: 'Shutdown',
|
||||
message:
|
||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||
})
|
||||
}>
|
||||
Shutdown
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
fetchMachineEvents()
|
||||
setAction({
|
||||
command: 'restartServices',
|
||||
display: 'Restart services for'
|
||||
})
|
||||
}}>
|
||||
Restart Services
|
||||
</ActionButton>
|
||||
</div>
|
||||
<MachineActions
|
||||
machine={machine}
|
||||
onActionSuccess={onActionSuccess}></MachineActions>
|
||||
</Item>
|
||||
<Item xs={2}>
|
||||
<Label>Network speed</Label>
|
||||
<span>
|
||||
{machine.downloadSpeed
|
||||
? new BigNumber(machine.downloadSpeed).toFixed(4).toString() +
|
||||
' MB/s'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</Item>
|
||||
<Item xs={2}>
|
||||
<Label>Latency</Label>
|
||||
<span>
|
||||
{machine.responseTime
|
||||
? new BigNumber(machine.responseTime).toFixed(3).toString() +
|
||||
' ms'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</Item>
|
||||
<Item xs={2}>
|
||||
<Label>Packet Loss</Label>
|
||||
<span>
|
||||
{machine.packetLoss
|
||||
? new BigNumber(machine.packetLoss).toFixed(3).toString() +
|
||||
' %'
|
||||
: 'unavailable'}
|
||||
</span>
|
||||
</Item>
|
||||
</Container>
|
||||
{/* <Container>
|
||||
<Item>
|
||||
<Label>Actions</Label>
|
||||
<div className={classes.stack}>
|
||||
<ActionButton
|
||||
className={classes.mr}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
Icon={EditIcon}
|
||||
InverseIcon={EditReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'rename',
|
||||
display: 'Rename',
|
||||
confirmationMessage: 'Write the new name for this machine'
|
||||
})
|
||||
}>
|
||||
Rename
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={UnpairIcon}
|
||||
InverseIcon={UnpairReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'unpair',
|
||||
display: 'Unpair'
|
||||
})
|
||||
}>
|
||||
Unpair
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.mr}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'reboot',
|
||||
display: 'Reboot'
|
||||
})
|
||||
}>
|
||||
Reboot
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
className={classes.mr}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
Icon={ShutdownIcon}
|
||||
InverseIcon={ShutdownReversedIcon}
|
||||
onClick={() =>
|
||||
setAction({
|
||||
command: 'shutdown',
|
||||
display: 'Shutdown',
|
||||
message:
|
||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||
})
|
||||
}>
|
||||
Shutdown
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
className={classes.inlineChip}
|
||||
Icon={RebootIcon}
|
||||
InverseIcon={RebootReversedIcon}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
fetchMachineEvents()
|
||||
setAction({
|
||||
command: 'restartServices',
|
||||
display: 'Restart services for'
|
||||
})
|
||||
}}>
|
||||
Restart Services
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Item>
|
||||
</Container> */}
|
||||
</Item>
|
||||
</Container>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,29 +4,10 @@ import {
|
|||
detailsRowStyles,
|
||||
labelStyles
|
||||
} from 'src/pages/Transactions/Transactions.styles'
|
||||
import {
|
||||
spacer,
|
||||
comet,
|
||||
primaryColor,
|
||||
fontSize4,
|
||||
errorColor
|
||||
} from 'src/styling/variables'
|
||||
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
|
||||
|
||||
const machineDetailsStyles = {
|
||||
...detailsRowStyles,
|
||||
colDivider: {
|
||||
width: 1,
|
||||
margin: [[spacer * 2, spacer * 4]],
|
||||
backgroundColor: comet,
|
||||
border: 'none'
|
||||
},
|
||||
inlineChip: {
|
||||
marginInlineEnd: '0.25em'
|
||||
},
|
||||
stack: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
},
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
// marginTop: 24,
|
||||
|
|
@ -53,12 +34,6 @@ const machineDetailsStyles = {
|
|||
color: primaryColor,
|
||||
textDecoration: 'none'
|
||||
},
|
||||
divider: {
|
||||
margin: '0 1rem'
|
||||
},
|
||||
mr: {
|
||||
marginRight: spacer
|
||||
},
|
||||
separator: {
|
||||
width: 1,
|
||||
height: 170,
|
||||
|
|
@ -66,9 +41,6 @@ const machineDetailsStyles = {
|
|||
marginRight: 60,
|
||||
marginLeft: 'auto',
|
||||
background: fade(comet, 0.5)
|
||||
},
|
||||
warning: {
|
||||
color: errorColor
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useQuery } from '@apollo/react-hooks'
|
||||
import { makeStyles } from '@material-ui/core'
|
||||
import BigNumber from 'bignumber.js'
|
||||
import gql from 'graphql-tag'
|
||||
import moment from 'moment'
|
||||
import * as R from 'ramda'
|
||||
|
|
@ -62,7 +61,7 @@ const MachineStatus = () => {
|
|||
const elements = [
|
||||
{
|
||||
header: 'Machine Name',
|
||||
width: 150,
|
||||
width: 250,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => (
|
||||
|
|
@ -80,48 +79,18 @@ const MachineStatus = () => {
|
|||
},
|
||||
{
|
||||
header: 'Status',
|
||||
width: 150,
|
||||
width: 350,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => <MainStatus statuses={m.statuses} />
|
||||
},
|
||||
{
|
||||
header: 'Last ping',
|
||||
width: 175,
|
||||
width: 200,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m => (m.lastPing ? moment(m.lastPing).fromNow() : 'unknown')
|
||||
},
|
||||
{
|
||||
header: 'Network speed',
|
||||
width: 150,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m =>
|
||||
m.downloadSpeed
|
||||
? new BigNumber(m.downloadSpeed).toFixed(4).toString() + ' MB/s'
|
||||
: 'unavailable'
|
||||
},
|
||||
{
|
||||
header: 'Latency',
|
||||
width: 150,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m =>
|
||||
m.responseTime
|
||||
? new BigNumber(m.responseTime).toFixed(3).toString() + ' ms'
|
||||
: 'unavailable'
|
||||
},
|
||||
{
|
||||
header: 'Packet Loss',
|
||||
width: 125,
|
||||
size: 'sm',
|
||||
textAlign: 'left',
|
||||
view: m =>
|
||||
m.packetLoss
|
||||
? new BigNumber(m.packetLoss).toFixed(3).toString() + ' %'
|
||||
: 'unavailable'
|
||||
},
|
||||
{
|
||||
header: 'Software Version',
|
||||
width: 200,
|
||||
|
|
|
|||
|
|
@ -285,7 +285,7 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
) : (
|
||||
errorElements
|
||||
)}
|
||||
{tx.txClass === 'cashOut' && getStatus(tx) !== 'Cancelled' && (
|
||||
{tx.txClass === 'cashOut' && getStatus(tx) === 'Pending' && (
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={CancelIcon}
|
||||
|
|
@ -338,4 +338,8 @@ const DetailsRow = ({ it: tx, timezone }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export default memo(DetailsRow)
|
||||
export default memo(
|
||||
DetailsRow,
|
||||
(prev, next) =>
|
||||
prev.it.id === next.it.id && prev.it.hasError === next.it.hasError
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,12 +33,14 @@ const GET_DATA = gql`
|
|||
|
||||
const GET_TRANSACTIONS_CSV = gql`
|
||||
query transactions(
|
||||
$simplified: Boolean
|
||||
$limit: Int
|
||||
$from: Date
|
||||
$until: Date
|
||||
$timezone: String
|
||||
) {
|
||||
transactionsCsv(
|
||||
simplified: $simplified
|
||||
limit: $limit
|
||||
from: $from
|
||||
until: $until
|
||||
|
|
@ -194,13 +196,13 @@ const Transactions = () => {
|
|||
},
|
||||
{
|
||||
header: 'Crypto',
|
||||
width: 144,
|
||||
width: 150,
|
||||
textAlign: 'right',
|
||||
size: 'sm',
|
||||
view: it =>
|
||||
`${coinUtils
|
||||
.toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode)
|
||||
.toFormat(5)} ${it.cryptoCode}`
|
||||
`${coinUtils.toUnit(new BigNumber(it.cryptoAtoms), it.cryptoCode)} ${
|
||||
it.cryptoCode
|
||||
}`
|
||||
},
|
||||
{
|
||||
header: 'Address',
|
||||
|
|
@ -277,9 +279,8 @@ const Transactions = () => {
|
|||
title="Download logs"
|
||||
name="transactions"
|
||||
query={GET_TRANSACTIONS_CSV}
|
||||
args={{ timezone }}
|
||||
getLogs={logs => R.path(['transactionsCsv'])(logs)}
|
||||
timezone={timezone}
|
||||
simplified
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Stepper from 'src/components/Stepper'
|
|||
import { Button } from 'src/components/buttons'
|
||||
import { H5, Info3 } from 'src/components/typography'
|
||||
import { comet } from 'src/styling/variables'
|
||||
import { singularOrPlural } from 'src/utils/string'
|
||||
|
||||
import { type, requirements } from './helper'
|
||||
|
||||
|
|
@ -105,21 +106,29 @@ const getTypeText = (config, currency, classes) => {
|
|||
<>
|
||||
makes {orUnderline(config.threshold.threshold, classes)} {currency}{' '}
|
||||
worth of transactions within{' '}
|
||||
{orUnderline(config.threshold.thresholdDays, classes)} days
|
||||
{orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||
</>
|
||||
)
|
||||
case 'txVelocity':
|
||||
return (
|
||||
<>
|
||||
makes {orUnderline(config.threshold.threshold, classes)} transactions
|
||||
in {orUnderline(config.threshold.thresholdDays, classes)} days
|
||||
makes {orUnderline(config.threshold.threshold, classes)}{' '}
|
||||
{singularOrPlural(
|
||||
config.threshold.threshold,
|
||||
'transaction',
|
||||
'transactions'
|
||||
)}{' '}
|
||||
in {orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||
</>
|
||||
)
|
||||
case 'consecutiveDays':
|
||||
return (
|
||||
<>
|
||||
at least one transaction every day for{' '}
|
||||
{orUnderline(config.threshold.thresholdDays, classes)} days
|
||||
{orUnderline(config.threshold.thresholdDays, classes)}{' '}
|
||||
{singularOrPlural(config.threshold.thresholdDays, 'day', 'days')}
|
||||
</>
|
||||
)
|
||||
default:
|
||||
|
|
@ -147,7 +156,8 @@ const getRequirementText = (config, classes) => {
|
|||
return (
|
||||
<>
|
||||
suspended for{' '}
|
||||
{orUnderline(config.requirement.suspensionDays, classes)} days
|
||||
{orUnderline(config.requirement.suspensionDays, classes)}{' '}
|
||||
{singularOrPlural(config.requirement.suspensionDays, 'day', 'days')}
|
||||
</>
|
||||
)
|
||||
case 'block':
|
||||
|
|
@ -212,6 +222,41 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const createErrorMessage = (errors, touched, values) => {
|
||||
const triggerType = values?.triggerType
|
||||
const containsType = R.contains(triggerType)
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
|
||||
const hasRequirementError =
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.suspensionDays &&
|
||||
(!values.requirement?.suspensionDays ||
|
||||
values.requirement?.suspensionDays < 0)
|
||||
|
||||
const hasAmountError =
|
||||
!!errors.threshold &&
|
||||
!!touched.threshold?.threshold &&
|
||||
!containsType(['consecutiveDays']) &&
|
||||
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||
|
||||
const hasDaysError =
|
||||
!!errors.threshold &&
|
||||
!!touched.threshold?.thresholdDays &&
|
||||
!containsType(['txAmount']) &&
|
||||
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||
|
||||
if (containsType(['txAmount', 'txVolume', 'txVelocity']) && hasAmountError)
|
||||
return errors.threshold
|
||||
|
||||
if (
|
||||
containsType(['txVolume', 'txVelocity', 'consecutiveDays']) &&
|
||||
hasDaysError
|
||||
)
|
||||
return errors.threshold
|
||||
|
||||
if (isSuspend && hasRequirementError) return errors.requirement
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
|
|
@ -236,21 +281,28 @@ const Wizard = ({ onClose, save, error, currency }) => {
|
|||
/>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
validateOnChange={true}
|
||||
enableReinitialize
|
||||
onSubmit={onContinue}
|
||||
initialValues={stepOptions.initialValues}
|
||||
validationSchema={stepOptions.schema}>
|
||||
<Form className={classes.form}>
|
||||
<GetValues setValues={setLiveValues} />
|
||||
<stepOptions.Component {...stepOptions.props} />
|
||||
<div className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
<Button className={classes.button} type="submit">
|
||||
{isLastStep ? 'Finish' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{({ errors, touched, values }) => (
|
||||
<Form className={classes.form}>
|
||||
<GetValues setValues={setLiveValues} />
|
||||
<stepOptions.Component {...stepOptions.props} />
|
||||
<div className={classes.submit}>
|
||||
{error && <ErrorMessage>Failed to save</ErrorMessage>}
|
||||
{createErrorMessage(errors, touched, values) && (
|
||||
<ErrorMessage>
|
||||
{createErrorMessage(errors, touched, values)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button className={classes.button} type="submit">
|
||||
{isLastStep ? 'Finish' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { fromNamespace, toNamespace, namespaces } from 'src/utils/config'
|
|||
|
||||
import {
|
||||
defaultSchema,
|
||||
overridesSchema,
|
||||
getOverridesSchema,
|
||||
defaults,
|
||||
overridesDefaults,
|
||||
getDefaultSettings,
|
||||
|
|
@ -95,7 +95,7 @@ const AdvancedTriggersSettings = memo(() => {
|
|||
enableCreate
|
||||
initialValues={overridesDefaults}
|
||||
save={saveOverrides}
|
||||
validationSchema={overridesSchema}
|
||||
validationSchema={getOverridesSchema(requirementsOverrides)}
|
||||
data={requirementsOverrides}
|
||||
elements={getOverrides()}
|
||||
setEditing={onEditingOverrides}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,23 @@
|
|||
import * as R from 'ramda'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import Autocomplete from 'src/components/inputs/formik/Autocomplete.js'
|
||||
import { getView, requirementOptions } from 'src/pages/Triggers/helper'
|
||||
import { getView } from 'src/pages/Triggers/helper'
|
||||
|
||||
const advancedRequirementOptions = [
|
||||
{ display: 'Sanctions', code: 'sanctions' },
|
||||
{ display: 'ID card image', code: 'idCardPhoto' },
|
||||
{ display: 'ID data', code: 'idCardData' },
|
||||
{ display: 'Customer camera', code: 'facephoto' },
|
||||
{ display: 'US SSN', code: 'usSsn' }
|
||||
]
|
||||
|
||||
const displayRequirement = code => {
|
||||
return R.prop(
|
||||
'display',
|
||||
R.find(R.propEq('code', code))(advancedRequirementOptions)
|
||||
)
|
||||
}
|
||||
|
||||
const defaultSchema = Yup.object().shape({
|
||||
expirationTime: Yup.string()
|
||||
|
|
@ -13,18 +29,33 @@ const defaultSchema = Yup.object().shape({
|
|||
.required()
|
||||
})
|
||||
|
||||
const overridesSchema = Yup.object().shape({
|
||||
id: Yup.string()
|
||||
.label('Requirement')
|
||||
.required(),
|
||||
expirationTime: Yup.string()
|
||||
.label('Expiration time')
|
||||
.required(),
|
||||
automation: Yup.string()
|
||||
.label('Automation')
|
||||
.matches(/(Manual|Automatic)/)
|
||||
.required()
|
||||
})
|
||||
const getOverridesSchema = values => {
|
||||
return Yup.object().shape({
|
||||
id: Yup.string()
|
||||
.label('Requirement')
|
||||
.required()
|
||||
.test({
|
||||
test() {
|
||||
const { requirement } = this.parent
|
||||
if (R.find(R.propEq('requirement', requirement))(values)) {
|
||||
return this.createError({
|
||||
message: `Requirement ${displayRequirement(
|
||||
requirement
|
||||
)} already overriden`
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
}),
|
||||
expirationTime: Yup.string()
|
||||
.label('Expiration time')
|
||||
.required(),
|
||||
automation: Yup.string()
|
||||
.label('Automation')
|
||||
.matches(/(Manual|Automatic)/)
|
||||
.required()
|
||||
})
|
||||
}
|
||||
|
||||
const getDefaultSettings = () => {
|
||||
return [
|
||||
|
|
@ -60,10 +91,10 @@ const getOverrides = () => {
|
|||
header: 'Requirement',
|
||||
width: 196,
|
||||
size: 'sm',
|
||||
view: getView(requirementOptions, 'display'),
|
||||
view: getView(advancedRequirementOptions, 'display'),
|
||||
input: Autocomplete,
|
||||
inputProps: {
|
||||
options: requirementOptions,
|
||||
options: advancedRequirementOptions,
|
||||
labelProp: 'display',
|
||||
valueProp: 'code'
|
||||
}
|
||||
|
|
@ -108,7 +139,7 @@ const overridesDefaults = {
|
|||
|
||||
export {
|
||||
defaultSchema,
|
||||
overridesSchema,
|
||||
getOverridesSchema,
|
||||
defaults,
|
||||
overridesDefaults,
|
||||
getDefaultSettings,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import * as R from 'ramda'
|
|||
import React, { memo } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import {
|
||||
NumberInput,
|
||||
TextInput,
|
||||
RadioGroup
|
||||
} from 'src/components/inputs/formik'
|
||||
import { NumberInput, RadioGroup } from 'src/components/inputs/formik'
|
||||
import { H4, Label2, Label1, Info1, Info2 } from 'src/components/typography'
|
||||
import { errorColor } from 'src/styling/variables'
|
||||
import { transformNumber } from 'src/utils/number'
|
||||
|
|
@ -100,16 +96,9 @@ const threshold = Yup.object().shape({
|
|||
|
||||
const requirement = Yup.object().shape({
|
||||
requirement: Yup.string().required(),
|
||||
suspensionDays: Yup.number().when('requirement', {
|
||||
is: 'suspend',
|
||||
then: Yup.number()
|
||||
.required()
|
||||
.min(1)
|
||||
.label('Invalid value'),
|
||||
otherwise: Yup.number()
|
||||
.nullable()
|
||||
.transform(() => null)
|
||||
})
|
||||
suspensionDays: Yup.number()
|
||||
.transform(transformNumber)
|
||||
.nullable()
|
||||
})
|
||||
|
||||
const Schema = Yup.object()
|
||||
|
|
@ -119,24 +108,56 @@ const Schema = Yup.object()
|
|||
threshold
|
||||
// direction
|
||||
})
|
||||
.test(
|
||||
'are-fields-set',
|
||||
'Invalid values',
|
||||
({ threshold, triggerType }, context) => {
|
||||
const validator = {
|
||||
txAmount: threshold => threshold.threshold >= 0,
|
||||
txVolume: threshold =>
|
||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||
txVelocity: threshold =>
|
||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||
}
|
||||
return (
|
||||
(triggerType && validator?.[triggerType](threshold)) ||
|
||||
context.createError({ path: 'threshold' })
|
||||
)
|
||||
.test(({ threshold, triggerType }, context) => {
|
||||
const errorMessages = {
|
||||
txAmount: threshold => 'Amount must be greater than or equal to 0',
|
||||
txVolume: threshold => {
|
||||
const thresholdMessage = 'Volume must be greater than or equal to 0'
|
||||
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||
const message = []
|
||||
if (threshold.threshold < 0) message.push(thresholdMessage)
|
||||
if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage)
|
||||
return message.join(', ')
|
||||
},
|
||||
txVelocity: threshold => {
|
||||
const thresholdMessage = 'Transactions must be greater than 0'
|
||||
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||
const message = []
|
||||
if (threshold.threshold <= 0) message.push(thresholdMessage)
|
||||
if (threshold.thresholdDays <= 0) message.push(thresholdDaysMessage)
|
||||
return message.join(', ')
|
||||
},
|
||||
consecutiveDays: threshold => 'Days must be greater than 0'
|
||||
}
|
||||
)
|
||||
const thresholdValidator = {
|
||||
txAmount: threshold => threshold.threshold >= 0,
|
||||
txVolume: threshold =>
|
||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||
txVelocity: threshold =>
|
||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||
}
|
||||
|
||||
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||
|
||||
return context.createError({
|
||||
path: 'threshold',
|
||||
message: errorMessages[triggerType](threshold)
|
||||
})
|
||||
})
|
||||
.test(({ requirement }, context) => {
|
||||
const requirementValidator = requirement =>
|
||||
requirement.requirement === 'suspend'
|
||||
? requirement.suspensionDays > 0
|
||||
: true
|
||||
|
||||
if (requirement && requirementValidator(requirement)) return
|
||||
|
||||
return context.createError({
|
||||
path: 'requirement',
|
||||
message: 'Suspension days must be greater than 0'
|
||||
})
|
||||
})
|
||||
|
||||
// Direction V2 only
|
||||
// const directionSchema = Yup.object().shape({ direction })
|
||||
|
|
@ -237,25 +258,47 @@ const typeSchema = Yup.object()
|
|||
.nullable()
|
||||
})
|
||||
})
|
||||
.test(
|
||||
'are-fields-set',
|
||||
'All fields must be set.',
|
||||
({ triggerType, threshold }, context) => {
|
||||
const validator = {
|
||||
txAmount: threshold => threshold.threshold >= 0,
|
||||
txVolume: threshold =>
|
||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||
txVelocity: threshold =>
|
||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||
}
|
||||
|
||||
return (
|
||||
(triggerType && validator?.[triggerType](threshold)) ||
|
||||
context.createError({ path: 'threshold' })
|
||||
)
|
||||
.test(({ threshold, triggerType }, context) => {
|
||||
const errorMessages = {
|
||||
txAmount: threshold => 'Amount must be greater than or equal to 0',
|
||||
txVolume: threshold => {
|
||||
const thresholdMessage = 'Volume must be greater than or equal to 0'
|
||||
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||
const message = []
|
||||
if (!threshold.threshold || threshold.threshold < 0)
|
||||
message.push(thresholdMessage)
|
||||
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||
message.push(thresholdDaysMessage)
|
||||
return message.join(', ')
|
||||
},
|
||||
txVelocity: threshold => {
|
||||
const thresholdMessage = 'Transactions must be greater than 0'
|
||||
const thresholdDaysMessage = 'Days must be greater than 0'
|
||||
const message = []
|
||||
if (!threshold.threshold || threshold.threshold <= 0)
|
||||
message.push(thresholdMessage)
|
||||
if (!threshold.thresholdDays || threshold.thresholdDays <= 0)
|
||||
message.push(thresholdDaysMessage)
|
||||
return message.join(', ')
|
||||
},
|
||||
consecutiveDays: threshold => 'Days must be greater than 0'
|
||||
}
|
||||
)
|
||||
const thresholdValidator = {
|
||||
txAmount: threshold => threshold.threshold >= 0,
|
||||
txVolume: threshold =>
|
||||
threshold.threshold >= 0 && threshold.thresholdDays > 0,
|
||||
txVelocity: threshold =>
|
||||
threshold.threshold > 0 && threshold.thresholdDays > 0,
|
||||
consecutiveDays: threshold => threshold.thresholdDays > 0
|
||||
}
|
||||
|
||||
if (triggerType && thresholdValidator[triggerType](threshold)) return
|
||||
|
||||
return context.createError({
|
||||
path: 'threshold',
|
||||
message: errorMessages[triggerType](threshold)
|
||||
})
|
||||
})
|
||||
|
||||
const typeOptions = [
|
||||
{ display: 'Transaction amount', code: 'txAmount' },
|
||||
|
|
@ -266,7 +309,13 @@ const typeOptions = [
|
|||
|
||||
const Type = ({ ...props }) => {
|
||||
const classes = useStyles()
|
||||
const { errors, touched, values } = useFormikContext()
|
||||
const {
|
||||
errors,
|
||||
touched,
|
||||
values,
|
||||
setTouched,
|
||||
handleChange
|
||||
} = useFormikContext()
|
||||
|
||||
const typeClass = {
|
||||
[classes.error]: errors.triggerType && touched.triggerType
|
||||
|
|
@ -278,11 +327,21 @@ const Type = ({ ...props }) => {
|
|||
const isThresholdDaysEnabled = containsType(['txVolume', 'txVelocity'])
|
||||
const isConsecutiveDaysEnabled = containsType(['consecutiveDays'])
|
||||
|
||||
const hasAmountError =
|
||||
!!errors.threshold &&
|
||||
!!touched.threshold?.threshold &&
|
||||
!isConsecutiveDaysEnabled &&
|
||||
(!values.threshold?.threshold || values.threshold?.threshold < 0)
|
||||
const hasDaysError =
|
||||
!!errors.threshold &&
|
||||
!!touched.threshold?.thresholdDays &&
|
||||
!containsType(['txAmount']) &&
|
||||
(!values.threshold?.thresholdDays || values.threshold?.thresholdDays < 0)
|
||||
|
||||
const triggerTypeError = !!(hasDaysError || hasAmountError)
|
||||
|
||||
const thresholdClass = {
|
||||
[classes.error]:
|
||||
errors.threshold &&
|
||||
((!containsType(['consecutiveDays']) && touched.threshold?.threshold) ||
|
||||
(!containsType(['txAmount']) && touched.threshold?.thresholdDays))
|
||||
[classes.error]: triggerTypeError
|
||||
}
|
||||
|
||||
const isRadioGroupActive = () => {
|
||||
|
|
@ -306,6 +365,13 @@ const Type = ({ ...props }) => {
|
|||
labelClassName={classes.radioLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classes.radioGroup}
|
||||
onChange={e => {
|
||||
handleChange(e)
|
||||
setTouched({
|
||||
threshold: false,
|
||||
thresholdDays: false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className={classes.thresholdWrapper}>
|
||||
|
|
@ -322,6 +388,7 @@ const Type = ({ ...props }) => {
|
|||
component={NumberInput}
|
||||
size="lg"
|
||||
name="threshold.threshold"
|
||||
error={hasAmountError}
|
||||
/>
|
||||
<Info1 className={classnames(classes.description)}>
|
||||
{props.currency}
|
||||
|
|
@ -335,6 +402,7 @@ const Type = ({ ...props }) => {
|
|||
component={NumberInput}
|
||||
size="lg"
|
||||
name="threshold.threshold"
|
||||
error={hasAmountError}
|
||||
/>
|
||||
<Info1 className={classnames(classes.description)}>
|
||||
transactions
|
||||
|
|
@ -356,6 +424,7 @@ const Type = ({ ...props }) => {
|
|||
component={NumberInput}
|
||||
size="lg"
|
||||
name="threshold.thresholdDays"
|
||||
error={hasDaysError}
|
||||
/>
|
||||
<Info1 className={classnames(classes.description)}>days</Info1>
|
||||
</>
|
||||
|
|
@ -367,6 +436,7 @@ const Type = ({ ...props }) => {
|
|||
component={NumberInput}
|
||||
size="lg"
|
||||
name="threshold.thresholdDays"
|
||||
error={hasDaysError}
|
||||
/>
|
||||
<Info1 className={classnames(classes.description)}>
|
||||
consecutive days
|
||||
|
|
@ -390,20 +460,34 @@ const type = currency => ({
|
|||
}
|
||||
})
|
||||
|
||||
const requirementSchema = Yup.object().shape({
|
||||
requirement: Yup.object({
|
||||
requirement: Yup.string().required(),
|
||||
suspensionDays: Yup.number().when('requirement', {
|
||||
is: value => value === 'suspend',
|
||||
then: Yup.number()
|
||||
.required()
|
||||
.min(1),
|
||||
otherwise: Yup.number()
|
||||
.nullable()
|
||||
.transform(() => null)
|
||||
const requirementSchema = Yup.object()
|
||||
.shape({
|
||||
requirement: Yup.object({
|
||||
requirement: Yup.string().required(),
|
||||
suspensionDays: Yup.number().when('requirement', {
|
||||
is: value => value === 'suspend',
|
||||
then: Yup.number()
|
||||
.nullable()
|
||||
.transform(transformNumber),
|
||||
otherwise: Yup.number()
|
||||
.nullable()
|
||||
.transform(() => null)
|
||||
})
|
||||
}).required()
|
||||
})
|
||||
.test(({ requirement }, context) => {
|
||||
const requirementValidator = requirement =>
|
||||
requirement.requirement === 'suspend'
|
||||
? requirement.suspensionDays > 0
|
||||
: true
|
||||
|
||||
if (requirement && requirementValidator(requirement)) return
|
||||
|
||||
return context.createError({
|
||||
path: 'requirement',
|
||||
message: 'Suspension days must be greater than 0'
|
||||
})
|
||||
}).required()
|
||||
})
|
||||
})
|
||||
|
||||
const requirementOptions = [
|
||||
{ display: 'SMS verification', code: 'sms' },
|
||||
|
|
@ -419,19 +503,27 @@ const requirementOptions = [
|
|||
|
||||
const Requirement = () => {
|
||||
const classes = useStyles()
|
||||
const { touched, errors, values } = useFormikContext()
|
||||
const {
|
||||
touched,
|
||||
errors,
|
||||
values,
|
||||
handleChange,
|
||||
setTouched
|
||||
} = useFormikContext()
|
||||
|
||||
const hasRequirementError =
|
||||
!!errors.requirement &&
|
||||
!!touched.requirement?.suspensionDays &&
|
||||
(!values.requirement?.suspensionDays ||
|
||||
values.requirement?.suspensionDays < 0)
|
||||
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
|
||||
const titleClass = {
|
||||
[classes.error]:
|
||||
!R.isEmpty(R.omit(['suspensionDays'], errors.requirement)) ||
|
||||
(errors.requirement &&
|
||||
touched.requirement &&
|
||||
errors.requirement.suspensionDays &&
|
||||
touched.requirement.suspensionDays)
|
||||
(!!errors.requirement && !isSuspend) || (isSuspend && hasRequirementError)
|
||||
}
|
||||
|
||||
const isSuspend = values?.requirement?.requirement === 'suspend'
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box display="flex" alignItems="center">
|
||||
|
|
@ -444,6 +536,12 @@ const Requirement = () => {
|
|||
labelClassName={classes.specialLabel}
|
||||
radioClassName={classes.radio}
|
||||
className={classnames(classes.radioGroup, classes.specialGrid)}
|
||||
onChange={e => {
|
||||
handleChange(e)
|
||||
setTouched({
|
||||
suspensionDays: false
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
{isSuspend && (
|
||||
|
|
@ -453,6 +551,7 @@ const Requirement = () => {
|
|||
label="Days"
|
||||
size="lg"
|
||||
name="requirement.suspensionDays"
|
||||
error={hasRequirementError}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -504,7 +603,7 @@ const RequirementInput = () => {
|
|||
bold
|
||||
className={classes.suspensionDays}
|
||||
name="requirement.suspensionDays"
|
||||
component={TextInput}
|
||||
component={NumberInput}
|
||||
textAlign="center"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -104,11 +104,12 @@ const getElements = (cryptoCurrencies, accounts, onChange, wizard = false) => {
|
|||
},
|
||||
{
|
||||
name: 'zeroConf',
|
||||
header: 'Confidence Checking',
|
||||
size: 'sm',
|
||||
stripe: true,
|
||||
view: getDisplayName('zeroConf'),
|
||||
input: Autocomplete,
|
||||
width: 190 - widthAdjust,
|
||||
width: 220 - widthAdjust,
|
||||
inputProps: {
|
||||
options: getOptions('zeroConf'),
|
||||
valueProp: 'code',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
||||
<path fill="#F7931A" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||
<path fill="#0AC18E" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -26,4 +26,7 @@ const startCase = R.compose(
|
|||
splitOnUpper
|
||||
)
|
||||
|
||||
export { startCase, onlyFirstToUpper, formatLong }
|
||||
const singularOrPlural = (amount, singularStr, pluralStr) =>
|
||||
parseInt(amount) === 1 ? singularStr : pluralStr
|
||||
|
||||
export { startCase, onlyFirstToUpper, formatLong, singularOrPlural }
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "lamassu-server",
|
||||
"version": "7.5.0-beta.3",
|
||||
"version": "7.5.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
@ -14372,7 +14372,7 @@
|
|||
"version": "git+https://github.com/lamassu/lamassu-coins.git#de843fb210ad8adfa29a0441796125fcb0ab3b67",
|
||||
"from": "git+https://github.com/lamassu/lamassu-coins.git",
|
||||
"requires": {
|
||||
"bech32": "^1.1.3",
|
||||
"bech32": "2.0.0",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"bitcoinjs-lib": "4.0.3",
|
||||
"bs58check": "^2.0.2",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "lamassu-server",
|
||||
"description": "bitcoin atm client server protocol module",
|
||||
"keywords": [],
|
||||
"version": "7.5.0-beta.3",
|
||||
"version": "7.5.3",
|
||||
"license": "Unlicense",
|
||||
"author": "Lamassu (https://lamassu.is)",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"files": {
|
||||
"main.js": "/static/js/main.b833e621.chunk.js",
|
||||
"main.js.map": "/static/js/main.b833e621.chunk.js.map",
|
||||
"main.js": "/static/js/main.fc66d358.chunk.js",
|
||||
"main.js.map": "/static/js/main.fc66d358.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.ee1cbb9c.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.ee1cbb9c.js.map",
|
||||
"static/js/2.346a7f7f.chunk.js": "/static/js/2.346a7f7f.chunk.js",
|
||||
"static/js/2.346a7f7f.chunk.js.map": "/static/js/2.346a7f7f.chunk.js.map",
|
||||
"static/js/2.ee7f7ea2.chunk.js": "/static/js/2.ee7f7ea2.chunk.js",
|
||||
"static/js/2.ee7f7ea2.chunk.js.map": "/static/js/2.ee7f7ea2.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"static/js/2.346a7f7f.chunk.js.LICENSE.txt": "/static/js/2.346a7f7f.chunk.js.LICENSE.txt",
|
||||
"static/js/2.ee7f7ea2.chunk.js.LICENSE.txt": "/static/js/2.ee7f7ea2.chunk.js.LICENSE.txt",
|
||||
"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",
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
"static/media/false.7f926859.svg": "/static/media/false.7f926859.svg",
|
||||
"static/media/full.67b8cd67.svg": "/static/media/full.67b8cd67.svg",
|
||||
"static/media/icon-bitcoin-colour.bd8da481.svg": "/static/media/icon-bitcoin-colour.bd8da481.svg",
|
||||
"static/media/icon-bitcoincash-colour.3b27f3ed.svg": "/static/media/icon-bitcoincash-colour.3b27f3ed.svg",
|
||||
"static/media/icon-bitcoincash-colour.ed917caa.svg": "/static/media/icon-bitcoincash-colour.ed917caa.svg",
|
||||
"static/media/icon-dash-colour.e01c021b.svg": "/static/media/icon-dash-colour.e01c021b.svg",
|
||||
"static/media/icon-ethereum-colour.761723a2.svg": "/static/media/icon-ethereum-colour.761723a2.svg",
|
||||
"static/media/icon-litecoin-colour.bd861b5e.svg": "/static/media/icon-litecoin-colour.bd861b5e.svg",
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
},
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.ee1cbb9c.js",
|
||||
"static/js/2.346a7f7f.chunk.js",
|
||||
"static/js/main.b833e621.chunk.js"
|
||||
"static/js/2.ee7f7ea2.chunk.js",
|
||||
"static/js/main.fc66d358.chunk.js"
|
||||
]
|
||||
}
|
||||
|
|
@ -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.346a7f7f.chunk.js"></script><script src="/static/js/main.b833e621.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.ee7f7ea2.chunk.js"></script><script src="/static/js/main.fc66d358.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
1
public/static/js/2.ee7f7ea2.chunk.js.map
Normal file
1
public/static/js/2.ee7f7ea2.chunk.js.map
Normal file
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
2
public/static/js/main.fc66d358.chunk.js
Normal file
2
public/static/js/main.fc66d358.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
public/static/js/main.fc66d358.chunk.js.map
Normal file
1
public/static/js/main.fc66d358.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="64" version="1.1">
|
||||
<path fill="#F7931A" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||
<path fill="#0AC18E" d="m0,0l29.7,0a39,39,0,0,0,0,64l-29.7,0zm52,0a32,32,0,0,0,0,64a32,32,0,0,0,0,-64m52,0l-29.7,0a39,39,0,0,1,0,64l29.7,0z"/>
|
||||
<path fill="#FFF" transform="rotate(-28 52 32)" d="m66.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Loading…
Add table
Add a link
Reference in a new issue