feat: create blacklist page

This commit is contained in:
Cesar 2020-11-16 15:14:51 +00:00 committed by Josh Harvey
parent 8a9de5d185
commit fd6f1a2fe0
9 changed files with 446 additions and 14 deletions

View file

@ -1,22 +1,52 @@
const db = require('./db') const db = require('./db')
function blocked (address, cryptoCode) { // Get all blacklist rows from the DB "blacklist" table
const sql = `select * from blacklist where address = $1 and crypto_code = $2` const getBlacklist = () => {
return db.any(sql, [ return db.any('select * from blacklist').then(res =>
address, res.map(item => ({
cryptoCode cryptoCode: item.crypto_code,
]) address: item.address,
createdByOperator: item.created_by_operator
}))
)
} }
function addToUsedAddresses (address, cryptoCode) { // 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) {
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
return db.any(sql, [address, cryptoCode])
}
function addToUsedAddresses(address, cryptoCode) {
// ETH reuses addresses // ETH reuses addresses
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
}

View file

@ -10,6 +10,7 @@ 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 tokenManager = require('../../token-manager')
const blacklist = require('../../blacklist')
const serverVersion = require('../../../package.json').version const serverVersion = require('../../../package.json').version
@ -18,7 +19,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
@ -192,7 +199,7 @@ const typeDefs = gql`
customerId: ID customerId: ID
txVersion: Int! txVersion: Int!
termsAccepted: Boolean termsAccepted: Boolean
commissionPercentage: String commissionPercentage: String
rawTickerPrice: String rawTickerPrice: String
isPaperWallet: Boolean isPaperWallet: Boolean
customerPhone: String customerPhone: String
@ -206,6 +213,12 @@ const typeDefs = gql`
machineName: String machineName: String
} }
type Blacklist {
createdByOperator: Boolean!
cryptoCode: String!
address: String!
}
type Query { type Query {
countries: [Country] countries: [Country]
currencies: [Currency] currencies: [Currency]
@ -226,6 +239,7 @@ 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] userTokens: [UserToken]
} }
@ -246,6 +260,8 @@ const typeDefs = gql`
createPairingTotem(name: String!): String createPairingTotem(name: String!): String
saveAccounts(accounts: JSONObject): JSONObject saveAccounts(accounts: JSONObject): JSONObject
revokeToken(token: String!): UserToken revokeToken(token: String!): UserToken
deleteBlacklistRow(cryptoCode: String, address: String): Blacklist
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist
} }
` `
@ -285,6 +301,7 @@ const resolvers = {
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() userTokens: () => tokenManager.getTokenList()
}, },
Mutation: { Mutation: {
@ -297,6 +314,10 @@ const resolvers = {
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) revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
} }
} }

View file

@ -0,0 +1,183 @@
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 toggleModal = () => setShowModal(!showModal)
const addToBlacklist = (cryptoCode, address) => {
addEntry({ variables: { cryptoCode, address } })
}
return (
<>
<TitleSection title="Blacklisted addresses">
<Link onClick={toggleModal}>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

View 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
}
}

View 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

View 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

View file

@ -0,0 +1,3 @@
import Blacklist from './Blacklist'
export default Blacklist

View file

@ -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'
@ -148,6 +149,12 @@ 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',

View 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