refactor: send form utxos

This commit is contained in:
fbwoolf
2023-06-20 14:15:03 -05:00
committed by Fara Woolf
parent ab7d24f98b
commit 11f8047f40
28 changed files with 230 additions and 115 deletions

View File

@@ -1,5 +1,9 @@
import { useMemo } from 'react';
import BigNumber from 'bignumber.js';
import { createMoney } from '@shared/models/money.model';
import { baseCurrencyAmountInQuote, subtractMoney } from '@app/common/money/calculate-money';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '@app/query/bitcoin/address/address.utils';
@@ -10,8 +14,11 @@ import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/marke
export function useBtcAssetBalance(btcAddress: string) {
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const btcAssetBalance = useNativeSegwitBalance(btcAddress);
const pendingBalance = useBitcoinPendingTransactionsBalance(btcAddress);
const availableBalance = subtractMoney(btcAssetBalance.balance, pendingBalance);
const { data: pendingBalance } = useBitcoinPendingTransactionsBalance(btcAddress);
const availableBalance = subtractMoney(
btcAssetBalance.balance,
pendingBalance ?? createMoney(new BigNumber(0), 'BTC')
);
return useMemo(
() => ({

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { isFunction } from '@shared/utils';
export function useOnMount(effect: () => void | (() => void)) {
export function useOnMount(effect: () => void | (() => void) | Promise<unknown>) {
useEffect(() => {
const fn = effect();
return () => (isFunction(fn) ? fn() : undefined);

View File

@@ -9,7 +9,7 @@ import {
determineUtxosForSpend,
determineUtxosForSpendAll,
} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/use-spendable-native-segwit-utxos';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
@@ -18,7 +18,6 @@ interface GenerateNativeSegwitTxValues {
recipient: string;
}
export function useGenerateSignedNativeSegwitTx() {
const { data: utxos } = useSpendableCurrentNativeSegwitAccountUtxos();
const {
address,
publicKeychain: currentAddressIndexKeychain,
@@ -28,8 +27,13 @@ export function useGenerateSignedNativeSegwitTx() {
const networkMode = useBitcoinScureLibNetworkConfig();
return useCallback(
(values: GenerateNativeSegwitTxValues, feeRate: number, isSendingMax?: boolean) => {
if (!utxos) return;
(
values: GenerateNativeSegwitTxValues,
feeRate: number,
utxos: UtxoResponseItem[],
isSendingMax?: boolean
) => {
if (!utxos.length) return;
if (!feeRate) return;
try {
@@ -87,6 +91,6 @@ export function useGenerateSignedNativeSegwitTx() {
return null;
}
},
[address, currentAddressIndexKeychain?.publicKey, networkMode, sign, utxos]
[address, currentAddressIndexKeychain?.publicKey, networkMode, sign]
);
}

View File

@@ -11,6 +11,7 @@ import {
satToBtc,
stxToMicroStx,
} from '@app/common/money/unit-conversion';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { formatInsufficientBalanceError, formatPrecisionError } from '../../error-formatters';
import { FormErrorMessages } from '../../error-messages';
@@ -27,14 +28,19 @@ function amountValidator() {
}
interface BtcInsufficientBalanceValidatorArgs {
recipient: string;
calcMaxSpend(recipient: string): {
calcMaxSpend(
recipient: string,
utxos: UtxoResponseItem[]
): {
spendableBitcoin: BigNumber;
};
recipient: string;
utxos: UtxoResponseItem[];
}
export function btcInsufficientBalanceValidator({
recipient,
calcMaxSpend,
recipient,
utxos,
}: BtcInsufficientBalanceValidatorArgs) {
return yup
.number()
@@ -43,7 +49,7 @@ export function btcInsufficientBalanceValidator({
message: FormErrorMessages.InsufficientFunds,
test(value) {
if (!value) return false;
const maxSpend = calcMaxSpend(recipient);
const maxSpend = calcMaxSpend(recipient, utxos);
if (!maxSpend) return false;
const desiredSpend = new BigNumber(value);
if (desiredSpend.isGreaterThan(maxSpend.spendableBitcoin)) return false;

View File

@@ -11,10 +11,9 @@ import {
determineUtxosForSpend,
determineUtxosForSpendAll,
} from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useSpendableNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { FeesListItem } from './bitcoin-fees-list';
@@ -32,11 +31,14 @@ interface UseBitcoinFeesListArgs {
amount: number;
isSendingMax?: boolean;
recipient: string;
utxos: UtxoResponseItem[];
}
export function useBitcoinFeesList({ amount, isSendingMax, recipient }: UseBitcoinFeesListArgs) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { data: utxos } = useSpendableNativeSegwitUtxos(nativeSegwitSigner.address);
export function useBitcoinFeesList({
amount,
isSendingMax,
recipient,
utxos,
}: UseBitcoinFeesListArgs) {
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const { data: feeRates, isLoading } = useAverageBitcoinFeeRates();
@@ -47,7 +49,7 @@ export function useBitcoinFeesList({ amount, isSendingMax, recipient }: UseBitco
)}`;
}
if (!feeRates || !utxos || !utxos.length) return [];
if (!feeRates || !utxos.length) return [];
const satAmount = btcToSat(amount).toNumber();

View File

@@ -6,7 +6,7 @@ import { useGetBitcoinTransactionsByAddressQuery } from '@app/query/bitcoin/addr
import { useConfigBitcoinEnabled } from '@app/query/common/remote-config/remote-config.query';
import { useStacksPendingTransactions } from '@app/query/stacks/mempool/mempool.hooks';
import { useGetAccountTransactionsWithTransfersQuery } from '@app/query/stacks/transactions/transactions-with-transfers.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useSubmittedTransactions } from '@app/store/submitted-transactions/submitted-transactions.selectors';
import { convertBitcoinTxsToListType, convertStacksTxsToListType } from './activity-list.utils';
@@ -16,10 +16,12 @@ import { SubmittedTransactionList } from './components/submitted-transaction-lis
import { TransactionList } from './components/transaction-list/transaction-list';
export function ActivityList() {
const bitcoinAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { isInitialLoading: isInitialLoadingBitcoinTransactions, data: bitcoinTransactions } =
useGetBitcoinTransactionsByAddressQuery(bitcoinAddress);
const bitcoinPendingTxs = useBitcoinPendingTransactions(bitcoinAddress);
useGetBitcoinTransactionsByAddressQuery(nativeSegwitSigner.address);
const { data: bitcoinPendingTxs = [] } = useBitcoinPendingTransactions(
nativeSegwitSigner.address
);
const {
isInitialLoading: isInitialLoadingStacksTransactions,
data: stacksTransactionsWithTransfers,

View File

@@ -9,7 +9,7 @@ import { AvailableBalance } from '@app/components/available-balance';
import { OnChooseFeeArgs } from '@app/components/bitcoin-fees-list/bitcoin-fees-list';
import { BitcoinCustomFee } from '@app/features/bitcoin-choose-fee/bitcoin-custom-fee/bitcoin-custom-fee';
import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { BitcoinChooseFeeLayout } from './components/bitcoin-choose-fee.layout';
import { ChooseFeeSubtitle } from './components/choose-fee-subtitle';
@@ -38,8 +38,8 @@ export function BitcoinChooseFee({
isLoading,
recommendedFeeRate,
}: BitcoinChooseFeeProps) {
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const btcBalance = useNativeSegwitBalance(currentAccountBtcAddress);
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const btcBalance = useNativeSegwitBalance(nativeSegwitSigner.address);
const hasAmount = amount.amount.isGreaterThan(0);
const [customFeeInitialValue, setCustomFeeInitialValue] = useState(recommendedFeeRate);

View File

@@ -13,6 +13,8 @@ import { TextInputField } from '../../../components/text-input-field';
import { BitcoinCustomFeeFiat } from './bitcoin-custom-fee-fiat';
import { useBitcoinCustomFee } from './hooks/use-bitcoin-custom-fee';
const feeInputLabel = 'sats/vB';
interface BitcoinCustomFeeProps {
onChooseFee({ feeRate, feeValue, time, isCustomFee }: OnChooseFeeArgs): Promise<void>;
onValidateBitcoinSpend(value: number): boolean;
@@ -22,9 +24,6 @@ interface BitcoinCustomFeeProps {
customFeeInitialValue: string;
setCustomFeeInitialValue: Dispatch<SetStateAction<string>>;
}
const feeInputLabel = 'sats/vB';
export function BitcoinCustomFee({
onChooseFee,
recipient,
@@ -44,7 +43,7 @@ export function BitcoinCustomFee({
if (!isValid) return;
await onChooseFee({ feeRate: Number(feeRate), feeValue, time: '', isCustomFee: true });
},
[onChooseFee, onValidateBitcoinSpend, getCustomFeeValues]
[getCustomFeeValues, onValidateBitcoinSpend, onChooseFee]
);
const validationSchema = yup.object({

View File

@@ -5,9 +5,8 @@ import { createMoney } from '@shared/models/money.model';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { i18nFormatCurrency } from '@app/common/money/format-money';
import { determineUtxosForSpend } from '@app/common/transactions/bitcoin/coinselect/local-coin-selection';
import { useSpendableNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
interface UseBitcoinCustomFeeArgs {
recipient: string;
@@ -15,15 +14,12 @@ interface UseBitcoinCustomFeeArgs {
}
export function useBitcoinCustomFee({ recipient, amount }: UseBitcoinCustomFeeArgs) {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const currentAccountBtcAddress = nativeSegwitSigner.address;
const { data: utxos } = useSpendableNativeSegwitUtxos(currentAccountBtcAddress);
const { data: utxos = [] } = useSpendableCurrentNativeSegwitAccountUtxos();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
return useCallback(
(feeRate: number) => {
if (!feeRate || !utxos || !utxos.length) return { fee: 0, fiatFeeValue: '' };
if (!feeRate || !utxos.length) return { fee: 0, fiatFeeValue: '' };
const determineUtxosArgs = {
amount,

View File

@@ -17,6 +17,7 @@ import {
import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoin-fees-list';
import { BitcoinChooseFee } from '@app/features/bitcoin-choose-fee/bitcoin-choose-fee';
import { useValidateBitcoinSpend } from '@app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { formFeeRowValue } from '../../common/send/utils';
import { useRpcSendTransferState } from './rpc-send-transfer-container';
@@ -25,29 +26,32 @@ function useRpcSendTransferFeeState() {
const location = useLocation();
const amount = get(location.state, 'amount');
const amountAsMoney = createMoney(Number(amount), 'BTC');
const utxos = get(location.state, 'utxos') as UtxoResponseItem[];
return {
address: get(location.state, 'address') as string,
amountAsMoney,
utxos,
};
}
export function RpcSendTransferChooseFee() {
const { selectedFeeType, setSelectedFeeType } = useRpcSendTransferState();
const { address, amountAsMoney } = useRpcSendTransferFeeState();
const { address, amountAsMoney, utxos } = useRpcSendTransferFeeState();
const navigate = useNavigate();
const { whenWallet } = useWalletType();
const generateTx = useGenerateSignedNativeSegwitTx();
const { feesList, isLoading } = useBitcoinFeesList({
amount: Number(amountAsMoney.amount),
recipient: address,
utxos,
});
const recommendedFeeRate = feesList[1]?.feeRate.toString() || '';
const { showInsufficientBalanceError, onValidateBitcoinFeeSpend } = useValidateBitcoinSpend();
async function previewTransfer({ feeRate, feeValue, time, isCustomFee }: OnChooseFeeArgs) {
const resp = generateTx({ amount: amountAsMoney, recipient: address }, feeRate);
const resp = generateTx({ amount: amountAsMoney, recipient: address }, feeRate, utxos);
if (!resp) return logger.error('Attempted to generate raw tx, but no tx exists');

View File

@@ -13,7 +13,7 @@ import { useAnalytics } from '@app/common/hooks/analytics/use-analytics';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { formatMoney, formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { satToBtc } from '@app/common/money/unit-conversion';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

View File

@@ -5,8 +5,10 @@ import { RouteUrls } from '@shared/route-urls';
import { noop } from '@shared/utils';
import { useDefaultRequestParams } from '@app/common/hooks/use-default-request-search-params';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { initialSearchParams } from '@app/common/initial-search-params';
import { useWalletType } from '@app/common/use-wallet-type';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
export function useRpcSendTransferRequestParams() {
const defaultParams = useDefaultRequestParams();
@@ -25,11 +27,16 @@ export function useRpcSendTransfer() {
const navigate = useNavigate();
const { whenWallet } = useWalletType();
const { address, amount, origin } = useRpcSendTransferRequestParams();
const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos();
// Forcing a refetch to ensure UTXOs are fresh
useOnMount(() => refetch());
return {
address,
amount,
origin,
utxos,
async onChooseTransferFee() {
whenWallet({
software: () =>
@@ -37,6 +44,7 @@ export function useRpcSendTransfer() {
state: {
address,
amount,
utxos,
},
}),
ledger: noop,

View File

@@ -3,7 +3,7 @@ import * as btc from '@scure/btc-signer';
import { logger } from '@shared/logger';
import { OrdinalSendFormValues } from '@shared/models/form.model';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useBitcoinScureLibNetworkConfig } from '@app/store/accounts/blockchain/bitcoin/bitcoin-keychain';
import { useCurrentAccountNativeSegwitSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';

View File

@@ -6,7 +6,7 @@ import { createMoney } from '@shared/models/money.model';
import { baseCurrencyAmountInQuote } from '@app/common/money/calculate-money';
import { formatMoneyPadded, i18nFormatCurrency } from '@app/common/money/format-money';
import { FeesListItem } from '@app/components/bitcoin-fees-list/bitcoin-fees-list';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { TaprootUtxo } from '@app/query/bitcoin/ordinals/use-taproot-address-utxos.query';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';

View File

@@ -11,7 +11,7 @@ import { BaseDrawer } from '@app/components/drawer/base-drawer';
import { InfoCard, InfoCardRow, InfoCardSeparator } from '@app/components/info-card/info-card';
import { InscriptionPreview } from '@app/components/inscription-preview-card/components/inscription-preview';
import { PrimaryButton } from '@app/components/primary-button';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useAppDispatch } from '@app/store';
import { inscriptionSent } from '@app/store/ordinals/ordinals.slice';

View File

@@ -7,20 +7,17 @@ import { createMoney } from '@shared/models/money.model';
import { satToBtc } from '@app/common/money/unit-conversion';
import { BtcSizeFeeEstimator } from '@app/common/transactions/bitcoin/fees/btc-size-fee-estimator';
import { useSpendableNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useCurrentNativeSegwitAddressBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { useAverageBitcoinFeeRates } from '@app/query/bitcoin/fees/fee-estimates.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
export function useCalculateMaxBitcoinSpend() {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const balance = useCurrentNativeSegwitAddressBalance();
const { data: utxos } = useSpendableNativeSegwitUtxos(nativeSegwitSigner.address);
const { data: feeRates } = useAverageBitcoinFeeRates();
return useCallback(
(address = '', feeRate?: number) => {
if (!utxos || !feeRates)
(address = '', utxos: UtxoResponseItem[], feeRate?: number) => {
if (!utxos.length || !feeRates)
return {
spendAllFee: 0,
amount: createMoney(0, 'BTC'),
@@ -45,6 +42,6 @@ export function useCalculateMaxBitcoinSpend() {
spendableBitcoin: satToBtc(spendableAmount),
};
},
[balance.amount, feeRates, utxos]
[balance.amount, feeRates]
);
}

View File

@@ -22,6 +22,7 @@ import { LoadingSpinner } from '@app/components/loading-spinner';
import { ModalHeader } from '@app/components/modal-header';
import { BitcoinChooseFee } from '@app/features/bitcoin-choose-fee/bitcoin-choose-fee';
import { useValidateBitcoinSpend } from '@app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/use-brc-20';
import { useSendBitcoinAssetContextState } from '../../family/bitcoin/components/send-bitcoin-asset-container';
@@ -32,18 +33,20 @@ function useBrc20ChooseFeeState() {
tick: get(location.state, 'tick') as string,
amount: get(location.state, 'amount') as string,
recipient: get(location.state, 'recipient') as string,
utxos: get(location.state, 'utxos') as UtxoResponseItem[],
};
}
export function BrcChooseFee() {
const navigate = useNavigate();
const { amount, recipient, tick } = useBrc20ChooseFeeState();
const { amount, recipient, tick, utxos } = useBrc20ChooseFeeState();
const generateTx = useGenerateSignedNativeSegwitTx();
const { selectedFeeType, setSelectedFeeType } = useSendBitcoinAssetContextState();
const { initiateTransfer } = useBrc20Transfers();
const { feesList, isLoading } = useBitcoinFeesList({
amount: Number(amount),
recipient,
utxos,
});
const recommendedFeeRate = feesList[1]?.feeRate.toString() || '';
@@ -72,7 +75,8 @@ export function BrcChooseFee() {
amount: serviceFeeAsMoney,
recipient: serviceFeeRecipient,
},
feeRate
feeRate,
utxos
);
if (!resp) return logger.error('Attempted to generate raw tx, but no tx exists');

View File

@@ -21,7 +21,7 @@ import {
} from '@app/components/info-card/info-card';
import { ModalHeader } from '@app/components/modal-header';
import { PrimaryButton } from '@app/components/primary-button';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useBrc20Transfers } from '@app/query/bitcoin/ordinals/brc20/use-brc-20';
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';

View File

@@ -10,6 +10,7 @@ import { createMoney } from '@shared/models/money.model';
import { RouteUrls } from '@shared/route-urls';
import { noop } from '@shared/utils';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { unitToFractionalUnit } from '@app/common/money/unit-conversion';
import { useWalletType } from '@app/common/use-wallet-type';
import {
@@ -19,7 +20,8 @@ import {
import { brc20TokenAmountValidator } from '@app/common/validation/forms/amount-validators';
import { currencyAmountValidator } from '@app/common/validation/forms/currency-validators';
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { createDefaultInitialFormValues } from '../../send-form.utils';
@@ -41,11 +43,15 @@ export function useBrc20SendForm({ balance, tick, decimals }: UseBrc20SendFormAr
const { whenWallet } = useWalletType();
const navigate = useNavigate();
const currentNetwork = useCurrentNetwork();
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos();
// Forcing a refetch to ensure UTXOs are fresh
useOnMount(() => refetch());
// TODO: change recipient to that one user iputs
const initialValues = createDefaultInitialFormValues({
recipient: currentAccountBtcAddress,
recipient: nativeSegwitSigner.address,
amount: '',
symbol: tick,
});
@@ -73,7 +79,7 @@ export function useBrc20SendForm({ balance, tick, decimals }: UseBrc20SendFormAr
whenWallet({
software: () =>
navigate(RouteUrls.SendBrc20ChooseFee.replace(':ticker', tick), {
state: { ...values, tick, hasHeaderTitle: true },
state: { ...values, tick, utxos, hasHeaderTitle: true },
}),
ledger: noop,
})();

View File

@@ -11,6 +11,7 @@ import { useBitcoinFeesList } from '@app/components/bitcoin-fees-list/use-bitcoi
import { ModalHeader } from '@app/components/modal-header';
import { BitcoinChooseFee } from '@app/features/bitcoin-choose-fee/bitcoin-choose-fee';
import { useValidateBitcoinSpend } from '@app/features/bitcoin-choose-fee/hooks/use-validate-bitcoin-spend';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
import { useSendBitcoinAssetContextState } from '../../family/bitcoin/components/send-bitcoin-asset-container';
import { useBtcChooseFee } from './use-btc-choose-fee';
@@ -20,16 +21,18 @@ export function useBtcChooseFeeState() {
return {
isSendingMax: get(location.state, 'isSendingMax') as boolean,
txValues: get(location.state, 'values') as BitcoinSendFormValues,
utxos: get(location.state, 'utxos') as UtxoResponseItem[],
};
}
export function BtcChooseFee() {
const { isSendingMax, txValues } = useBtcChooseFeeState();
const { isSendingMax, txValues, utxos } = useBtcChooseFeeState();
const { selectedFeeType, setSelectedFeeType } = useSendBitcoinAssetContextState();
const { feesList, isLoading } = useBitcoinFeesList({
amount: Number(txValues.amount),
isSendingMax,
recipient: txValues.recipient,
utxos,
});
const recommendedFeeRate = feesList[1]?.feeRate.toString() || '';

View File

@@ -23,7 +23,7 @@ import {
} from '@app/components/info-card/info-card';
import { ModalHeader } from '@app/components/modal-header';
import { PrimaryButton } from '@app/components/primary-button';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/use-current-account-native-segwit-utxos';
import { useCurrentNativeSegwitUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useBitcoinBroadcastTransaction } from '@app/query/bitcoin/transaction/use-bitcoin-broadcast-transaction';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';

View File

@@ -10,7 +10,7 @@ import { BtcIcon } from '@app/components/icons/btc-icon';
import { HighFeeDrawer } from '@app/features/high-fee-drawer/high-fee-drawer';
import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useCryptoCurrencyMarketData } from '@app/query/common/market-data/market-data.hooks';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { AmountField } from '../../components/amount-field';
import { FormFooter } from '../../components/form-footer';
@@ -28,8 +28,8 @@ export function BtcSendForm() {
const routeState = useSendFormRouteState();
const btcMarketData = useCryptoCurrencyMarketData('BTC');
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const btcBalance = useNativeSegwitBalance(currentAccountBtcAddress);
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const btcBalance = useNativeSegwitBalance(nativeSegwitSigner.address);
const {
calcMaxSpend,
@@ -39,6 +39,7 @@ export function BtcSendForm() {
isSendingMax,
onFormStateChange,
onSetIsSendingMax,
utxos,
validationSchema,
} = useBtcSendForm();
@@ -56,7 +57,7 @@ export function BtcSendForm() {
>
{props => {
onFormStateChange(props.values);
const sendMaxCalculation = calcMaxSpend(props.values.recipient);
const sendMaxCalculation = calcMaxSpend(props.values.recipient, utxos);
return (
<Form>
@@ -88,7 +89,7 @@ export function BtcSendForm() {
<Outlet />
{/* This is for testing purposes only, to make sure the form is ready to be submitted */}
{calcMaxSpend(props.values.recipient).spendableBitcoin.toNumber() > 0 ? (
{calcMaxSpend(props.values.recipient, utxos).spendableBitcoin.toNumber() > 0 ? (
<Box data-testid={SendCryptoAssetSelectors.SendPageReady}></Box>
) : null}
</Form>

View File

@@ -16,7 +16,7 @@ import { useSendFormNavigate } from '../../hooks/use-send-form-navigate';
import { useBtcChooseFeeState } from './btc-choose-fee';
export function useBtcChooseFee() {
const { isSendingMax, txValues } = useBtcChooseFeeState();
const { isSendingMax, txValues, utxos } = useBtcChooseFeeState();
const navigate = useNavigate();
const { whenWallet } = useWalletType();
const sendFormNavigate = useSendFormNavigate();
@@ -38,13 +38,14 @@ export function useBtcChooseFee() {
{
amount: isSendingMax
? createMoney(
btcToSat(calcMaxSpend(txValues.recipient, feeRate).spendableBitcoin),
btcToSat(calcMaxSpend(txValues.recipient, utxos, feeRate).spendableBitcoin),
'BTC'
)
: amountAsMoney,
recipient: txValues.recipient,
},
feeRate,
utxos,
isSendingMax
);

View File

@@ -8,6 +8,7 @@ import { BitcoinSendFormValues } from '@shared/models/form.model';
import { noop } from '@shared/utils';
import { formatPrecisionError } from '@app/common/error-formatters';
import { useOnMount } from '@app/common/hooks/use-on-mount';
import { useWalletType } from '@app/common/use-wallet-type';
import {
btcAddressNetworkValidator,
@@ -23,8 +24,9 @@ import {
currencyAmountValidator,
} from '@app/common/validation/forms/currency-validators';
import { useUpdatePersistedSendFormValues } from '@app/features/popup-send-form-restoration/use-update-persisted-send-form-values';
import { useSpendableCurrentNativeSegwitAccountUtxos } from '@app/query/bitcoin/address/utxos-by-address.hooks';
import { useNativeSegwitBalance } from '@app/query/bitcoin/balance/bitcoin-balances.query';
import { useCurrentAccountNativeSegwitAddressIndexZero } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { useCurrentNetwork } from '@app/store/networks/networks.selectors';
import { useCalculateMaxBitcoinSpend } from '../../family/bitcoin/hooks/use-calculate-max-spend';
@@ -34,13 +36,17 @@ export function useBtcSendForm() {
const [isSendingMax, setIsSendingMax] = useState(false);
const formRef = useRef<FormikProps<BitcoinSendFormValues>>(null);
const currentNetwork = useCurrentNetwork();
const currentAccountBtcAddress = useCurrentAccountNativeSegwitAddressIndexZero();
const btcCryptoCurrencyAssetBalance = useNativeSegwitBalance(currentAccountBtcAddress);
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
const { data: utxos = [], refetch } = useSpendableCurrentNativeSegwitAccountUtxos();
const btcCryptoCurrencyAssetBalance = useNativeSegwitBalance(nativeSegwitSigner.address);
const { whenWallet } = useWalletType();
const sendFormNavigate = useSendFormNavigate();
const calcMaxSpend = useCalculateMaxBitcoinSpend();
const { onFormStateChange } = useUpdatePersistedSendFormValues();
// Forcing a refetch to ensure UTXOs are fresh
useOnMount(() => refetch());
return {
calcMaxSpend,
currentNetwork,
@@ -50,6 +56,7 @@ export function useBtcSendForm() {
onSetIsSendingMax(value: boolean) {
setIsSendingMax(value);
},
utxos,
validationSchema: yup.object({
amount: yup
.number()
@@ -60,17 +67,18 @@ export function useBtcSendForm() {
.concat(currencyAmountValidator())
.concat(
btcInsufficientBalanceValidator({
calcMaxSpend,
// TODO: investigate yup features for cross-field validation
// to prevent need to access form via ref
recipient: formRef.current?.values.recipient ?? '',
calcMaxSpend,
utxos,
})
),
recipient: yup
.string()
.concat(btcAddressValidator())
.concat(btcAddressNetworkValidator(currentNetwork.chain.bitcoin.network))
.concat(notCurrentAddressValidator(currentAccountBtcAddress || ''))
.concat(notCurrentAddressValidator(nativeSegwitSigner.address || ''))
.required('Enter a bitcoin address'),
}),
@@ -83,7 +91,7 @@ export function useBtcSendForm() {
await formikHelpers.validateForm();
whenWallet({
software: () => sendFormNavigate.toChooseTransactionFee(isSendingMax, values),
software: () => sendFormNavigate.toChooseTransactionFee(isSendingMax, utxos, values),
ledger: noop,
})();
},

View File

@@ -7,6 +7,8 @@ import { StacksTransaction } from '@stacks/transactions';
import { BitcoinSendFormValues } from '@shared/models/form.model';
import { RouteUrls } from '@shared/route-urls';
import { UtxoResponseItem } from '@app/query/bitcoin/bitcoin-client';
interface ConfirmationRouteState {
decimals?: number;
token?: string;
@@ -36,10 +38,15 @@ export function useSendFormNavigate() {
backToSendForm(state: any) {
return navigate('../', { relative: 'path', replace: true, state });
},
toChooseTransactionFee(isSendingMax: boolean, values: BitcoinSendFormValues) {
toChooseTransactionFee(
isSendingMax: boolean,
utxos: UtxoResponseItem[],
values: BitcoinSendFormValues
) {
return navigate('choose-fee', {
state: {
isSendingMax,
utxos,
values,
hasHeaderTitle: true,
},

View File

@@ -1,40 +1,66 @@
import { useMemo } from 'react';
import { useCallback } from 'react';
import { createMoney } from '@shared/models/money.model';
import { BitcoinTransaction } from '@shared/models/transactions/bitcoin-transaction.model';
import { sumNumbers } from '@app/common/math/helpers';
import { useGetBitcoinTransactionsByAddressQuery } from './transactions-by-address.query';
function useFilterAddressPendingTransactions() {
return useCallback((txs: BitcoinTransaction[]) => {
return txs.filter(tx => !tx.status.confirmed);
}, []);
}
export function useBitcoinPendingTransactions(address: string) {
const filterPendingTransactions = useFilterAddressPendingTransactions();
return useGetBitcoinTransactionsByAddressQuery(address, {
select(txs) {
return txs.filter(tx => !tx.status.confirmed);
return filterPendingTransactions(txs);
},
});
}
export function useBitcoinPendingTransactionsBalance(address: string) {
const { data: pendingTransactions = [] } = useBitcoinPendingTransactions(address);
export function useBitcoinPendingTransactionsInputs(address: string) {
const filterPendingTransactions = useFilterAddressPendingTransactions();
return useMemo(
() =>
createMoney(
sumNumbers(
pendingTransactions
.flatMap(tx => tx.vout.filter(output => output.scriptpubkey_address !== address))
.map(vout => vout.value)
),
'BTC'
),
[address, pendingTransactions]
return useGetBitcoinTransactionsByAddressQuery(address, {
select(txs) {
return filterPendingTransactions(txs).flatMap(tx => tx.vin.map(input => input));
},
});
}
function useFilterAddressPendingTxsOutputs(address: string) {
return useCallback(
(pendingTxs: BitcoinTransaction[]) => {
return pendingTxs.flatMap(tx => {
const inputsFromAddress = tx.vin.filter(
input => input.prevout.scriptpubkey_address === address
);
// Output is possibly change, so we only subtract the value if the address
// is funding the tx and sending utxos to a different address
return tx.vout.filter(
output => inputsFromAddress.length && output.scriptpubkey_address !== address
);
});
},
[address]
);
}
export function useBitcoinPendingTransactionsInputs(address: string) {
const { data: pendingTransactions = [] } = useBitcoinPendingTransactions(address);
export function useBitcoinPendingTransactionsBalance(address: string) {
const filterPendingTransactions = useFilterAddressPendingTransactions();
const filterPendingTxsOutputs = useFilterAddressPendingTxsOutputs(address);
return useMemo(() => {
return pendingTransactions.flatMap(tx => tx.vin.map(input => input));
}, [pendingTransactions]);
return useGetBitcoinTransactionsByAddressQuery(address, {
select(txs) {
return createMoney(
sumNumbers(filterPendingTxsOutputs(filterPendingTransactions(txs)).map(vout => vout.value)),
'BTC'
);
},
});
}

View File

@@ -1,5 +1,8 @@
import { useCallback } from 'react';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { UtxoResponseItem } from '../bitcoin-client';
import { useInscriptionByAddressQuery } from '../ordinals/use-inscriptions.query';
import { useBitcoinPendingTransactionsInputs } from './transactions-by-address.hooks';
import { useGetUtxosByAddressQuery } from './utxos-by-address.query';
@@ -13,39 +16,69 @@ export function useCurrentNativeSegwitUtxos() {
return useGetUtxosByAddressQuery(nativeSegwitSigner.address);
}
export function useSpendableNativeSegwitUtxos(address: string) {
function useFilterAddressNativeSegwitInscriptions(address: string) {
const {
data: inscriptions,
hasNextPage: hasMoreInscriptionsToLoad,
isLoading: isLoadingInscriptions,
} = useInscriptionByAddressQuery(address);
const pendingInputs = useBitcoinPendingTransactionsInputs(address);
return useGetUtxosByAddressQuery(address, {
select(utxos) {
// While infinite query check has more data to load, or Stamps are loading
// assume nothing is spendable
return useCallback(
(utxos: UtxoResponseItem[]) => {
// While infinite query checks if has more data to load, or Stamps
// are loading, assume nothing is spendable
if (hasMoreInscriptionsToLoad || isLoadingInscriptions) return [];
const inscribedUtxos = inscriptions?.pages.flatMap(page => page.results) ?? [];
return (
utxos
.filter(
utxo =>
!inscribedUtxos.some(
inscription =>
utxo.txid === inscription.tx_id && utxo.vout === Number(inscription.offset)
)
return utxos.filter(
utxo =>
!inscribedUtxos.some(
inscription =>
utxo.txid === inscription.tx_id && utxo.vout === Number(inscription.offset)
)
// Safety check to make sure we don't reuse utxos in a pending tx
.filter(utxo => !pendingInputs.find(input => input.txid === utxo.txid))
);
},
[hasMoreInscriptionsToLoad, inscriptions?.pages, isLoadingInscriptions]
);
}
function useFilterAddressNativeSegwitPendingTxsUtxos(address: string) {
const { data: pendingInputs = [] } = useBitcoinPendingTransactionsInputs(address);
return useCallback(
(utxos: UtxoResponseItem[]) => {
return utxos.filter(
utxo =>
!pendingInputs.find(
input => input.prevout.scriptpubkey_address === address && input.txid === utxo.txid
)
);
},
[address, pendingInputs]
);
}
export function useAllSpendableNativeSegwitUtxos(address: string) {
const filterOutInscriptions = useFilterAddressNativeSegwitInscriptions(address);
return useGetUtxosByAddressQuery(address, {
select(utxos) {
return filterOutInscriptions(utxos);
},
});
}
function useSpendableAndNotPendingNativeSegwitUtxos(address: string) {
const filterOutInscriptions = useFilterAddressNativeSegwitInscriptions(address);
const filterOutPendingTxsUtxos = useFilterAddressNativeSegwitPendingTxsUtxos(address);
return useGetUtxosByAddressQuery(address, {
select(utxos) {
return filterOutPendingTxsUtxos(filterOutInscriptions(utxos));
},
});
}
export function useSpendableCurrentNativeSegwitAccountUtxos() {
const nativeSegwitSigner = useCurrentAccountNativeSegwitIndexZeroSigner();
return useSpendableNativeSegwitUtxos(nativeSegwitSigner.address);
return useSpendableAndNotPendingNativeSegwitUtxos(nativeSegwitSigner.address);
}

View File

@@ -3,20 +3,21 @@ import { useMemo } from 'react';
import BigNumber from 'bignumber.js';
import { createMoney } from '@shared/models/money.model';
import { isDefined } from '@shared/utils';
import { isDefined, isUndefined } from '@shared/utils';
import { sumNumbers } from '@app/common/math/helpers';
import { useCurrentAccountNativeSegwitIndexZeroSigner } from '@app/store/accounts/blockchain/bitcoin/native-segwit-account.hooks';
import { createBitcoinCryptoCurrencyAssetTypeWrapper } from '../address/address.utils';
import { useSpendableNativeSegwitUtxos } from '../address/utxos-by-address.hooks';
import { useAllSpendableNativeSegwitUtxos } from '../address/utxos-by-address.hooks';
import { useOrdinalsAwareUtxoQueries } from '../ordinals/ordinals-aware-utxo.query';
import { useTaprootAccountUtxosQuery } from '../ordinals/use-taproot-address-utxos.query';
function useGetBitcoinBalanceByAddress(address: string) {
const utxos = useSpendableNativeSegwitUtxos(address).data;
const { data: utxos } = useAllSpendableNativeSegwitUtxos(address);
return useMemo(() => {
if (!utxos) return createMoney(new BigNumber(0), 'BTC');
if (isUndefined(utxos)) return createMoney(new BigNumber(0), 'BTC');
return createMoney(sumNumbers(utxos.map(utxo => utxo.value)), 'BTC');
}, [utxos]);
}