Skip to content

Commit

Permalink
fix: dust change amounts, closes #4979
Browse files Browse the repository at this point in the history
  • Loading branch information
friedger authored and kyranjamie committed Jun 3, 2024
1 parent ab7fb16 commit 100184a
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { createMoney, createNullArrayOfLength, sumNumbers } from '@leather-walle

import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';

import { determineUtxosForSpend } from './local-coin-selection';
import { filterUneconomicalUtxos, getSizeInfo } from '../utils';
import { determineUtxosForSpend, determineUtxosForSpendAll } from './local-coin-selection';

const demoUtxos = [
{ value: 8200 },
Expand Down Expand Up @@ -169,4 +170,64 @@ describe(determineUtxosForSpend.name, () => {
.toString()
);
});

test('that spending all economical spendable utxos does not result in dust utxos', () => {
const feeRate = 3;
const recipients = [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(Number(1), 'BTC'),
},
];
const filteredUtxos = filterUneconomicalUtxos({
utxos: demoUtxos.sort((a, b) => b.value - a.value) as any,
feeRate,
recipients,
});
const amount = filteredUtxos.reduce((total, utxo) => total + utxo.value, 0) - 2251;
recipients[0].amount = createMoney(Number(amount), 'BTC');

const result = determineUtxosForSpend({
utxos: filteredUtxos as any,
recipients: [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(Number(amount), 'BTC'),
},
],
feeRate,
});
expect(result.inputs.length).toEqual(10);
expect(result.outputs.length).toEqual(1);
expect(result.fee).toEqual(2251);
});

