commit
e381efed6d
36 changed files with 5881 additions and 216 deletions
|
|
@ -225,6 +225,7 @@
|
||||||
"it-CH",
|
"it-CH",
|
||||||
"it-IT",
|
"it-IT",
|
||||||
"ja-JP",
|
"ja-JP",
|
||||||
|
"ka-GE",
|
||||||
"ko-KR",
|
"ko-KR",
|
||||||
"ky-KG",
|
"ky-KG",
|
||||||
"lt-LT",
|
"lt-LT",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,38 @@
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
|
||||||
|
// Get all blacklist rows from the DB "blacklist" table
|
||||||
|
const getBlacklist = () => {
|
||||||
|
return db.any('select * from blacklist').then(res =>
|
||||||
|
res.map(item => ({
|
||||||
|
cryptoCode: item.crypto_code,
|
||||||
|
address: item.address,
|
||||||
|
createdByOperator: item.created_by_operator
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete row from blacklist table by crypto code and address
|
||||||
|
const deleteFromBlacklist = (cryptoCode, address) => {
|
||||||
|
return db.none(
|
||||||
|
'delete from blacklist where crypto_code = $1 and address = $2;',
|
||||||
|
[cryptoCode, address]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertIntoBlacklist = (cryptoCode, address) => {
|
||||||
|
return db
|
||||||
|
.any(
|
||||||
|
'insert into blacklist(crypto_code, address, created_by_operator) values($1, $2, $3);',
|
||||||
|
[cryptoCode, address, true]
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
return { cryptoCode, address }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function blocked(address, cryptoCode) {
|
function blocked(address, cryptoCode) {
|
||||||
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
|
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
|
||||||
return db.any(sql, [
|
return db.any(sql, [address, cryptoCode])
|
||||||
address,
|
|
||||||
cryptoCode
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToUsedAddresses(address, cryptoCode) {
|
function addToUsedAddresses(address, cryptoCode) {
|
||||||
|
|
@ -13,10 +40,13 @@ function addToUsedAddresses (address, cryptoCode) {
|
||||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||||
|
|
||||||
const sql = `insert into blacklist(crypto_code, address, created_by_operator) values ($1, $2, 'f')`
|
const sql = `insert into blacklist(crypto_code, address, created_by_operator) values ($1, $2, 'f')`
|
||||||
return db.oneOrNone(sql, [
|
return db.oneOrNone(sql, [cryptoCode, address])
|
||||||
cryptoCode,
|
|
||||||
address
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { blocked, addToUsedAddresses }
|
module.exports = {
|
||||||
|
blocked,
|
||||||
|
addToUsedAddresses,
|
||||||
|
getBlacklist,
|
||||||
|
deleteFromBlacklist,
|
||||||
|
insertIntoBlacklist
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,6 @@ connections=40
|
||||||
keypool=10000
|
keypool=10000
|
||||||
prune=4000
|
prune=4000
|
||||||
daemon=0
|
daemon=0
|
||||||
addresstype=p2sh-segwit`
|
addresstype=p2sh-segwit
|
||||||
|
walletrbf=1`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,24 +25,24 @@ const BINARIES = {
|
||||||
dir: 'bitcoin-0.20.1/bin'
|
dir: 'bitcoin-0.20.1/bin'
|
||||||
},
|
},
|
||||||
ETH: {
|
ETH: {
|
||||||
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz',
|
url: 'https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.24-cc05b050.tar.gz',
|
||||||
dir: 'geth-linux-amd64-1.9.23-8c2f2715'
|
dir: 'geth-linux-amd64-1.9.24-cc05b050'
|
||||||
},
|
},
|
||||||
ZEC: {
|
ZEC: {
|
||||||
url: 'https://download.z.cash/downloads/zcash-4.1.0-linux64-debian-stretch.tar.gz',
|
url: 'https://z.cash/downloads/zcash-4.1.1-linux64-debian-stretch.tar.gz',
|
||||||
dir: 'zcash-4.1.0/bin'
|
dir: 'zcash-4.1.1/bin'
|
||||||
},
|
},
|
||||||
DASH: {
|
DASH: {
|
||||||
url: 'https://github.com/dashpay/dash/releases/download/v0.16.0.1/dashcore-0.16.0.1-x86_64-linux-gnu.tar.gz',
|
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.0/bin'
|
dir: 'dashcore-0.16.1/bin'
|
||||||
},
|
},
|
||||||
LTC: {
|
LTC: {
|
||||||
url: 'https://download.litecoin.org/litecoin-0.18.1/linux/litecoin-0.18.1-x86_64-linux-gnu.tar.gz',
|
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'
|
dir: 'litecoin-0.18.1/bin'
|
||||||
},
|
},
|
||||||
BCH: {
|
BCH: {
|
||||||
url: 'https://github.com/bitcoin-cash-node/bitcoin-cash-node/releases/download/v22.1.0/bitcoin-cash-node-22.1.0-x86_64-linux-gnu.tar.gz',
|
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.1.0/bin',
|
dir: 'bitcoin-cash-node-22.2.0/bin',
|
||||||
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
files: [['bitcoind', 'bitcoincashd'], ['bitcoin-cli', 'bitcoincash-cli']]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ module.exports = { update }
|
||||||
function mapCoin (rates, deviceId, settings, cryptoCode) {
|
function mapCoin (rates, deviceId, settings, cryptoCode) {
|
||||||
const config = settings.config
|
const config = settings.config
|
||||||
const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null }
|
const buildedRates = plugins(settings, deviceId).buildRates(rates)[cryptoCode] || { cashIn: null, cashOut: null }
|
||||||
|
|
||||||
const commissions = configManager.getCommissions(cryptoCode, deviceId, config)
|
const commissions = configManager.getCommissions(cryptoCode, deviceId, config)
|
||||||
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
||||||
|
|
||||||
|
|
@ -64,28 +63,17 @@ function mapMachine (rates, settings, machineRow) {
|
||||||
|
|
||||||
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
const coinAtmRadar = configManager.getCoinAtmRadar(config)
|
||||||
const triggers = configManager.getTriggers(config)
|
const triggers = configManager.getTriggers(config)
|
||||||
const triggerCashLimit = complianceTriggers.getCashLimit(triggers)
|
|
||||||
const locale = configManager.getLocale(deviceId, config)
|
const locale = configManager.getLocale(deviceId, config)
|
||||||
const cashOutConfig = configManager.getCashOut(deviceId, config)
|
const cashOutConfig = configManager.getCashOut(deviceId, config)
|
||||||
|
const cashOutEnabled = cashOutConfig.active ? cashOutConfig.active : false
|
||||||
|
|
||||||
const lastOnline = machineRow.last_online.toISOString()
|
const lastOnline = machineRow.last_online.toISOString()
|
||||||
const status = machineRow.stale ? 'online' : 'offline'
|
const status = machineRow.stale ? 'online' : 'offline'
|
||||||
const showSupportedCryptocurrencies = coinAtmRadar.supportedCryptocurrencies
|
|
||||||
const showSupportedFiat = coinAtmRadar.supportedFiat
|
|
||||||
const showSupportedBuySellDirection = coinAtmRadar.supportedBuySellDirection
|
|
||||||
const showLimitsAndVerification = coinAtmRadar.limitsAndVerification
|
const showLimitsAndVerification = coinAtmRadar.limitsAndVerification
|
||||||
|
const cashLimit = showLimitsAndVerification ? ( complianceTriggers.getCashLimit(triggers)?.threshold || Infinity ) : null
|
||||||
const cashLimit = showLimitsAndVerification ? ( triggerCashLimit || Infinity ) : null
|
|
||||||
|
|
||||||
const cryptoCurrencies = locale.cryptoCurrencies
|
const cryptoCurrencies = locale.cryptoCurrencies
|
||||||
const cashInEnabled = showSupportedBuySellDirection ? true : null
|
|
||||||
const cashOutEnabled = showSupportedBuySellDirection ? cashOutConfig.active : null
|
|
||||||
const fiat = showSupportedFiat ? locale.fiatCurrency : null
|
|
||||||
const identification = mapIdentification(config)
|
const identification = mapIdentification(config)
|
||||||
const coins = showSupportedCryptocurrencies ?
|
const coins = _.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
|
||||||
_.map(_.partial(mapCoin, [rates, deviceId, settings]), cryptoCurrencies)
|
|
||||||
: null
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
machineId: deviceId,
|
machineId: deviceId,
|
||||||
address: {
|
address: {
|
||||||
|
|
@ -102,14 +90,14 @@ function mapMachine (rates, settings, machineRow) {
|
||||||
},
|
},
|
||||||
status,
|
status,
|
||||||
lastOnline,
|
lastOnline,
|
||||||
cashIn: cashInEnabled,
|
cashIn: true,
|
||||||
cashOut: cashOutEnabled,
|
cashOut: cashOutEnabled,
|
||||||
manufacturer: 'lamassu',
|
manufacturer: 'lamassu',
|
||||||
cashInTxLimit: cashLimit,
|
cashInTxLimit: cashLimit,
|
||||||
cashOutTxLimit: cashLimit,
|
cashOutTxLimit: cashLimit,
|
||||||
cashInDailyLimit: cashLimit,
|
cashInDailyLimit: cashLimit,
|
||||||
cashOutDailyLimit: cashLimit,
|
cashOutDailyLimit: cashLimit,
|
||||||
fiatCurrency: fiat,
|
fiatCurrency: locale.fiatCurrency,
|
||||||
identification,
|
identification,
|
||||||
coins
|
coins
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +108,6 @@ function getMachines (rates, settings) {
|
||||||
where display=TRUE and
|
where display=TRUE and
|
||||||
paired=TRUE
|
paired=TRUE
|
||||||
order by created`
|
order by created`
|
||||||
|
|
||||||
return db.any(sql, [STALE_INTERVAL])
|
return db.any(sql, [STALE_INTERVAL])
|
||||||
.then(_.map(_.partial(mapMachine, [rates, settings])))
|
.then(_.map(_.partial(mapMachine, [rates, settings])))
|
||||||
}
|
}
|
||||||
|
|
@ -140,9 +127,7 @@ function sendRadar (data) {
|
||||||
maxContentLength: MAX_CONTENT_LENGTH
|
maxContentLength: MAX_CONTENT_LENGTH
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('%j', data)
|
return axios.default(config)
|
||||||
|
|
||||||
return axios(config)
|
|
||||||
.then(r => console.log(r.status))
|
.then(r => console.log(r.status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
259
lib/coinatmradar/test/coinatmradar.test.js
Normal file
259
lib/coinatmradar/test/coinatmradar.test.js
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
const yup = require('yup')
|
||||||
|
const BigNumber = require('../../../lib/bn')
|
||||||
|
const car = require('../coinatmradar')
|
||||||
|
const db = require('../../db')
|
||||||
|
|
||||||
|
jest.mock('../../db')
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// https://stackoverflow.com/questions/58151010/difference-between-resetallmocks-resetmodules-resetmoduleregistry-restoreallm
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
config: {
|
||||||
|
wallets_BTC_coin: 'BTC',
|
||||||
|
wallets_BTC_wallet: 'mock-wallet',
|
||||||
|
wallets_BTC_ticker: 'kraken',
|
||||||
|
wallets_BTC_exchange: 'mock-exchange',
|
||||||
|
wallets_BTC_zeroConf: 'all-zero-conf',
|
||||||
|
locale_id: '1983951f-6c73-4308-ae6e-f6f56dfa5e11',
|
||||||
|
locale_country: 'US',
|
||||||
|
locale_fiatCurrency: 'USD',
|
||||||
|
locale_languages: ['en-US'],
|
||||||
|
locale_cryptoCurrencies: ['BTC', 'ETH', 'LTC', 'DASH', 'ZEC', 'BCH'],
|
||||||
|
commissions_minimumTx: 1,
|
||||||
|
commissions_fixedFee: 2,
|
||||||
|
commissions_cashOut: 11,
|
||||||
|
commissions_cashIn: 11,
|
||||||
|
commissions_id: '960bb192-db37-40eb-9b59-2c2c78620de6',
|
||||||
|
wallets_ETH_active: true,
|
||||||
|
wallets_ETH_ticker: 'bitstamp',
|
||||||
|
wallets_ETH_wallet: 'mock-wallet',
|
||||||
|
wallets_ETH_exchange: 'mock-exchange',
|
||||||
|
wallets_ETH_zeroConf: 'mock-zero-conf',
|
||||||
|
wallets_LTC_active: true,
|
||||||
|
wallets_LTC_ticker: 'kraken',
|
||||||
|
wallets_LTC_wallet: 'mock-wallet',
|
||||||
|
wallets_LTC_exchange: 'mock-exchange',
|
||||||
|
wallets_LTC_zeroConf: 'mock-zero-conf',
|
||||||
|
wallets_DASH_active: true,
|
||||||
|
wallets_DASH_ticker: 'coinbase',
|
||||||
|
wallets_DASH_wallet: 'mock-wallet',
|
||||||
|
wallets_DASH_exchange: 'mock-exchange',
|
||||||
|
wallets_DASH_zeroConf: 'mock-zero-conf',
|
||||||
|
wallets_ZEC_active: true,
|
||||||
|
wallets_ZEC_ticker: 'coinbase',
|
||||||
|
wallets_ZEC_wallet: 'mock-wallet',
|
||||||
|
wallets_ZEC_exchange: 'mock-exchange',
|
||||||
|
wallets_ZEC_zeroConf: 'mock-zero-conf',
|
||||||
|
wallets_BCH_active: true,
|
||||||
|
wallets_BCH_ticker: 'bitpay',
|
||||||
|
wallets_BCH_wallet: 'mock-wallet',
|
||||||
|
wallets_BCH_exchange: 'mock-exchange',
|
||||||
|
wallets_BCH_zeroConf: 'mock-zero-conf',
|
||||||
|
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_zeroConfLimit: 50,
|
||||||
|
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_bottom: 20,
|
||||||
|
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_top: 5,
|
||||||
|
cashOut_7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4_active: true,
|
||||||
|
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_zeroConfLimit: 200,
|
||||||
|
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_bottom: 20,
|
||||||
|
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_top: 5,
|
||||||
|
cashOut_f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05_active: true,
|
||||||
|
notifications_email_active: false,
|
||||||
|
notifications_sms_active: true,
|
||||||
|
notifications_email_errors: false,
|
||||||
|
notifications_sms_errors: true,
|
||||||
|
coinAtmRadar_active: true,
|
||||||
|
coinAtmRadar_commissions: true,
|
||||||
|
coinAtmRadar_limitsAndVerification: true,
|
||||||
|
triggers: [
|
||||||
|
{
|
||||||
|
requirement: 'suspend',
|
||||||
|
suspensionDays: 1,
|
||||||
|
threshold: 123,
|
||||||
|
id: '9c3b5af8-b1d1-4125-b169-0e913b33894c',
|
||||||
|
direction: 'both',
|
||||||
|
triggerType: 'txAmount'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requirement: 'sms',
|
||||||
|
threshold: 999,
|
||||||
|
thresholdDays: 1,
|
||||||
|
id: 'b0e1e6a8-be1b-4e43-ac5f-3e4951e86f8b',
|
||||||
|
direction: 'both',
|
||||||
|
triggerType: 'txVelocity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
requirement: 'sms',
|
||||||
|
threshold: 888,
|
||||||
|
thresholdDays: 1,
|
||||||
|
id: '6ac38fe6-172c-48a4-8a7f-605213cbd600',
|
||||||
|
direction: 'both',
|
||||||
|
triggerType: 'txVolume'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
notifications_sms_transactions: true,
|
||||||
|
notifications_highValueTransaction: 50
|
||||||
|
},
|
||||||
|
accounts: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const rates = [
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(19164.3),
|
||||||
|
bid: BigNumber(19164.2)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(594.54),
|
||||||
|
bid: BigNumber(594.09)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(84.38),
|
||||||
|
bid: BigNumber(84.37)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(102.8),
|
||||||
|
bid: BigNumber(101.64)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(74.91),
|
||||||
|
bid: BigNumber(74.12)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rates: {
|
||||||
|
ask: BigNumber(284.4),
|
||||||
|
bid: BigNumber(284.4)
|
||||||
|
},
|
||||||
|
timestamp: +new Date()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const dbResponse = [
|
||||||
|
{
|
||||||
|
device_id:
|
||||||
|
'mock7e531a2666987aa27b9917ca17df7998f72771c57fdb21c90bc033999edd17e4',
|
||||||
|
last_online: new Date('2020-11-16T13:11:03.169Z'),
|
||||||
|
stale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id:
|
||||||
|
'9871e58aa2643ff9445cbc299b50397430ada75157d6c29b4c93548fff0f48f7',
|
||||||
|
last_online: new Date('2020-11-16T16:21:35.948Z'),
|
||||||
|
stale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id:
|
||||||
|
'5ae0d02dedeb77b6521bd5eb7c9159bdc025873fa0bcb6f87aaddfbda0c50913',
|
||||||
|
last_online: new Date('2020-11-19T15:07:57.089Z'),
|
||||||
|
stale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id:
|
||||||
|
'f02af604ca9010bd9ae04c427a24da90130da10d355f0a9b235886a89008fc05',
|
||||||
|
last_online: new Date('2020-11-26T20:05:57.792Z'),
|
||||||
|
stale: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id:
|
||||||
|
'490ab16ee0c124512dc769be1f3e7ee3894ce1e5b4b8b975e134fb326e551e88',
|
||||||
|
last_online: new Date('2020-12-04T16:48:05.129Z'),
|
||||||
|
stale: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function validateData(data) {
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
operatorId: yup.string().required('operatorId not provided'),
|
||||||
|
operator: yup.object().shape({
|
||||||
|
name: yup.string().nullable(),
|
||||||
|
phone: yup.string().nullable(),
|
||||||
|
email: yup.string().email().nullable()
|
||||||
|
}),
|
||||||
|
timestamp: yup.string().required('timestamp not provided'),
|
||||||
|
machines: yup.array().of(
|
||||||
|
yup.object().shape({
|
||||||
|
machineId: yup.string().required('machineId not provided'),
|
||||||
|
address: yup.object().required('address object not provided').shape({
|
||||||
|
streetAddress: yup.string().nullable(),
|
||||||
|
city: yup.string().nullable(),
|
||||||
|
region: yup.string().nullable(),
|
||||||
|
postalCode: yup.string().nullable(),
|
||||||
|
country: yup.string().nullable()
|
||||||
|
}),
|
||||||
|
location: yup.object().required('location object not provided').shape({
|
||||||
|
name: yup.string().nullable(),
|
||||||
|
url: yup.string().nullable(),
|
||||||
|
phone: yup.string().nullable()
|
||||||
|
}),
|
||||||
|
status: yup
|
||||||
|
.string()
|
||||||
|
.required('status not provided')
|
||||||
|
.oneOf(['online', 'offline']),
|
||||||
|
lastOnline: yup
|
||||||
|
.string()
|
||||||
|
.required('date in isostring format not provided'),
|
||||||
|
cashIn: yup.boolean().required('cashIn boolean not defined'),
|
||||||
|
cashOut: yup.boolean().required('cashOut boolean not defined'),
|
||||||
|
manufacturer: yup.string().required('manufacturer not provided'),
|
||||||
|
cashInTxLimit: yup.number().nullable(),
|
||||||
|
cashOutTxLimit: yup.number().nullable(),
|
||||||
|
cashInDailyLimit: yup.number().nullable(),
|
||||||
|
cashOutDailyLimit: yup.number().nullable(),
|
||||||
|
fiatCurrency: yup.string().required('fiatCurrency not provided'),
|
||||||
|
identification: yup.object().shape({
|
||||||
|
isPhone: yup.boolean().required('isPhone boolean not defined'),
|
||||||
|
isPalmVein: yup.boolean().required('isPalmVein boolean not defined'),
|
||||||
|
isPhoto: yup.boolean().required('isPhoto boolean not defined'),
|
||||||
|
isIdDocScan: yup
|
||||||
|
.boolean()
|
||||||
|
.required('isIdDocScan boolean not defined'),
|
||||||
|
isFingerprint: yup
|
||||||
|
.boolean()
|
||||||
|
.required('isFingerprint boolean not defined')
|
||||||
|
}),
|
||||||
|
coins: yup.array().of(
|
||||||
|
yup.object().shape({
|
||||||
|
cryptoCode: yup.string().required('cryptoCode not provided'),
|
||||||
|
cashInFee: yup.number().nullable(),
|
||||||
|
cashOutFee: yup.number().nullable(),
|
||||||
|
cashInFixedFee: yup.number().nullable(),
|
||||||
|
cashInRate: yup.number().nullable(),
|
||||||
|
cashOutRate: yup.number().nullable()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
return schema.validate(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Verify axios request schema', async () => {
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
jest.spyOn(axios, 'default').mockImplementation(
|
||||||
|
jest.fn(req =>
|
||||||
|
validateData(req.data)
|
||||||
|
.then(() => ({ status: 'mock status 200' }))
|
||||||
|
.catch(e => fail(e))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.any.mockResolvedValue(dbResponse)
|
||||||
|
await car.update(rates, settings)
|
||||||
|
})
|
||||||
|
|
@ -15,9 +15,8 @@ function maxDaysThreshold (triggers) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCashLimit (triggers) {
|
function getCashLimit (triggers) {
|
||||||
const withFiat = _.filter(({ triggerType }) => _.includes(['txVolume', 'txAmount'])(triggerType))
|
const withFiat = _.filter(({ triggerType }) => _.includes(triggerType, ['txVolume', 'txAmount']))
|
||||||
const blocking = _.filter(({ requirement }) => _.includes(['block', 'suspend'])(requirement))
|
const blocking = _.filter(({ requirement }) => _.includes(requirement, ['block', 'suspend']))
|
||||||
|
|
||||||
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
|
return _.compose(_.minBy('threshold'), blocking, withFiat)(triggers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ function update (id, data, userToken, txId) {
|
||||||
*
|
*
|
||||||
* @returns {Promise} Newly updated Customer
|
* @returns {Promise} Newly updated Customer
|
||||||
*/
|
*/
|
||||||
async function updateCustomer (id, data) {
|
async function updateCustomer (id, data, userToken) {
|
||||||
const formattedData = _.pick(
|
const formattedData = _.pick(
|
||||||
[
|
[
|
||||||
'authorized_override',
|
'authorized_override',
|
||||||
|
|
@ -110,7 +110,10 @@ async function updateCustomer (id, data) {
|
||||||
],
|
],
|
||||||
_.mapKeys(_.snakeCase, data))
|
_.mapKeys(_.snakeCase, data))
|
||||||
|
|
||||||
const sql = Pgp.helpers.update(formattedData, _.keys(formattedData), 'customers') +
|
const enhancedUpdateData = enhanceAtFields(enhanceOverrideFields(formattedData, userToken))
|
||||||
|
const updateData = updateOverride(enhancedUpdateData)
|
||||||
|
|
||||||
|
const sql = Pgp.helpers.update(updateData, _.keys(updateData), 'customers') +
|
||||||
' where id=$1'
|
' where id=$1'
|
||||||
|
|
||||||
await db.none(sql, [id])
|
await db.none(sql, [id])
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const dbm = require('./postgresql_interface')
|
||||||
const configManager = require('./new-config-manager')
|
const configManager = require('./new-config-manager')
|
||||||
const settingsLoader = require('./new-settings-loader')
|
const settingsLoader = require('./new-settings-loader')
|
||||||
|
|
||||||
module.exports = {getMachineName, getMachines, getMachineNames, setMachine}
|
module.exports = {getMachineName, getMachines, getMachine, getMachineNames, setMachine}
|
||||||
|
|
||||||
function getMachines () {
|
function getMachines () {
|
||||||
return db.any('select * from devices where display=TRUE order by created')
|
return db.any('select * from devices where display=TRUE order by created')
|
||||||
|
|
@ -88,6 +88,11 @@ function getMachineName (machineId) {
|
||||||
.then(it => it.name)
|
.then(it => it.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getMachine (machineId) {
|
||||||
|
const sql = 'select * from devices where device_id=$1'
|
||||||
|
return db.oneOrNone(sql, [machineId]).then(res => _.mapKeys(_.camelCase)(res))
|
||||||
|
}
|
||||||
|
|
||||||
function renameMachine (rec) {
|
function renameMachine (rec) {
|
||||||
const sql = 'update devices set name=$1 where device_id=$2'
|
const sql = 'update devices set name=$1 where device_id=$2'
|
||||||
return db.none(sql, [rec.newName, rec.deviceId])
|
return db.none(sql, [rec.newName, rec.deviceId])
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ const apolloServer = new ApolloServer({
|
||||||
|
|
||||||
const success = await login.authenticate(token)
|
const success = await login.authenticate(token)
|
||||||
if (!success) throw new AuthenticationError('Authentication failed')
|
if (!success) throw new AuthenticationError('Authentication failed')
|
||||||
|
return { req: { ...req } }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -68,10 +69,12 @@ app.use('/front-camera-photo', serveStatic(frontCameraBasedir, { index: false })
|
||||||
|
|
||||||
app.get('/api/register', (req, res, next) => {
|
app.get('/api/register', (req, res, next) => {
|
||||||
const otp = req.query.otp
|
const otp = req.query.otp
|
||||||
|
const ua = req.headers['user-agent']
|
||||||
|
const ip = req.ip
|
||||||
|
|
||||||
if (!otp) return next()
|
if (!otp) return next()
|
||||||
|
|
||||||
return login.register(otp)
|
return login.register(otp, ua, ip)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
|
if (r.expired) return res.status(401).send('OTP expired, generate new registration link')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@ const { parseAsync } = require('json2csv')
|
||||||
const { GraphQLDateTime } = require('graphql-iso-date')
|
const { GraphQLDateTime } = require('graphql-iso-date')
|
||||||
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
const { GraphQLJSON, GraphQLJSONObject } = require('graphql-type-json')
|
||||||
const got = require('got')
|
const got = require('got')
|
||||||
|
const DataLoader = require('dataloader')
|
||||||
|
|
||||||
const machineLoader = require('../../machine-loader')
|
const machineLoader = require('../../machine-loader')
|
||||||
const customers = require('../../customers')
|
const customers = require('../../customers')
|
||||||
const { machineAction } = require('../machines')
|
const { machineAction } = require('../machines')
|
||||||
const logs = require('../../logs')
|
const logs = require('../../logs')
|
||||||
const settingsLoader = require('../../new-settings-loader')
|
const settingsLoader = require('../../new-settings-loader')
|
||||||
|
const tokenManager = require('../../token-manager')
|
||||||
|
const blacklist = require('../../blacklist')
|
||||||
|
const machineEventsByIdBatch = require("../../postgresql_interface").machineEventsByIdBatch
|
||||||
|
|
||||||
const serverVersion = require('../../../package.json').version
|
const serverVersion = require('../../../package.json').version
|
||||||
|
|
||||||
|
|
@ -17,7 +21,13 @@ const funding = require('../funding')
|
||||||
const supervisor = require('../supervisor')
|
const supervisor = require('../supervisor')
|
||||||
const serverLogs = require('../server-logs')
|
const serverLogs = require('../server-logs')
|
||||||
const pairing = require('../pairing')
|
const pairing = require('../pairing')
|
||||||
const { accounts: accountsConfig, coins, countries, currencies, languages } = require('../config')
|
const {
|
||||||
|
accounts: accountsConfig,
|
||||||
|
coins,
|
||||||
|
countries,
|
||||||
|
currencies,
|
||||||
|
languages
|
||||||
|
} = require('../config')
|
||||||
|
|
||||||
const typeDefs = gql`
|
const typeDefs = gql`
|
||||||
scalar JSON
|
scalar JSON
|
||||||
|
|
@ -61,6 +71,7 @@ const typeDefs = gql`
|
||||||
cassette1: Int
|
cassette1: Int
|
||||||
cassette2: Int
|
cassette2: Int
|
||||||
statuses: [MachineStatus]
|
statuses: [MachineStatus]
|
||||||
|
latestEvent: MachineEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
type Customer {
|
type Customer {
|
||||||
|
|
@ -155,6 +166,14 @@ const typeDefs = gql`
|
||||||
uptime: Int!
|
uptime: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserToken {
|
||||||
|
token: String!
|
||||||
|
name: String!
|
||||||
|
created: Date!
|
||||||
|
user_agent: String
|
||||||
|
ip_address: String
|
||||||
|
}
|
||||||
|
|
||||||
type Transaction {
|
type Transaction {
|
||||||
id: ID!
|
id: ID!
|
||||||
txClass: String!
|
txClass: String!
|
||||||
|
|
@ -197,6 +216,22 @@ const typeDefs = gql`
|
||||||
machineName: String
|
machineName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Blacklist {
|
||||||
|
createdByOperator: Boolean!
|
||||||
|
cryptoCode: String!
|
||||||
|
address: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type MachineEvent {
|
||||||
|
id: ID
|
||||||
|
deviceId: String
|
||||||
|
eventType: String
|
||||||
|
note: String
|
||||||
|
created: Date
|
||||||
|
age: Float
|
||||||
|
deviceTime: Date
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
countries: [Country]
|
countries: [Country]
|
||||||
currencies: [Currency]
|
currencies: [Currency]
|
||||||
|
|
@ -204,6 +239,7 @@ const typeDefs = gql`
|
||||||
accountsConfig: [AccountConfig]
|
accountsConfig: [AccountConfig]
|
||||||
cryptoCurrencies: [CryptoCurrency]
|
cryptoCurrencies: [CryptoCurrency]
|
||||||
machines: [Machine]
|
machines: [Machine]
|
||||||
|
machine(deviceId: ID!): Machine
|
||||||
customers: [Customer]
|
customers: [Customer]
|
||||||
customer(customerId: ID!): Customer
|
customer(customerId: ID!): Customer
|
||||||
machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog]
|
machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog]
|
||||||
|
|
@ -217,6 +253,8 @@ const typeDefs = gql`
|
||||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
|
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
|
||||||
accounts: JSONObject
|
accounts: JSONObject
|
||||||
config: JSONObject
|
config: JSONObject
|
||||||
|
blacklist: [Blacklist]
|
||||||
|
userTokens: [UserToken]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MachineAction {
|
enum MachineAction {
|
||||||
|
|
@ -235,9 +273,17 @@ const typeDefs = gql`
|
||||||
saveConfig(config: JSONObject): JSONObject
|
saveConfig(config: JSONObject): JSONObject
|
||||||
createPairingTotem(name: String!): String
|
createPairingTotem(name: String!): String
|
||||||
saveAccounts(accounts: JSONObject): JSONObject
|
saveAccounts(accounts: JSONObject): JSONObject
|
||||||
|
revokeToken(token: String!): UserToken
|
||||||
|
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
||||||
|
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const transactionsLoader = new DataLoader(ids => transactions.getCustomerTransactionsBatch(ids))
|
||||||
|
const machineEventsLoader = new DataLoader(ids => {
|
||||||
|
return machineEventsByIdBatch(ids)
|
||||||
|
}, { cache: false })
|
||||||
|
|
||||||
const notify = () => got.post('http://localhost:3030/dbChange')
|
const notify = () => got.post('http://localhost:3030/dbChange')
|
||||||
.catch(e => console.error('Error: lamassu-server not responding'))
|
.catch(e => console.error('Error: lamassu-server not responding'))
|
||||||
|
|
||||||
|
|
@ -246,7 +292,10 @@ const resolvers = {
|
||||||
JSONObject: GraphQLJSONObject,
|
JSONObject: GraphQLJSONObject,
|
||||||
Date: GraphQLDateTime,
|
Date: GraphQLDateTime,
|
||||||
Customer: {
|
Customer: {
|
||||||
transactions: parent => transactions.getCustomerTransactions(parent.id)
|
transactions: parent => transactionsLoader.load(parent.id)
|
||||||
|
},
|
||||||
|
Machine: {
|
||||||
|
latestEvent: parent => machineEventsLoader.load(parent.deviceId)
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
countries: () => countries,
|
countries: () => countries,
|
||||||
|
|
@ -255,6 +304,7 @@ const resolvers = {
|
||||||
accountsConfig: () => accountsConfig,
|
accountsConfig: () => accountsConfig,
|
||||||
cryptoCurrencies: () => coins,
|
cryptoCurrencies: () => coins,
|
||||||
machines: () => machineLoader.getMachineNames(),
|
machines: () => machineLoader.getMachineNames(),
|
||||||
|
machine: (...[, { deviceId }]) => machineLoader.getMachine(deviceId),
|
||||||
customers: () => customers.getCustomersList(),
|
customers: () => customers.getCustomersList(),
|
||||||
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
|
||||||
funding: () => funding.getFunding(),
|
funding: () => funding.getFunding(),
|
||||||
|
|
@ -273,18 +323,28 @@ const resolvers = {
|
||||||
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
||||||
transactions.batch(from, until, limit, offset).then(parseAsync),
|
transactions.batch(from, until, limit, offset).then(parseAsync),
|
||||||
config: () => settingsLoader.loadLatestConfigOrNone(),
|
config: () => settingsLoader.loadLatestConfigOrNone(),
|
||||||
accounts: () => settingsLoader.loadAccounts()
|
accounts: () => settingsLoader.loadAccounts(),
|
||||||
|
blacklist: () => blacklist.getBlacklist(),
|
||||||
|
userTokens: () => tokenManager.getTokenList()
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }),
|
machineAction: (...[, { deviceId, action, cassette1, cassette2, newName }]) => machineAction({ deviceId, action, cassette1, cassette2, newName }),
|
||||||
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
createPairingTotem: (...[, { name }]) => pairing.totem(name),
|
||||||
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
saveAccounts: (...[, { accounts }]) => settingsLoader.saveAccounts(accounts),
|
||||||
setCustomer: (...[, { customerId, customerInput } ]) => customers.updateCustomer(customerId, customerInput),
|
setCustomer: (root, args, context, info) => {
|
||||||
|
const token = context.req.cookies && context.req.cookies.token
|
||||||
|
return customers.updateCustomer(args.customerId, args.customerInput, token)
|
||||||
|
},
|
||||||
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
saveConfig: (...[, { config }]) => settingsLoader.saveConfig(config)
|
||||||
.then(it => {
|
.then(it => {
|
||||||
notify()
|
notify()
|
||||||
return it
|
return it
|
||||||
})
|
}),
|
||||||
|
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||||
|
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||||
|
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||||
|
blacklist.insertIntoBlacklist(cryptoCode, address),
|
||||||
|
revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,15 @@ function validateOTP (otp) {
|
||||||
.catch(() => ({ success: false, expired: false }))
|
.catch(() => ({ success: false, expired: false }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function register (otp) {
|
function register (otp, ua, ip) {
|
||||||
return validateOTP(otp)
|
return validateOTP(otp)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
if (!r.success) return r
|
if (!r.success) return r
|
||||||
|
|
||||||
const token = crypto.randomBytes(32).toString('hex')
|
const token = crypto.randomBytes(32).toString('hex')
|
||||||
const sql = 'insert into user_tokens (token, name) values ($1, $2)'
|
const sql = 'insert into user_tokens (token, name, user_agent, ip_address) values ($1, $2, $3, $4)'
|
||||||
|
|
||||||
return db.none(sql, [token, r.name])
|
return db.none(sql, [token, r.name, ua, ip])
|
||||||
.then(() => ({ success: true, token: token }))
|
.then(() => ({ success: true, token: token }))
|
||||||
})
|
})
|
||||||
.catch(() => ({ success: false, expired: false }))
|
.catch(() => ({ success: false, expired: false }))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const _ = require('lodash/fp')
|
const _ = require('lodash/fp')
|
||||||
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
const db = require('../db')
|
const db = require('../db')
|
||||||
const machineLoader = require('../machine-loader')
|
const machineLoader = require('../machine-loader')
|
||||||
|
|
@ -65,9 +66,8 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
||||||
.then(packager)
|
.then(packager)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCustomerTransactions (customerId) {
|
function getCustomerTransactionsBatch (ids) {
|
||||||
const packager = _.flow(it => {
|
const packager = _.flow(it => {
|
||||||
console.log()
|
|
||||||
return it
|
return it
|
||||||
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
}, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ function getCustomerTransactions (customerId) {
|
||||||
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
|
((not txs.send_confirmed) and (txs.created <= now() - interval $2)) as expired
|
||||||
from cash_in_txs as txs
|
from cash_in_txs as txs
|
||||||
left outer join customers c on txs.customer_id = c.id
|
left outer join customers c on txs.customer_id = c.id
|
||||||
where c.id = $1
|
where c.id IN ($1^)
|
||||||
order by created desc limit $3`
|
order by created desc limit $3`
|
||||||
|
|
||||||
const cashOutSql = `select 'cashOut' as tx_class,
|
const cashOutSql = `select 'cashOut' as tx_class,
|
||||||
|
|
@ -100,14 +100,16 @@ function getCustomerTransactions (customerId) {
|
||||||
inner join cash_out_actions actions on txs.id = actions.tx_id
|
inner join cash_out_actions actions on txs.id = actions.tx_id
|
||||||
and actions.action = 'provisionAddress'
|
and actions.action = 'provisionAddress'
|
||||||
left outer join customers c on txs.customer_id = c.id
|
left outer join customers c on txs.customer_id = c.id
|
||||||
where c.id = $1
|
where c.id IN ($1^)
|
||||||
order by created desc limit $2`
|
order by created desc limit $2`
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
db.any(cashInSql, [customerId, cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
|
db.any(cashInSql, [_.map(pgp.as.text, ids).join(','), cashInTx.PENDING_INTERVAL, NUM_RESULTS]),
|
||||||
db.any(cashOutSql, [customerId, NUM_RESULTS, REDEEMABLE_AGE])
|
db.any(cashOutSql, [_.map(pgp.as.text, ids).join(','), NUM_RESULTS, REDEEMABLE_AGE])
|
||||||
])
|
])
|
||||||
.then(packager)
|
.then(packager).then(transactions => {
|
||||||
|
const transactionMap = _.groupBy('customerId', transactions)
|
||||||
|
return ids.map(id => transactionMap[id])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function single (txId) {
|
function single (txId) {
|
||||||
|
|
@ -156,4 +158,4 @@ function cancel (txId) {
|
||||||
.then(() => single(txId))
|
.then(() => single(txId))
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { batch, getCustomerTransactions, single, cancel }
|
module.exports = { batch, single, cancel, getCustomerTransactionsBatch }
|
||||||
|
|
|
||||||
|
|
@ -637,7 +637,7 @@ function plugins (settings, deviceId) {
|
||||||
|
|
||||||
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
const notifications = configManager.getNotifications(null, device.deviceId, settings.config)
|
||||||
|
|
||||||
const machineName = device.machineName
|
const machineName = device.name
|
||||||
|
|
||||||
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
const cashInAlert = device.cashbox > notifications.cashInAlertThreshold
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
const _ = require('lodash/fp')
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
|
const pgp = require('pg-promise')()
|
||||||
|
|
||||||
function getInsertQuery (tableName, fields) {
|
function getInsertQuery (tableName, fields) {
|
||||||
// outputs string like: '$1, $2, $3...' with proper No of items
|
// outputs string like: '$1, $2, $3...' with proper No of items
|
||||||
|
|
@ -48,6 +50,16 @@ exports.machineEvent = function machineEvent (rec) {
|
||||||
.then(() => db.none(deleteSql))
|
.then(() => db.none(deleteSql))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.machineEventsByIdBatch = function machineEventsByIdBatch (machineIds) {
|
||||||
|
const formattedIds = _.map(pgp.as.text, machineIds).join(',')
|
||||||
|
const sql = `SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events WHERE device_id IN ($1^) ORDER BY age ASC LIMIT 1`
|
||||||
|
return db.any(sql, [formattedIds]).then(res => {
|
||||||
|
const events = _.map(_.mapKeys(_.camelCase))(res)
|
||||||
|
const eventMap = _.groupBy('deviceId', events)
|
||||||
|
return machineIds.map(id => _.prop([0], eventMap[id]))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
exports.machineEvents = function machineEvents () {
|
exports.machineEvents = function machineEvents () {
|
||||||
const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events'
|
const sql = 'SELECT *, (EXTRACT(EPOCH FROM (now() - created))) * 1000 AS age FROM machine_events'
|
||||||
|
|
||||||
|
|
|
||||||
13
lib/token-manager.js
Normal file
13
lib/token-manager.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
function getTokenList () {
|
||||||
|
const sql = `select * from user_tokens`
|
||||||
|
return db.any(sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeToken (token) {
|
||||||
|
const sql = `delete from user_tokens where token = $1`
|
||||||
|
return db.none(sql, [token])
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { getTokenList, revokeToken }
|
||||||
14
migrations/1603438527057-add-browser-os-info.js
Normal file
14
migrations/1603438527057-add-browser-os-info.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const db = require('./db')
|
||||||
|
|
||||||
|
exports.up = function (next) {
|
||||||
|
var sql = [
|
||||||
|
'ALTER TABLE user_tokens ADD COLUMN user_agent text',
|
||||||
|
'ALTER TABLE user_tokens ADD COLUMN ip_address inet',
|
||||||
|
]
|
||||||
|
|
||||||
|
db.multi(sql, next)
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.down = function (next) {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline'
|
import CssBaseline from '@material-ui/core/CssBaseline'
|
||||||
|
import Grid from '@material-ui/core/Grid'
|
||||||
import {
|
import {
|
||||||
StylesProvider,
|
StylesProvider,
|
||||||
jssPreset,
|
jssPreset,
|
||||||
|
|
@ -8,12 +9,18 @@ import {
|
||||||
import { create } from 'jss'
|
import { create } from 'jss'
|
||||||
import extendJss from 'jss-plugin-extend'
|
import extendJss from 'jss-plugin-extend'
|
||||||
import React, { createContext, useContext, useState } from 'react'
|
import React, { createContext, useContext, useState } from 'react'
|
||||||
import { useLocation, BrowserRouter as Router } from 'react-router-dom'
|
import {
|
||||||
|
useLocation,
|
||||||
|
useHistory,
|
||||||
|
BrowserRouter as Router
|
||||||
|
} from 'react-router-dom'
|
||||||
|
|
||||||
|
import Sidebar from 'src/components/layout/Sidebar'
|
||||||
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
import ApolloProvider from 'src/utils/apollo'
|
import ApolloProvider from 'src/utils/apollo'
|
||||||
|
|
||||||
import Header from './components/layout/Header'
|
import Header from './components/layout/Header'
|
||||||
import { tree, Routes } from './routing/routes'
|
import { tree, hasSidebar, Routes, getParent } from './routing/routes'
|
||||||
import global from './styling/global'
|
import global from './styling/global'
|
||||||
import theme from './styling/theme'
|
import theme from './styling/theme'
|
||||||
import { backgroundColor, mainWidth } from './styling/variables'
|
import { backgroundColor, mainWidth } from './styling/variables'
|
||||||
|
|
@ -46,6 +53,18 @@ const useStyles = makeStyles({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection
|
flexDirection
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
flex: 1,
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
contentWithSidebar: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 48,
|
||||||
|
paddingTop: 15
|
||||||
|
},
|
||||||
|
contentWithoutSidebar: {
|
||||||
|
width: mainWidth
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -54,15 +73,45 @@ const AppContext = createContext()
|
||||||
const Main = () => {
|
const Main = () => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const history = useHistory()
|
||||||
const { wizardTested } = useContext(AppContext)
|
const { wizardTested } = useContext(AppContext)
|
||||||
|
|
||||||
|
const route = location.pathname
|
||||||
|
|
||||||
|
const sidebar = hasSidebar(route)
|
||||||
|
const parent = sidebar ? getParent(route) : {}
|
||||||
|
|
||||||
const is404 = location.pathname === '/404'
|
const is404 = location.pathname === '/404'
|
||||||
|
|
||||||
|
const isSelected = it => location.pathname === it.route
|
||||||
|
|
||||||
|
const onClick = it => history.push(it.route)
|
||||||
|
|
||||||
|
const contentClassName = sidebar
|
||||||
|
? classes.contentWithSidebar
|
||||||
|
: classes.contentWithoutSidebar
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
{!is404 && wizardTested && <Header tree={tree} />}
|
{!is404 && wizardTested && <Header tree={tree} />}
|
||||||
<main className={classes.wrapper}>
|
<main className={classes.wrapper}>
|
||||||
|
{sidebar && !is404 && wizardTested && (
|
||||||
|
<TitleSection title={parent.title}></TitleSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Grid container className={classes.grid}>
|
||||||
|
{sidebar && !is404 && wizardTested && (
|
||||||
|
<Sidebar
|
||||||
|
data={parent.children}
|
||||||
|
isSelected={isSelected}
|
||||||
|
displayName={it => it.label}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={contentClassName}>
|
||||||
<Routes />
|
<Routes />
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export const ConfirmDialog = memo(
|
||||||
onConfirmed,
|
onConfirmed,
|
||||||
onDissmised,
|
onDissmised,
|
||||||
initialValue = '',
|
initialValue = '',
|
||||||
|
disabled = false,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
@ -101,6 +102,7 @@ export const ConfirmDialog = memo(
|
||||||
<DialogContent className={classes.dialogContent}>
|
<DialogContent className={classes.dialogContent}>
|
||||||
{message && <P>{message}</P>}
|
{message && <P>{message}</P>}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
disabled={disabled}
|
||||||
label={confirmationMessage}
|
label={confirmationMessage}
|
||||||
name="confirm-input"
|
name="confirm-input"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ import classnames from 'classnames'
|
||||||
import React, { memo, useState } from 'react'
|
import React, { memo, useState } from 'react'
|
||||||
import { NavLink, useHistory } from 'react-router-dom'
|
import { NavLink, useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
import { Link } from 'src/components/buttons'
|
import ActionButton from 'src/components/buttons/ActionButton'
|
||||||
import { H4 } from 'src/components/typography'
|
import { H4 } from 'src/components/typography'
|
||||||
import AddMachine from 'src/pages/AddMachine'
|
import AddMachine from 'src/pages/AddMachine'
|
||||||
|
import { ReactComponent as AddIconReverse } from 'src/styling/icons/button/add/white.svg'
|
||||||
|
import { ReactComponent as AddIcon } from 'src/styling/icons/button/add/zodiac.svg'
|
||||||
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
import { ReactComponent as Logo } from 'src/styling/icons/menu/logo.svg'
|
||||||
|
|
||||||
import styles from './Header.styles'
|
import styles from './Header.styles'
|
||||||
|
|
@ -76,9 +78,13 @@ const Header = memo(({ tree }) => {
|
||||||
</NavLink>
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<Link color="action" onClick={() => setOpen(true)}>
|
<ActionButton
|
||||||
Add Machine
|
color="secondary"
|
||||||
</Link>
|
Icon={AddIcon}
|
||||||
|
InverseIcon={AddIconReverse}
|
||||||
|
onClick={() => setOpen(true)}>
|
||||||
|
Add machine
|
||||||
|
</ActionButton>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
181
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal file
181
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import { Box } from '@material-ui/core'
|
||||||
|
import Grid from '@material-ui/core/Grid'
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
import Tooltip from 'src/components/Tooltip'
|
||||||
|
import { Link } from 'src/components/buttons'
|
||||||
|
import { Switch } from 'src/components/inputs'
|
||||||
|
import Sidebar from 'src/components/layout/Sidebar'
|
||||||
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
import { H4, Label2, P } from 'src/components/typography'
|
||||||
|
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||||
|
|
||||||
|
import styles from './Blacklist.styles'
|
||||||
|
import BlackListModal from './BlacklistModal'
|
||||||
|
import BlacklistTable from './BlacklistTable'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const groupByCode = R.groupBy(obj => obj.cryptoCode)
|
||||||
|
|
||||||
|
const DELETE_ROW = gql`
|
||||||
|
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||||
|
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||||
|
cryptoCode
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const GET_BLACKLIST = gql`
|
||||||
|
query getBlacklistData {
|
||||||
|
blacklist {
|
||||||
|
cryptoCode
|
||||||
|
address
|
||||||
|
}
|
||||||
|
cryptoCurrencies {
|
||||||
|
display
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SAVE_CONFIG = gql`
|
||||||
|
mutation Save($config: JSONObject) {
|
||||||
|
saveConfig(config: $config)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const GET_INFO = gql`
|
||||||
|
query getData {
|
||||||
|
config
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ADD_ROW = gql`
|
||||||
|
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||||
|
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||||
|
cryptoCode
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Blacklist = () => {
|
||||||
|
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
||||||
|
const { data: configData } = useQuery(GET_INFO)
|
||||||
|
const [showModal, setShowModal] = useState(false)
|
||||||
|
const [clickedItem, setClickedItem] = useState({
|
||||||
|
code: 'BTC',
|
||||||
|
display: 'Bitcoin'
|
||||||
|
})
|
||||||
|
const [deleteEntry] = useMutation(DELETE_ROW, {
|
||||||
|
onError: () => console.error('Error while deleting row'),
|
||||||
|
refetchQueries: () => ['getBlacklistData']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [addEntry] = useMutation(ADD_ROW, {
|
||||||
|
onError: () => console.error('Error while adding row'),
|
||||||
|
onCompleted: () => setShowModal(false),
|
||||||
|
refetchQueries: () => ['getBlacklistData']
|
||||||
|
})
|
||||||
|
|
||||||
|
const [saveConfig] = useMutation(SAVE_CONFIG, {
|
||||||
|
refetchQueries: () => ['getData']
|
||||||
|
})
|
||||||
|
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
|
||||||
|
const availableCurrencies =
|
||||||
|
R.path(['cryptoCurrencies'], blacklistResponse) ?? []
|
||||||
|
|
||||||
|
const formattedData = groupByCode(blacklistData)
|
||||||
|
|
||||||
|
const complianceConfig =
|
||||||
|
configData?.config && fromNamespace('compliance')(configData.config)
|
||||||
|
|
||||||
|
const rejectAddressReuse = complianceConfig?.rejectAddressReuse ?? false
|
||||||
|
|
||||||
|
const addressReuseSave = rawConfig => {
|
||||||
|
const config = toNamespace('compliance')(rawConfig)
|
||||||
|
return saveConfig({ variables: { config } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickSidebarItem = e => {
|
||||||
|
setClickedItem({ code: e.code, display: e.display })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteEntry = (cryptoCode, address) => {
|
||||||
|
deleteEntry({ variables: { cryptoCode, address } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const addToBlacklist = (cryptoCode, address) => {
|
||||||
|
addEntry({ variables: { cryptoCode, address } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleSection title="Blacklisted addresses">
|
||||||
|
<Link onClick={() => setShowModal(false)}>Blacklist new addresses</Link>
|
||||||
|
</TitleSection>
|
||||||
|
<Grid container className={classes.grid}>
|
||||||
|
<Sidebar
|
||||||
|
data={availableCurrencies}
|
||||||
|
isSelected={R.propEq('code', clickedItem.code)}
|
||||||
|
displayName={it => it.display}
|
||||||
|
onClick={onClickSidebarItem}
|
||||||
|
/>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<Box display="flex" justifyContent="space-between" mb={3}>
|
||||||
|
<H4 noMargin className={classes.subtitle}>
|
||||||
|
{clickedItem.display
|
||||||
|
? `${clickedItem.display} blacklisted addresses`
|
||||||
|
: ''}{' '}
|
||||||
|
</H4>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="end"
|
||||||
|
mr="-5px">
|
||||||
|
<P>Reject reused addresses</P>
|
||||||
|
<Switch
|
||||||
|
checked={rejectAddressReuse}
|
||||||
|
onChange={event => {
|
||||||
|
addressReuseSave({ rejectAddressReuse: event.target.checked })
|
||||||
|
}}
|
||||||
|
value={rejectAddressReuse}
|
||||||
|
/>
|
||||||
|
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
|
||||||
|
<Tooltip width={304}>
|
||||||
|
<P>
|
||||||
|
The "Reject reused addresses" option means that all addresses
|
||||||
|
that are used once will be automatically rejected if there's
|
||||||
|
an attempt to use them again on a new transaction.
|
||||||
|
</P>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<BlacklistTable
|
||||||
|
data={formattedData}
|
||||||
|
selectedCoin={clickedItem}
|
||||||
|
handleDeleteEntry={handleDeleteEntry}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Grid>
|
||||||
|
{showModal && (
|
||||||
|
<BlackListModal
|
||||||
|
onClose={() => setShowModal(false)}
|
||||||
|
selectedCoin={clickedItem}
|
||||||
|
addToBlacklist={addToBlacklist}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blacklist
|
||||||
39
new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js
Normal file
39
new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { spacer, fontPrimary, primaryColor, white } from 'src/styling/variables'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
grid: {
|
||||||
|
flex: 1,
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: spacer * 6
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
lineHeight: '120%',
|
||||||
|
color: primaryColor,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: fontPrimary,
|
||||||
|
fontWeight: 900
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
white: {
|
||||||
|
color: white
|
||||||
|
},
|
||||||
|
deleteButton: {
|
||||||
|
paddingLeft: 13
|
||||||
|
},
|
||||||
|
addressRow: {
|
||||||
|
marginLeft: 8
|
||||||
|
}
|
||||||
|
}
|
||||||
76
new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js
Normal file
76
new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import { Formik, Form, Field } from 'formik'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React from 'react'
|
||||||
|
import * as Yup from 'yup'
|
||||||
|
|
||||||
|
import Modal from 'src/components/Modal'
|
||||||
|
import { Link } from 'src/components/buttons'
|
||||||
|
import { TextInput } from 'src/components/inputs/formik'
|
||||||
|
import { H3 } from 'src/components/typography'
|
||||||
|
|
||||||
|
import styles from './Blacklist.styles'
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const BlackListModal = ({ onClose, selectedCoin, addToBlacklist }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const handleAddToBlacklist = address => {
|
||||||
|
addToBlacklist(selectedCoin.code, address)
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholderAddress = {
|
||||||
|
BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD',
|
||||||
|
ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
|
||||||
|
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
|
||||||
|
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||||
|
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||||
|
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
closeOnBackdropClick={true}
|
||||||
|
width={676}
|
||||||
|
height={200}
|
||||||
|
handleClose={onClose}
|
||||||
|
open={true}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
address: ''
|
||||||
|
}}
|
||||||
|
validationSchema={Yup.object({
|
||||||
|
address: Yup.string()
|
||||||
|
.trim()
|
||||||
|
.required('An address is required')
|
||||||
|
})}
|
||||||
|
onSubmit={({ address }, { resetForm }) => {
|
||||||
|
handleAddToBlacklist(address)
|
||||||
|
resetForm()
|
||||||
|
}}>
|
||||||
|
<Form id="address-form">
|
||||||
|
<H3>
|
||||||
|
{selectedCoin.display
|
||||||
|
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
||||||
|
: ''}
|
||||||
|
</H3>
|
||||||
|
<Field
|
||||||
|
name="address"
|
||||||
|
fullWidth
|
||||||
|
autoComplete="off"
|
||||||
|
label="Paste new address to blacklist here"
|
||||||
|
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
||||||
|
component={TextInput}
|
||||||
|
/>
|
||||||
|
</Form>
|
||||||
|
</Formik>
|
||||||
|
<div className={classes.footer}>
|
||||||
|
<Link type="submit" form="address-form">
|
||||||
|
Blacklist address
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlackListModal
|
||||||
60
new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js
Normal file
60
new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { IconButton } from 'src/components/buttons'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { Label1 } from 'src/components/typography'
|
||||||
|
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
|
||||||
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
|
||||||
|
import styles from './Blacklist.styles'
|
||||||
|
|
||||||
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
|
const BlacklistTable = ({ data, selectedCoin, handleDeleteEntry }) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
name: 'address',
|
||||||
|
header: <Label1 className={classes.white}>{'Addresses'}</Label1>,
|
||||||
|
width: 800,
|
||||||
|
textAlign: 'left',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => (
|
||||||
|
<div className={classes.addressRow}>
|
||||||
|
<CopyToClipboard>{R.path(['address'], it)}</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deleteButton',
|
||||||
|
header: <Label1 className={classes.white}>{'Delete'}</Label1>,
|
||||||
|
width: 130,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: it => (
|
||||||
|
<IconButton
|
||||||
|
className={classes.deleteButton}
|
||||||
|
onClick={() =>
|
||||||
|
handleDeleteEntry(
|
||||||
|
R.path(['cryptoCode'], it),
|
||||||
|
R.path(['address'], it)
|
||||||
|
)
|
||||||
|
}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const dataToShow = selectedCoin
|
||||||
|
? data[selectedCoin.code]
|
||||||
|
: data[R.keys(data)[0]]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable data={dataToShow} elements={elements} name="blacklistTable" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlacklistTable
|
||||||
3
new-lamassu-admin/src/pages/Blacklist/index.js
Normal file
3
new-lamassu-admin/src/pages/Blacklist/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import Blacklist from './Blacklist'
|
||||||
|
|
||||||
|
export default Blacklist
|
||||||
|
|
@ -89,7 +89,7 @@ const CashOut = ({ name: SCREEN_KEY }) => {
|
||||||
<Tooltip width={304}>
|
<Tooltip width={304}>
|
||||||
<P>
|
<P>
|
||||||
Automatically accept customer deposits as complete if their
|
Automatically accept customer deposits as complete if their
|
||||||
received amount is 10 crypto atoms or less.
|
received amount is 100 crypto atoms or less.
|
||||||
</P>
|
</P>
|
||||||
<P>
|
<P>
|
||||||
(Crypto atoms are the smallest unit in each cryptocurrency. E.g.,
|
(Crypto atoms are the smallest unit in each cryptocurrency. E.g.,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useMutation } from '@apollo/react-hooks'
|
import { useMutation, useLazyQuery } from '@apollo/react-hooks'
|
||||||
import { Grid, Divider } from '@material-ui/core'
|
import { Grid, Divider } from '@material-ui/core'
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
|
|
@ -32,6 +32,16 @@ const MACHINE_ACTION = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const MACHINE = gql`
|
||||||
|
query getMachine($deviceId: ID!) {
|
||||||
|
machine(deviceId: $deviceId) {
|
||||||
|
latestEvent {
|
||||||
|
note
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
const supportArtices = [
|
const supportArtices = [
|
||||||
{
|
{
|
||||||
// Default article for non-maped statuses
|
// Default article for non-maped statuses
|
||||||
|
|
@ -43,6 +53,24 @@ const supportArtices = [
|
||||||
// TODO add Stuck and Fully Functional statuses articles for the new-admins
|
// 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 }) =>
|
const article = ({ code: status }) =>
|
||||||
supportArtices.find(({ code: article }) => article === status)
|
supportArtices.find(({ code: article }) => article === status)
|
||||||
|
|
||||||
|
|
@ -68,11 +96,37 @@ const Item = ({ children, ...props }) => (
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const getState = machineEventsLazy =>
|
||||||
|
JSON.parse(machineEventsLazy.machine.latestEvent?.note ?? '{"state": null}')
|
||||||
|
.state
|
||||||
|
|
||||||
const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
const [action, setAction] = useState(null)
|
const [action, setAction] = useState({ command: null })
|
||||||
const [errorMessage, setErrorMessage] = useState(null)
|
const [errorMessage, setErrorMessage] = useState(null)
|
||||||
const classes = useMDStyles()
|
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, {
|
const [machineAction, { loading }] = useMutation(MACHINE_ACTION, {
|
||||||
onError: ({ message }) => {
|
onError: ({ message }) => {
|
||||||
const errorMessage = message ?? 'An error ocurred'
|
const errorMessage = message ?? 'An error ocurred'
|
||||||
|
|
@ -80,11 +134,12 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
},
|
},
|
||||||
onCompleted: () => {
|
onCompleted: () => {
|
||||||
onActionSuccess && onActionSuccess()
|
onActionSuccess && onActionSuccess()
|
||||||
setAction(null)
|
setAction({ command: null })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const confirmDialogOpen = Boolean(action)
|
const confirmDialogOpen = Boolean(action.command)
|
||||||
|
const disabled = !!(action?.command === 'restartServices' && loadingEvents)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -127,25 +182,26 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
className={classes.separator}
|
className={classes.separator}
|
||||||
/>
|
/>
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
disabled={disabled}
|
||||||
open={confirmDialogOpen}
|
open={confirmDialogOpen}
|
||||||
title={`${action?.command} this machine?`}
|
title={`${action?.display} this machine?`}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
toBeConfirmed={machine.name}
|
toBeConfirmed={machine.name}
|
||||||
message={action?.message}
|
message={action?.message}
|
||||||
confirmationMessage={action?.confirmationMessage}
|
confirmationMessage={action?.confirmationMessage}
|
||||||
saveButtonAlwaysEnabled={action?.command === 'Rename'}
|
saveButtonAlwaysEnabled={action?.command === 'rename'}
|
||||||
onConfirmed={value => {
|
onConfirmed={value => {
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
machineAction({
|
machineAction({
|
||||||
variables: {
|
variables: {
|
||||||
deviceId: machine.deviceId,
|
deviceId: machine.deviceId,
|
||||||
action: `${action?.command}`.toLowerCase(),
|
action: `${action?.command}`,
|
||||||
...(action?.command === 'Rename' && { newName: value })
|
...(action?.command === 'rename' && { newName: value })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
onDissmised={() => {
|
onDissmised={() => {
|
||||||
setAction(null)
|
setAction({ command: null })
|
||||||
setErrorMessage(null)
|
setErrorMessage(null)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
@ -174,7 +230,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
InverseIcon={EditReversedIcon}
|
InverseIcon={EditReversedIcon}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAction({
|
setAction({
|
||||||
command: 'Rename',
|
command: 'rename',
|
||||||
|
display: 'Rename',
|
||||||
confirmationMessage: 'Write the new name for this machine'
|
confirmationMessage: 'Write the new name for this machine'
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
|
|
@ -188,7 +245,8 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAction({
|
setAction({
|
||||||
command: 'Unpair'
|
command: 'unpair',
|
||||||
|
display: 'Unpair'
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
Unpair
|
Unpair
|
||||||
|
|
@ -201,26 +259,43 @@ const MachineDetailsRow = ({ it: machine, onActionSuccess }) => {
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAction({
|
setAction({
|
||||||
command: 'Reboot'
|
command: 'reboot',
|
||||||
|
display: 'Reboot'
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
Reboot
|
Reboot
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className={classes.inlineChip}
|
className={classes.mr}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
color="primary"
|
color="primary"
|
||||||
Icon={ShutdownIcon}
|
Icon={ShutdownIcon}
|
||||||
InverseIcon={ShutdownReversedIcon}
|
InverseIcon={ShutdownReversedIcon}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setAction({
|
setAction({
|
||||||
command: 'Shutdown',
|
command: 'shutdown',
|
||||||
|
display: 'Shutdown',
|
||||||
message:
|
message:
|
||||||
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
'In order to bring it back online, the machine will need to be visited and its power reset.'
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
Shutdown
|
Shutdown
|
||||||
</ActionButton>
|
</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>
|
</div>
|
||||||
</Item>
|
</Item>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,13 @@ import {
|
||||||
detailsRowStyles,
|
detailsRowStyles,
|
||||||
labelStyles
|
labelStyles
|
||||||
} from 'src/pages/Transactions/Transactions.styles'
|
} from 'src/pages/Transactions/Transactions.styles'
|
||||||
import { spacer, comet, primaryColor, fontSize4 } from 'src/styling/variables'
|
import {
|
||||||
|
spacer,
|
||||||
|
comet,
|
||||||
|
primaryColor,
|
||||||
|
fontSize4,
|
||||||
|
errorColor
|
||||||
|
} from 'src/styling/variables'
|
||||||
|
|
||||||
const machineDetailsStyles = {
|
const machineDetailsStyles = {
|
||||||
...detailsRowStyles,
|
...detailsRowStyles,
|
||||||
|
|
@ -58,6 +64,9 @@ const machineDetailsStyles = {
|
||||||
marginRight: 60,
|
marginRight: 60,
|
||||||
marginLeft: 'auto',
|
marginLeft: 'auto',
|
||||||
background: fade(comet, 0.5)
|
background: fade(comet, 0.5)
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
color: errorColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import Autocomplete from 'src/components/inputs/formik/Autocomplete'
|
||||||
import NotificationsCtx from '../NotificationsContext'
|
import NotificationsCtx from '../NotificationsContext'
|
||||||
import { transformNumber } from '../helper'
|
import { transformNumber } from '../helper'
|
||||||
|
|
||||||
const CASSETTE_1_KEY = 'cassette1'
|
const CASSETTE_1_KEY = 'fiatBalanceCassette1'
|
||||||
const CASSETTE_2_KEY = 'cassette2'
|
const CASSETTE_2_KEY = 'fiatBalanceCassette2'
|
||||||
const MACHINE_KEY = 'machine'
|
const MACHINE_KEY = 'machine'
|
||||||
const NAME = 'fiatBalanceOverrides'
|
const NAME = 'fiatBalanceOverrides'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
import { makeStyles } from '@material-ui/core'
|
|
||||||
import Grid from '@material-ui/core/Grid'
|
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Route,
|
|
||||||
Switch,
|
|
||||||
Redirect,
|
|
||||||
useLocation,
|
|
||||||
useHistory
|
|
||||||
} from 'react-router-dom'
|
|
||||||
|
|
||||||
import Sidebar from 'src/components/layout/Sidebar'
|
|
||||||
import TitleSection from 'src/components/layout/TitleSection'
|
|
||||||
|
|
||||||
import CoinAtmRadar from './CoinATMRadar'
|
|
||||||
import ContactInfo from './ContactInfo'
|
|
||||||
import ReceiptPrinting from './ReceiptPrinting'
|
|
||||||
import TermsConditions from './TermsConditions'
|
|
||||||
|
|
||||||
const styles = {
|
|
||||||
grid: {
|
|
||||||
flex: 1,
|
|
||||||
height: '100%'
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
marginLeft: 48,
|
|
||||||
paddingTop: 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
|
||||||
|
|
||||||
const innerRoutes = [
|
|
||||||
{
|
|
||||||
label: 'Contact information',
|
|
||||||
route: '/settings/operator-info/contact-info',
|
|
||||||
component: ContactInfo
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Receipt',
|
|
||||||
route: '/settings/operator-info/receipt-printing',
|
|
||||||
component: ReceiptPrinting
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Coin ATM Radar',
|
|
||||||
route: '/settings/operator-info/coin-atm-radar',
|
|
||||||
component: CoinAtmRadar
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Terms & Conditions',
|
|
||||||
route: '/settings/operator-info/terms-conditions',
|
|
||||||
component: TermsConditions
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const Routes = ({ wizard }) => (
|
|
||||||
<Switch>
|
|
||||||
<Redirect
|
|
||||||
exact
|
|
||||||
from="/settings/operator-info"
|
|
||||||
to="/settings/operator-info/contact-info"
|
|
||||||
/>
|
|
||||||
<Route exact path="/" />
|
|
||||||
{innerRoutes.map(({ route, component: Page, key }) => (
|
|
||||||
<Route path={route} key={key}>
|
|
||||||
<Page name={key} wizard={wizard} />
|
|
||||||
</Route>
|
|
||||||
))}
|
|
||||||
</Switch>
|
|
||||||
)
|
|
||||||
|
|
||||||
const OperatorInfo = ({ wizard = false }) => {
|
|
||||||
const classes = useStyles()
|
|
||||||
const history = useHistory()
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
const isSelected = it => location.pathname === it.route
|
|
||||||
|
|
||||||
const onClick = it => history.push(it.route)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TitleSection title="Operator information"></TitleSection>
|
|
||||||
<Grid container className={classes.grid}>
|
|
||||||
<Sidebar
|
|
||||||
data={innerRoutes}
|
|
||||||
isSelected={isSelected}
|
|
||||||
displayName={it => it.label}
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
<div className={classes.content}>
|
|
||||||
<Routes wizard={wizard} />
|
|
||||||
</div>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default OperatorInfo
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||||
|
import gql from 'graphql-tag'
|
||||||
|
import moment from 'moment'
|
||||||
|
import * as R from 'ramda'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import { IconButton } from 'src/components/buttons'
|
||||||
|
import TitleSection from 'src/components/layout/TitleSection'
|
||||||
|
import DataTable from 'src/components/tables/DataTable'
|
||||||
|
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||||
|
|
||||||
|
const GET_USER_TOKENS = gql`
|
||||||
|
query userTokens {
|
||||||
|
userTokens {
|
||||||
|
token
|
||||||
|
name
|
||||||
|
created
|
||||||
|
user_agent
|
||||||
|
ip_address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const REVOKE_USER_TOKEN = gql`
|
||||||
|
mutation revokeToken($token: String!) {
|
||||||
|
revokeToken(token: $token) {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Tokens = () => {
|
||||||
|
const { data: tknResponse } = useQuery(GET_USER_TOKENS)
|
||||||
|
|
||||||
|
const [revokeToken] = useMutation(REVOKE_USER_TOKEN, {
|
||||||
|
refetchQueries: () => ['userTokens']
|
||||||
|
})
|
||||||
|
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
header: 'Name',
|
||||||
|
width: 257,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: t => t.name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Token',
|
||||||
|
width: 505,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: t => t.token
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Date (UTC)',
|
||||||
|
width: 145,
|
||||||
|
textAlign: 'right',
|
||||||
|
size: 'sm',
|
||||||
|
view: t => moment.utc(t.created).format('YYYY-MM-DD')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Time (UTC)',
|
||||||
|
width: 145,
|
||||||
|
textAlign: 'right',
|
||||||
|
size: 'sm',
|
||||||
|
view: t => moment.utc(t.created).format('HH:mm:ss')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: '',
|
||||||
|
width: 80,
|
||||||
|
textAlign: 'center',
|
||||||
|
size: 'sm',
|
||||||
|
view: t => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
revokeToken({ variables: { token: t.token } })
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TitleSection title="Token Management" />
|
||||||
|
<DataTable
|
||||||
|
elements={elements}
|
||||||
|
data={R.path(['userTokens'])(tknResponse)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tokens
|
||||||
|
|
@ -76,7 +76,10 @@ const threshold = Yup.object().shape({
|
||||||
})
|
})
|
||||||
const requirement = Yup.object().shape({
|
const requirement = Yup.object().shape({
|
||||||
requirement: Yup.string().required(),
|
requirement: Yup.string().required(),
|
||||||
suspensionDays: Yup.number()
|
suspensionDays: Yup.number().when('requirement', {
|
||||||
|
is: 'suspend',
|
||||||
|
then: Yup.number().required()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const Schema = Yup.object().shape({
|
const Schema = Yup.object().shape({
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
|
|
||||||
import { AppContext } from 'src/App'
|
import { AppContext } from 'src/App'
|
||||||
import AuthRegister from 'src/pages/AuthRegister'
|
import AuthRegister from 'src/pages/AuthRegister'
|
||||||
|
import Blacklist from 'src/pages/Blacklist'
|
||||||
import Cashout from 'src/pages/Cashout'
|
import Cashout from 'src/pages/Cashout'
|
||||||
import Commissions from 'src/pages/Commissions'
|
import Commissions from 'src/pages/Commissions'
|
||||||
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
||||||
|
|
@ -19,9 +20,13 @@ import MachineLogs from 'src/pages/MachineLogs'
|
||||||
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
import CashCassettes from 'src/pages/Maintenance/CashCassettes'
|
||||||
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
import MachineStatus from 'src/pages/Maintenance/MachineStatus'
|
||||||
import Notifications from 'src/pages/Notifications/Notifications'
|
import Notifications from 'src/pages/Notifications/Notifications'
|
||||||
import OperatorInfo from 'src/pages/OperatorInfo/OperatorInfo'
|
import CoinAtmRadar from 'src/pages/OperatorInfo/CoinATMRadar'
|
||||||
|
import ContactInfo from 'src/pages/OperatorInfo/ContactInfo'
|
||||||
|
import ReceiptPrinting from 'src/pages/OperatorInfo/ReceiptPrinting'
|
||||||
|
import TermsConditions from 'src/pages/OperatorInfo/TermsConditions'
|
||||||
import ServerLogs from 'src/pages/ServerLogs'
|
import ServerLogs from 'src/pages/ServerLogs'
|
||||||
import Services from 'src/pages/Services/Services'
|
import Services from 'src/pages/Services/Services'
|
||||||
|
import TokenManagement from 'src/pages/TokenManagement/TokenManagement'
|
||||||
import Transactions from 'src/pages/Transactions/Transactions'
|
import Transactions from 'src/pages/Transactions/Transactions'
|
||||||
import Triggers from 'src/pages/Triggers'
|
import Triggers from 'src/pages/Triggers'
|
||||||
import WalletSettings from 'src/pages/Wallet/Wallet'
|
import WalletSettings from 'src/pages/Wallet/Wallet'
|
||||||
|
|
@ -123,7 +128,36 @@ const tree = [
|
||||||
key: namespaces.OPERATOR_INFO,
|
key: namespaces.OPERATOR_INFO,
|
||||||
label: 'Operator Info',
|
label: 'Operator Info',
|
||||||
route: '/settings/operator-info',
|
route: '/settings/operator-info',
|
||||||
component: OperatorInfo
|
title: 'Operator Information',
|
||||||
|
get component() {
|
||||||
|
return () => <Redirect to={this.children[0].route} />
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'contact-info',
|
||||||
|
label: 'Contact information',
|
||||||
|
route: '/settings/operator-info/contact-info',
|
||||||
|
component: ContactInfo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'receipt-printing',
|
||||||
|
label: 'Receipt',
|
||||||
|
route: '/settings/operator-info/receipt-printing',
|
||||||
|
component: ReceiptPrinting
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'coin-atm-radar',
|
||||||
|
label: 'Coin ATM Radar',
|
||||||
|
route: '/settings/operator-info/coin-atm-radar',
|
||||||
|
component: CoinAtmRadar
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'terms-conditions',
|
||||||
|
label: 'Terms & Conditions',
|
||||||
|
route: '/settings/operator-info/terms-conditions',
|
||||||
|
component: TermsConditions
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -147,20 +181,66 @@ const tree = [
|
||||||
route: '/compliance/customers',
|
route: '/compliance/customers',
|
||||||
component: Customers
|
component: Customers
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'blacklist',
|
||||||
|
label: 'Blacklist',
|
||||||
|
route: '/compliance/blacklist',
|
||||||
|
component: Blacklist
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'customer',
|
key: 'customer',
|
||||||
route: '/compliance/customer/:id',
|
route: '/compliance/customer/:id',
|
||||||
component: CustomerProfile
|
component: CustomerProfile
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system',
|
||||||
|
label: 'System',
|
||||||
|
route: '/system',
|
||||||
|
get component() {
|
||||||
|
return () => <Redirect to={this.children[0].route} />
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'token-management',
|
||||||
|
label: 'Token Management',
|
||||||
|
route: '/system/token-management',
|
||||||
|
component: TokenManagement
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const map = R.map(R.when(R.has('children'), R.prop('children')))
|
const map = R.map(R.when(R.has('children'), R.prop('children')))
|
||||||
const leafRoutes = R.compose(R.flatten, map)(tree)
|
const mappedRoutes = R.compose(R.flatten, map)(tree)
|
||||||
const parentRoutes = R.filter(R.has('children'))(tree)
|
const parentRoutes = R.filter(R.has('children'))(mappedRoutes).concat(
|
||||||
|
R.filter(R.has('children'))(tree)
|
||||||
|
)
|
||||||
|
const leafRoutes = R.compose(R.flatten, map)(mappedRoutes)
|
||||||
|
|
||||||
const flattened = R.concat(leafRoutes, parentRoutes)
|
const flattened = R.concat(leafRoutes, parentRoutes)
|
||||||
|
|
||||||
|
const hasSidebar = route =>
|
||||||
|
R.any(r => r.route === route)(
|
||||||
|
R.compose(
|
||||||
|
R.flatten,
|
||||||
|
R.map(R.prop('children')),
|
||||||
|
R.filter(R.has('children'))
|
||||||
|
)(mappedRoutes)
|
||||||
|
)
|
||||||
|
|
||||||
|
const getParent = route =>
|
||||||
|
R.find(
|
||||||
|
R.propEq(
|
||||||
|
'route',
|
||||||
|
R.dropLast(
|
||||||
|
1,
|
||||||
|
R.dropLastWhile(x => x !== '/', route)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)(flattened)
|
||||||
|
|
||||||
const Routes = () => {
|
const Routes = () => {
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
|
@ -191,4 +271,4 @@ const Routes = () => {
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export { tree, Routes }
|
export { tree, getParent, hasSidebar, Routes }
|
||||||
|
|
|
||||||
13
new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg
Normal file
13
new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="nav-/-primary-/-1440" transform="translate(-1239.000000, -19.000000)" stroke="#1B2559" stroke-width="2">
|
||||||
|
<g id="icon/menu/search" transform="translate(1240.000000, 20.000000)">
|
||||||
|
<path d="M12.3100952,6.15542857 C12.3100952,9.55504762 9.55428571,12.3108571 6.15466667,12.3108571 C2.75580952,12.3108571 -2.72670775e-13,9.55504762 -2.72670775e-13,6.15542857 C-2.72670775e-13,2.75580952 2.75580952,8.08242362e-14 6.15466667,8.08242362e-14 C9.55428571,8.08242362e-14 12.3100952,2.75580952 12.3100952,6.15542857 Z" id="Stroke-1"></path>
|
||||||
|
<line x1="10.5820952" y1="10.5829333" x2="15.2068571" y2="15.2076952" id="Stroke-3" stroke-linecap="round"></line>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
4714
package-lock.json
generated
4714
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -23,6 +23,7 @@
|
||||||
"console-log-level": "^1.4.0",
|
"console-log-level": "^1.4.0",
|
||||||
"cookie-parser": "^1.4.3",
|
"cookie-parser": "^1.4.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dataloader": "^2.0.0",
|
||||||
"ethereumjs-tx": "^1.3.3",
|
"ethereumjs-tx": "^1.3.3",
|
||||||
"ethereumjs-util": "^5.2.0",
|
"ethereumjs-util": "^5.2.0",
|
||||||
"ethereumjs-wallet": "^0.6.3",
|
"ethereumjs-wallet": "^0.6.3",
|
||||||
|
|
@ -67,13 +68,15 @@
|
||||||
"socket.io-client": "^2.0.3",
|
"socket.io-client": "^2.0.3",
|
||||||
"talisman": "^0.20.0",
|
"talisman": "^0.20.0",
|
||||||
"twilio": "^3.6.1",
|
"twilio": "^3.6.1",
|
||||||
|
"ua-parser-js": "^0.7.22",
|
||||||
"uuid": "^3.1.0",
|
"uuid": "^3.1.0",
|
||||||
"web3": "^0.20.6",
|
"web3": "^0.20.6",
|
||||||
"winston": "^2.4.2",
|
"winston": "^2.4.2",
|
||||||
"winston-transport": "^4.3.0",
|
"winston-transport": "^4.3.0",
|
||||||
"ws": "^3.1.0",
|
"ws": "^3.1.0",
|
||||||
"xml-stream": "^0.4.5",
|
"xml-stream": "^0.4.5",
|
||||||
"xmlrpc": "^1.3.2"
|
"xmlrpc": "^1.3.2",
|
||||||
|
"yup": "^0.31.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -107,6 +110,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node bin/lamassu-server",
|
"start": "node bin/lamassu-server",
|
||||||
"test": "mocha --recursive tests",
|
"test": "mocha --recursive tests",
|
||||||
|
"jtest": "jest",
|
||||||
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
|
"build-admin": "npm run build-admin:css && npm run build-admin:main && npm run build-admin:lamassu",
|
||||||
"server": "nodemon bin/lamassu-server --mockSms",
|
"server": "nodemon bin/lamassu-server --mockSms",
|
||||||
"admin-server": "nodemon bin/lamassu-admin-server --dev",
|
"admin-server": "nodemon bin/lamassu-admin-server --dev",
|
||||||
|
|
@ -121,6 +125,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"ava": "3.8.2",
|
"ava": "3.8.2",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
|
"jest": "^26.6.3",
|
||||||
"mocha": "^5.0.1",
|
"mocha": "^5.0.1",
|
||||||
"nodemon": "^2.0.6",
|
"nodemon": "^2.0.6",
|
||||||
"rewire": "^4.0.1",
|
"rewire": "^4.0.1",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue