feat: add advanced address blacklisting

This commit is contained in:
Sérgio Salgado 2022-10-06 17:18:12 +01:00 committed by Rafael
parent 473bb15c24
commit 8af7c97c16
7 changed files with 367 additions and 66 deletions

View file

@ -1,16 +1,16 @@
const _ = require('lodash/fp')
const db = require('./db') const db = require('./db')
const notifierQueries = require('./notifier/queries') const notifierQueries = require('./notifier/queries')
// Get all blacklist rows from the DB "blacklist" table that were manually inserted by the operator
const getBlacklist = () => { const getBlacklist = () => {
return db.any(`SELECT * FROM blacklist`).then(res => const blacklistSql = `SELECT * FROM blacklist`
res.map(item => ({ const messagesSql = `SELECT * FROM blacklist_messages`
address: item.address return Promise.all([db.any(blacklistSql), db.any(messagesSql)])
})) .then(([blacklist, messages]) => Promise.all([_.map(_.mapKeys(_.camelCase), blacklist), _.map(_.mapKeys(_.camelCase), messages)]))
) .then(([blacklist, messages]) => _.map(it => ({ ...it, blacklistMessage: _.find(ite => it.blacklistMessageId === ite.id, messages) }), blacklist))
} }
// Delete row from blacklist table by crypto code and address
const deleteFromBlacklist = address => { const deleteFromBlacklist = address => {
const sql = `DELETE FROM blacklist WHERE address = $1` const sql = `DELETE FROM blacklist WHERE address = $1`
notifierQueries.clearBlacklistNotification(address) notifierQueries.clearBlacklistNotification(address)
@ -38,10 +38,22 @@ function addToUsedAddresses (address) {
return db.oneOrNone(sql, [address]) return db.oneOrNone(sql, [address])
} }
function getMessages () {
const sql = `SELECT * FROM blacklist_messages`
return db.any(sql)
}
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 = { module.exports = {
blocked, blocked,
addToUsedAddresses, addToUsedAddresses,
getBlacklist, getBlacklist,
deleteFromBlacklist, deleteFromBlacklist,
insertIntoBlacklist insertIntoBlacklist,
getMessages,
editBlacklistMessage
} }

View file

@ -2,13 +2,16 @@ const blacklist = require('../../../blacklist')
const resolvers = { const resolvers = {
Query: { Query: {
blacklist: () => blacklist.getBlacklist() blacklist: () => blacklist.getBlacklist(),
blacklistMessages: () => blacklist.getMessages()
}, },
Mutation: { Mutation: {
deleteBlacklistRow: (...[, { address }]) => deleteBlacklistRow: (...[, { address }]) =>
blacklist.deleteFromBlacklist(address), blacklist.deleteFromBlacklist(address),
insertBlacklistRow: (...[, { address }]) => insertBlacklistRow: (...[, { address }]) =>
blacklist.insertIntoBlacklist(address) blacklist.insertIntoBlacklist(address),
editBlacklistMessage: (...[, { id, content }]) =>
blacklist.editBlacklistMessage(id, content)
} }
} }

View file

@ -3,15 +3,25 @@ const { gql } = require('apollo-server-express')
const typeDef = gql` const typeDef = gql`
type Blacklist { type Blacklist {
address: String! address: String!
blacklistMessage: BlacklistMessage!
}
type BlacklistMessage {
id: ID
label: String
content: String
allowToggle: Boolean
} }
type Query { type Query {
blacklist: [Blacklist] @auth blacklist: [Blacklist] @auth
blacklistMessages: [BlacklistMessage] @auth
} }
type Mutation { type Mutation {
deleteBlacklistRow(address: String!): Blacklist @auth deleteBlacklistRow(address: String!): Blacklist @auth
insertBlacklistRow(address: String!): Blacklist @auth insertBlacklistRow(address: String!): Blacklist @auth
editBlacklistMessage(id: ID, content: String): BlacklistMessage @auth
} }
` `

View file

@ -0,0 +1,26 @@
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)`,
`UPDATE blacklist SET blacklist_message_id = '${defaultMessageId}'`,
`ALTER TABLE blacklist ALTER COLUMN blacklist_message_id SET NOT NULL`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -1,7 +1,6 @@
import { useQuery, useMutation } from '@apollo/react-hooks' import { useQuery, useMutation } from '@apollo/react-hooks'
import { addressDetector } from '@lamassu/coins' import { addressDetector } from '@lamassu/coins'
import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core' import { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import * as R from 'ramda' import * as R from 'ramda'
@ -13,9 +12,12 @@ import { Switch } from 'src/components/inputs'
import TitleSection from 'src/components/layout/TitleSection' import TitleSection from 'src/components/layout/TitleSection'
import { 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 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 { fromNamespace, toNamespace } from 'src/utils/config'
import styles from './Blacklist.styles' import styles from './Blacklist.styles'
import BlackListAdvanced from './BlacklistAdvanced'
import BlackListModal from './BlacklistModal' import BlackListModal from './BlacklistModal'
import BlacklistTable from './BlacklistTable' import BlacklistTable from './BlacklistTable'
@ -33,6 +35,11 @@ const GET_BLACKLIST = gql`
query getBlacklistData { query getBlacklistData {
blacklist { blacklist {
address address
blacklistMessage {
id
label
content
}
} }
cryptoCurrencies { cryptoCurrencies {
display display
@ -61,6 +68,25 @@ const ADD_ROW = gql`
} }
` `
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 PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
const classes = useStyles() const classes = useStyles()
@ -106,10 +132,13 @@ const PaperWalletDialog = ({ onConfirmed, onDissmised, open, props }) => {
const Blacklist = () => { const Blacklist = () => {
const { data: blacklistResponse } = useQuery(GET_BLACKLIST) const { data: blacklistResponse } = useQuery(GET_BLACKLIST)
const { data: configData } = useQuery(GET_INFO) const { data: configData } = useQuery(GET_INFO)
const { data: messagesResponse, refetch } = useQuery(GET_BLACKLIST_MESSAGES)
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [errorMsg, setErrorMsg] = useState(null) const [errorMsg, setErrorMsg] = useState(null)
const [editMessageError, setEditMessageError] = useState(null)
const [deleteDialog, setDeleteDialog] = useState(false) const [deleteDialog, setDeleteDialog] = useState(false)
const [confirmDialog, setConfirmDialog] = useState(false) const [confirmDialog, setConfirmDialog] = useState(false)
const [advancedSettings, setAdvancedSettings] = useState(false)
const [deleteEntry] = useMutation(DELETE_ROW, { const [deleteEntry] = useMutation(DELETE_ROW, {
onError: ({ message }) => { onError: ({ message }) => {
@ -129,6 +158,11 @@ const Blacklist = () => {
refetchQueries: () => ['getData'] refetchQueries: () => ['getData']
}) })
const [editMessage] = useMutation(EDIT_BLACKLIST_MESSAGE, {
onError: e => setEditMessageError(e),
refetchQueries: () => ['getBlacklistData']
})
const classes = useStyles() const classes = useStyles()
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? [] const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? []
@ -184,6 +218,15 @@ const Blacklist = () => {
} }
} }
const editBlacklistMessage = r => {
editMessage({
variables: {
id: r.id,
content: r.content
}
})
}
return ( return (
<> <>
<PaperWalletDialog <PaperWalletDialog
@ -193,7 +236,17 @@ const Blacklist = () => {
setConfirmDialog(false) setConfirmDialog(false)
}} }}
/> />
<TitleSection title="Blacklisted addresses"> <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="flex-end">
<Box <Box
display="flex" display="flex"
@ -238,8 +291,8 @@ const Blacklist = () => {
<HelpTooltip width={304}> <HelpTooltip width={304}>
<P> <P>
The "Reject reused addresses" option means that all addresses The "Reject reused addresses" option means that all addresses
that are used once will be automatically rejected if there's an that are used once will be automatically rejected if there's
attempt to use them again on a new transaction. an attempt to use them again on a new transaction.
</P> </P>
</HelpTooltip> </HelpTooltip>
</Box> </Box>
@ -247,8 +300,9 @@ const Blacklist = () => {
Blacklist new addresses Blacklist new addresses
</Link> </Link>
</Box> </Box>
)}
</TitleSection> </TitleSection>
<Grid container className={classes.grid}> {!advancedSettings && (
<div className={classes.content}> <div className={classes.content}>
<BlacklistTable <BlacklistTable
data={blacklistData} data={blacklistData}
@ -259,7 +313,15 @@ const Blacklist = () => {
setDeleteDialog={setDeleteDialog} setDeleteDialog={setDeleteDialog}
/> />
</div> </div>
</Grid> )}
{advancedSettings && (
<BlackListAdvanced
data={messagesResponse}
editBlacklistMessage={editBlacklistMessage}
mutationError={editMessageError}
onClose={() => refetch()}
/>
)}
{showModal && ( {showModal && (
<BlackListModal <BlackListModal
onClose={() => { onClose={() => {

View file

@ -9,6 +9,14 @@ const styles = {
flexDirection: 'column', flexDirection: 'column',
flex: 1 flex: 1
}, },
advancedForm: {
'& > *': {
marginTop: 20
},
display: 'flex',
flexDirection: 'column',
height: '100%'
},
footer: { footer: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
@ -58,6 +66,9 @@ const styles = {
cancelButton: { cancelButton: {
marginRight: 8, marginRight: 8,
padding: 0 padding: 0
},
resetToDefault: {
width: 145
} }
} }

View file

@ -0,0 +1,177 @@
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 (!formikErrors || !formikTouched) return null
if (mutationError) return 'Internal server error'
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']) && !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