Skip to content

Commit

Permalink
frontend/coin-control: add address reuse warning
Browse files Browse the repository at this point in the history
Add a warning to the coin control modal when a user
selects  one or more UTXOs from reused addresses.
The warning: "One or more UTXOs have a reused address.
Be aware  the receiver will see all UTXOs associated
with those addresses."

The UTXOs now have red badge when their address was
re-used.
  • Loading branch information
strmci committed May 24, 2024
1 parent ab47bcf commit 35d23bb
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Format amounts using localized decimal and group separator
- Support pasting different localized number formats, i.e. dot and comma separated amounts
- Fix BitBoxApp crash on GrapheneOS and other phones without Google Play Services when scanning QR codes.
- Show address re-use warning and group UTXOs with the same address together in coin control.

## 4.42.0
- Preselect backup when there's only one backup available
Expand Down
25 changes: 18 additions & 7 deletions backend/coins/btc/handlers/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,16 +324,27 @@ func (handlers *Handlers) getUTXOs(*http.Request) (interface{}, error) {
return result, errp.New("Interface must be of type btc.Account")
}

addressCounts := make(map[string]int)

for _, output := range t.SpendableOutputs() {
address := output.Address.EncodeForHumans()
addressCounts[address]++
}

for _, output := range t.SpendableOutputs() {
address := output.Address.EncodeForHumans()
addressReused := addressCounts[address] > 1

result = append(result,
map[string]interface{}{
"outPoint": output.OutPoint.String(),
"txId": output.OutPoint.Hash.String(),
"txOutput": output.OutPoint.Index,
"amount": handlers.formatBTCAmountAsJSON(btcutil.Amount(output.TxOut.Value), false),
"address": output.Address.EncodeForHumans(),
"scriptType": output.Address.Configuration.ScriptType(),
"note": handlers.account.TxNote(output.OutPoint.Hash.String()),
"outPoint": output.OutPoint.String(),
"txId": output.OutPoint.Hash.String(),
"txOutput": output.OutPoint.Index,
"amount": handlers.formatBTCAmountAsJSON(btcutil.Amount(output.TxOut.Value), false),
"address": address,
"scriptType": output.Address.Configuration.ScriptType(),
"note": handlers.account.TxNote(output.OutPoint.Hash.String()),
"addressReused": addressReused,
})
}

Expand Down
1 change: 1 addition & 0 deletions frontends/web/src/api/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ export type TUTXO = {
amount: IAmount;
note: string;
scriptType: ScriptType;
addressReused: boolean;
};

export const getUTXOs = (code: AccountCode): Promise<TUTXO[]> => {
Expand Down
7 changes: 7 additions & 0 deletions frontends/web/src/components/badge/badge.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@
border-color: var(--color-darkyellow);
color: var(--color-olive);
}

/* swiss-red with different opacities */
.danger {
background: rgba(255, 0, 0, 0.2);
border-color: rgba(255, 0, 0, 0.33);
color: rgba(255, 0, 0, 1);
}
2 changes: 1 addition & 1 deletion frontends/web/src/components/badge/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { ReactElement, ReactNode } from 'react';
import style from './badge.module.css';

type TBadgeStyles = 'success' | 'warning'; // TODO: not yet implemented 'info' | 'danger'
type TBadgeStyles = 'success' | 'warning' | 'danger'; // TODO: not yet implemented 'info'

type TProps = {
children?: ReactNode;
Expand Down
2 changes: 2 additions & 0 deletions frontends/web/src/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,7 @@
"button": "Review",
"coincontrol": {
"address": "Address",
"addressReused": "Address re-used",
"outpoint": "Outpoint",
"title": "Send from output"
},
Expand Down Expand Up @@ -1780,6 +1781,7 @@
"walletConnect": "WalletConnect"
},
"warning": {
"coincontrol": "One or more UTXOs have a reused address. Be aware the receiver will see all UTXOs associated with those addresses.",
"receivePairing": "Please pair the BitBox to enable secure address verification. Go to 'Manage device' in the sidebar.",
"sdcard": "Keep the microSD card stored separate from the BitBox, unless you want to manage backups.",
"sendPairing": "Please pair the BitBox to securely verify transaction details. Go to 'Manage device' in the sidebar."
Expand Down
37 changes: 31 additions & 6 deletions frontends/web/src/routes/account/send/utxos.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { ExternalLink } from '../../../components/icon';
import { Amount } from '../../../components/amount/amount';
import { FiatConversion } from '../../../components/rates/rates';
import { getScriptName } from '../utils';
import { Message } from '../../../components/message/message';
import { Badge } from '../../../components/badge/badge';
import style from './utxos.module.css';

export type TSelectedUTXOs = {
Expand All @@ -56,6 +58,7 @@ export const UTXOs = ({
const { t } = useTranslation();
const [utxos, setUtxos] = useState<TUTXO[]>([]);
const [selectedUTXOs, setSelectedUTXOs] = useState<TSelectedUTXOs>({});
const [showAddressReusedMessage, setShowAddressReusedMessage] = useState(0);

useEffect(() => {
getUTXOs(accountCode).then(setUtxos);
Expand All @@ -71,14 +74,22 @@ export const UTXOs = ({
return () => unsubscribe();
}, [accountCode]);

const handleUTXOChange = (event: ChangeEvent<HTMLInputElement>) => {
const handleUTXOChange = (
event: ChangeEvent<HTMLInputElement>,
utxo: TUTXO,
) => {
const target = event.target;
const outPoint = target.dataset.outpoint as string;
const proposedUTXOs = Object.assign({}, selectedUTXOs);
if (target.checked) {
proposedUTXOs[outPoint] = true;
proposedUTXOs[utxo.outPoint] = true;
if (utxo.addressReused) {
setShowAddressReusedMessage(showAddressReusedMessage + 1);
}
} else {
delete proposedUTXOs[outPoint];
delete proposedUTXOs[utxo.outPoint];
if (utxo.addressReused) {
setShowAddressReusedMessage(showAddressReusedMessage - 1);
}
}
setSelectedUTXOs(proposedUTXOs);
onChange(proposedUTXOs);
Expand All @@ -98,8 +109,7 @@ export const UTXOs = ({
<Checkbox
checked={!!selectedUTXOs[utxo.outPoint]}
id={'utxo-' + utxo.outPoint}
data-outpoint={utxo.outPoint}
onChange={handleUTXOChange}>
onChange={event => handleUTXOChange(event, utxo)}>
{utxo.note && (
<div className={style.note}>
<strong>{utxo.note}{' '}</strong>
Expand All @@ -124,6 +134,14 @@ export const UTXOs = ({
<span className={style.shrink}>
{utxo.address}
</span>
<div className="m-left-quarter">
{utxo.addressReused ?
<Badge type="danger">
{t('send.coincontrol.addressReused')}
</Badge> :
null
}
</div>
</div>
<div className={style.transaction}>
<span className={style.label}>
Expand Down Expand Up @@ -155,6 +173,13 @@ export const UTXOs = ({
title={t('send.coincontrol.title')}
large
onClose={onClose}>
<div>
{(showAddressReusedMessage > 0) && (
<Message type="warning">
{t('warning.coincontrol')}
</Message>
)}
</div>
<div>
{ allScriptTypes.map(renderUTXOs) }
<div className="buttons text-center m-top-none m-bottom-half">
Expand Down

0 comments on commit 35d23bb

Please sign in to comment.