feat: create blacklist page
This commit is contained in:
parent
8a9de5d185
commit
fd6f1a2fe0
9 changed files with 446 additions and 14 deletions
|
|
@ -1,22 +1,52 @@
|
|||
const db = require('./db')
|
||||
|
||||
function blocked (address, cryptoCode) {
|
||||
const sql = `select * from blacklist where address = $1 and crypto_code = $2`
|
||||
return db.any(sql, [
|
||||
address,
|
||||
cryptoCode
|
||||
])
|
||||
// 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
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
if (cryptoCode === 'ETH') return Promise.resolve()
|
||||
|
||||
const sql = `insert into blacklist(crypto_code, address, created_by_operator) values ($1, $2, 'f')`
|
||||
return db.oneOrNone(sql, [
|
||||
cryptoCode,
|
||||
address
|
||||
])
|
||||
return db.oneOrNone(sql, [cryptoCode, address])
|
||||
}
|
||||
|
||||
module.exports = { blocked, addToUsedAddresses }
|
||||
module.exports = {
|
||||
blocked,
|
||||
addToUsedAddresses,
|
||||
getBlacklist,
|
||||
deleteFromBlacklist,
|
||||
insertIntoBlacklist
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const { machineAction } = require('../machines')
|
|||
const logs = require('../../logs')
|
||||
const settingsLoader = require('../../new-settings-loader')
|
||||
const tokenManager = require('../../token-manager')
|
||||
const blacklist = require('../../blacklist')
|
||||
|
||||
const serverVersion = require('../../../package.json').version
|
||||
|
||||
|
|
@ -18,7 +19,13 @@ const funding = require('../funding')
|
|||
const supervisor = require('../supervisor')
|
||||
const serverLogs = require('../server-logs')
|
||||
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`
|
||||
scalar JSON
|
||||
|
|
@ -206,6 +213,12 @@ const typeDefs = gql`
|
|||
machineName: String
|
||||
}
|
||||
|
||||
type Blacklist {
|
||||
createdByOperator: Boolean!
|
||||
cryptoCode: String!
|
||||
address: String!
|
||||
}
|
||||
|
||||
type Query {
|
||||
countries: [Country]
|
||||
currencies: [Currency]
|
||||
|
|
@ -226,6 +239,7 @@ const typeDefs = gql`
|
|||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String
|
||||
accounts: JSONObject
|
||||
config: JSONObject
|
||||
blacklist: [Blacklist]
|
||||
userTokens: [UserToken]
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +260,8 @@ const typeDefs = gql`
|
|||
createPairingTotem(name: String!): String
|
||||
saveAccounts(accounts: JSONObject): JSONObject
|
||||
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),
|
||||
config: () => settingsLoader.loadLatestConfigOrNone(),
|
||||
accounts: () => settingsLoader.loadAccounts(),
|
||||
blacklist: () => blacklist.getBlacklist(),
|
||||
userTokens: () => tokenManager.getTokenList()
|
||||
},
|
||||
Mutation: {
|
||||
|
|
@ -297,6 +314,10 @@ const resolvers = {
|
|||
notify()
|
||||
return it
|
||||
}),
|
||||
deleteBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.deleteFromBlacklist(cryptoCode, address),
|
||||
insertBlacklistRow: (...[, { cryptoCode, address }]) =>
|
||||
blacklist.insertIntoBlacklist(cryptoCode, address),
|
||||
revokeToken: (...[, { token }]) => tokenManager.revokeToken(token)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
183
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal file
183
new-lamassu-admin/src/pages/Blacklist/Blacklist.js
Normal 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
|
||||
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
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
|
||||
import { AppContext } from 'src/App'
|
||||
import AuthRegister from 'src/pages/AuthRegister'
|
||||
import Blacklist from 'src/pages/Blacklist'
|
||||
import Cashout from 'src/pages/Cashout'
|
||||
import Commissions from 'src/pages/Commissions'
|
||||
import { Customers, CustomerProfile } from 'src/pages/Customers'
|
||||
|
|
@ -148,6 +149,12 @@ const tree = [
|
|||
route: '/compliance/customers',
|
||||
component: Customers
|
||||
},
|
||||
{
|
||||
key: 'blacklist',
|
||||
label: 'Blacklist',
|
||||
route: '/compliance/blacklist',
|
||||
component: Blacklist
|
||||
},
|
||||
{
|
||||
key: 'customer',
|
||||
route: '/compliance/customer/:id',
|
||||
|
|
|
|||
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 |
Loading…
Add table
Add a link
Reference in a new issue