1
1
import { useEffect , useMemo } from 'react' ;
2
2
import { UseFormReturn } from 'react-hook-form' ;
3
3
4
- import { ExcludedUtxos , FormState } from '@suite-common/wallet-types' ;
4
+ import { ExcludedUtxos , FormState , UtxoSorting } from '@suite-common/wallet-types' ;
5
5
import type { AccountUtxo , PROTO } from '@trezor/connect' ;
6
6
import { getUtxoOutpoint , isSameUtxo } from '@suite-common/wallet-utils' ;
7
+ import { BigNumber } from '@trezor/utils' ;
8
+ import { selectAccountTransactionsWithNulls } from '@suite-common/wallet-core' ;
9
+
10
+ import { useSelector } from 'src/hooks/suite' ;
7
11
8
12
import { useCoinjoinRegisteredUtxos } from './useCoinjoinRegisteredUtxos' ;
9
13
import {
@@ -28,19 +32,26 @@ export const useUtxoSelection = ({
28
32
setValue,
29
33
watch,
30
34
} : UtxoSelectionContextProps ) : UtxoSelectionContext => {
35
+ const accountTransactions = useSelector ( state =>
36
+ selectAccountTransactionsWithNulls ( state , account . key ) ,
37
+ ) ;
38
+
31
39
// register custom form field (without HTMLElement)
32
40
useEffect ( ( ) => {
33
41
register ( 'isCoinControlEnabled' ) ;
34
42
register ( 'selectedUtxos' ) ;
35
43
register ( 'anonymityWarningChecked' ) ;
44
+ register ( 'utxoSorting' ) ;
36
45
} , [ register ] ) ;
37
46
38
47
const coinjoinRegisteredUtxos = useCoinjoinRegisteredUtxos ( { account } ) ;
39
48
40
- // has coin control been enabled manually?
41
- const isCoinControlEnabled = watch ( 'isCoinControlEnabled' ) ;
42
- // fee level
43
- const selectedFee = watch ( 'selectedFee' ) ;
49
+ const [ isCoinControlEnabled , options , selectedFee , utxoSorting ] = watch ( [
50
+ 'isCoinControlEnabled' ,
51
+ 'options' ,
52
+ 'selectedFee' ,
53
+ 'utxoSorting' ,
54
+ ] ) ;
44
55
// confirmation of spending low-anonymity UTXOs - only relevant for coinjoin account
45
56
const anonymityWarningChecked = ! ! watch ( 'anonymityWarningChecked' ) ;
46
57
// manually selected UTXOs
@@ -76,23 +87,76 @@ export const useUtxoSelection = ({
76
87
composeRequest ,
77
88
] ) ;
78
89
90
+ const sortUtxos = ( utxos : AccountUtxo [ ] ) : AccountUtxo [ ] => {
91
+ if ( ! utxoSorting ) {
92
+ return utxos ;
93
+ }
94
+
95
+ const sortFromNewestToOldest = ( a : AccountUtxo , b : AccountUtxo ) => {
96
+ let valueA : number ;
97
+ let valueB : number ;
98
+
99
+ if ( a . blockHeight > 0 && b . blockHeight > 0 ) {
100
+ valueA = a . blockHeight ;
101
+ valueB = b . blockHeight ;
102
+ } else {
103
+ // Pending transactions do not have blockHeight, so we must use blockTime of the transaction instead.
104
+ const getBlockTime = ( txid : string ) => {
105
+ const transaction = accountTransactions . find (
106
+ transaction => transaction . txid === txid ,
107
+ ) ;
108
+
109
+ return transaction ?. blockTime ?? 0 ;
110
+ } ;
111
+ valueA = getBlockTime ( a . txid ) ;
112
+ valueB = getBlockTime ( b . txid ) ;
113
+ }
114
+
115
+ return new BigNumber ( valueB ?? 0 ) . comparedTo ( new BigNumber ( valueA ?? 0 ) ) ;
116
+ } ;
117
+
118
+ const sortFromLargestToSmallest = ( a : AccountUtxo , b : AccountUtxo ) =>
119
+ new BigNumber ( b . amount ) . comparedTo ( new BigNumber ( a . amount ) ) ;
120
+
121
+ // Manipulating the array directly would cause a runtime error, so we create a copy.
122
+ const utxosForSorting = utxos . slice ( ) ;
123
+
124
+ switch ( utxoSorting ) {
125
+ case 'newestFirst' :
126
+ return utxosForSorting . sort ( sortFromNewestToOldest ) ;
127
+ case 'oldestFirst' :
128
+ return utxosForSorting . sort ( sortFromNewestToOldest ) . reverse ( ) ;
129
+ case 'largestFirst' :
130
+ return utxosForSorting . sort ( sortFromLargestToSmallest ) ;
131
+ case 'smallestFirst' :
132
+ return utxosForSorting . sort ( sortFromLargestToSmallest ) . reverse ( ) ;
133
+ }
134
+ } ;
135
+
79
136
const spendableUtxos : AccountUtxo [ ] = [ ] ;
80
137
const lowAnonymityUtxos : AccountUtxo [ ] = [ ] ;
81
138
const dustUtxos : AccountUtxo [ ] = [ ] ;
82
- account ?. utxo ?. forEach ( utxo => {
83
- switch ( excludedUtxos [ getUtxoOutpoint ( utxo ) ] ) {
84
- case 'low-anonymity' :
85
- lowAnonymityUtxos . push ( utxo ) ;
86
-
87
- return ;
88
- case 'dust' :
89
- dustUtxos . push ( utxo ) ;
90
-
91
- return ;
92
- default :
93
- spendableUtxos . push ( utxo ) ;
94
- }
95
- } ) ;
139
+ // Skip sorting and categorizing UTXOs if coin control is not enabled.
140
+ const utxos =
141
+ options ?. includes ( 'utxoSelection' ) && account ?. utxo
142
+ ? sortUtxos ( account ?. utxo )
143
+ : account ?. utxo ;
144
+ if ( utxos ?. length ) {
145
+ utxos ?. forEach ( utxo => {
146
+ switch ( excludedUtxos [ getUtxoOutpoint ( utxo ) ] ) {
147
+ case 'low-anonymity' :
148
+ lowAnonymityUtxos . push ( utxo ) ;
149
+
150
+ return ;
151
+ case 'dust' :
152
+ dustUtxos . push ( utxo ) ;
153
+
154
+ return ;
155
+ default :
156
+ spendableUtxos . push ( utxo ) ;
157
+ }
158
+ } ) ;
159
+ }
96
160
97
161
// category displayed on top and controlled by the check-all checkbox
98
162
const topCategory =
@@ -139,6 +203,8 @@ export const useUtxoSelection = ({
139
203
setValue ( 'anonymityWarningChecked' , false ) ;
140
204
}
141
205
206
+ const selectUtxoSorting = ( sorting : UtxoSorting ) => setValue ( 'utxoSorting' , sorting ) ;
207
+
142
208
const toggleAnonymityWarning = ( ) =>
143
209
setValue ( 'anonymityWarningChecked' , ! anonymityWarningChecked ) ;
144
210
@@ -204,6 +270,8 @@ export const useUtxoSelection = ({
204
270
selectedUtxos,
205
271
spendableUtxos,
206
272
coinjoinRegisteredUtxos,
273
+ utxoSorting,
274
+ selectUtxoSorting,
207
275
toggleAnonymityWarning,
208
276
toggleCheckAllUtxos,
209
277
toggleCoinControl,
0 commit comments