5
5
"encoding/hex"
6
6
"errors"
7
7
"fmt"
8
+ "sort"
8
9
"strconv"
9
10
"strings"
10
11
@@ -14,6 +15,8 @@ import (
14
15
"github.com/lightninglabs/loop/staticaddr/deposit"
15
16
"github.com/lightninglabs/loop/staticaddr/loopin"
16
17
"github.com/lightninglabs/loop/swapserverrpc"
18
+ "github.com/lightningnetwork/lnd/input"
19
+ "github.com/lightningnetwork/lnd/lnwallet"
17
20
"github.com/lightningnetwork/lnd/routing/route"
18
21
"github.com/urfave/cli"
19
22
)
@@ -458,6 +461,13 @@ var staticAddressLoopInCommand = cli.Command{
458
461
"The client can retry the swap with adjusted " +
459
462
"parameters after the payment timed out." ,
460
463
},
464
+ cli.IntFlag {
465
+ Name : "amount" ,
466
+ Usage : "the number of satoshis that should be " +
467
+ "swapped from the selected deposits. If there" +
468
+ "is change it is sent back to the static " +
469
+ "address." ,
470
+ },
461
471
lastHopFlag ,
462
472
labelFlag ,
463
473
routeHintsFlag ,
@@ -483,11 +493,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
483
493
ctxb = context .Background ()
484
494
isAllSelected = ctx .IsSet ("all" )
485
495
isUtxoSelected = ctx .IsSet ("utxo" )
496
+ isAmountSelected bool
497
+ selectedAmount = ctx .Int64 ("amount" )
486
498
label = ctx .String ("static-loop-in" )
487
499
hints []* swapserverrpc.RouteHint
488
500
lastHop []byte
489
501
paymentTimeoutSeconds = uint32 (loopin .DefaultPaymentTimeoutSeconds )
490
502
)
503
+ isAmountSelected = selectedAmount > 0
491
504
492
505
// Validate our label early so that we can fail before getting a quote.
493
506
if err := labels .Validate (label ); err != nil {
@@ -522,7 +535,9 @@ func staticAddressLoopIn(ctx *cli.Context) error {
522
535
return err
523
536
}
524
537
525
- if len (depositList .FilteredDeposits ) == 0 {
538
+ allDeposits := depositList .FilteredDeposits
539
+
540
+ if len (allDeposits ) == 0 {
526
541
errString := fmt .Sprintf ("no confirmed deposits available, " +
527
542
"deposits need at least %v confirmations" ,
528
543
deposit .MinConfs )
@@ -532,17 +547,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {
532
547
533
548
var depositOutpoints []string
534
549
switch {
535
- case isAllSelected == isUtxoSelected :
536
- return errors .New ("must select either all or some utxos" )
550
+ case isAllSelected && isUtxoSelected :
551
+ return errors .New ("cannot select all and specific utxos" )
537
552
538
553
case isAllSelected :
539
- depositOutpoints = depositsToOutpoints (
540
- depositList .FilteredDeposits ,
541
- )
554
+ depositOutpoints = depositsToOutpoints (allDeposits )
542
555
543
556
case isUtxoSelected :
544
557
depositOutpoints = ctx .StringSlice ("utxo" )
545
558
559
+ case isAmountSelected :
560
+ // If there's only a swap amount specified we'll coin-select
561
+ // deposits to cover the swap amount.
562
+ depositOutpoints , err = selectDeposits (
563
+ allDeposits , selectedAmount ,
564
+ )
565
+ if err != nil {
566
+ return err
567
+ }
568
+
546
569
default :
547
570
return fmt .Errorf ("unknown quote request" )
548
571
}
@@ -552,6 +575,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
552
575
}
553
576
554
577
quoteReq := & looprpc.QuoteRequest {
578
+ Amt : selectedAmount ,
555
579
LoopInRouteHints : hints ,
556
580
LoopInLastHop : lastHop ,
557
581
Private : ctx .Bool (privateFlag .Name ),
@@ -564,15 +588,6 @@ func staticAddressLoopIn(ctx *cli.Context) error {
564
588
565
589
limits := getInLimits (quote )
566
590
567
- // populate the quote request with the sum of selected deposits and
568
- // prompt the user for acceptance.
569
- quoteReq .Amt , err = sumDeposits (
570
- depositOutpoints , depositList .FilteredDeposits ,
571
- )
572
- if err != nil {
573
- return err
574
- }
575
-
576
591
if ! (ctx .Bool ("force" ) || ctx .Bool ("f" )) {
577
592
err = displayInDetails (quoteReq , quote , ctx .Bool ("verbose" ))
578
593
if err != nil {
@@ -585,6 +600,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
585
600
}
586
601
587
602
req := & looprpc.StaticAddressLoopInRequest {
603
+ Amount : quoteReq .Amt ,
588
604
Outpoints : depositOutpoints ,
589
605
MaxSwapFeeSatoshis : int64 (limits .maxSwapFee ),
590
606
LastHop : lastHop ,
@@ -605,36 +621,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
605
621
return nil
606
622
}
607
623
608
- func containsDuplicates (outpoints []string ) bool {
609
- found := make (map [string ]struct {})
610
- for _ , outpoint := range outpoints {
611
- if _ , ok := found [outpoint ]; ok {
612
- return true
613
- }
614
- found [outpoint ] = struct {}{}
615
- }
624
+ // selectDeposits sorts the deposits by amount in descending order, then by
625
+ // blocks-until-expiry in ascending order. It then selects the deposits that
626
+ // are needed to cover the amount requested without leaving a dust change. It
627
+ // returns an error if the sum of deposits minus dust is less than the requested
628
+ // amount.
629
+ func selectDeposits (deposits []* looprpc.Deposit , amount int64 ) ([]string ,
630
+ error ) {
616
631
617
- return false
618
- }
632
+ // Check that sum of deposits covers the swap amount while leaving no
633
+ // dust change.
634
+ dustLimit := lnwallet .DustLimitForSize (input .P2TRSize )
635
+ var depositSum int64
636
+ for _ , deposit := range deposits {
637
+ depositSum += deposit .Value
638
+ }
639
+ if depositSum - int64 (dustLimit ) < amount {
640
+ return nil , fmt .Errorf ("insufficient funds to cover swap " +
641
+ "amount" )
642
+ }
619
643
620
- func sumDeposits (outpoints []string , deposits []* looprpc.Deposit ) (int64 ,
621
- error ) {
644
+ // Sort the deposits by amount in descending order, then by
645
+ // blocks-until-expiry in ascending order.
646
+ sort .Slice (deposits , func (i , j int ) bool {
647
+ if deposits [i ].Value == deposits [j ].Value {
648
+ return deposits [i ].BlocksUntilExpiry <
649
+ deposits [j ].BlocksUntilExpiry
650
+ }
651
+ return deposits [i ].Value > deposits [j ].Value
652
+ })
622
653
623
- var sum int64
624
- depositMap := make (map [string ]* looprpc.Deposit )
654
+ // Select the deposits that are needed to cover the swap amount without
655
+ // leaving a dust change.
656
+ var selectedDeposits []string
657
+ var selectedAmount int64
625
658
for _ , deposit := range deposits {
626
- depositMap [deposit .Outpoint ] = deposit
659
+ if selectedAmount >= amount + int64 (dustLimit ) {
660
+ break
661
+ }
662
+ selectedDeposits = append (selectedDeposits , deposit .Outpoint )
663
+ selectedAmount += deposit .Value
627
664
}
628
665
666
+ return selectedDeposits , nil
667
+ }
668
+
669
+ func containsDuplicates (outpoints []string ) bool {
670
+ found := make (map [string ]struct {})
629
671
for _ , outpoint := range outpoints {
630
- if _ , ok := depositMap [outpoint ]; ! ok {
631
- return 0 , fmt . Errorf ( "deposit %v not found" , outpoint )
672
+ if _ , ok := found [outpoint ]; ok {
673
+ return true
632
674
}
633
-
634
- sum += depositMap [outpoint ].Value
675
+ found [outpoint ] = struct {}{}
635
676
}
636
677
637
- return sum , nil
678
+ return false
638
679
}
639
680
640
681
func depositsToOutpoints (deposits []* looprpc.Deposit ) []string {
0 commit comments