Merge pull request #1767 from RafaelTaranto/backport/blacklist
LAM-628 LAM-629 backport: blacklist message
This commit is contained in:
commit
7ec9121a14
12 changed files with 404 additions and 174 deletions
|
|
@ -1,48 +1,49 @@
|
|||
const _ = require('lodash/fp')
|
||||
|
||||
const db = require('./db')
|
||||
const notifierQueries = require('./notifier/queries')
|
||||
|
||||
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
|
||||
const getBlacklist = () => {
|
||||
return db.any(`SELECT * FROM blacklist`).then(res =>
|
||||
res.map(item => ({
|
||||
cryptoCode: item.crypto_code,
|
||||
address: item.address
|
||||
}))
|
||||
const getBlacklist = () =>
|
||||
db.any(
|
||||
`SELECT blacklist.address AS address, blacklist_messages.content AS blacklistMessage
|
||||
FROM blacklist JOIN blacklist_messages
|
||||
ON blacklist.blacklist_message_id = blacklist_messages.id`
|
||||
)
|
||||
|
||||
const deleteFromBlacklist = address => {
|
||||
const sql = `DELETE FROM blacklist WHERE address = $1`
|
||||
notifierQueries.clearBlacklistNotification(address)
|
||||
return db.none(sql, [address])
|
||||
}
|
||||
|
||||
// Delete row from blacklist table by crypto code and address
|
||||
const deleteFromBlacklist = (cryptoCode, address) => {
|
||||
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2`
|
||||
notifierQueries.clearBlacklistNotification(cryptoCode, address)
|
||||
return db.none(sql, [cryptoCode, address])
|
||||
}
|
||||
|
||||
const insertIntoBlacklist = (cryptoCode, address) => {
|
||||
const insertIntoBlacklist = address => {
|
||||
return db
|
||||
.none(
|
||||
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);',
|
||||
[cryptoCode, address]
|
||||
'INSERT INTO blacklist (address) VALUES ($1);',
|
||||
[address]
|
||||
)
|
||||
}
|
||||
|
||||
function blocked (address, cryptoCode) {
|
||||
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2`
|
||||
return db.any(sql, [address, cryptoCode])
|
||||
function blocked (address) {
|
||||
const sql = `SELECT address, content FROM blacklist b LEFT OUTER JOIN blacklist_messages bm ON bm.id = b.blacklist_message_id WHERE address = $1`
|
||||
return db.oneOrNone(sql, [address])
|
||||
}
|
||||
|
||||
function addToUsedAddresses (address, cryptoCode) {
|
||||
// ETH reuses addresses
|
||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||
function getMessages () {
|
||||
const sql = `SELECT * FROM blacklist_messages`
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2)`
|
||||
return db.oneOrNone(sql, [cryptoCode, address])
|
||||
function editBlacklistMessage (id, content) {
|
||||
const sql = `UPDATE blacklist_messages SET content = $1 WHERE id = $2 RETURNING id`
|
||||
return db.oneOrNone(sql, [content, id])
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
blocked,
|
||||
addToUsedAddresses,
|
||||
getBlacklist,
|
||||
deleteFromBlacklist,
|
||||
insertIntoBlacklist
|
||||
insertIntoBlacklist,
|
||||
getMessages,
|
||||
editBlacklistMessage
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const E = require('../error')
|
|||
|
||||
const PENDING_INTERVAL_MS = 60 * T.minutes
|
||||
|
||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||
const massageFields = ['direction', 'cryptoNetwork', 'bills', 'blacklisted', 'blacklistMessage', 'addressReuse', 'promoCodeApplied', 'validWalletScore', 'cashInFeeCrypto']
|
||||
const massageUpdateFields = _.concat(massageFields, 'cryptoAtoms')
|
||||
|
||||
const massage = _.flow(_.omit(massageFields),
|
||||
|
|
|
|||
|
|
@ -32,38 +32,41 @@ function post (machineTx, pi) {
|
|||
return cashInAtomic.atomic(machineTx, pi)
|
||||
.then(r => {
|
||||
const updatedTx = r.tx
|
||||
let blacklisted = false
|
||||
let addressReuse = false
|
||||
let walletScore = {}
|
||||
|
||||
const promises = [settingsLoader.loadLatestConfig()]
|
||||
|
||||
const isFirstPost = !r.tx.fiat || r.tx.fiat.isZero()
|
||||
if (isFirstPost) {
|
||||
promises.push(checkForBlacklisted(updatedTx), doesTxReuseAddress(updatedTx), getWalletScore(updatedTx, pi))
|
||||
promises.push(
|
||||
checkForBlacklisted(updatedTx),
|
||||
doesTxReuseAddress(updatedTx),
|
||||
getWalletScore(updatedTx, pi)
|
||||
)
|
||||
}
|
||||
|
||||
return Promise.all(promises)
|
||||
.then(([config, blacklistItems = false, isReusedAddress = false, fetchedWalletScore = null]) => {
|
||||
const rejectAddressReuse = configManager.getCompliance(config).rejectAddressReuse
|
||||
.then(([config, blacklisted = false, isReusedAddress = false, walletScore = null]) => {
|
||||
const { rejectAddressReuse } = configManager.getCompliance(config)
|
||||
const isBlacklisted = !!blacklisted
|
||||
|
||||
walletScore = fetchedWalletScore
|
||||
|
||||
if (_.some(it => it.address === updatedTx.toAddress)(blacklistItems)) {
|
||||
blacklisted = true
|
||||
if (isBlacklisted) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, false)
|
||||
} else if (isReusedAddress && rejectAddressReuse) {
|
||||
notifier.notifyIfActive('compliance', 'blacklistNotify', r.tx, true)
|
||||
addressReuse = true
|
||||
}
|
||||
return postProcess(r, pi, blacklisted, addressReuse, walletScore)
|
||||
return postProcess(r, pi, isBlacklisted, addressReuse, walletScore)
|
||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||
.then(_.flow(
|
||||
_.set('bills', machineTx.bills),
|
||||
_.set('blacklisted', isBlacklisted),
|
||||
_.set('blacklistMessage', blacklisted?.content),
|
||||
_.set('addressReuse', addressReuse),
|
||||
_.set('validWalletScore', _.isNil(walletScore) || walletScore.isValid),
|
||||
))
|
||||
})
|
||||
.then(changes => _.set('walletScore', _.isNil(walletScore) ? null : walletScore.score, changes))
|
||||
.then(changes => cashInLow.update(db, updatedTx, changes))
|
||||
.then(tx => _.set('bills', machineTx.bills, tx))
|
||||
.then(tx => _.set('blacklisted', blacklisted, tx))
|
||||
.then(tx => _.set('addressReuse', addressReuse, tx))
|
||||
.then(tx => _.set('validWalletScore', _.isNil(walletScore) ? true : walletScore.isValid, tx))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -94,7 +97,7 @@ function logActionById (action, _rec, txId) {
|
|||
}
|
||||
|
||||
function checkForBlacklisted (tx) {
|
||||
return blacklist.blocked(tx.toAddress, tx.cryptoCode)
|
||||
return blacklist.blocked(tx.toAddress)
|
||||
}
|
||||
|
||||
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {
|
||||
|
|
|
|||
|
|
@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
|
|||
|
||||
const resolvers = {
|
||||
Query: {
|
||||
blacklist: () => blacklist.getBlacklist()
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
blacklistMessages: () => blacklist.getMessages()
|
||||
},
|
||||
Mutation: {
|
||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address)
|
||||
deleteBlacklistRow: (...[, { address }]) =>
|
||||
blacklist.deleteFromBlacklist(address),
|
||||
insertBlacklistRow: (...[, { address }]) =>
|
||||
blacklist.insertIntoBlacklist(address),
|
||||
editBlacklistMessage: (...[, { id, content }]) =>
|
||||
blacklist.editBlacklistMessage(id, content)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,17 +2,26 @@ const { gql } = require('apollo-server-express')
|
|||
|
||||
const typeDef = gql`
|
||||
type Blacklist {
|
||||
cryptoCode: String!
|
||||
address: String!
|
||||
blacklistMessage: BlacklistMessage!
|
||||
}
|
||||
|
||||
type BlacklistMessage {
|
||||
id: ID
|
||||
label: String
|
||||
content: String
|
||||
allowToggle: Boolean
|
||||
}
|
||||
|
||||
type Query {
|
||||
blacklist: [Blacklist] @auth
|
||||
blacklistMessages: [BlacklistMessage] @auth
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth
|
||||
deleteBlacklistRow(address: String!): Blacklist @auth
|
||||
insertBlacklistRow(address: String!): Blacklist @auth
|
||||
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
18
migrations/1732881489395-coin-agnostic-blacklist.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
var sql = [
|
||||
`CREATE TABLE blacklist_temp (
|
||||
address TEXT NOT NULL UNIQUE
|
||||
)`,
|
||||
`INSERT INTO blacklist_temp (address) SELECT DISTINCT address FROM blacklist`,
|
||||
`DROP TABLE blacklist`,
|
||||
`ALTER TABLE blacklist_temp RENAME TO blacklist`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
24
migrations/1732881489396-advanced-blacklisting.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const uuid = require('uuid')
|
||||
|
||||
var db = require('./db')
|
||||
|
||||
exports.up = function (next) {
|
||||
const defaultMessageId = uuid.v4()
|
||||
|
||||
var sql = [
|
||||
`CREATE TABLE blacklist_messages (
|
||||
id UUID PRIMARY KEY,
|
||||
label TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
allow_toggle BOOLEAN NOT NULL DEFAULT true
|
||||
)`,
|
||||
`INSERT INTO blacklist_messages (id, label, content, allow_toggle) VALUES ('${defaultMessageId}', 'Suspicious address', 'This address may be associated with a deceptive offer or a prohibited group. Please make sure you''re using an address from your own wallet.', false)`,
|
||||
`ALTER TABLE blacklist ADD COLUMN blacklist_message_id UUID REFERENCES blacklist_messages(id) NOT NULL DEFAULT '${defaultMessageId}'`
|
||||
]
|
||||
|
||||
db.multi(sql, next)
|
||||
}
|
||||
|
||||
exports.down = function (next) {
|
||||
next()
|
||||
}
|
||||
|
|
@ -1,38 +1,31 @@
|
|||
import { useQuery, useMutation } from '@apollo/react-hooks'
|
||||
import { utils as coinUtils } from '@lamassu/coins'
|
||||
import { addressDetector } from '@lamassu/coins'
|
||||
import { Box, Dialog, DialogContent, DialogActions } 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 { HelpTooltip } from 'src/components/Tooltip'
|
||||
import {
|
||||
Link,
|
||||
Button,
|
||||
IconButton,
|
||||
SupportLinkButton
|
||||
} from 'src/components/buttons'
|
||||
import { Link, Button, IconButton } 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, H2, Label2, P, Info3, Info2 } from 'src/components/typography'
|
||||
import { H2, Label2, P, Info3, Info2 } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { ReactComponent as ReverseSettingsIcon } from 'src/styling/icons/circle buttons/settings/white.svg'
|
||||
import { ReactComponent as SettingsIcon } from 'src/styling/icons/circle buttons/settings/zodiac.svg'
|
||||
import { fromNamespace, toNamespace } from 'src/utils/config'
|
||||
|
||||
import styles from './Blacklist.styles'
|
||||
import BlackListAdvanced from './BlacklistAdvanced'
|
||||
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
|
||||
mutation DeleteBlacklistRow($address: String!) {
|
||||
deleteBlacklistRow(address: $address) {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +34,6 @@ const DELETE_ROW = gql`
|
|||
const GET_BLACKLIST = gql`
|
||||
query getBlacklistData {
|
||||
blacklist {
|
||||
cryptoCode
|
||||
address
|
||||
}
|
||||
cryptoCurrencies {
|
||||
|
|
@ -64,14 +56,32 @@ const GET_INFO = gql`
|
|||
`
|
||||
|
||||
const ADD_ROW = gql`
|
||||
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) {
|
||||
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) {
|
||||
cryptoCode
|
||||
mutation InsertBlacklistRow($address: String!) {
|
||||
insertBlacklistRow(address: $address) {
|
||||
address
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_BLACKLIST_MESSAGES = gql`
|
||||
query getBlacklistMessages {
|
||||
blacklistMessages {
|
||||
id
|
||||
label
|
||||
content
|
||||
allowToggle
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const EDIT_BLACKLIST_MESSAGE = gql`
|
||||
mutation editBlacklistMessage($id: ID, $content: String) {
|
||||
editBlacklistMessage(id: $id, content: $content) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
||||
const classes = useStyles()
|
||||
|
||||
|
|
@ -117,14 +127,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
|
|||
const Blacklist = () => {
|
||||
const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
|
||||
const { data: configData } = useQuery(GET_INFO)
|
||||
const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES)
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const [clickedItem, setClickedItem] = useState({
|
||||
code: 'BTC',
|
||||
display: 'Bitcoin'
|
||||
})
|
||||
const [errorMsg, setErrorMsg] = useState(null)
|
||||
const [editMessageError, setEditMessageError] = useState(null)
|
||||
const [deleteDialog, setDeleteDialog] = useState(false)
|
||||
const [confirmDialog, setConfirmDialog] = useState(false)
|
||||
const [advancedSettings, setAdvancedSettings] = useState(false)
|
||||
|
||||
const [deleteEntry] = useMutation(DELETE_ROW, {
|
||||
onError: ({ message }) => {
|
||||
|
|
@ -144,14 +153,14 @@ const Blacklist = () => {
|
|||
refetchQueries: () => ['getData']
|
||||
})
|
||||
|
||||
const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, {
|
||||
onError: e => setEditMessageError(e),
|
||||
refetchQueries: () => ['getBlacklistData']
|
||||
})
|
||||
|
||||
const classes = useStyles()
|
||||
|
||||
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
|
||||
const availableCurrencies = R.filter(
|
||||
coin => coinUtils.getEquivalentCode(coin.code) === coin.code
|
||||
)(R.path(['cryptoCurrencies'], blacklistResponse) ?? [])
|
||||
|
||||
const formattedData = groupByCode(blacklistData)
|
||||
|
||||
const complianceConfig =
|
||||
configData?.config && fromNamespace('compliance')(configData.config)
|
||||
|
|
@ -165,12 +174,8 @@ const Blacklist = () => {
|
|||
return saveConfig({ variables: { config } })
|
||||
}
|
||||
|
||||
const onClickSidebarItem = e => {
|
||||
setClickedItem({ code: e.code, display: e.display })
|
||||
}
|
||||
|
||||
const handleDeleteEntry = (cryptoCode, address) => {
|
||||
deleteEntry({ variables: { cryptoCode, address } })
|
||||
const handleDeleteEntry = address => {
|
||||
deleteEntry({ variables: { address } })
|
||||
}
|
||||
|
||||
const handleConfirmDialog = confirm => {
|
||||
|
|
@ -180,21 +185,21 @@ const Blacklist = () => {
|
|||
setConfirmDialog(false)
|
||||
}
|
||||
|
||||
const validateAddress = (cryptoCode, address) => {
|
||||
const validateAddress = address => {
|
||||
try {
|
||||
return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address))
|
||||
return !R.isEmpty(addressDetector.detectAddress(address).matches)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const addToBlacklist = async (cryptoCode, address) => {
|
||||
const addToBlacklist = async address => {
|
||||
setErrorMsg(null)
|
||||
if (!validateAddress(cryptoCode, address)) {
|
||||
if (!validateAddress(address)) {
|
||||
setErrorMsg('Invalid address')
|
||||
return
|
||||
}
|
||||
const res = await addEntry({ variables: { cryptoCode, address } })
|
||||
const res = await addEntry({ variables: { address } })
|
||||
if (!res.errors) {
|
||||
return setShowModal(false)
|
||||
}
|
||||
|
|
@ -208,6 +213,15 @@ const Blacklist = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const editBlacklistMessage = r => {
|
||||
editMessage({
|
||||
variables: {
|
||||
id: r.id,
|
||||
content: r.content
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PaperWalletDialog
|
||||
|
|
@ -217,32 +231,23 @@ const Blacklist = () => {
|
|||
setConfirmDialog(false)
|
||||
}}
|
||||
/>
|
||||
<TitleSection title="Blacklisted addresses">
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||
Blacklist new addresses
|
||||
</Link>
|
||||
</Box>
|
||||
</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>
|
||||
<TitleSection
|
||||
title="Blacklisted addresses"
|
||||
buttons={[
|
||||
{
|
||||
text: 'Advanced settings',
|
||||
icon: SettingsIcon,
|
||||
inverseIcon: ReverseSettingsIcon,
|
||||
toggle: setAdvancedSettings
|
||||
}
|
||||
]}>
|
||||
{!advancedSettings && (
|
||||
<Box display="flex" alignItems="center" justifyContent="flex-end">
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
mr="-140px">
|
||||
mr="15px">
|
||||
<P>Enable paper wallet (only)</P>
|
||||
<Switch
|
||||
checked={enablePaperWalletOnly}
|
||||
|
|
@ -268,7 +273,7 @@ const Blacklist = () => {
|
|||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
mr="-5px">
|
||||
mr="15px">
|
||||
<P>Reject reused addresses</P>
|
||||
<Switch
|
||||
checked={rejectAddressReuse}
|
||||
|
|
@ -284,20 +289,18 @@ const Blacklist = () => {
|
|||
they attempt to scan one that had been previously used for a
|
||||
transaction in your network.
|
||||
</P>
|
||||
<P>
|
||||
For details please read the relevant knowledgebase article:
|
||||
</P>
|
||||
<SupportLinkButton
|
||||
link="https://support.lamassu.is/hc/en-us/articles/360033622211-Reject-Address-Reuse"
|
||||
label="Reject Address Reuse"
|
||||
bottomSpace="1"
|
||||
/>
|
||||
</HelpTooltip>
|
||||
</Box>
|
||||
<Link color="primary" onClick={() => setShowModal(true)}>
|
||||
Blacklist new addresses
|
||||
</Link>
|
||||
</Box>
|
||||
)}
|
||||
</TitleSection>
|
||||
{!advancedSettings && (
|
||||
<div className={classes.content}>
|
||||
<BlacklistTable
|
||||
data={formattedData}
|
||||
selectedCoin={clickedItem}
|
||||
data={blacklistData}
|
||||
handleDeleteEntry={handleDeleteEntry}
|
||||
errorMessage={errorMsg}
|
||||
setErrorMessage={setErrorMsg}
|
||||
|
|
@ -305,7 +308,15 @@ const Blacklist = () => {
|
|||
setDeleteDialog={setDeleteDialog}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
{advancedSettings && (
|
||||
<BlackListAdvanced
|
||||
data={messagesResponse}
|
||||
editBlacklistMessage={editBlacklistMessage}
|
||||
mutationError={editMessageError}
|
||||
onClose={() => refetch()}
|
||||
/>
|
||||
)}
|
||||
{showModal && (
|
||||
<BlackListModal
|
||||
onClose={() => {
|
||||
|
|
@ -313,7 +324,6 @@ const Blacklist = () => {
|
|||
setShowModal(false)
|
||||
}}
|
||||
errorMsg={errorMsg}
|
||||
selectedCoin={clickedItem}
|
||||
addToBlacklist={addToBlacklist}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,23 @@ const styles = {
|
|||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
marginLeft: spacer * 6
|
||||
flex: 1
|
||||
},
|
||||
advancedForm: {
|
||||
'& > *': {
|
||||
marginTop: 20
|
||||
},
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%'
|
||||
},
|
||||
footer: {
|
||||
margin: [['auto', 0, spacer * 3, 'auto']]
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
margin: [['auto', 0, spacer * 3, 0]]
|
||||
},
|
||||
submit: {
|
||||
margin: [['auto', 0, 0, 'auto']]
|
||||
},
|
||||
modalTitle: {
|
||||
margin: [['auto', 0, 8.5, 'auto']]
|
||||
|
|
@ -54,6 +66,9 @@ const styles = {
|
|||
cancelButton: {
|
||||
marginRight: 8,
|
||||
padding: 0
|
||||
},
|
||||
resetToDefault: {
|
||||
width: 145
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
178
new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js
Normal file
178
new-lamassu-admin/src/pages/Blacklist/BlacklistAdvanced.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { Form, Formik, Field } from 'formik'
|
||||
import * as R from 'ramda'
|
||||
import React, { useState } from 'react'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
import ErrorMessage from 'src/components/ErrorMessage'
|
||||
import Modal from 'src/components/Modal'
|
||||
import { ActionButton, IconButton, Button } from 'src/components/buttons'
|
||||
import { TextInput } from 'src/components/inputs/formik'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { ReactComponent as DisabledDeleteIcon } from 'src/styling/icons/action/delete/disabled.svg'
|
||||
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
|
||||
import { ReactComponent as EditIcon } from 'src/styling/icons/action/edit/enabled.svg'
|
||||
import { ReactComponent as DefaultIconReverse } from 'src/styling/icons/button/retry/white.svg'
|
||||
import { ReactComponent as DefaultIcon } from 'src/styling/icons/button/retry/zodiac.svg'
|
||||
|
||||
import styles from './Blacklist.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const DEFAULT_MESSAGE = `This address may be associated with a deceptive offer or a prohibited group. Please make sure you're using an address from your own wallet.`
|
||||
|
||||
const getErrorMsg = (formikErrors, formikTouched, mutationError) => {
|
||||
if (mutationError) return 'Internal server error'
|
||||
if (!formikErrors || !formikTouched) return null
|
||||
if (formikErrors.event && formikTouched.event) return formikErrors.event
|
||||
if (formikErrors.message && formikTouched.message) return formikErrors.message
|
||||
return null
|
||||
}
|
||||
|
||||
const BlacklistAdvanced = ({
|
||||
data,
|
||||
editBlacklistMessage,
|
||||
onClose,
|
||||
mutationError
|
||||
}) => {
|
||||
const classes = useStyles()
|
||||
const [selectedMessage, setSelectedMessage] = useState(null)
|
||||
|
||||
const elements = [
|
||||
{
|
||||
name: 'label',
|
||||
header: 'Label',
|
||||
width: 250,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: it => R.path(['label'], it)
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
header: 'Content',
|
||||
width: 690,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: it => R.path(['content'], it)
|
||||
},
|
||||
{
|
||||
name: 'edit',
|
||||
header: 'Edit',
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
<IconButton
|
||||
className={classes.deleteButton}
|
||||
onClick={() => setSelectedMessage(it)}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'deleteButton',
|
||||
header: 'Delete',
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
<IconButton
|
||||
className={classes.deleteButton}
|
||||
disabled={
|
||||
!R.isNil(R.path(['allowToggle'], it)) &&
|
||||
!R.path(['allowToggle'], it)
|
||||
}>
|
||||
{R.path(['allowToggle'], it) ? (
|
||||
<DeleteIcon />
|
||||
) : (
|
||||
<DisabledDeleteIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const handleModalClose = () => {
|
||||
setSelectedMessage(null)
|
||||
}
|
||||
|
||||
const handleSubmit = values => {
|
||||
editBlacklistMessage(values)
|
||||
handleModalClose()
|
||||
!R.isNil(onClose) && onClose()
|
||||
}
|
||||
|
||||
const initialValues = {
|
||||
label: !R.isNil(selectedMessage) ? selectedMessage.label : '',
|
||||
content: !R.isNil(selectedMessage) ? selectedMessage.content : ''
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
label: Yup.string().required('A label is required!'),
|
||||
content: Yup.string()
|
||||
.required('The message content is required!')
|
||||
.trim()
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={R.path(['blacklistMessages'], data)}
|
||||
elements={elements}
|
||||
emptyText="No blacklisted addresses so far"
|
||||
name="blacklistTable"
|
||||
/>
|
||||
{selectedMessage && (
|
||||
<Modal
|
||||
title={`Blacklist message - ${selectedMessage?.label}`}
|
||||
open={true}
|
||||
width={676}
|
||||
height={400}
|
||||
handleClose={handleModalClose}>
|
||||
<Formik
|
||||
validateOnBlur={false}
|
||||
validateOnChange={false}
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={values =>
|
||||
handleSubmit({ id: selectedMessage.id, ...values })
|
||||
}>
|
||||
{({ errors, touched, setFieldValue }) => (
|
||||
<Form className={classes.advancedForm}>
|
||||
<ActionButton
|
||||
color="primary"
|
||||
Icon={DefaultIcon}
|
||||
InverseIcon={DefaultIconReverse}
|
||||
className={classes.resetToDefault}
|
||||
type="button"
|
||||
onClick={() => setFieldValue('content', DEFAULT_MESSAGE)}>
|
||||
Reset to default
|
||||
</ActionButton>
|
||||
<Field
|
||||
name="content"
|
||||
label="Message content"
|
||||
fullWidth
|
||||
multiline={true}
|
||||
rows={6}
|
||||
component={TextInput}
|
||||
/>
|
||||
<div className={classes.footer}>
|
||||
{getErrorMsg(errors, touched, mutationError) && (
|
||||
<ErrorMessage>
|
||||
{getErrorMsg(errors, touched, mutationError)}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<Button type="submit" className={classes.submit}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlacklistAdvanced
|
||||
|
|
@ -14,31 +14,14 @@ import { H3 } from 'src/components/typography'
|
|||
import styles from './Blacklist.styles'
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const BlackListModal = ({
|
||||
onClose,
|
||||
selectedCoin,
|
||||
addToBlacklist,
|
||||
errorMsg
|
||||
}) => {
|
||||
const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => {
|
||||
const classes = useStyles()
|
||||
const handleAddToBlacklist = address => {
|
||||
if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) {
|
||||
address = 'bitcoincash:' + address
|
||||
}
|
||||
addToBlacklist(selectedCoin.code, address)
|
||||
}
|
||||
const placeholderAddress = {
|
||||
BTC: '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD',
|
||||
ETH: '0x71C7656EC7ab88b098defB751B7401B5f6d8976F',
|
||||
LTC: 'LPKvbjwV1Kaksktzkr7TMK3FQtQEEe6Wqa',
|
||||
DASH: 'XqQ7gU8eM76rEfey726cJpT2RGKyJyBrcn',
|
||||
ZEC: 't1KGyyv24eL354C9gjveBGEe8Xz9UoPKvHR',
|
||||
BCH: 'qrd6za97wm03lfyg82w0c9vqgc727rhemg5yd9k3dm',
|
||||
USDT: '0x5754284f345afc66a98fbb0a0afe71e0f007b949',
|
||||
XMR:
|
||||
'888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H'
|
||||
addToBlacklist(address)
|
||||
}
|
||||
|
||||
const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closeOnBackdropClick={true}
|
||||
|
|
@ -61,26 +44,20 @@ const BlackListModal = ({
|
|||
handleAddToBlacklist(address.trim())
|
||||
}}>
|
||||
<Form id="address-form">
|
||||
<H3 className={classes.modalTitle}>
|
||||
{selectedCoin.display
|
||||
? `Blacklist ${R.toLower(selectedCoin.display)} address`
|
||||
: ''}
|
||||
</H3>
|
||||
<H3 className={classes.modalTitle}>Blacklist new address</H3>
|
||||
<Field
|
||||
name="address"
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
label="Paste new address to blacklist here"
|
||||
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`}
|
||||
placeholder={`ex: ${placeholderAddress}`}
|
||||
component={TextInput}
|
||||
/>
|
||||
{!R.isNil(errorMsg) && (
|
||||
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
|
||||
)}
|
||||
</Form>
|
||||
</Formik>
|
||||
<div className={classes.footer}>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
{!R.isNil(errorMsg) && <ErrorMessage>{errorMsg}</ErrorMessage>}
|
||||
<Box className={classes.submit}>
|
||||
<Link type="submit" form="address-form">
|
||||
Blacklist address
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import React, { useState } from 'react'
|
|||
import { DeleteDialog } from 'src/components/DeleteDialog'
|
||||
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'
|
||||
|
||||
|
|
@ -15,7 +14,6 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const BlacklistTable = ({
|
||||
data,
|
||||
selectedCoin,
|
||||
handleDeleteEntry,
|
||||
errorMessage,
|
||||
setErrorMessage,
|
||||
|
|
@ -29,8 +27,8 @@ const BlacklistTable = ({
|
|||
const elements = [
|
||||
{
|
||||
name: 'address',
|
||||
header: <Label1 className={classes.white}>{'Addresses'}</Label1>,
|
||||
width: 800,
|
||||
header: 'Address',
|
||||
width: 1070,
|
||||
textAlign: 'left',
|
||||
size: 'sm',
|
||||
view: it => (
|
||||
|
|
@ -41,7 +39,7 @@ const BlacklistTable = ({
|
|||
},
|
||||
{
|
||||
name: 'deleteButton',
|
||||
header: <Label1 className={classes.white}>{'Delete'}</Label1>,
|
||||
header: 'Delete',
|
||||
width: 130,
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
|
|
@ -57,14 +55,11 @@ const BlacklistTable = ({
|
|||
)
|
||||
}
|
||||
]
|
||||
const dataToShow = selectedCoin
|
||||
? data[selectedCoin.code]
|
||||
: data[R.keys(data)[0]]
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataTable
|
||||
data={dataToShow}
|
||||
data={data}
|
||||
elements={elements}
|
||||
emptyText="No blacklisted addresses so far"
|
||||
name="blacklistTable"
|
||||
|
|
@ -77,10 +72,7 @@ const BlacklistTable = ({
|
|||
}}
|
||||
onConfirmed={() => {
|
||||
setErrorMessage(null)
|
||||
handleDeleteEntry(
|
||||
R.path(['cryptoCode'], toBeDeleted),
|
||||
R.path(['address'], toBeDeleted)
|
||||
)
|
||||
handleDeleteEntry(R.path(['address'], toBeDeleted))
|
||||
}}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue