1
- use std:: collections:: HashMap ;
2
- use std:: str:: FromStr ;
3
- use std:: sync:: Arc ;
1
+ use std:: { cmp:: Ordering , collections:: HashMap , str:: FromStr , sync:: Arc } ;
4
2
5
3
use iced:: { Command , Subscription } ;
6
4
use liana:: {
@@ -65,6 +63,9 @@ pub trait Step {
65
63
pub struct DefineSpend {
66
64
balance_available : Amount ,
67
65
recipients : Vec < Recipient > ,
66
+ /// If set, this is the index of a recipient that should
67
+ /// receive the max amount.
68
+ send_max_to_recipient : Option < usize > ,
68
69
/// Will be `true` if coins for spend were manually selected by user.
69
70
/// Otherwise, will be `false` (including for self-send).
70
71
is_user_coin_selection : bool ,
@@ -123,6 +124,7 @@ impl DefineSpend {
123
124
coins_labels : HashMap :: new ( ) ,
124
125
batch_label : form:: Value :: default ( ) ,
125
126
recipients : vec ! [ Recipient :: default ( ) ] ,
127
+ send_max_to_recipient : None ,
126
128
is_user_coin_selection : false , // Start with auto-selection until user edits selection.
127
129
is_valid : false ,
128
130
is_duplicate : false ,
@@ -162,12 +164,16 @@ impl DefineSpend {
162
164
self
163
165
}
164
166
165
- fn form_values_are_valid ( & self ) -> bool {
167
+ // If `is_redraft`, the validation of recipients will take into account
168
+ // whether any should receive the max amount. Otherwise, all recipients
169
+ // will be fully validated.
170
+ fn form_values_are_valid ( & self , is_redraft : bool ) -> bool {
166
171
self . feerate . valid
167
172
&& !self . feerate . value . is_empty ( )
168
173
&& ( self . batch_label . valid || self . recipients . len ( ) < 2 )
169
174
// Recipients will be empty for self-send.
170
- && self . recipients . iter ( ) . all ( |r| r. valid ( ) )
175
+ && self . recipients . iter ( ) . enumerate ( ) . all ( |( i, r) |
176
+ r. valid ( ) || ( is_redraft && self . send_max_to_recipient == Some ( i) && r. address_valid ( ) ) )
171
177
}
172
178
173
179
fn exists_duplicate ( & self ) -> bool {
@@ -185,34 +191,68 @@ impl DefineSpend {
185
191
186
192
fn check_valid ( & mut self ) {
187
193
self . is_valid =
188
- self . form_values_are_valid ( ) && self . coins . iter ( ) . any ( |( _, selected) | * selected) ;
194
+ self . form_values_are_valid ( false ) && self . coins . iter ( ) . any ( |( _, selected) | * selected) ;
189
195
self . is_duplicate = self . exists_duplicate ( ) ;
190
196
}
191
197
/// redraft calculates the amount left to select and auto selects coins
192
198
/// if the user did not select a coin manually
193
199
fn redraft ( & mut self , daemon : Arc < dyn Daemon + Sync + Send > ) {
194
- if !self . form_values_are_valid ( ) || self . exists_duplicate ( ) || self . recipients . is_empty ( ) {
200
+ if !self . form_values_are_valid ( true )
201
+ || self . exists_duplicate ( )
202
+ || self . recipients . is_empty ( )
203
+ {
195
204
// The current form details are not valid to draft a spend, so remove any previously
196
205
// calculated amount as it will no longer be valid and could be misleading, e.g. if
197
206
// the user removes the amount from one of the recipients.
198
207
// We can leave any coins selected as they will either be automatically updated
199
208
// as soon as the form is valid or the user has selected these specific coins and
200
209
// so we should not touch them.
201
210
self . amount_left_to_select = None ;
211
+ // Remove any max amount from a recipient as it could be misleading.
212
+ if let Some ( i) = self . send_max_to_recipient {
213
+ self . recipients
214
+ . get_mut ( i)
215
+ . expect ( "max has been requested for this recipient so it must exist" )
216
+ . update (
217
+ self . network ,
218
+ view:: CreateSpendMessage :: RecipientEdited ( i, "amount" , "" . to_string ( ) ) ,
219
+ ) ;
220
+ }
202
221
return ;
203
222
}
204
223
205
224
let destinations: HashMap < Address < address:: NetworkUnchecked > , u64 > = self
206
225
. recipients
207
226
. iter ( )
208
- . map ( |recipient| {
209
- (
210
- Address :: from_str ( & recipient. address . value ) . expect ( "Checked before" ) ,
211
- recipient. amount ( ) . expect ( "Checked before" ) ,
212
- )
227
+ . enumerate ( )
228
+ . filter_map ( |( i, recipient) | {
229
+ // A recipient that receives the max should be treated as change for coin selection.
230
+ // Note that we only give a change output if its value is above the dust
231
+ // threshold, but a user can only send payments above the same dust threshold,
232
+ // so using change output to determine the max amount for a recipient will
233
+ // not prevent a value that could otherwise be entered manually by the user.
234
+ if self . send_max_to_recipient == Some ( i) {
235
+ None
236
+ } else {
237
+ Some ( (
238
+ Address :: from_str ( & recipient. address . value ) . expect ( "Checked before" ) ,
239
+ recipient. amount ( ) . expect ( "Checked before" ) ,
240
+ ) )
241
+ }
213
242
} )
214
243
. collect ( ) ;
215
244
245
+ let recipient_with_max = if let Some ( i) = self . send_max_to_recipient {
246
+ Some ( (
247
+ i,
248
+ self . recipients
249
+ . get_mut ( i)
250
+ . expect ( "max has been requested for this recipient so it must exist" ) ,
251
+ ) )
252
+ } else {
253
+ None
254
+ } ;
255
+
216
256
let outpoints = if self . is_user_coin_selection {
217
257
let outpoints: Vec < _ > = self
218
258
. coins
@@ -228,30 +268,53 @@ impl DefineSpend {
228
268
)
229
269
. collect ( ) ;
230
270
if outpoints. is_empty ( ) {
231
- // If the user has deselected all coins, simply set the amount left to select as the
232
- // total destination value. Note this doesn't take account of the fee, but passing
233
- // an empty list to `create_spend_tx` would use auto-selection and so we settle for
234
- // this approximation.
271
+ // If the user has deselected all coins, set any recipient's max amount to 0.
272
+ if let Some ( ( i, recipient) ) = recipient_with_max {
273
+ recipient. update (
274
+ self . network ,
275
+ view:: CreateSpendMessage :: RecipientEdited ( i, "amount" , "0" . to_string ( ) ) ,
276
+ ) ;
277
+ }
278
+ // Simply set the amount left to select as the total destination value. Note this
279
+ // doesn't take account of the fee, but passing an empty list to `create_spend_tx`
280
+ // would use auto-selection and so we settle for this approximation.
235
281
self . amount_left_to_select = Some ( Amount :: from_sat ( destinations. values ( ) . sum ( ) ) ) ;
236
282
return ;
237
283
}
238
284
outpoints
285
+ } else if self . send_max_to_recipient . is_some ( ) {
286
+ // If user has not selected coins, send the max available from all coins.
287
+ self . coins . iter ( ) . map ( |( c, _) | c. outpoint ) . collect ( )
239
288
} else {
240
289
Vec :: new ( ) // pass empty list for auto-selection
241
290
} ;
242
291
243
- // Use a fixed change address so that we don't increment the change index.
244
- let dummy_address = self
245
- . descriptor
246
- . change_descriptor ( )
247
- . derive ( 0 . into ( ) , & self . curve )
248
- . address ( self . network )
249
- . as_unchecked ( )
250
- . clone ( ) ;
292
+ // If sending the max to a recipient, use that recipient's address as the
293
+ // change address.
294
+ // Otherwise, use a fixed change address from the user's own wallet so that
295
+ // we don't increment the change index.
296
+ let change_address = if let Some ( ( _, recipient) ) = & recipient_with_max {
297
+ Address :: from_str ( & recipient. address . value )
298
+ . expect ( "Checked before" )
299
+ . as_unchecked ( )
300
+ . clone ( )
301
+ } else {
302
+ self . descriptor
303
+ . change_descriptor ( )
304
+ . derive ( 0 . into ( ) , & self . curve )
305
+ . address ( self . network )
306
+ . as_unchecked ( )
307
+ . clone ( )
308
+ } ;
251
309
252
310
let feerate_vb = self . feerate . value . parse :: < u64 > ( ) . expect ( "Checked before" ) ;
253
311
254
- match daemon. create_spend_tx ( & outpoints, & destinations, feerate_vb, Some ( dummy_address) ) {
312
+ match daemon. create_spend_tx (
313
+ & outpoints,
314
+ & destinations,
315
+ feerate_vb,
316
+ Some ( change_address. clone ( ) ) ,
317
+ ) {
255
318
Ok ( CreateSpendResult :: Success { psbt, .. } ) => {
256
319
self . warning = None ;
257
320
if !self . is_user_coin_selection {
@@ -268,6 +331,25 @@ impl DefineSpend {
268
331
}
269
332
// As coin selection was successful, we can assume there is nothing left to select.
270
333
self . amount_left_to_select = Some ( Amount :: from_sat ( 0 ) ) ;
334
+ if let Some ( ( i, recipient) ) = recipient_with_max {
335
+ // If there's no change output, any excess must be below the dust threshold
336
+ // and so the max available for this recipient is 0.
337
+ let amount = psbt
338
+ . unsigned_tx
339
+ . output
340
+ . iter ( )
341
+ . find ( |o| {
342
+ o. script_pubkey
343
+ == change_address. clone ( ) . assume_checked ( ) . script_pubkey ( )
344
+ } )
345
+ . map ( |change_output| change_output. value . to_btc ( ) )
346
+ . unwrap_or ( 0.0 )
347
+ . to_string ( ) ;
348
+ recipient. update (
349
+ self . network ,
350
+ view:: CreateSpendMessage :: RecipientEdited ( i, "amount" , amount) ,
351
+ ) ;
352
+ }
271
353
}
272
354
// For coin selection error (insufficient funds), do not make any changes to
273
355
// selected coins on screen and just show user how much is left to select.
@@ -276,6 +358,25 @@ impl DefineSpend {
276
358
// - select coins manually.
277
359
Ok ( CreateSpendResult :: InsufficientFunds { missing } ) => {
278
360
self . amount_left_to_select = Some ( Amount :: from_sat ( missing) ) ;
361
+ if let Some ( ( i, recipient) ) = recipient_with_max {
362
+ let amount = Amount :: from_sat ( if destinations. is_empty ( ) {
363
+ // If there are no other recipients, then the missing value will
364
+ // be the amount left to select in order to create an output at the dust
365
+ // threshold. Therefore, set this recipient's amount to this value so
366
+ // that the information shown is consistent.
367
+ // Otherwise, there are already insufficient funds for the other
368
+ // recipients and so the max available for this recipient is 0.
369
+ DUST_OUTPUT_SATS
370
+ } else {
371
+ 0
372
+ } )
373
+ . to_btc ( )
374
+ . to_string ( ) ;
375
+ recipient. update (
376
+ self . network ,
377
+ view:: CreateSpendMessage :: RecipientEdited ( i, "amount" , amount) ,
378
+ ) ;
379
+ }
279
380
}
280
381
Err ( e) => {
281
382
self . warning = Some ( e. into ( ) ) ;
@@ -307,6 +408,20 @@ impl Step for DefineSpend {
307
408
self . batch_label . valid = true ;
308
409
self . batch_label . value = "" . to_string ( ) ;
309
410
}
411
+ if let Some ( j) = self . send_max_to_recipient {
412
+ match j. cmp ( & i) {
413
+ Ordering :: Equal => {
414
+ self . send_max_to_recipient = None ;
415
+ }
416
+ Ordering :: Greater => {
417
+ self . send_max_to_recipient = Some (
418
+ j. checked_sub ( 1 )
419
+ . expect ( "j must be greater than 0 in this case" ) ,
420
+ ) ;
421
+ }
422
+ _ => { }
423
+ }
424
+ }
310
425
}
311
426
view:: CreateSpendMessage :: RecipientEdited ( i, _, _) => {
312
427
self . recipients
@@ -377,6 +492,17 @@ impl Step for DefineSpend {
377
492
self . is_user_coin_selection = true ;
378
493
}
379
494
}
495
+ view:: CreateSpendMessage :: SendMaxToRecipient ( i) => {
496
+ if self . recipients . get ( i) . is_some ( ) {
497
+ if self . send_max_to_recipient == Some ( i) {
498
+ // If already set to this recipient, then unset it.
499
+ self . send_max_to_recipient = None ;
500
+ } else {
501
+ // Either it's set to some other recipient or not at all.
502
+ self . send_max_to_recipient = Some ( i) ;
503
+ } ;
504
+ }
505
+ }
380
506
_ => { }
381
507
}
382
508
@@ -453,7 +579,11 @@ impl Step for DefineSpend {
453
579
self . recipients
454
580
. iter ( )
455
581
. enumerate ( )
456
- . map ( |( i, recipient) | recipient. view ( i) . map ( view:: Message :: CreateSpend ) )
582
+ . map ( |( i, recipient) | {
583
+ recipient
584
+ . view ( i, self . send_max_to_recipient == Some ( i) )
585
+ . map ( view:: Message :: CreateSpend )
586
+ } )
457
587
. collect ( ) ,
458
588
Amount :: from_sat (
459
589
self . recipients
@@ -509,9 +639,12 @@ impl Recipient {
509
639
Ok ( amount. to_sat ( ) )
510
640
}
511
641
642
+ fn address_valid ( & self ) -> bool {
643
+ !self . address . value . is_empty ( ) && self . address . valid
644
+ }
645
+
512
646
fn valid ( & self ) -> bool {
513
- !self . address . value . is_empty ( )
514
- && self . address . valid
647
+ self . address_valid ( )
515
648
&& !self . amount . value . is_empty ( )
516
649
&& self . amount . valid
517
650
&& self . label . valid
@@ -550,8 +683,8 @@ impl Recipient {
550
683
} ;
551
684
}
552
685
553
- fn view ( & self , i : usize ) -> Element < view:: CreateSpendMessage > {
554
- view:: spend:: recipient_view ( i, & self . address , & self . amount , & self . label )
686
+ fn view ( & self , i : usize , is_max_selected : bool ) -> Element < view:: CreateSpendMessage > {
687
+ view:: spend:: recipient_view ( i, & self . address , & self . amount , & self . label , is_max_selected )
555
688
}
556
689
}
557
690
0 commit comments