From fd6f1a2fe04c7ee0a2fdf0430ae82b66e930e4d8 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 16 Nov 2020 15:14:51 +0000 Subject: [PATCH] feat: create blacklist page --- lib/blacklist.js | 54 ++++-- lib/new-admin/graphql/schema.js | 25 ++- .../src/pages/Blacklist/Blacklist.js | 183 ++++++++++++++++++ .../src/pages/Blacklist/Blacklist.styles.js | 39 ++++ .../src/pages/Blacklist/BlacklistModal.js | 76 ++++++++ .../src/pages/Blacklist/BlacklistTable.js | 60 ++++++ .../src/pages/Blacklist/index.js | 3 + new-lamassu-admin/src/routing/routes.js | 7 + .../src/styling/icons/menu/search-zodiac.svg | 13 ++ 9 files changed, 446 insertions(+), 14 deletions(-) create mode 100644 new-lamassu-admin/src/pages/Blacklist/Blacklist.js create mode 100644 new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js create mode 100644 new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js create mode 100644 new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js create mode 100644 new-lamassu-admin/src/pages/Blacklist/index.js create mode 100644 new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg diff --git a/lib/blacklist.js b/lib/blacklist.js index e66ac3ff..4415675f 100644 --- a/lib/blacklist.js +++ b/lib/blacklist.js @@ -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 +} diff --git a/lib/new-admin/graphql/schema.js b/lib/new-admin/graphql/schema.js index 09e7645b..b474e550 100644 --- a/lib/new-admin/graphql/schema.js +++ b/lib/new-admin/graphql/schema.js @@ -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 @@ -192,7 +199,7 @@ const typeDefs = gql` customerId: ID txVersion: Int! termsAccepted: Boolean - commissionPercentage: String + commissionPercentage: String rawTickerPrice: String isPaperWallet: Boolean customerPhone: String @@ -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) } } diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js new file mode 100644 index 00000000..3f673d9c --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.js @@ -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 ( + <> + + Blacklist new addresses + + + it.display} + onClick={onClickSidebarItem} + /> +
+ +

+ {clickedItem.display + ? `${clickedItem.display} blacklisted addresses` + : ''}{' '} +

+ +

Reject reused addresses

+ { + addressReuseSave({ rejectAddressReuse: event.target.checked }) + }} + value={rejectAddressReuse} + /> + {rejectAddressReuse ? 'On' : 'Off'} + +

+ 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. +

+
+
+
+ +
+
+ {showModal && ( + setShowModal(false)} + selectedCoin={clickedItem} + addToBlacklist={addToBlacklist} + /> + )} + + ) +} + +export default Blacklist diff --git a/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js new file mode 100644 index 00000000..b4374d41 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/Blacklist.styles.js @@ -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 + } +} diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js new file mode 100644 index 00000000..1e6877e2 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistModal.js @@ -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 ( + + { + handleAddToBlacklist(address) + resetForm() + }}> +
+

+ {selectedCoin.display + ? `Blacklist ${R.toLower(selectedCoin.display)} address` + : ''} +

+ + +
+
+ + Blacklist address + +
+
+ ) +} + +export default BlackListModal diff --git a/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js b/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js new file mode 100644 index 00000000..95b66110 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/BlacklistTable.js @@ -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: {'Addresses'}, + width: 800, + textAlign: 'left', + size: 'sm', + view: it => ( +
+ {R.path(['address'], it)} +
+ ) + }, + { + name: 'deleteButton', + header: {'Delete'}, + width: 130, + textAlign: 'center', + size: 'sm', + view: it => ( + + handleDeleteEntry( + R.path(['cryptoCode'], it), + R.path(['address'], it) + ) + }> + + + ) + } + ] + const dataToShow = selectedCoin + ? data[selectedCoin.code] + : data[R.keys(data)[0]] + + return ( + + ) +} + +export default BlacklistTable diff --git a/new-lamassu-admin/src/pages/Blacklist/index.js b/new-lamassu-admin/src/pages/Blacklist/index.js new file mode 100644 index 00000000..4a7e8ce4 --- /dev/null +++ b/new-lamassu-admin/src/pages/Blacklist/index.js @@ -0,0 +1,3 @@ +import Blacklist from './Blacklist' + +export default Blacklist diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index 43f5deb6..404ade02 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -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', diff --git a/new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg b/new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg new file mode 100644 index 00000000..99fb94be --- /dev/null +++ b/new-lamassu-admin/src/styling/icons/menu/search-zodiac.svg @@ -0,0 +1,13 @@ + + + + Created with Sketch. + + + + + + + + + \ No newline at end of file