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,86 +194,64 @@ const Blacklist = () => {
}} }}
/> />
<TitleSection title="Blacklisted addresses"> <TitleSection title="Blacklisted addresses">
<Box display="flex" justifyContent="flex-end"> <Box display="flex" alignItems="center" justifyContent="flex-end">
<Box
display="flex"
alignItems="center"
justifyContent="end"
mr="15px">
<P>Enable paper wallet (only)</P>
<Switch
checked={enablePaperWalletOnly}
onChange={e =>
enablePaperWalletOnly
? addressReuseSave({
enablePaperWalletOnly: e.target.checked
})
: setConfirmDialog(true)
}
value={enablePaperWalletOnly}
/>
<Label2>{enablePaperWalletOnly ? 'On' : 'Off'}</Label2>
<HelpTooltip width={304}>
<P>
The "Enable paper wallet (only)" option means that only paper
wallets will be printed for users, and they won't be permitted
to scan an address from their own wallet.
</P>
</HelpTooltip>
</Box>
<Box
display="flex"
alignItems="center"
justifyContent="flex-end"
mr="15px">
<P>Reject reused addresses</P>
<Switch
checked={rejectAddressReuse}
onChange={event => {
addressReuseSave({ rejectAddressReuse: event.target.checked })
}}
value={rejectAddressReuse}
/>
<Label2>{rejectAddressReuse ? 'On' : 'Off'}</Label2>
<HelpTooltip 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>
</HelpTooltip>
</Box>
<Link color="primary" onClick={() => setShowModal(true)}> <Link color="primary" onClick={() => setShowModal(true)}>
Blacklist new addresses Blacklist new addresses
</Link> </Link>
</Box> </Box>
</TitleSection> </TitleSection>
<Grid container className={classes.grid}> <Grid container className={classes.grid}>
<Sidebar
data={availableCurrencies}
isSelected={R.propEq('code', clickedItem.code)}
displayName={it => it.display}
onClick={onClickSidebarItem}
/>
<div className={classes.content}> <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="-140px">
<P>Enable paper wallet (only)</P>
<Switch
checked={enablePaperWalletOnly}
onChange={e =>
enablePaperWalletOnly
? addressReuseSave({
enablePaperWalletOnly: e.target.checked
})
: setConfirmDialog(true)
}
value={enablePaperWalletOnly}
/>
<Label2>{enablePaperWalletOnly ? 'On' : 'Off'}</Label2>
<HelpTooltip width={304}>
<P>
The "Enable paper wallet (only)" option means that only paper
wallets will be printed for users, and they won't be permitted
to scan an address from their own wallet.
</P>
</HelpTooltip>
</Box>
<Box
display="flex"
alignItems="center"
justifyContent="flex-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>
<HelpTooltip 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>
<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>
</Box>
</Box>
<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"