test('that spending all utxos with sendMax does not result in dust utxos', () => {
const utxos = [{ value: 1000 }, { value: 2000 }, { value: 3000 }];
const recipients = [
{
address: 'tb1qt28eagxcl9gvhq2rpj5slg7dwgxae2dn2hk93m',
amount: createMoney(Number(1), 'BTC'),
},
];
const sizeInfo = getSizeInfo({
inputLength: utxos.length,
isSendMax: true,
recipients,
});
const feeRate = 3;
const fee = Math.floor(sizeInfo.txVBytes * feeRate);
const amount = utxos.reduce((total, utxo) => total + utxo.value, 0) - fee;
recipients[0].amount = createMoney(Number(amount), 'BTC');

const result = determineUtxosForSpendAll({
utxos: utxos as any,
recipients,
feeRate,
});
expect(result.inputs.length).toEqual(utxos.length);
expect(result.outputs.length).toEqual(1);
expect(result.fee).toEqual(735);
expect(fee).toEqual(735);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { sumMoney, sumNumbers } from '@leather-wallet/utils';
import BigNumber from 'bignumber.js';
import { validate } from 'bitcoin-address-validation';

import { BTC_P2WPKH_DUST_AMOUNT } from '@shared/constants';
import type { TransferRecipient } from '@shared/models/form.model';

import { filterUneconomicalUtxos, getSizeInfo } from '../utils';
Expand All @@ -13,6 +14,11 @@ export class InsufficientFundsError extends Error {
}
}

interface Output {
value: bigint;
address?: string;
}

export interface DetermineUtxosForSpendArgs {
feeRate: number;
recipients: TransferRecipient[];
Expand Down Expand Up @@ -100,20 +106,24 @@ export function determineUtxosForSpend({ feeRate, recipients, utxos }: Determine
new BigNumber(estimateTransactionSize().txVBytes).multipliedBy(feeRate).toNumber()
);

const outputs: {
value: bigint;
address?: string;
}[] = [
const changeAmount =
BigInt(getUtxoTotal(neededUtxos).toString()) - BigInt(amount.amount.toNumber()) - BigInt(fee);

const changeUtxos: Output[] =
changeAmount > BTC_P2WPKH_DUST_AMOUNT
? [
{
value: changeAmount,
},
]
: [];

const outputs: Output[] = [
...recipients.map(({ address, amount }) => ({
value: BigInt(amount.amount.toNumber()),
address,
})),
{
value:
BigInt(getUtxoTotal(neededUtxos).toString()) -
BigInt(amount.amount.toNumber()) -
BigInt(fee),
},
...changeUtxos,
];

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const feeInputLabel = 'sats/vB';

interface Props {
onClick?(): void;
amount: number;
isSendingMax: boolean;
recipients: TransferRecipient[];
hasInsufficientBalanceError: boolean;
Expand All @@ -29,7 +28,6 @@ interface Props {

export function BitcoinCustomFeeInput({
onClick,
amount,
isSendingMax,
recipients,
hasInsufficientBalanceError,
Expand All @@ -44,7 +42,6 @@ export function BitcoinCustomFeeInput({
}>(null);

const getCustomFeeValues = useBitcoinCustomFee({
amount: createMoney(amount, 'BTC'),
isSendingMax,
recipients,
});
Expand Down
3 changes: 0 additions & 3 deletions src/app/components/bitcoin-custom-fee/bitcoin-custom-fee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ interface BitcoinCustomFeeProps {
}

export function BitcoinCustomFee({
amount,
customFeeInitialValue,
hasInsufficientBalanceError,
isSendingMax,
Expand All @@ -44,7 +43,6 @@ export function BitcoinCustomFee({
}: BitcoinCustomFeeProps) {
const feeInputRef = useRef<HTMLInputElement | null>(null);
const getCustomFeeValues = useBitcoinCustomFee({
amount: createMoney(amount, 'BTC'),
isSendingMax,
recipients,
});
Expand Down Expand Up @@ -97,7 +95,6 @@ export function BitcoinCustomFee({
</Link>
</styled.span>
<BitcoinCustomFeeInput
amount={amount}
isSendingMax={isSendingMax}
onClick={async () => {
feeInputRef.current?.focus();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
import { useCallback } from 'react';

import type { Money } from '@leather-wallet/models';
import { useCryptoCurrencyMarketDataMeanAverage } from '@leather-wallet/query';
import { baseCurrencyAmountInQuote, createMoney, i18nFormatCurrency } from '@leather-wallet/utils';

import type { TransferRecipient } from '@shared/models/form.model';
import '@shared/models/money.model';

import {
type DetermineUtxosForSpendArgs,
determineUtxosForSpend,
determineUtxosForSpendAll,
} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useCurrentBtcCryptoAssetBalanceNativeSegwit } from '@app/query/bitcoin/balance/btc-balance-native-segwit.hooks';

export const MAX_FEE_RATE_MULTIPLIER = 50;

interface UseBitcoinCustomFeeArgs {
amount: Money;
isSendingMax: boolean;
recipients: TransferRecipient[];
}

export function useBitcoinCustomFee({ amount, isSendingMax, recipients }: UseBitcoinCustomFeeArgs) {
const { balance } = useCurrentBtcCryptoAssetBalanceNativeSegwit();
export function useBitcoinCustomFee({ isSendingMax, recipients }: UseBitcoinCustomFeeArgs) {
const { data: utxos = [] } = useCurrentNativeSegwitUtxos();
const btcMarketData = useCryptoCurrencyMarketDataMeanAverage('BTC');

return useCallback(
(feeRate: number) => {
if (!feeRate || !utxos.length) return { fee: 0, fiatFeeValue: '' };

const satAmount = isSendingMax
? balance.availableBalance.amount.toNumber()
: amount.amount.toNumber();

const determineUtxosArgs = {
amount: satAmount,
const determineUtxosArgs: DetermineUtxosForSpendArgs = {
recipients,
utxos,
feeRate,
Expand All @@ -51,6 +44,6 @@ export function useBitcoinCustomFee({ amount, isSendingMax, recipients }: UseBit
)}`,
};
},
[utxos, isSendingMax, balance.availableBalance.amount, amount.amount, recipients, btcMarketData]
[utxos, isSendingMax, recipients, btcMarketData]
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,6 @@ export function IncreaseBtcFeeDialog() {
<Stack gap="space.04">
<Stack gap="space.01">
<BitcoinCustomFeeInput
amount={Math.abs(
btcToSat(getBitcoinTxValue(currentBitcoinAddress, btcTx)).toNumber()
)}
isSendingMax={false}
recipients={recipients}
hasInsufficientBalanceError={false}
Expand Down

0 comments on commit 100184a

Please sign in to comment.