feat: turn address into a coin-agnostic solution

This commit is contained in:
Sérgio Salgado 2022-10-04 19:28:18 +01:00 committed by Rafael
parent c5f3caab2f
commit 473bb15c24
9 changed files with 122 additions and 177 deletions

View file

@ -5,38 +5,37 @@ const notifierQueries = require('./notifier/queries')
const getBlacklist = () => { const getBlacklist = () => {
return db.any(`SELECT * FROM blacklist`).then(res => return db.any(`SELECT * FROM blacklist`).then(res =>
res.map(item => ({ res.map(item => ({
cryptoCode: item.crypto_code,
address: item.address address: item.address
})) }))
) )
} }
// Delete row from blacklist table by crypto code and address // Delete row from blacklist table by crypto code and address
const deleteFromBlacklist = (cryptoCode, address) => { const deleteFromBlacklist = address => {
const sql = `DELETE FROM blacklist WHERE crypto_code = $1 AND address = $2` const sql = `DELETE FROM blacklist WHERE address = $1`
notifierQueries.clearBlacklistNotification(cryptoCode, address) notifierQueries.clearBlacklistNotification(address)
return db.none(sql, [cryptoCode, address]) return db.none(sql, [address])
} }
const insertIntoBlacklist = (cryptoCode, address) => { const insertIntoBlacklist = address => {
return db return db
.none( .none(
'INSERT INTO blacklist (crypto_code, address) VALUES ($1, $2);', 'INSERT INTO blacklist (address) VALUES ($1);',
[cryptoCode, address] [address]
) )
} }
function blocked (address, cryptoCode) { function blocked (address) {
const sql = `SELECT * FROM blacklist WHERE address = $1 AND crypto_code = $2` const sql = `SELECT * FROM blacklist WHERE address = $1`
return db.any(sql, [address, cryptoCode]) return db.any(sql, [address])
} }
function addToUsedAddresses (address, cryptoCode) { function addToUsedAddresses (address) {
// 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) VALUES ($1, $2)` const sql = `INSERT INTO blacklist (address) VALUES ($1)`
return db.oneOrNone(sql, [cryptoCode, address]) return db.oneOrNone(sql, [address])
} }
module.exports = { module.exports = {

View file

@ -94,7 +94,7 @@ function logActionById (action, _rec, txId) {
} }
function checkForBlacklisted (tx) { function checkForBlacklisted (tx) {
return blacklist.blocked(tx.toAddress, tx.cryptoCode) return blacklist.blocked(tx.toAddress)
} }
function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) { function postProcess (r, pi, isBlacklisted, addressReuse, walletScore) {

View file

@ -5,10 +5,10 @@ const resolvers = {
blacklist: () => blacklist.getBlacklist() blacklist: () => blacklist.getBlacklist()
}, },
Mutation: { Mutation: {
deleteBlacklistRow: (...[, { cryptoCode, address }]) => deleteBlacklistRow: (...[, { address }]) =>
blacklist.deleteFromBlacklist(cryptoCode, address), blacklist.deleteFromBlacklist(address),
insertBlacklistRow: (...[, { cryptoCode, address }]) => insertBlacklistRow: (...[, { address }]) =>
blacklist.insertIntoBlacklist(cryptoCode, address) blacklist.insertIntoBlacklist(address)
} }
} }

View file

@ -2,7 +2,6 @@ const { gql } = require('apollo-server-express')
const typeDef = gql` const typeDef = gql`
type Blacklist { type Blacklist {
cryptoCode: String!
address: String! address: String!
} }
@ -11,8 +10,8 @@ const typeDef = gql`
} }
type Mutation { type Mutation {
deleteBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth deleteBlacklistRow(address: String!): Blacklist @auth
insertBlacklistRow(cryptoCode: String!, address: String!): Blacklist @auth insertBlacklistRow(address: String!): Blacklist @auth
} }
` `

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

View file

@ -1,5 +1,5 @@
import { useQuery, useMutation } from '@apollo/react-hooks' 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 { Box, Dialog, DialogContent, DialogActions } from '@material-ui/core'
import Grid from '@material-ui/core/Grid' import Grid from '@material-ui/core/Grid'
import { makeStyles } from '@material-ui/core/styles' import { makeStyles } from '@material-ui/core/styles'
@ -8,16 +8,10 @@ import * as R from 'ramda'
import React, { useState } from 'react' import React, { useState } from 'react'
import { HelpTooltip } from 'src/components/Tooltip' import { HelpTooltip } from 'src/components/Tooltip'
import { import { Link, Button, IconButton } from 'src/components/buttons'
Link,
Button,
IconButton,
SupportLinkButton
} from 'src/components/buttons'
import { Switch } from 'src/components/inputs' import { Switch } from 'src/components/inputs'
import Sidebar from 'src/components/layout/Sidebar'
import TitleSection from 'src/components/layout/TitleSection' 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 CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
import { fromNamespace, toNamespace } from 'src/utils/config' import { fromNamespace, toNamespace } from 'src/utils/config'
@ -27,12 +21,9 @@ import BlacklistTable from './BlacklistTable'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const groupByCode = R.groupBy(obj => obj.cryptoCode)
const DELETE_ROW = gql` const DELETE_ROW = gql`
mutation DeleteBlacklistRow($cryptoCode: String!, $address: String!) { mutation DeleteBlacklistRow($address: String!) {
deleteBlacklistRow(cryptoCode: $cryptoCode, address: $address) { deleteBlacklistRow(address: $address) {
cryptoCode
address address
} }
} }
@ -41,7 +32,6 @@ const DELETE_ROW = gql`
const GET_BLACKLIST = gql` const GET_BLACKLIST = gql`
query getBlacklistData { query getBlacklistData {
blacklist { blacklist {
cryptoCode
address address
} }
cryptoCurrencies { cryptoCurrencies {
@ -64,9 +54,8 @@ const GET_INFO = gql`
` `
const ADD_ROW = gql` const ADD_ROW = gql`
mutation InsertBlacklistRow($cryptoCode: String!, $address: String!) { mutation InsertBlacklistRow($address: String!) {
insertBlacklistRow(cryptoCode: $cryptoCode, address: $address) { insertBlacklistRow(address: $address) {
cryptoCode
address address
} }
} }
@ -118,10 +107,6 @@ 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 [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false)
const [clickedItem, setClickedItem] = useState({
code: 'BTC',
display: 'Bitcoin'
})
const [errorMsg, setErrorMsg] = useState(null) const [errorMsg, setErrorMsg] = useState(null)
const [deleteDialog, setDeleteDialog] = useState(false) const [deleteDialog, setDeleteDialog] = useState(false)
const [confirmDialog, setConfirmDialog] = useState(false) const [confirmDialog, setConfirmDialog] = useState(false)
@ -147,11 +132,6 @@ const Blacklist = () => {
const classes = useStyles() const classes = useStyles()
const blacklistData = R.path(['blacklist'])(blacklistResponse) ?? [] 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 = const complianceConfig =
configData?.config && fromNamespace('compliance')(configData.config) configData?.config && fromNamespace('compliance')(configData.config)
@ -165,12 +145,8 @@ const Blacklist = () => {
return saveConfig({ variables: { config } }) return saveConfig({ variables: { config } })
} }
const onClickSidebarItem = e => { const handleDeleteEntry = address => {
setClickedItem({ code: e.code, display: e.display }) deleteEntry({ variables: { address } })
}
const handleDeleteEntry = (cryptoCode, address) => {
deleteEntry({ variables: { cryptoCode, address } })
} }
const handleConfirmDialog = confirm => { const handleConfirmDialog = confirm => {
@ -180,21 +156,21 @@ const Blacklist = () => {
setConfirmDialog(false) setConfirmDialog(false)
} }
const validateAddress = (cryptoCode, address) => { const validateAddress = address => {
try { try {
return !R.isNil(coinUtils.parseUrl(cryptoCode, 'main', address)) return !R.isEmpty(addressDetector.detectAddress(address).matches)
} catch { } catch {
return false return false
} }
} }
const addToBlacklist = async (cryptoCode, address) => { const addToBlacklist = async address => {
setErrorMsg(null) setErrorMsg(null)
if (!validateAddress(cryptoCode, address)) { if (!validateAddress(address)) {
setErrorMsg('Invalid address') setErrorMsg('Invalid address')
return return
} }
const res = await addEntry({ variables: { cryptoCode, address } }) const res = await addEntry({ variables: { address } })
if (!res.errors) { if (!res.errors) {
return setShowModal(false) return setShowModal(false)
} }
@ -218,31 +194,12 @@ const Blacklist = () => {
}} }}
/> />
<TitleSection title="Blacklisted addresses"> <TitleSection title="Blacklisted addresses">
<Box display="flex" justifyContent="flex-end"> <Box display="flex" alignItems="center" 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>
<Box <Box
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="end" justifyContent="end"
mr="-140px"> mr="15px">
<P>Enable paper wallet (only)</P> <P>Enable paper wallet (only)</P>
<Switch <Switch
checked={enablePaperWalletOnly} checked={enablePaperWalletOnly}
@ -268,7 +225,7 @@ const Blacklist = () => {
display="flex" display="flex"
alignItems="center" alignItems="center"
justifyContent="flex-end" justifyContent="flex-end"
mr="-5px"> mr="15px">
<P>Reject reused addresses</P> <P>Reject reused addresses</P>
<Switch <Switch
checked={rejectAddressReuse} checked={rejectAddressReuse}
@ -281,23 +238,20 @@ 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 that are used once will be automatically rejected if there's an
an attempt to use them again on a new transaction. attempt to use them again on a new transaction.
</P> </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> </HelpTooltip>
</Box> </Box>
<Link color="primary" onClick={() => setShowModal(true)}>
Blacklist new addresses
</Link>
</Box> </Box>
</TitleSection>
<Grid container className={classes.grid}>
<div className={classes.content}>
<BlacklistTable <BlacklistTable
data={formattedData} data={blacklistData}
selectedCoin={clickedItem}
handleDeleteEntry={handleDeleteEntry} handleDeleteEntry={handleDeleteEntry}
errorMessage={errorMsg} errorMessage={errorMsg}
setErrorMessage={setErrorMsg} setErrorMessage={setErrorMsg}
@ -313,7 +267,6 @@ const Blacklist = () => {
setShowModal(false) setShowModal(false)
}} }}
errorMsg={errorMsg} errorMsg={errorMsg}
selectedCoin={clickedItem}
addToBlacklist={addToBlacklist} addToBlacklist={addToBlacklist}
/> />
)} )}

View file

@ -7,11 +7,15 @@ const styles = {
content: { content: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: 1, flex: 1
marginLeft: spacer * 6
}, },
footer: { footer: {
margin: [['auto', 0, spacer * 3, 'auto']] display: 'flex',
flexDirection: 'row',
margin: [['auto', 0, spacer * 3, 0]]
},
submit: {
margin: [['auto', 0, 0, 'auto']]
}, },
modalTitle: { modalTitle: {
margin: [['auto', 0, 8.5, 'auto']] margin: [['auto', 0, 8.5, 'auto']]

View file

@ -14,31 +14,14 @@ import { H3 } from 'src/components/typography'
import styles from './Blacklist.styles' import styles from './Blacklist.styles'
const useStyles = makeStyles(styles) const useStyles = makeStyles(styles)
const BlackListModal = ({ const BlackListModal = ({ onClose, addToBlacklist, errorMsg }) => {
onClose,
selectedCoin,
addToBlacklist,
errorMsg
}) => {
const classes = useStyles() const classes = useStyles()
const handleAddToBlacklist = address => { const handleAddToBlacklist = address => {
if (selectedCoin.code === 'BCH' && !address.startsWith('bitcoincash:')) { addToBlacklist(address)
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'
} }
const placeholderAddress = '1ADwinnimZKGgQ3dpyfoUZvJh4p1UWSSpD'
return ( return (
<Modal <Modal
closeOnBackdropClick={true} closeOnBackdropClick={true}
@ -61,26 +44,20 @@ const BlackListModal = ({
handleAddToBlacklist(address.trim()) handleAddToBlacklist(address.trim())
}}> }}>
<Form id="address-form"> <Form id="address-form">
<H3 className={classes.modalTitle}> <H3 className={classes.modalTitle}>Blacklist new address</H3>
{selectedCoin.display
? `Blacklist ${R.toLower(selectedCoin.display)} address`
: ''}
</H3>
<Field <Field
name="address" name="address"
fullWidth fullWidth
autoComplete="off" autoComplete="off"
label="Paste new address to blacklist here" label="Paste new address to blacklist here"
placeholder={`ex: ${placeholderAddress[selectedCoin.code]}`} placeholder={`ex: ${placeholderAddress}`}
component={TextInput} component={TextInput}
/> />
{!R.isNil(errorMsg) && (
<ErrorMessage className={classes.error}>{errorMsg}</ErrorMessage>
)}
</Form> </Form>
</Formik> </Formik>
<div className={classes.footer}> <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"> <Link type="submit" form="address-form">
Blacklist address Blacklist address
</Link> </Link>

View file

@ -5,7 +5,6 @@ import React, { useState } from 'react'
import { DeleteDialog } from 'src/components/DeleteDialog' import { DeleteDialog } from 'src/components/DeleteDialog'
import { IconButton } from 'src/components/buttons' import { IconButton } from 'src/components/buttons'
import DataTable from 'src/components/tables/DataTable' import DataTable from 'src/components/tables/DataTable'
import { Label1 } from 'src/components/typography'
import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard' import CopyToClipboard from 'src/pages/Transactions/CopyToClipboard'
import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg' import { ReactComponent as DeleteIcon } from 'src/styling/icons/action/delete/enabled.svg'
@ -15,7 +14,6 @@ const useStyles = makeStyles(styles)
const BlacklistTable = ({ const BlacklistTable = ({
data, data,
selectedCoin,
handleDeleteEntry, handleDeleteEntry,
errorMessage, errorMessage,
setErrorMessage, setErrorMessage,
@ -29,8 +27,8 @@ const BlacklistTable = ({
const elements = [ const elements = [
{ {
name: 'address', name: 'address',
header: <Label1 className={classes.white}>{'Addresses'}</Label1>, header: 'Address',
width: 800, width: 1070,
textAlign: 'left', textAlign: 'left',
size: 'sm', size: 'sm',
view: it => ( view: it => (
@ -41,7 +39,7 @@ const BlacklistTable = ({
}, },
{ {
name: 'deleteButton', name: 'deleteButton',
header: <Label1 className={classes.white}>{'Delete'}</Label1>, header: 'Delete',
width: 130, width: 130,
textAlign: 'center', textAlign: 'center',
size: 'sm', size: 'sm',
@ -57,14 +55,11 @@ const BlacklistTable = ({
) )
} }
] ]
const dataToShow = selectedCoin
? data[selectedCoin.code]
: data[R.keys(data)[0]]
return ( return (
<> <>
<DataTable <DataTable
data={dataToShow} data={data}
elements={elements} elements={elements}
emptyText="No blacklisted addresses so far" emptyText="No blacklisted addresses so far"
name="blacklistTable" name="blacklistTable"