Feat: crypto balance notifications saving in DB
Chore: add new column "detail" to transactions table migration Feat: check if older notification is valid before sending new one Feat: error saving to database Fix: fix error when invalidating notification on clearCryptoBalanceNotifications Chre: code refactor in new-settings-loader for simplicity Chore: refactor code on notifier and merge similar functions
This commit is contained in:
parent
196a05549f
commit
3b3bdf839b
9 changed files with 224 additions and 34 deletions
|
|
@ -101,7 +101,7 @@ function renameMachine (rec) {
|
||||||
function resetCashOutBills (rec) {
|
function resetCashOutBills (rec) {
|
||||||
const sql = `
|
const sql = `
|
||||||
update devices set cassette1=$1, cassette2=$2 where device_id=$3;
|
update devices set cassette1=$1, cassette2=$2 where device_id=$3;
|
||||||
update notifications set read = 't' where device_id = $3 AND type = 'fiatBalance' AND read = 'f';
|
update notifications set read = 't', valid = 'f' where read = 'f' AND device_id = $3 AND type = 'fiatBalance';
|
||||||
`
|
`
|
||||||
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
|
return db.none(sql, [rec.cassettes[0], rec.cassettes[1], rec.deviceId])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const prettyMs = require('pretty-ms')
|
const utils = require("./utils")
|
||||||
|
|
||||||
const email = require('../email')
|
const email = require('../email')
|
||||||
|
|
||||||
|
|
@ -66,28 +66,24 @@ function emailAlerts(alerts) {
|
||||||
return alerts.map(emailAlert).join('\n') + '\n'
|
return alerts.map(emailAlert).join('\n') + '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(num, code) {
|
|
||||||
return numeral(num).format('0,0.00') + ' ' + code
|
|
||||||
}
|
|
||||||
|
|
||||||
function emailAlert(alert) {
|
function emailAlert(alert) {
|
||||||
switch (alert.code) {
|
switch (alert.code) {
|
||||||
case PING:
|
case PING:
|
||||||
if (alert.age) {
|
if (alert.age) {
|
||||||
const pingAge = prettyMs(alert.age, { compact: true, verbose: true })
|
const pingAge = utils.formatAge(alert.age, { compact: true, verbose: true })
|
||||||
return `Machine down for ${pingAge}`
|
return `Machine down for ${pingAge}`
|
||||||
}
|
}
|
||||||
return 'Machine down for a while.'
|
return 'Machine down for a while.'
|
||||||
case STALE: {
|
case STALE: {
|
||||||
const stuckAge = prettyMs(alert.age, { compact: true, verbose: true })
|
const stuckAge = utils.formatAge(alert.age, { compact: true, verbose: true })
|
||||||
return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
|
return `Machine is stuck on ${alert.state} screen for ${stuckAge}`
|
||||||
}
|
}
|
||||||
case LOW_CRYPTO_BALANCE: {
|
case LOW_CRYPTO_BALANCE: {
|
||||||
const balance = formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
|
const balance = utils.formatCurrency(alert.fiatBalance.balance, alert.fiatCode)
|
||||||
return `Low balance in ${alert.cryptoCode} [${balance}]`
|
return `Low balance in ${alert.cryptoCode} [${balance}]`
|
||||||
}
|
}
|
||||||
case HIGH_CRYPTO_BALANCE: {
|
case HIGH_CRYPTO_BALANCE: {
|
||||||
const highBalance = formatCurrency(
|
const highBalance = utils.formatCurrency(
|
||||||
alert.fiatBalance.balance,
|
alert.fiatBalance.balance,
|
||||||
alert.fiatCode
|
alert.fiatCode
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ const customers = require('../customers')
|
||||||
const utils = require('./utils')
|
const utils = require('./utils')
|
||||||
const emailFuncs = require('./email')
|
const emailFuncs = require('./email')
|
||||||
const smsFuncs = require('./sms')
|
const smsFuncs = require('./sms')
|
||||||
const { STALE, STALE_STATE } = require('./codes')
|
const { STALE, STALE_STATE, PING } = require('./codes')
|
||||||
|
|
||||||
function buildMessage(alerts, notifications) {
|
function buildMessage(alerts, notifications) {
|
||||||
const smsEnabled = utils.isActive(notifications.sms)
|
const smsEnabled = utils.isActive(notifications.sms)
|
||||||
|
|
@ -43,6 +43,7 @@ function checkNotification(plugins) {
|
||||||
|
|
||||||
return getAlerts(plugins)
|
return getAlerts(plugins)
|
||||||
.then(alerts => {
|
.then(alerts => {
|
||||||
|
errorAlertsNotify(alerts)
|
||||||
const currentAlertFingerprint = utils.buildAlertFingerprint(
|
const currentAlertFingerprint = utils.buildAlertFingerprint(
|
||||||
alerts,
|
alerts,
|
||||||
notifications
|
notifications
|
||||||
|
|
@ -75,9 +76,10 @@ function getAlerts(plugins) {
|
||||||
plugins.checkBalances(),
|
plugins.checkBalances(),
|
||||||
queries.machineEvents(),
|
queries.machineEvents(),
|
||||||
plugins.getMachineNames()
|
plugins.getMachineNames()
|
||||||
]).then(([balances, events, devices]) =>
|
]).then(([balances, events, devices]) => {
|
||||||
buildAlerts(checkPings(devices), balances, events, devices)
|
balancesNotify(balances)
|
||||||
)
|
return buildAlerts(checkPings(devices), balances, events, devices)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAlerts(pings, balances, events, devices) {
|
function buildAlerts(pings, balances, events, devices) {
|
||||||
|
|
@ -220,7 +222,7 @@ const cashCassettesNotify = (cassettes, deviceId) => {
|
||||||
|
|
||||||
if(cashOutEnabled) {
|
if(cashOutEnabled) {
|
||||||
// we only want to add this notification if there isn't one already set and unread in the database
|
// we only want to add this notification if there isn't one already set and unread in the database
|
||||||
Promise.all([queries.getUnreadCassetteNotifications(1), queries.getUnreadCassetteNotifications(2)]).then(res => {
|
Promise.all([queries.getUnreadCassetteNotifications(1, deviceId), queries.getUnreadCassetteNotifications(2, deviceId)]).then(res => {
|
||||||
if(res[0].length === 0 && cassette1Count < cassette1Threshold) {
|
if(res[0].length === 0 && cassette1Count < cassette1Threshold) {
|
||||||
console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold )
|
console.log("Adding fiatBalance alert for cashbox 1 in database - count & threshold: ", cassette1Count, cassette1Threshold )
|
||||||
queries.addCashCassetteWarning(1, deviceId)
|
queries.addCashCassetteWarning(1, deviceId)
|
||||||
|
|
@ -234,6 +236,133 @@ const cashCassettesNotify = (cassettes, deviceId) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Notes for new "valid" column on notifications table:
|
||||||
|
|
||||||
|
- We only want to add a new notification if it is present in the high or low warning consts.
|
||||||
|
- Before we add the notification we need to see if there is no "valid" notification in the database.
|
||||||
|
- Since the poller runs every few seconds, if the user marks it as read, this code would add a new notification
|
||||||
|
immediately. This new column helps us decide if a new notification should be added.
|
||||||
|
- "Valid" is defaulted to "true". When the cryptobalance goes over the low threshold or under the high threshold,
|
||||||
|
the notification will be marked as invalid. This will allow a new one to be sent. If the cryptobalance never goes
|
||||||
|
into the middle of the high and low thresholds, the old, "read" notification will still be relevant so we won't add
|
||||||
|
a new one.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const clearOldCryptoNotifications = (balances) => {
|
||||||
|
// get valid crypto notifications from DB
|
||||||
|
// if that notification doesn't exist in balances, then make it invalid on the DB
|
||||||
|
queries.getAllValidNotifications('cryptoBalance').then(res => {
|
||||||
|
const notifications = _.map(it => {
|
||||||
|
return {
|
||||||
|
cryptoCode: it.detail.split('_')[0],
|
||||||
|
code: it.detail.split('_').splice(1).join('_')
|
||||||
|
}
|
||||||
|
}, res)
|
||||||
|
_.forEach(notification => {
|
||||||
|
const idx = _.findIndex(balance => {
|
||||||
|
return balance.code === notification.code && balance.cryptoCode === notification.cryptoCode
|
||||||
|
}, balances)
|
||||||
|
|
||||||
|
if(idx !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if the notification doesn't exist in the new balances object, then it is outdated and is not valid anymore
|
||||||
|
queries.invalidateNotification(notification.id)
|
||||||
|
}, notifications)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const balancesNotify = (balances) => {
|
||||||
|
const highFilter = o => o.code === 'HIGH_CRYPTO_BALANCE'
|
||||||
|
const lowFilter = o => o.code === 'LOW_CRYPTO_BALANCE'
|
||||||
|
const highWarnings = _.filter(highFilter, balances)
|
||||||
|
const lowWarnings = _.filter(lowFilter, balances)
|
||||||
|
|
||||||
|
clearOldCryptoNotifications(balances)
|
||||||
|
|
||||||
|
highWarnings.forEach(warning => {
|
||||||
|
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
|
||||||
|
if (res.length > 0) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
console.log("Adding high balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
|
||||||
|
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
|
||||||
|
queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `High balance in ${warning.cryptoCode} [${balance}]`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
lowWarnings.forEach(warning => {
|
||||||
|
queries.getValidNotifications('cryptoBalance', `${warning.cryptoCode}_${warning.code}`).then(res => {
|
||||||
|
if (res.length > 0) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
console.log("Adding low balance alert for " + warning.cryptoCode + " - " + warning.fiatBalance.balance)
|
||||||
|
const balance = utils.formatCurrency(warning.fiatBalance.balance, warning.fiatCode)
|
||||||
|
queries.addCryptoBalanceWarning(`${warning.cryptoCode}_${warning.code}`, `Low balance in ${warning.cryptoCode} [${balance}]`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearOldErrorNotifications = (alerts) => {
|
||||||
|
queries.getAllValidNotifications('error').then(res => {
|
||||||
|
_.forEach(notification => {
|
||||||
|
const idx = _.findIndex(alert => {
|
||||||
|
return alert.code === notification.detail.split('_')[0] && alert.deviceId === notification.device_id
|
||||||
|
}, alerts)
|
||||||
|
if(idx !== -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// if the notification doesn't exist, then it is outdated and is not valid anymore
|
||||||
|
queries.invalidateNotification(notification.id)
|
||||||
|
}, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorAlertsNotify = (alertRec) => {
|
||||||
|
let alerts = []
|
||||||
|
_.keys(alertRec.devices).forEach(function (device) {
|
||||||
|
// embed device ID in each alert object inside the deviceAlerts array
|
||||||
|
alertRec.devices[device].deviceAlerts = _.map(alert => {
|
||||||
|
return {...alert, deviceId: device}
|
||||||
|
}, alertRec.devices[device].deviceAlerts)
|
||||||
|
// concat every array into one
|
||||||
|
alerts = _.concat(alerts, alertRec.devices[device].deviceAlerts)
|
||||||
|
})
|
||||||
|
|
||||||
|
// now that we have all the alerts, we want to add PING and STALE alerts to the DB
|
||||||
|
// if there is a valid alert on the DB that doesn't exist on the new alerts array,
|
||||||
|
// that alert should be considered invalid
|
||||||
|
// after that, for the alerts array, we have to see if there is a valid alert of
|
||||||
|
// the sorts already on the DB
|
||||||
|
clearOldErrorNotifications(alerts)
|
||||||
|
|
||||||
|
_.forEach(alert => {
|
||||||
|
switch(alert.code) {
|
||||||
|
case PING:
|
||||||
|
return queries.getValidNotifications('error', PING, alert.deviceId).then(res => {
|
||||||
|
if(res.length > 0) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
console.log("Adding PING alert on database for " + alert.machineName)
|
||||||
|
const message = `Machine down`
|
||||||
|
queries.addErrorNotification(`${PING}_${alert.age ? alert.age : '-1'}`, message, alert.deviceId)
|
||||||
|
})
|
||||||
|
case STALE:
|
||||||
|
return queries.getValidNotifications('error', STALE, alert.deviceId).then(res => {
|
||||||
|
if(res.length > 0) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
console.log("Adding STALE alert on database for " + alert.machineName)
|
||||||
|
const message = `Machine is stuck on ${alert.state} screen`
|
||||||
|
queries.addErrorNotification(STALE, message, alert.deviceId)
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, alerts)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
transactionNotify,
|
transactionNotify,
|
||||||
checkNotification,
|
checkNotification,
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,50 @@ const addCashCassetteWarning = (cassetteNumber, deviceId) => {
|
||||||
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
|
return db.oneOrNone(sql, [uuidv4(), 'fiatBalance', cassetteNumber, deviceId, message])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUnreadCassetteNotifications = (cassetteNumber) => {
|
const getUnreadCassetteNotifications = (cassetteNumber, deviceId) => {
|
||||||
const sql = `SELECT * FROM notifications WHERE read = 'f' AND TYPE = 'fiatBalance' AND detail = '$1'`
|
const sql = `SELECT * FROM notifications WHERE read = 'f' AND device_id = $1 AND TYPE = 'fiatBalance' AND detail = '$2'`
|
||||||
return db.any(sql, [cassetteNumber])
|
return db.any(sql, [deviceId, cassetteNumber])
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { machineEvents: dbm.machineEvents, addHighValueTx, addCashCassetteWarning, getUnreadCassetteNotifications }
|
const addCryptoBalanceWarning = (detail, message) => {
|
||||||
|
const sql = `INSERT INTO notifications (id, type, detail, message, created) values ($1, $2, $3, $4, CURRENT_TIMESTAMP)`
|
||||||
|
return db.oneOrNone(sql, [uuidv4(), 'cryptoBalance', detail, message])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAllValidNotifications = (type) => {
|
||||||
|
const sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't'`
|
||||||
|
return db.any(sql, [type])
|
||||||
|
}
|
||||||
|
|
||||||
|
const addErrorNotification = (detail, message, deviceId) => {
|
||||||
|
const sql = `INSERT INTO notifications (id, type, detail, device_id, message, created) values ($1, $2, $3, $4, $5, CURRENT_TIMESTAMP)`
|
||||||
|
return db.oneOrNone(sql, [uuidv4(), 'error', detail, deviceId, message])
|
||||||
|
}
|
||||||
|
|
||||||
|
const getValidNotifications = (type, detail, deviceId = null) => {
|
||||||
|
let sql;
|
||||||
|
if(!deviceId) {
|
||||||
|
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sql = `SELECT * FROM notifications WHERE type = $1 AND valid = 't' AND detail LIKE $2 AND device_id = $3`
|
||||||
|
}
|
||||||
|
return db.any(sql, [type, `%${detail}%`, deviceId])
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidateNotification = (id) => {
|
||||||
|
const sql = `UPDATE notifications SET valid = 'f', read = 't' WHERE id = $1`
|
||||||
|
return db.none(sql, [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
machineEvents: dbm.machineEvents,
|
||||||
|
addHighValueTx,
|
||||||
|
addCashCassetteWarning,
|
||||||
|
addCryptoBalanceWarning,
|
||||||
|
addErrorNotification,
|
||||||
|
getUnreadCassetteNotifications,
|
||||||
|
getAllValidNotifications,
|
||||||
|
getValidNotifications,
|
||||||
|
invalidateNotification,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,31 @@ function printSmsAlerts(alertRec, config) {
|
||||||
const code = entry[0]
|
const code = entry[0]
|
||||||
const machineNames = _.filter(
|
const machineNames = _.filter(
|
||||||
_.negate(_.isEmpty),
|
_.negate(_.isEmpty),
|
||||||
_.map('machineName', entry[1])
|
_.map('machineName', entry[1]),
|
||||||
|
)
|
||||||
|
|
||||||
|
const cryptoCodes = _.filter(
|
||||||
|
_.negate(_.isEmpty),
|
||||||
|
_.map('cryptoCode', entry[1]),
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
codeDisplay: utils.codeDisplay(code),
|
codeDisplay: utils.codeDisplay(code),
|
||||||
machineNames
|
machineNames,
|
||||||
|
cryptoCodes
|
||||||
}
|
}
|
||||||
}, _.toPairs(alertsMap))
|
}, _.toPairs(alertsMap))
|
||||||
|
|
||||||
const mapByCodeDisplay = _.map(it =>
|
const mapByCodeDisplay = _.map(it => {
|
||||||
_.isEmpty(it.machineNames)
|
if(_.isEmpty(it.machineNames) && _.isEmpty(it.cryptoCodes)) {
|
||||||
? it.codeDisplay
|
return it.codeDisplay
|
||||||
: `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
}
|
||||||
)
|
if(_.isEmpty(it.machineNames)) {
|
||||||
|
return `${it.codeDisplay} (${it.cryptoCodes.join(', ')})`
|
||||||
|
}
|
||||||
|
else return `${it.codeDisplay} (${it.machineNames.join(', ')})`
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const displayAlertTypes = _.compose(
|
const displayAlertTypes = _.compose(
|
||||||
_.uniq,
|
_.uniq,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
const crypto = require('crypto')
|
const crypto = require('crypto')
|
||||||
|
const numeral = require('numeral')
|
||||||
|
const prettyMs = require('pretty-ms')
|
||||||
|
|
||||||
const coinUtils = require('../coin-utils')
|
const coinUtils = require('../coin-utils')
|
||||||
const {
|
const {
|
||||||
|
|
@ -143,6 +145,14 @@ const buildTransactionMessage = (tx, rec, highValueTx, machineName, customer) =>
|
||||||
}, highValueTx]
|
}, highValueTx]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatCurrency(num, code) {
|
||||||
|
return numeral(num).format('0,0.00') + ' ' + code
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAge (age, settings) {
|
||||||
|
return prettyMs(age, settings)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
codeDisplay,
|
codeDisplay,
|
||||||
parseEventNote,
|
parseEventNote,
|
||||||
|
|
@ -155,5 +165,7 @@ module.exports = {
|
||||||
shouldNotAlert,
|
shouldNotAlert,
|
||||||
buildAlertFingerprint,
|
buildAlertFingerprint,
|
||||||
sendNoAlerts,
|
sendNoAlerts,
|
||||||
buildTransactionMessage
|
buildTransactionMessage,
|
||||||
|
formatCurrency,
|
||||||
|
formatAge
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
|
||||||
const plugins = require('./plugins')
|
const plugins = require('./plugins')
|
||||||
const notifier = require('./notifier')
|
const notifier = require('./notifier/index')
|
||||||
const T = require('./time')
|
const T = require('./time')
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
const cashOutTx = require('./cash-out/cash-out-tx')
|
const cashOutTx = require('./cash-out/cash-out-tx')
|
||||||
|
|
|
||||||
|
|
@ -22,10 +22,11 @@ exports.up = function (next) {
|
||||||
"id" uuid NOT NULL PRIMARY KEY,
|
"id" uuid NOT NULL PRIMARY KEY,
|
||||||
"type" notification_type NOT NULL,
|
"type" notification_type NOT NULL,
|
||||||
"detail" TEXT,
|
"detail" TEXT,
|
||||||
"device_id" TEXT NOT NULL,
|
"device_id" TEXT,
|
||||||
"message" TEXT NOT NULL,
|
"message" TEXT NOT NULL,
|
||||||
"created" time with time zone NOT NULL,
|
"created" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||||
"read" BOOLEAN NOT NULL DEFAULT 'false',
|
"read" BOOLEAN NOT NULL DEFAULT 'false',
|
||||||
|
"valid" BOOLEAN NOT NULL DEFAULT 'true',
|
||||||
CONSTRAINT fk_devices
|
CONSTRAINT fk_devices
|
||||||
FOREIGN KEY(device_id)
|
FOREIGN KEY(device_id)
|
||||||
REFERENCES devices(device_id)
|
REFERENCES devices(device_id)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { transformNumber } from 'src/utils/number'
|
||||||
|
|
||||||
import NotificationsCtx from '../NotificationsContext'
|
import NotificationsCtx from '../NotificationsContext'
|
||||||
|
|
||||||
const HIGH_BALANCE_KEY = 'highBalance'
|
const HIGH_BALANCE_KEY = 'cryptoHighBalance'
|
||||||
const LOW_BALANCE_KEY = 'lowBalance'
|
const LOW_BALANCE_KEY = 'cryptoLowBalance'
|
||||||
const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
|
const CRYPTOCURRENCY_KEY = 'cryptoCurrency'
|
||||||
const NAME = 'cryptoBalanceOverrides'
|
const NAME = 'cryptoBalanceOverrides'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue