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)
@@ -458,6 +461,13 @@ var staticAddressLoopInCommand = cli.Command{
458461 "The client can retry the swap with adjusted " +
459462 "parameters after the payment timed out." ,
460463 },
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+ },
461471 lastHopFlag ,
462472 labelFlag ,
463473 routeHintsFlag ,
@@ -483,11 +493,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
483493 ctxb = context .Background ()
484494 isAllSelected = ctx .IsSet ("all" )
485495 isUtxoSelected = ctx .IsSet ("utxo" )
496+ isAmountSelected bool
497+ selectedAmount = ctx .Int64 ("amount" )
486498 label = ctx .String ("static-loop-in" )
487499 hints []* swapserverrpc.RouteHint
488500 lastHop []byte
489501 paymentTimeoutSeconds = uint32 (loopin .DefaultPaymentTimeoutSeconds )
490502 )
503+ isAmountSelected = selectedAmount > 0
491504
492505 // Validate our label early so that we can fail before getting a quote.
493506 if err := labels .Validate (label ); err != nil {
@@ -522,7 +535,9 @@ func staticAddressLoopIn(ctx *cli.Context) error {
522535 return err
523536 }
524537
525- if len (depositList .FilteredDeposits ) == 0 {
538+ allDeposits := depositList .FilteredDeposits
539+
540+ if len (allDeposits ) == 0 {
526541 errString := fmt .Sprintf ("no confirmed deposits available, " +
527542 "deposits need at least %v confirmations" ,
528543 deposit .MinConfs )
@@ -532,17 +547,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {
532547
533548 var depositOutpoints []string
534549 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" )
537552
538553 case isAllSelected :
539- depositOutpoints = depositsToOutpoints (
540- depositList .FilteredDeposits ,
541- )
554+ depositOutpoints = depositsToOutpoints (allDeposits )
542555
543556 case isUtxoSelected :
544557 depositOutpoints = ctx .StringSlice ("utxo" )
545558
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+
546569 default :
547570 return fmt .Errorf ("unknown quote request" )
548571 }
@@ -552,6 +575,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
552575 }
553576
554577 quoteReq := & looprpc.QuoteRequest {
578+ Amt : selectedAmount ,
555579 LoopInRouteHints : hints ,
556580 LoopInLastHop : lastHop ,
557581 Private : ctx .Bool (privateFlag .Name ),
@@ -564,15 +588,6 @@ func staticAddressLoopIn(ctx *cli.Context) error {
564588
565589 limits := getInLimits (quote )
566590
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-
576591 if ! (ctx .Bool ("force" ) || ctx .Bool ("f" )) {
577592 err = displayInDetails (quoteReq , quote , ctx .Bool ("verbose" ))
578593 if err != nil {
@@ -585,6 +600,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
585600 }
586601
587602 req := & looprpc.StaticAddressLoopInRequest {
603+ Amount : quoteReq .Amt ,
588604 Outpoints : depositOutpoints ,
589605 MaxSwapFeeSatoshis : int64 (limits .maxSwapFee ),
590606 LastHop : lastHop ,
@@ -605,36 +621,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
605621 return nil
606622}
607623
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 ) {
616631
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+ }
619643
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+ })
622653
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
625658 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
627664 }
628665
666+ return selectedDeposits , nil
667+ }
668+
669+ func containsDuplicates (outpoints []string ) bool {
670+ found := make (map [string ]struct {})
629671 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
632674 }
633-
634- sum += depositMap [outpoint ].Value
675+ found [outpoint ] = struct {}{}
635676 }
636677
637- return sum , nil
678+ return false
638679}
639680
640681func depositsToOutpoints (deposits []* looprpc.Deposit ) []string {
0 commit comments