Skip to content

Commit

Permalink
fix: show proper info for unspendable coinbase transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-sanderson committed Dec 20, 2024
1 parent 6365662 commit 610108e
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 7 deletions.
5 changes: 5 additions & 0 deletions packages/suite/src/support/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5805,6 +5805,11 @@ export default defineMessages({
defaultMessage: 'Rejected by coordinator',
description: 'Tooltip over an icon in Coin control section',
},
TR_UTXO_NOT_MATURED_COINBASE: {
id: 'TR_UTXO_NOT_MATURED_COINBASE',
defaultMessage:
'Coinbase transaction has to have at least {confirmations} confirmations to be spendable',
},
TR_CHANGE_ADDRESS_TOOLTIP: {
id: 'TR_CHANGE_ADDRESS_TOOLTIP',
defaultMessage: 'This is a change address created from a previous send.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { MouseEventHandler } from 'react';
import { MouseEventHandler, ReactNode } from 'react';

import styled, { css, useTheme } from 'styled-components';

import { formatNetworkAmount, isSameUtxo } from '@suite-common/wallet-utils';
import { Checkbox, Row, Spinner, TextButton, Tooltip } from '@trezor/components';
import type { AccountUtxo } from '@trezor/connect';
import { AccountUtxo } from '@trezor/connect';
import { borders, spacings, spacingsPx, typography } from '@trezor/theme';
import { CheckContainer } from '@trezor/components/src/components/form/Checkbox/Checkbox';

Expand Down Expand Up @@ -120,6 +120,36 @@ const StyledFiatValue = styled(FiatValue)`
${typography.hint}
`;

type ResolveUtxoSpendableProps = {
utxo: AccountUtxo;
coinjoinRegisteredUtxos: AccountUtxo[];
};

// Same as MINIMAL_COINBASE_CONFIRMATIONS in '@trezor/utxo-lib'; It is redeclared here to avoid
// some magic import/export errors. This is very niche stuff and probably never changes.
// Also, this most probably bothers only developers on Regtest.
const MINIMAL_COINBASE_CONFIRMATIONS = 100;

const resolveUtxoSpendable = ({
utxo,
coinjoinRegisteredUtxos,
}: ResolveUtxoSpendableProps): ReactNode | null => {
if (utxo.coinbase === true && utxo.confirmations < MINIMAL_COINBASE_CONFIRMATIONS) {
return (
<Translation
id="TR_UTXO_NOT_MATURED_COINBASE"
values={{ confirmations: MINIMAL_COINBASE_CONFIRMATIONS }}
/>
);
}

if (coinjoinRegisteredUtxos.includes(utxo)) {
return <Translation id="TR_UTXO_REGISTERED_IN_COINJOIN" />;
}

return null;
};

type UtxoSelectionProps = {
transaction?: WalletAccountTransaction;
utxo: AccountUtxo;
Expand Down Expand Up @@ -155,7 +185,10 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => {
const isChecked = isCoinControlEnabled
? selectedUtxos.some(selected => isSameUtxo(selected, utxo))
: composedInputs.some(u => u.prev_hash === utxo.txid && u.prev_index === utxo.vout);
const isDisabled = coinjoinRegisteredUtxos.includes(utxo);

const unspendableTooltip = resolveUtxoSpendable({ utxo, coinjoinRegisteredUtxos });
const isDisabled = unspendableTooltip !== null;

const utxoTagIconColor = isDisabled
? theme.legacy.TYPE_LIGHT_GREY
: theme.legacy.TYPE_DARK_GREY;
Expand All @@ -174,7 +207,7 @@ export const UtxoSelection = ({ transaction, utxo }: UtxoSelectionProps) => {
$isDisabled={isDisabled}
onClick={isDisabled ? undefined : handleCheckbox}
>
<Tooltip content={isDisabled && <Translation id="TR_UTXO_REGISTERED_IN_COINJOIN" />}>
<Tooltip content={unspendableTooltip}>
<Checkbox
isChecked={isChecked}
isDisabled={isDisabled}
Expand Down
3 changes: 3 additions & 0 deletions packages/utxo-lib/src/coinselect/coinselectUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const INPUT_SIZE = 160; // 4 * (32 + 4 + 4)

const DUST_RELAY_FEE_RATE = 3; // 3000 sat/kB https://github.com/bitcoin/bitcoin/blob/be443328037162f265cc85d05b1a7665b5f104d2/src/policy/policy.h#L55

/** Coinbase transaction must have at least 100 confirmations to be spendable.*/
export const MINIMAL_COINBASE_CONFIRMATIONS = 100;

type Vin = { type: CoinSelectInput['type']; script: { length: number }; weight?: number };
type VinVout = { script: { length: number }; weight?: number };

Expand Down
3 changes: 2 additions & 1 deletion packages/utxo-lib/src/coinselect/outputs/split.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getFee,
finalize,
ZERO,
MINIMAL_COINBASE_CONFIRMATIONS,
} from '../coinselectUtils';
import {
CoinSelectInput,
Expand All @@ -23,7 +24,7 @@ export function split(
feeRate: number,
options: CoinSelectOptions,
): CoinSelectResult {
const coinbase = options.coinbase || 100;
const coinbase = options.coinbase || MINIMAL_COINBASE_CONFIRMATIONS;
const utxos = filterCoinbase(utxosOrig, coinbase);

const fee = getFee(utxos, outputs, feeRate, options);
Expand Down
4 changes: 2 additions & 2 deletions packages/utxo-lib/src/coinselect/tryconfirmed.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filterCoinbase } from './coinselectUtils';
import { filterCoinbase, MINIMAL_COINBASE_CONFIRMATIONS } from './coinselectUtils';
import { CoinSelectAlgorithm, CoinSelectOptions, CoinSelectInput } from '../types';

function filterUtxos(utxos: CoinSelectInput[], minConfOwn: number, minConfOther: number) {
Expand Down Expand Up @@ -31,7 +31,7 @@ export function tryConfirmed(
): CoinSelectAlgorithm {
const own = options.own || 1;
const other = options.other || 6;
const coinbase = options.coinbase || 100;
const coinbase = options.coinbase || MINIMAL_COINBASE_CONFIRMATIONS;

return (utxosO, outputs, feeRate, optionsIn) => {
const utxos = filterCoinbase(utxosO, coinbase);
Expand Down

0 comments on commit 610108e

Please sign in to comment.