From 96bc77a269c37ff98e1ddd07bb2137a4f4876321 Mon Sep 17 00:00:00 2001 From: coreyphillips Date: Wed, 24 Apr 2024 23:52:20 -0400 Subject: [PATCH] fix(wallet): Fee Updates Prevents user from selecting or setting a fee-rate above what the max is. Resolves UI bug in #1619 --- src/screens/Wallets/Send/FeeCustom.tsx | 24 +++++++++++-- src/screens/Wallets/Send/FeeRate.tsx | 50 ++++++++++++++++++-------- src/store/actions/wallet.ts | 22 +++++++++--- src/utils/wallet/index.ts | 28 +++++++++++++++ 4 files changed, 104 insertions(+), 20 deletions(-) diff --git a/src/screens/Wallets/Send/FeeCustom.tsx b/src/screens/Wallets/Send/FeeCustom.tsx index 6239a3d95..4b4d7faac 100644 --- a/src/screens/Wallets/Send/FeeCustom.tsx +++ b/src/screens/Wallets/Send/FeeCustom.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement, memo, useMemo, useState } from 'react'; +import React, { ReactElement, memo, useMemo, useState, useEffect } from 'react'; import { StyleSheet, View } from 'react-native'; import { useTranslation } from 'react-i18next'; @@ -16,6 +16,7 @@ import { useAppSelector } from '../../../hooks/redux'; import { useDisplayValues } from '../../../hooks/displayValues'; import { transactionSelector } from '../../../store/reselect/wallet'; import type { SendScreenProps } from '../../../navigation/types'; +import { getFeeInfo } from '../../../utils/wallet'; const FeeCustom = ({ navigation, @@ -23,6 +24,17 @@ const FeeCustom = ({ const { t } = useTranslation('wallet'); const transaction = useAppSelector(transactionSelector); const [feeRate, setFeeRate] = useState(transaction.satsPerByte); + const [maxFee, setMaxFee] = useState(0); + + useEffect(() => { + const feeInfo = getFeeInfo({ + satsPerByte: transaction.satsPerByte, + transaction, + }); + if (feeInfo.isOk()) { + setMaxFee(feeInfo.value.maxSatPerByte); + } + }, [transaction]); const totalFee = getTotalFee({ satsPerByte: feeRate, @@ -43,7 +55,15 @@ const FeeCustom = ({ const onPress = (key: string): void => { const current = feeRate.toString(); - const newAmount = handleNumberPadPress(key, current, { maxLength: 3 }); + const newAmount = handleNumberPadPress(key, current, { maxLength: 4 }); + if (Number(newAmount) > maxFee) { + showToast({ + type: 'info', + title: 'Max possible fee rate', + description: `${maxFee} sats/vbyte`, + }); + return; + } setFeeRate(Number(newAmount)); }; diff --git a/src/screens/Wallets/Send/FeeRate.tsx b/src/screens/Wallets/Send/FeeRate.tsx index 41141316e..671bffbb7 100644 --- a/src/screens/Wallets/Send/FeeRate.tsx +++ b/src/screens/Wallets/Send/FeeRate.tsx @@ -1,4 +1,11 @@ -import React, { memo, ReactElement, useMemo, useCallback } from 'react'; +import React, { + memo, + ReactElement, + useMemo, + useCallback, + useState, + useEffect, +} from 'react'; import { StyleSheet, View } from 'react-native'; import { useTranslation } from 'react-i18next'; @@ -24,6 +31,7 @@ import { } from '../../../store/reselect/wallet'; import SafeAreaInset from '../../../components/SafeAreaInset'; import { EFeeId } from 'beignet'; +import { getFeeInfo } from '../../../utils/wallet'; const FeeRate = ({ navigation }: SendScreenProps<'FeeRate'>): ReactElement => { const { t } = useTranslation('wallet'); @@ -32,10 +40,18 @@ const FeeRate = ({ navigation }: SendScreenProps<'FeeRate'>): ReactElement => { const selectedNetwork = useAppSelector(selectedNetworkSelector); const transaction = useAppSelector(transactionSelector); const feeEstimates = useAppSelector((store) => store.fees.onchain); - + const [maxFee, setMaxFee] = useState(0); const selectedFeeId = transaction.selectedFeeId; const satsPerByte = transaction.satsPerByte; + useEffect(() => { + const feeInfo = getFeeInfo({ satsPerByte, transaction }); + if (feeInfo.isOk()) { + setMaxFee(feeInfo.value.maxSatPerByte); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [transaction]); + const transactionTotal = useCallback(() => { return getTransactionOutputValue({ outputs: transaction.outputs, @@ -73,40 +89,46 @@ const FeeRate = ({ navigation }: SendScreenProps<'FeeRate'>): ReactElement => { const displayFast = useMemo(() => { return ( - onchainBalance >= transactionTotal() + getFee(feeEstimates.fast) || - transaction.max + maxFee >= feeEstimates.fast && + (onchainBalance >= transactionTotal() + getFee(feeEstimates.fast) || + transaction.max) ); }, [ - onchainBalance, + maxFee, feeEstimates.fast, - getFee, + onchainBalance, transactionTotal, + getFee, transaction.max, ]); const displayNormal = useMemo(() => { return ( - onchainBalance >= transactionTotal() + getFee(feeEstimates.normal) || - transaction.max + feeEstimates.normal <= maxFee && + (onchainBalance >= transactionTotal() + getFee(feeEstimates.normal) || + transaction.max) ); }, [ - onchainBalance, + maxFee, feeEstimates.normal, - getFee, + onchainBalance, transactionTotal, + getFee, transaction.max, ]); const displaySlow = useMemo(() => { return ( - onchainBalance >= transactionTotal() + getFee(feeEstimates.slow) || - transaction.max + maxFee >= feeEstimates.slow && + (onchainBalance >= transactionTotal() + getFee(feeEstimates.slow) || + transaction.max) ); }, [ - onchainBalance, + maxFee, feeEstimates.slow, - getFee, + onchainBalance, transactionTotal, + getFee, transaction.max, ]); diff --git a/src/store/actions/wallet.ts b/src/store/actions/wallet.ts index 3af9c1974..0c36e693a 100644 --- a/src/store/actions/wallet.ts +++ b/src/store/actions/wallet.ts @@ -638,8 +638,11 @@ export const setupFeeForOnChainTransaction = (): Result => { transactionSpeed === ETransactionSpeed.custom ? customFeeRate : fees[transactionSpeed]; - const selectedFeeId = txSpeedToFeeId(getSettingsStore().transactionSpeed); - const satsPerByte = + const selectedFeeId = + transaction.selectedFeeId === 'none' + ? txSpeedToFeeId(getSettingsStore().transactionSpeed) + : transaction.selectedFeeId; + let satsPerByte = transaction.selectedFeeId === 'none' ? preferredFeeRate : transaction.satsPerByte; @@ -650,8 +653,19 @@ export const setupFeeForOnChainTransaction = (): Result => { if (feeSetupRes.isOk()) { return feeSetupRes; } - // If unable to set up fee using the selectedFeeId, attempt 1 satsPerByte. Otherwise, return error. - const updateRes = updateFee({ satsPerByte: 1, transaction }); + + // If unable to set up fee using the selectedFeeId set maxSatPerByte from getFeeInfo. + const txFeeInfo = wallet.getFeeInfo({ + satsPerByte, + transaction, + }); + if (txFeeInfo.isErr()) { + return err(txFeeInfo.error.message); + } + if (txFeeInfo.value.maxSatPerByte < satsPerByte) { + satsPerByte = txFeeInfo.value.maxSatPerByte; + } + const updateRes = updateFee({ satsPerByte, transaction }); if (updateRes.isErr()) { return err(feeSetupRes.error.message); } diff --git a/src/utils/wallet/index.ts b/src/utils/wallet/index.ts index 39e50612f..f79693daf 100644 --- a/src/utils/wallet/index.ts +++ b/src/utils/wallet/index.ts @@ -90,6 +90,7 @@ import { updateUi } from '../../store/slices/ui'; import { ICustomGetScriptHash } from 'beignet/src/types/wallet'; import { ldk } from '@synonymdev/react-native-ldk'; import { resetActivityState } from '../../store/slices/activity'; +import { TGetTotalFeeObj } from 'beignet/dist/types/types'; bitcoin.initEccLib(ecc); const bip32 = BIP32Factory(ecc); @@ -1525,3 +1526,30 @@ export const switchNetwork = async ( setTimeout(updateActivityList, 500); return ok(true); }; + +/** + * Returns a fee object for the current/provided transaction. + * @param {number} [satsPerByte] + * @param {string} [message] + * @param {Partial} [transaction] + * @param {boolean} [fundingLightning] + * @returns {Result} + */ +export const getFeeInfo = ({ + satsPerByte, + transaction, + message, + fundingLightning, +}: { + satsPerByte: number; + message?: string; + transaction?: Partial; + fundingLightning?: boolean; +}): Result => { + return wallet.getFeeInfo({ + satsPerByte, + transaction, + message, + fundingLightning, + }); +};