From 26879e447e165c5645c587303b4cc18235556615 Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Tue, 26 Nov 2024 13:48:58 +0100 Subject: [PATCH 1/3] refactor(suite): use design system syntax in CoinControl --- .../CoinControl/CoinControl.tsx | 107 +++++++----------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx index a7a7d0aadf7..72291c016e0 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import styled, { useTheme } from 'styled-components'; -import { typography } from '@trezor/theme'; import { COMPOSE_ERROR_TYPES } from '@suite-common/wallet-constants'; import { fetchAllTransactionsForAccountThunk } from '@suite-common/wallet-core'; import { getTxsPerPage } from '@suite-common/suite-utils'; import { amountToSmallestUnit, formatNetworkAmount } from '@suite-common/wallet-utils'; -import { Card, Checkbox, Icon, Switch, variables } from '@trezor/components'; +import { Card, Checkbox, Column, Icon, Row, Switch, Text } from '@trezor/components'; +import { spacings, spacingsPx } from '@trezor/theme'; import { FormattedCryptoAmount, Translation } from 'src/components/suite'; import { useDispatch, useSelector } from 'src/hooks/suite'; @@ -21,38 +21,13 @@ import { filterAndCategorizeUtxos } from 'src/utils/wallet/filterAndCategorizeUt import { UtxoSelectionList } from './UtxoSelectionList'; import { UtxoSearch } from './UtxoSearch'; -const Row = styled.div` - align-items: center; - display: flex; - font-weight: ${variables.FONT_WEIGHT.MEDIUM}; -`; - -const SecondRow = styled(Row)` - border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; - font-size: ${variables.FONT_SIZE.SMALL}; - margin-top: 20px; - padding-bottom: 14px; -`; - -const GreyText = styled.div` - ${typography.hint} - color: ${({ theme }) => theme.textSubdued}; -`; - -// eslint-disable-next-line local-rules/no-override-ds-component -const StyledSwitch = styled(Switch)` - margin: 0 14px 0 auto; -`; - -const AmountWrapper = styled.div` - display: flex; - flex-direction: column; - margin-left: auto; - text-align: right; +const Header = styled.header` + border-bottom: 1px solid ${({ theme }) => theme.borderElevation1}; + padding-bottom: ${spacingsPx.sm}; `; const SearchWrapper = styled.div` - margin-top: 20px; + margin-top: ${spacingsPx.lg}; `; const MissingToInput = styled.div<{ $isVisible: boolean }>` @@ -61,18 +36,18 @@ const MissingToInput = styled.div<{ $isVisible: boolean }>` `; const Empty = styled.div` - border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; - margin-bottom: 12px; - padding: 14px 0; + border-bottom: 1px solid ${({ theme }) => theme.borderElevation1}; + margin-bottom: ${spacingsPx.sm}; + padding: ${spacingsPx.sm} 0; `; const StyledPagination = styled(Pagination)` - margin: 20px 0; + margin: ${spacingsPx.lg} 0; `; -interface CoinControlProps { +type CoinControlProps = { close: () => void; -} +}; export const CoinControl = ({ close }: CoinControlProps) => { const [currentPage, setSelectedPage] = useState(1); @@ -197,33 +172,37 @@ export const CoinControl = ({ close }: CoinControlProps) => { return ( - - - - - - - - - - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{hasEligibleUtxos && ( Date: Mon, 2 Dec 2024 18:48:02 +0100 Subject: [PATCH 2/3] chore(suite): reorganize coin control file structure to resemble component structure --- .../Options/BitcoinOptions/CoinControl/CoinControl.tsx | 2 +- .../UtxoSelection}/UtxoSelection.tsx | 0 .../{ => UtxoSelectionList/UtxoSelection}/UtxoTag.tsx | 0 .../{ => UtxoSelectionList}/UtxoSelectionList.tsx | 10 ++++------ 4 files changed, 5 insertions(+), 7 deletions(-) rename packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/{ => UtxoSelectionList/UtxoSelection}/UtxoSelection.tsx (100%) rename packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/{ => UtxoSelectionList/UtxoSelection}/UtxoTag.tsx (100%) rename packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/{ => UtxoSelectionList}/UtxoSelectionList.tsx (88%) diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx index 72291c016e0..49941ce60d7 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx @@ -18,7 +18,7 @@ import { selectCurrentTargetAnonymity } from 'src/reducers/wallet/coinjoinReduce import { selectLabelingDataForSelectedAccount } from 'src/reducers/suite/metadataReducer'; import { filterAndCategorizeUtxos } from 'src/utils/wallet/filterAndCategorizeUtxosUtils'; -import { UtxoSelectionList } from './UtxoSelectionList'; +import { UtxoSelectionList } from './UtxoSelectionList/UtxoSelectionList'; import { UtxoSearch } from './UtxoSearch'; const Header = styled.header` diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelection.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoSelection.tsx similarity index 100% rename from packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelection.tsx rename to packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoSelection.tsx diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoTag.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoTag.tsx similarity index 100% rename from packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoTag.tsx rename to packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelection/UtxoTag.tsx diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx similarity index 88% rename from packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx rename to packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx index e4850aa1b82..33dfc3b9c7c 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSelectionList/UtxoSelectionList.tsx @@ -3,7 +3,7 @@ import { ReactNode } from 'react'; import styled from 'styled-components'; import { transparentize } from 'polished'; -import { selectAccountTransactionsWithNulls } from '@suite-common/wallet-core'; +import { selectAccountTransactions } from '@suite-common/wallet-core'; import { Icon, variables, IconName } from '@trezor/components'; import type { AccountUtxo } from '@trezor/connect'; import { CSSColor } from '@trezor/theme'; @@ -11,7 +11,7 @@ import { CSSColor } from '@trezor/theme'; import { useSelector } from 'src/hooks/suite'; import { useSendFormContext } from 'src/hooks/wallet'; -import { UtxoSelection } from './UtxoSelection'; +import { UtxoSelection } from './UtxoSelection/UtxoSelection'; const Wrapper = styled.section` border-bottom: 1px solid ${({ theme }) => theme.legacy.STROKE_GREY}; @@ -65,9 +65,7 @@ export const UtxoSelectionList = ({ }: UtxoSelectionListProps) => { const { account } = useSendFormContext(); - const accountTransactions = useSelector(state => - selectAccountTransactionsWithNulls(state, account.key), - ); + const accountTransactions = useSelector(state => selectAccountTransactions(state, account.key)); return ( @@ -89,7 +87,7 @@ export const UtxoSelectionList = ({ transaction?.txid === utxo.txid, + transaction => transaction.txid === utxo.txid, )} utxo={utxo} /> From 49f708890a0a431a6b3c515927390ece903b2d4f Mon Sep 17 00:00:00 2001 From: Jan Komarek Date: Mon, 2 Dec 2024 18:49:03 +0100 Subject: [PATCH 3/3] feat(suite, suite-common): add utxo sorting --- .../src/hooks/wallet/form/useUtxoSelection.ts | 62 ++++++++++----- packages/suite/src/support/messages.ts | 16 ++++ packages/suite/src/types/wallet/sendForm.ts | 3 + .../wallet/__tests__/utxoSortingUtils.test.ts | 74 +++++++++++++++++ .../src/utils/wallet/utxoSortingUtils.ts | 79 +++++++++++++++++++ .../CoinControl/CoinControl.tsx | 10 +-- .../CoinControl/UtxoSortingSelect.tsx | 35 ++++++++ suite-common/wallet-constants/src/sendForm.ts | 1 + suite-common/wallet-types/src/sendForm.ts | 3 + 9 files changed, 258 insertions(+), 25 deletions(-) create mode 100644 packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts create mode 100644 packages/suite/src/utils/wallet/utxoSortingUtils.ts create mode 100644 packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/UtxoSortingSelect.tsx diff --git a/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts b/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts index acc2940e200..ddd7681dd8e 100644 --- a/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts +++ b/packages/suite/src/hooks/wallet/form/useUtxoSelection.ts @@ -1,9 +1,13 @@ import { useEffect, useMemo } from 'react'; import { UseFormReturn } from 'react-hook-form'; -import { ExcludedUtxos, FormState } from '@suite-common/wallet-types'; +import { ExcludedUtxos, FormState, UtxoSorting } from '@suite-common/wallet-types'; import type { AccountUtxo, PROTO } from '@trezor/connect'; import { getUtxoOutpoint, isSameUtxo } from '@suite-common/wallet-utils'; +import { selectAccountTransactionsWithNulls } from '@suite-common/wallet-core'; + +import { useSelector } from 'src/hooks/suite'; +import { sortUtxos } from 'src/utils/wallet/utxoSortingUtils'; import { useCoinjoinRegisteredUtxos } from './useCoinjoinRegisteredUtxos'; import { @@ -28,19 +32,26 @@ export const useUtxoSelection = ({ setValue, watch, }: UtxoSelectionContextProps): UtxoSelectionContext => { + const accountTransactions = useSelector(state => + selectAccountTransactionsWithNulls(state, account.key), + ); + // register custom form field (without HTMLElement) useEffect(() => { register('isCoinControlEnabled'); register('selectedUtxos'); register('anonymityWarningChecked'); + register('utxoSorting'); }, [register]); const coinjoinRegisteredUtxos = useCoinjoinRegisteredUtxos({ account }); - // has coin control been enabled manually? - const isCoinControlEnabled = watch('isCoinControlEnabled'); - // fee level - const selectedFee = watch('selectedFee'); + const [isCoinControlEnabled, options, selectedFee, utxoSorting] = watch([ + 'isCoinControlEnabled', + 'options', + 'selectedFee', + 'utxoSorting', + ]); // confirmation of spending low-anonymity UTXOs - only relevant for coinjoin account const anonymityWarningChecked = !!watch('anonymityWarningChecked'); // manually selected UTXOs @@ -79,20 +90,29 @@ export const useUtxoSelection = ({ const spendableUtxos: AccountUtxo[] = []; const lowAnonymityUtxos: AccountUtxo[] = []; const dustUtxos: AccountUtxo[] = []; - account?.utxo?.forEach(utxo => { - switch (excludedUtxos[getUtxoOutpoint(utxo)]) { - case 'low-anonymity': - lowAnonymityUtxos.push(utxo); - - return; - case 'dust': - dustUtxos.push(utxo); - - return; - default: - spendableUtxos.push(utxo); - } - }); + + // Skip sorting and categorizing UTXOs if coin control is not enabled. + const utxos = + options?.includes('utxoSelection') && account?.utxo + ? sortUtxos(account?.utxo, utxoSorting, accountTransactions) + : account?.utxo; + + if (utxos?.length) { + utxos?.forEach(utxo => { + switch (excludedUtxos[getUtxoOutpoint(utxo)]) { + case 'low-anonymity': + lowAnonymityUtxos.push(utxo); + + return; + case 'dust': + dustUtxos.push(utxo); + + return; + default: + spendableUtxos.push(utxo); + } + }); + } // category displayed on top and controlled by the check-all checkbox const topCategory = @@ -139,6 +159,8 @@ export const useUtxoSelection = ({ setValue('anonymityWarningChecked', false); } + const selectUtxoSorting = (sorting: UtxoSorting) => setValue('utxoSorting', sorting); + const toggleAnonymityWarning = () => setValue('anonymityWarningChecked', !anonymityWarningChecked); @@ -204,6 +226,8 @@ export const useUtxoSelection = ({ selectedUtxos, spendableUtxos, coinjoinRegisteredUtxos, + utxoSorting, + selectUtxoSorting, toggleAnonymityWarning, toggleCheckAllUtxos, toggleCoinControl, diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 35b058fed43..1a5fb7e4dec 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -5704,6 +5704,22 @@ export default defineMessages({ defaultMessage: 'There are no spendable UTXOs in your account.', description: 'Message showing in Coin control section', }, + TR_LARGEST_FIRST: { + id: 'TR_LARGEST_FIRST', + defaultMessage: 'Largest first', + }, + TR_SMALLEST_FIRST: { + id: 'TR_SMALLEST_FIRST', + defaultMessage: 'Smallest first', + }, + TR_OLDEST_FIRST: { + id: 'TR_OLDEST_FIRST', + defaultMessage: 'Oldest first', + }, + TR_NEWEST_FIRST: { + id: 'TR_NEWEST_FIRST', + defaultMessage: 'Newest first', + }, TR_LOADING_TRANSACTION_DETAILS: { id: 'TR_LOADING_TRANSACTION_DETAILS', defaultMessage: 'Loading transaction details', diff --git a/packages/suite/src/types/wallet/sendForm.ts b/packages/suite/src/types/wallet/sendForm.ts index 1719cef74ed..c65242c6e84 100644 --- a/packages/suite/src/types/wallet/sendForm.ts +++ b/packages/suite/src/types/wallet/sendForm.ts @@ -14,6 +14,7 @@ import { PrecomposedLevels, PrecomposedLevelsCardano, Rate, + UtxoSorting, WalletAccountTransaction, } from '@suite-common/wallet-types'; import { FiatCurrencyCode } from '@suite-common/suite-config'; @@ -50,6 +51,8 @@ export interface UtxoSelectionContext { coinjoinRegisteredUtxos: AccountUtxo[]; isLowAnonymityUtxoSelected: boolean; anonymityWarningChecked: boolean; + utxoSorting?: UtxoSorting; + selectUtxoSorting: (ordering: UtxoSorting) => void; toggleAnonymityWarning: () => void; toggleCheckAllUtxos: () => void; toggleCoinControl: () => void; diff --git a/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts b/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts new file mode 100644 index 00000000000..ca753e77b59 --- /dev/null +++ b/packages/suite/src/utils/wallet/__tests__/utxoSortingUtils.test.ts @@ -0,0 +1,74 @@ +import { testMocks } from '@suite-common/test-utils'; + +import { sortUtxos } from '../utxoSortingUtils'; + +const UTXOS = [ + testMocks.getUtxo({ amount: '1', blockHeight: undefined, txid: 'txid1', vout: 0 }), + testMocks.getUtxo({ amount: '2', blockHeight: undefined, txid: 'txid2', vout: 1 }), + testMocks.getUtxo({ amount: '2', blockHeight: 1, txid: 'txid2', vout: 0 }), + testMocks.getUtxo({ amount: '2', blockHeight: 2, txid: 'txid3', vout: 0 }), +]; + +const ACCOUNT_TRANSACTIONS = [ + testMocks.getWalletTransaction({ txid: 'txid1', blockTime: undefined }), + testMocks.getWalletTransaction({ txid: 'txid2', blockTime: 1 }), + testMocks.getWalletTransaction({ txid: 'txid3', blockTime: 2 }), +]; + +const findTx = (txid: string) => ACCOUNT_TRANSACTIONS.find(tx => tx.txid === txid); + +describe(sortUtxos.name, () => { + it('sorts UTXOs by newest first', () => { + const sortedUtxos = sortUtxos(UTXOS, 'newestFirst', ACCOUNT_TRANSACTIONS); + expect( + sortedUtxos.map(it => [ + it.blockHeight ?? findTx(it.txid)?.blockTime, + `${it.txid}:${it.vout}`, // for stable sorting + ]), + ).toEqual([ + [2, 'txid3:0'], + [1, 'txid2:1'], + [1, 'txid2:0'], + [undefined, 'txid1:0'], + ]); + }); + + it('sorts UTXOs by oldest first', () => { + const sortedUtxos = sortUtxos(UTXOS, 'oldestFirst', ACCOUNT_TRANSACTIONS); + expect( + sortedUtxos.map(it => [ + it.blockHeight ?? findTx(it.txid)?.blockTime, + `${it.txid}:${it.vout}`, // for stable sorting + ]), + ).toEqual([ + [undefined, 'txid1:0'], + [1, 'txid2:0'], + [1, 'txid2:1'], + [2, 'txid3:0'], + ]); + }); + + it('sorts by size, largest first', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(0, 2), 'largestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => it.amount)).toEqual(['2', '1']); + }); + + it('sorts by size, smallest first', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(0, 2), 'smallestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => it.amount)).toEqual(['1', '2']); + }); + + it('sorts by secondary sorting by `txid` and `vout` in case of same values', () => { + const sortedUtxos = sortUtxos(UTXOS.slice(1, 4), 'smallestFirst', ACCOUNT_TRANSACTIONS); + expect(sortedUtxos.map(it => `${it.txid}:${it.vout}`)).toEqual([ + 'txid2:0', + 'txid2:1', + 'txid3:0', + ]); + }); + + it('returns the original array if utxoSorting is undefined', () => { + const sortedUtxos = sortUtxos(UTXOS, undefined, ACCOUNT_TRANSACTIONS); + expect(sortedUtxos).toEqual(UTXOS); + }); +}); diff --git a/packages/suite/src/utils/wallet/utxoSortingUtils.ts b/packages/suite/src/utils/wallet/utxoSortingUtils.ts new file mode 100644 index 00000000000..465bfcc8b22 --- /dev/null +++ b/packages/suite/src/utils/wallet/utxoSortingUtils.ts @@ -0,0 +1,79 @@ +import { UtxoSorting, WalletAccountTransaction } from '@suite-common/wallet-types'; +import type { AccountUtxo } from '@trezor/connect'; +import { BigNumber } from '@trezor/utils'; + +type UtxoSortingFunction = (a: AccountUtxo, b: AccountUtxo) => number; + +type UtxoSortingFunctionWithContext = (context: { + accountTransactions: WalletAccountTransaction[]; +}) => UtxoSortingFunction; + +const performSecondarySorting: UtxoSortingFunction = (a, b) => { + const secondaryComparison = b.txid.localeCompare(a.txid); + if (secondaryComparison === 0) { + return b.vout - a.vout; + } + + return secondaryComparison; +}; + +const wrapSecondarySorting = + (sortFunction: UtxoSortingFunctionWithContext): UtxoSortingFunctionWithContext => + context => + (a, b) => { + const result = sortFunction(context)(a, b); + + if (result !== 0) { + return result; + } + + return performSecondarySorting(a, b); + }; + +const sortFromLargestToSmallest: UtxoSortingFunctionWithContext = () => (a, b) => + new BigNumber(b.amount).comparedTo(new BigNumber(a.amount)); + +const sortFromNewestToOldest: UtxoSortingFunctionWithContext = + ({ accountTransactions }) => + (a, b) => { + if (a.blockHeight > 0 && b.blockHeight > 0) { + return b.blockHeight - a.blockHeight; + } else { + // Pending transactions do not have blockHeight, so we must use blockTime of the transaction instead. + const getBlockTime = (txid: string) => { + const transaction = accountTransactions.find( + transaction => transaction.txid === txid, + ); + + return transaction?.blockTime ?? 0; + }; + + return getBlockTime(b.txid) - getBlockTime(a.txid); + } + }; + +const utxoSortMap: Record = { + largestFirst: wrapSecondarySorting(sortFromLargestToSmallest), + smallestFirst: + context => + (...params) => + wrapSecondarySorting(sortFromLargestToSmallest)(context)(...params) * -1, + + newestFirst: wrapSecondarySorting(sortFromNewestToOldest), + oldestFirst: + context => + (...params) => + wrapSecondarySorting(sortFromNewestToOldest)(context)(...params) * -1, +}; + +export const sortUtxos = ( + utxos: AccountUtxo[], + utxoSorting: UtxoSorting | undefined, + accountTransactions: WalletAccountTransaction[], +): AccountUtxo[] => { + if (utxoSorting === undefined) { + return utxos; + } + + return [...utxos].sort(utxoSortMap[utxoSorting]({ accountTransactions })); +}; diff --git a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx index 49941ce60d7..fc3512276d3 100644 --- a/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx +++ b/packages/suite/src/views/wallet/send/Options/BitcoinOptions/CoinControl/CoinControl.tsx @@ -18,6 +18,7 @@ import { selectCurrentTargetAnonymity } from 'src/reducers/wallet/coinjoinReduce import { selectLabelingDataForSelectedAccount } from 'src/reducers/suite/metadataReducer'; import { filterAndCategorizeUtxos } from 'src/utils/wallet/filterAndCategorizeUtxosUtils'; +import { UtxoSortingSelect } from './UtxoSortingSelect'; import { UtxoSelectionList } from './UtxoSelectionList/UtxoSelectionList'; import { UtxoSearch } from './UtxoSearch'; @@ -26,10 +27,6 @@ const Header = styled.header` padding-bottom: ${spacingsPx.sm}; `; -const SearchWrapper = styled.div` - margin-top: ${spacingsPx.lg}; -`; - const MissingToInput = styled.div<{ $isVisible: boolean }>` /* using visibility rather than display to prevent line height change */ visibility: ${({ $isVisible }) => !$isVisible && 'hidden'}; @@ -204,13 +201,14 @@ export const CoinControl = ({ close }: CoinControlProps) => { {hasEligibleUtxos && ( - + - + + )} {!!spendableUtxosOnPage.length && ( }, + { value: 'oldestFirst', label: }, + { value: 'smallestFirst', label: }, + { value: 'largestFirst', label: }, +]; + +export const UtxoSortingSelect = () => { + const { + utxoSelection: { utxoSorting, selectUtxoSorting }, + } = useSendFormContext(); + + const selectedOption = sortingOptions.find(option => option.value === utxoSorting); + + const handleChange = ({ value }: Option) => selectUtxoSorting(value); + + return ( +