diff --git a/src/app/components/confirmBtcTransactionComponent/index.tsx b/src/app/components/confirmBtcTransactionComponent/index.tsx index 290f3e4ed..1a123ea85 100644 --- a/src/app/components/confirmBtcTransactionComponent/index.tsx +++ b/src/app/components/confirmBtcTransactionComponent/index.tsx @@ -1,7 +1,6 @@ import SettingIcon from '@assets/img/dashboard/faders_horizontal.svg'; import AssetIcon from '@assets/img/transactions/Assets.svg'; import ActionButton from '@components/button'; -import InfoContainer from '@components/infoContainer'; import RecipientComponent from '@components/recipientComponent'; import TransactionSettingAlert from '@components/transactionSetting'; import TransferFeeView from '@components/transferFeeView'; @@ -269,14 +268,10 @@ function ConfirmBtcTransactionComponent({ }, [signedNonOrdinalBtcSend]); useEffect(() => { - if ( + const isFeeHigh = feeMultipliers && - currentFee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighSatsFee)) - ) { - setShowFeeWarning(true); - } else if (showFeeWarning) { - setShowFeeWarning(false); - } + currentFee.isGreaterThan(new BigNumber(feeMultipliers.thresholdHighSatsFee)); + setShowFeeWarning(!!isFeeHigh); }, [currentFee, feeMultipliers]); const onAdvancedSettingClick = () => { @@ -358,7 +353,9 @@ function ConfirmBtcTransactionComponent({ <> {showFeeWarning && ( - + + + )} {/* TODO tim: refactor this not to use children. it should be just another prop */} {children} diff --git a/src/app/hooks/queries/useConfirmedBtcBalance.ts b/src/app/hooks/queries/useConfirmedBtcBalance.ts new file mode 100644 index 000000000..34c869813 --- /dev/null +++ b/src/app/hooks/queries/useConfirmedBtcBalance.ts @@ -0,0 +1,36 @@ +import useBtcClient from '@hooks/useBtcClient'; +import { useQuery } from '@tanstack/react-query'; +import { useMemo } from 'react'; +import useWalletSelector from '../useWalletSelector'; + +const useConfirmBtcBalance = () => { + const { btcAddress } = useWalletSelector(); + const btcClient = useBtcClient(); + + const fetchBtcAddressData = async () => btcClient.getAddressData(btcAddress); + + const { data, isLoading, isError, error } = useQuery({ + queryKey: ['btc-address-data'], + queryFn: fetchBtcAddressData, + }); + + const confirmedBalance = useMemo(() => { + if (!isLoading && !isError && data) { + const chainStats = data.chain_stats; + const mempoolStats = data.mempool_stats; + + if (chainStats && mempoolStats) { + return chainStats.funded_txo_sum - chainStats.spent_txo_sum - mempoolStats.spent_txo_sum; + } + } + return undefined; + }, [data, isLoading, isError]); + + return { + confirmedBalance, + isLoading, + error, + }; +}; + +export default useConfirmBtcBalance; diff --git a/src/app/screens/createInscription/index.tsx b/src/app/screens/createInscription/index.tsx index 3151a469c..15dd535aa 100644 --- a/src/app/screens/createInscription/index.tsx +++ b/src/app/screens/createInscription/index.tsx @@ -13,6 +13,7 @@ import { currencySymbolMap, fetchBtcFeeRate, getNonOrdinalUtxo, + InscriptionErrorCode, useInscriptionExecute, useInscriptionFees, UTXO, @@ -25,8 +26,9 @@ import { ExternalSatsMethods, MESSAGE_SOURCE } from '@common/types/message-types import AccountHeaderComponent from '@components/accountHeader'; import ConfirmScreen from '@components/confirmScreen'; import useWalletSelector from '@hooks/useWalletSelector'; -import { getShortTruncatedAddress } from '@utils/helper'; +import { getShortTruncatedAddress, isLedgerAccount } from '@utils/helper'; +import useConfirmedBtcBalance from '@hooks/queries/useConfirmedBtcBalance'; import useBtcClient from '@hooks/useBtcClient'; import useSeedVault from '@hooks/useSeedVault'; import Callout from '@ui-library/callout'; @@ -230,6 +232,7 @@ const ButtonImage = styled.img((props) => ({ })); const DEFAULT_FEE_RATE = 8; +const MAX_REPEATS = 24; function CreateInscription() { const { t } = useTranslation('translation', { keyPrefix: 'INSCRIPTION_REQUEST' }); @@ -261,10 +264,11 @@ function CreateInscription() { } = payload as CreateInscriptionPayload | CreateRepeatInscriptionsPayload; const { repeat } = payload as CreateRepeatInscriptionsPayload; - const showOver24RepeatsError = !Number.isNaN(repeat) && repeat > 24; + const showOver24RepeatsError = !Number.isNaN(repeat) && repeat > MAX_REPEATS; const [utxos, setUtxos] = useState(); const [showFeeSettings, setShowFeeSettings] = useState(false); + const [showConfirmedBalanceError, setShowConfirmedBalanceError] = useState(false); const [feeRate, setFeeRate] = useState(suggestedMinerFeeRate ?? DEFAULT_FEE_RATE); const [feeRates, setFeeRates] = useState(); const { getSeed } = useSeedVault(); @@ -381,8 +385,11 @@ function CreateInscription() { inscriptionValue, } = commitValueBreakdown ?? {}; + const { confirmedBalance, isLoading: confirmedBalanceLoading } = useConfirmedBtcBalance(); + const chainFee = (revealChainFee ?? 0) + (commitChainFee ?? 0); const totalFee = (revealServiceFee ?? 0) + (externalServiceFee ?? 0) + chainFee; + const showTotalFee = totalFee !== chainFee; const toFiat = (value: number | string = 0) => @@ -392,6 +399,20 @@ function CreateInscription() { .plus(new BigNumber(totalInscriptionValue ?? 0)) .toString(); + const errorCode = feeErrorCode || executeErrorCode; + + const isLoading = utxos === undefined || inscriptionFeesLoading; + + useEffect(() => { + const showConfirmError = + !isLoading && + !confirmedBalanceLoading && + errorCode !== InscriptionErrorCode.INSUFFICIENT_FUNDS && + confirmedBalance !== undefined && + Number(bundlePlusFees) > confirmedBalance; + setShowConfirmedBalanceError(!!showConfirmError); + }, [confirmedBalance, errorCode, bundlePlusFees, isLoading, confirmedBalanceLoading]); + if (complete && revealTransactionId) { const onClose = () => { const response = { @@ -420,9 +441,12 @@ function CreateInscription() { return ; } - const errorCode = feeErrorCode || executeErrorCode; - - const isLoading = utxos === undefined || inscriptionFeesLoading; + const disableConfirmButton = + !!errorCode || + isExecuting || + showOver24RepeatsError || + showConfirmedBalanceError || + isLedgerAccount(selectedAccount); return ( @@ -440,7 +464,16 @@ function CreateInscription() { {t('TITLE')} {t('SUBTITLE', { name: appName ?? '' })} {showOver24RepeatsError && ( - + + )} + {showConfirmedBalanceError && ( + + )} + {isLedgerAccount(selectedAccount) && ( + )} diff --git a/src/app/screens/sendStx/index.tsx b/src/app/screens/sendStx/index.tsx index 773b77cda..52eaa3a9d 100644 --- a/src/app/screens/sendStx/index.tsx +++ b/src/app/screens/sendStx/index.tsx @@ -14,9 +14,9 @@ import { import { useMutation } from '@tanstack/react-query'; import { replaceCommaByDot } from '@utils/helper'; import BigNumber from 'bignumber.js'; -import { useEffect,useState } from 'react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useLocation,useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import TopRow from '../../components/topRow'; function SendStxScreen() { diff --git a/src/app/screens/signatureRequest/clarityMessageView.tsx b/src/app/screens/signatureRequest/clarityMessageView.tsx index 734352da0..f347f9994 100644 --- a/src/app/screens/signatureRequest/clarityMessageView.tsx +++ b/src/app/screens/signatureRequest/clarityMessageView.tsx @@ -1,5 +1,5 @@ import { buf2hex } from '@secretkeylabs/xverse-core'; -import { ClarityType, ClarityValue, cvToString,principalToString } from '@stacks/transactions'; +import { ClarityType, ClarityValue, cvToString, principalToString } from '@stacks/transactions'; import styled from 'styled-components'; const Container = styled.div<{ isRoot: boolean }>((props) => ({ diff --git a/src/app/screens/signatureRequest/index.tsx b/src/app/screens/signatureRequest/index.tsx index 728d51659..9f21b1b6e 100644 --- a/src/app/screens/signatureRequest/index.tsx +++ b/src/app/screens/signatureRequest/index.tsx @@ -18,7 +18,7 @@ import useSignatureRequest, { import useWalletReducer from '@hooks/useWalletReducer'; import useWalletSelector from '@hooks/useWalletSelector'; import Transport from '@ledgerhq/hw-transport-webusb'; -import { bip0322Hash, hashMessage, signStxMessage, buf2hex } from '@secretkeylabs/xverse-core'; +import { bip0322Hash, buf2hex, hashMessage, signStxMessage } from '@secretkeylabs/xverse-core'; import { SignaturePayload, StructuredDataSignaturePayload } from '@stacks/connect'; import { getNetworkType, getTruncatedAddress, isHardwareAccount } from '@utils/helper'; import { handleBip322LedgerMessageSigning, signatureVrsToRsv } from '@utils/ledger'; diff --git a/src/app/screens/swap/useSwap.tsx b/src/app/screens/swap/useSwap.tsx index a2a846d77..c9418c6b5 100644 --- a/src/app/screens/swap/useSwap.tsx +++ b/src/app/screens/swap/useSwap.tsx @@ -2,12 +2,12 @@ import useStxPendingTxData from '@hooks/queries/useStxPendingTxData'; import useWalletSelector from '@hooks/useWalletSelector'; import { SwapConfirmationInput } from '@screens/swap/swapConfirmation/useConfirmSwap'; import { + buf2hex, FungibleToken, getNewNonce, getNonce, microstacksToStx, setNonce, - buf2hex } from '@secretkeylabs/xverse-core'; import { AnchorMode, makeUnsignedContractCall, PostConditionMode } from '@stacks/transactions'; import { AlexSDK, Currency } from 'alex-sdk'; diff --git a/src/locales/en.json b/src/locales/en.json index 00403c39c..18ec77fac 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -352,6 +352,7 @@ "CONITNUE": "Continue", "BACK": "Back", "HIGH_FEE_WARNING_TEXT": "The estimated transaction fee for this transaction is very high.", + "UNCONFIRMED_BALANCE_WARNING": "You are spending unconfirmed outputs in this transaction. This may lower the effective fee rate causing delays in transaction confirmation", "LEDGER": { "CONNECT": { "TITLE": "Connect your hardware wallet", @@ -973,7 +974,7 @@ }, "LONG": { "INVALID_JSON_CONTENT": "The content is not valid JSON. Please verify the content of your inscription.", - "INSUFFICIENT_FUNDS": "You have insufficient confirmed funds to process this transaction. This may be due to unconfirmed outputs in your balance. Please add more funds or try with a lower fee rate", + "INSUFFICIENT_FUNDS": "Your balance is insufficient to process this transaction. Please add more funds to your wallet or try with a lower fee rate", "INVALID_FEE_RATE": "The fee applied to this transaction is too low. To apply a higher fee, select edit fees at the bottom of this screen.", "INVALID_SERVICE_FEE_CONFIG": "The calling application sent an invalid payload. Please contact the application developer.", "INVALID_CONTENT": "The ordinal content you are trying to inscribe is not valid. Please verify the content of your inscription.", @@ -982,7 +983,9 @@ "FAILED_TO_FINALIZE": "The inscription transaction failed to finalize. Please try again or contact support.", "SERVER_ERROR": "An unknown server error occurred. Please try again or contact support." }, - "TOO_MANY_REPEATS": "You can only create up to 24 inscriptions in a single request" + "TOO_MANY_REPEATS": "You can only create up to {{maxRepeats}} inscriptions in a single request", + "UNCONFIRMED_UTXO": "Some of your balance consists of unconfirmed outputs. You don't have enough confirmed funds to proceed with this transaction", + "LEDGER_INSCRIPTION": "This inscription service is not compatible with Ledger accounts. Please switch to a standard account to inscribe." } }, "ERROR_SCREEN": {