55 "encoding/hex"
66 "errors"
77 "fmt"
8+ "sort"
89 "strconv"
910 "strings"
1011
@@ -14,6 +15,8 @@ import (
1415 "github.com/lightninglabs/loop/staticaddr/deposit"
1516 "github.com/lightninglabs/loop/staticaddr/loopin"
1617 "github.com/lightninglabs/loop/swapserverrpc"
18+ "github.com/lightningnetwork/lnd/input"
19+ "github.com/lightningnetwork/lnd/lnwallet"
1720 "github.com/lightningnetwork/lnd/routing/route"
1821 "github.com/urfave/cli"
1922)
@@ -477,6 +480,13 @@ var staticAddressLoopInCommand = cli.Command{
477480 "The client can retry the swap with adjusted " +
478481 "parameters after the payment timed out." ,
479482 },
483+ cli.IntFlag {
484+ Name : "amount" ,
485+ Usage : "the number of satoshis that should be " +
486+ "swapped from the selected deposits. If there" +
487+ "is change it is sent back to the static " +
488+ "address." ,
489+ },
480490 lastHopFlag ,
481491 labelFlag ,
482492 routeHintsFlag ,
@@ -502,11 +512,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
502512 ctxb = context .Background ()
503513 isAllSelected = ctx .IsSet ("all" )
504514 isUtxoSelected = ctx .IsSet ("utxo" )
515+ isAmountSelected bool
516+ selectedAmount = ctx .Int64 ("amount" )
505517 label = ctx .String ("static-loop-in" )
506518 hints []* swapserverrpc.RouteHint
507519 lastHop []byte
508520 paymentTimeoutSeconds = uint32 (loopin .DefaultPaymentTimeoutSeconds )
509521 )
522+ isAmountSelected = selectedAmount > 0
510523
511524 // Validate our label early so that we can fail before getting a quote.
512525 if err := labels .Validate (label ); err != nil {
@@ -541,7 +554,9 @@ func staticAddressLoopIn(ctx *cli.Context) error {
541554 return err
542555 }
543556
544- if len (depositList .FilteredDeposits ) == 0 {
557+ allDeposits := depositList .FilteredDeposits
558+
559+ if len (allDeposits ) == 0 {
545560 errString := fmt .Sprintf ("no confirmed deposits available, " +
546561 "deposits need at least %v confirmations" ,
547562 deposit .MinConfs )
@@ -551,17 +566,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {
551566
552567 var depositOutpoints []string
553568 switch {
554- case isAllSelected == isUtxoSelected :
555- return errors .New ("must select either all or some utxos" )
569+ case isAllSelected && isUtxoSelected :
570+ return errors .New ("cannot select all and specific utxos" )
556571
557572 case isAllSelected :
558- depositOutpoints = depositsToOutpoints (
559- depositList .FilteredDeposits ,
560- )
573+ depositOutpoints = depositsToOutpoints (allDeposits )
561574
562575 case isUtxoSelected :
563576 depositOutpoints = ctx .StringSlice ("utxo" )
564577
578+ case isAmountSelected :
579+ // If there's only a swap amount specified we'll coin-select
580+ // deposits to cover the swap amount.
581+ depositOutpoints , err = selectDeposits (
582+ allDeposits , selectedAmount ,
583+ )
584+ if err != nil {
585+ return err
586+ }
587+
565588 default :
566589 return fmt .Errorf ("unknown quote request" )
567590 }
@@ -571,6 +594,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
571594 }
572595
573596 quoteReq := & looprpc.QuoteRequest {
597+ Amt : selectedAmount ,
574598 LoopInRouteHints : hints ,
575599 LoopInLastHop : lastHop ,
576600 Private : ctx .Bool (privateFlag .Name ),
@@ -583,15 +607,6 @@ func staticAddressLoopIn(ctx *cli.Context) error {
583607
584608 limits := getInLimits (quote )
585609
586- // populate the quote request with the sum of selected deposits and
587- // prompt the user for acceptance.
588- quoteReq .Amt , err = sumDeposits (
589- depositOutpoints , depositList .FilteredDeposits ,
590- )
591- if err != nil {
592- return err
593- }
594-
595610 if ! (ctx .Bool ("force" ) || ctx .Bool ("f" )) {
596611 err = displayInDetails (quoteReq , quote , ctx .Bool ("verbose" ))
597612 if err != nil {
@@ -604,6 +619,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
604619 }
605620
606621 req := & looprpc.StaticAddressLoopInRequest {
622+ Amount : quoteReq .Amt ,
607623 Outpoints : depositOutpoints ,
608624 MaxSwapFeeSatoshis : int64 (limits .maxSwapFee ),
609625 LastHop : lastHop ,
@@ -624,36 +640,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
624640 return nil
625641}
626642
627- func containsDuplicates (outpoints []string ) bool {
628- found := make (map [string ]struct {})
629- for _ , outpoint := range outpoints {
630- if _ , ok := found [outpoint ]; ok {
631- return true
632- }
633- found [outpoint ] = struct {}{}
634- }
643+ // selectDeposits sorts the deposits by amount in descending order, then by
644+ // blocks-until-expiry in ascending order. It then selects the deposits that
645+ // are needed to cover the amount requested without leaving a dust change. It
646+ // returns an error if the sum of deposits minus dust is less than the requested
647+ // amount.
648+ func selectDeposits (deposits []* looprpc.Deposit , amount int64 ) ([]string ,
649+ error ) {
635650
636- return false
637- }
651+ // Check that sum of deposits covers the swap amount while leaving no
652+ // dust change.
653+ dustLimit := lnwallet .DustLimitForSize (input .P2TRSize )
654+ var depositSum int64
655+ for _ , deposit := range deposits {
656+ depositSum += deposit .Value
657+ }
658+ if depositSum - int64 (dustLimit ) < amount {
659+ return nil , fmt .Errorf ("insufficient funds to cover swap " +
660+ "amount" )
661+ }
638662
639- func sumDeposits (outpoints []string , deposits []* looprpc.Deposit ) (int64 ,
640- error ) {
663+ // Sort the deposits by amount in descending order, then by
664+ // blocks-until-expiry in ascending order.
665+ sort .Slice (deposits , func (i , j int ) bool {
666+ if deposits [i ].Value == deposits [j ].Value {
667+ return deposits [i ].BlocksUntilExpiry <
668+ deposits [j ].BlocksUntilExpiry
669+ }
670+ return deposits [i ].Value > deposits [j ].Value
671+ })
641672
642- var sum int64
643- depositMap := make (map [string ]* looprpc.Deposit )
673+ // Select the deposits that are needed to cover the swap amount without
674+ // leaving a dust change.
675+ var selectedDeposits []string
676+ var selectedAmount int64
644677 for _ , deposit := range deposits {
645- depositMap [deposit .Outpoint ] = deposit
678+ if selectedAmount >= amount + int64 (dustLimit ) {
679+ break
680+ }
681+ selectedDeposits = append (selectedDeposits , deposit .Outpoint )
682+ selectedAmount += deposit .Value
646683 }
647684
685+ return selectedDeposits , nil
686+ }
687+
688+ func containsDuplicates (outpoints []string ) bool {
689+ found := make (map [string ]struct {})
648690 for _ , outpoint := range outpoints {
649- if _ , ok := depositMap [outpoint ]; ! ok {
650- return 0 , fmt . Errorf ( "deposit %v not found" , outpoint )
691+ if _ , ok := found [outpoint ]; ok {
692+ return true
651693 }
652-
653- sum += depositMap [outpoint ].Value
694+ found [outpoint ] = struct {}{}
654695 }
655696
656- return sum , nil
697+ return false
657698}
658699
659700func depositsToOutpoints (deposits []* looprpc.Deposit ) []string {
0 commit comments