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)
@@ -444,6 +447,13 @@ var staticAddressLoopInCommand = cli.Command{
444447 "The client can retry the swap with adjusted " +
445448 "parameters after the payment timed out." ,
446449 },
450+ cli.IntFlag {
451+ Name : "amount" ,
452+ Usage : "the number of satoshis that should be " +
453+ "swapped from the selected deposits. If there" +
454+ "is change it is sent back to the static " +
455+ "address." ,
456+ },
447457 lastHopFlag ,
448458 labelFlag ,
449459 routeHintsFlag ,
@@ -469,11 +479,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
469479 ctxb = context .Background ()
470480 isAllSelected = ctx .IsSet ("all" )
471481 isUtxoSelected = ctx .IsSet ("utxo" )
482+ isAmountSelected bool
483+ selectedAmount = ctx .Int64 ("amount" )
472484 label = ctx .String ("static-loop-in" )
473485 hints []* swapserverrpc.RouteHint
474486 lastHop []byte
475487 paymentTimeoutSeconds = uint32 (loopin .DefaultPaymentTimeoutSeconds )
476488 )
489+ isAmountSelected = selectedAmount > 0
477490
478491 // Validate our label early so that we can fail before getting a quote.
479492 if err := labels .Validate (label ); err != nil {
@@ -508,7 +521,9 @@ func staticAddressLoopIn(ctx *cli.Context) error {
508521 return err
509522 }
510523
511- if len (depositList .FilteredDeposits ) == 0 {
524+ allDeposits := depositList .FilteredDeposits
525+
526+ if len (allDeposits ) == 0 {
512527 errString := fmt .Sprintf ("no confirmed deposits available, " +
513528 "deposits need at least %v confirmations" ,
514529 deposit .MinConfs )
@@ -518,17 +533,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {
518533
519534 var depositOutpoints []string
520535 switch {
521- case isAllSelected == isUtxoSelected :
522- return errors .New ("must select either all or some utxos" )
536+ case isAllSelected && isUtxoSelected :
537+ return errors .New ("cannot select all and specific utxos" )
523538
524539 case isAllSelected :
525- depositOutpoints = depositsToOutpoints (
526- depositList .FilteredDeposits ,
527- )
540+ depositOutpoints = depositsToOutpoints (allDeposits )
528541
529542 case isUtxoSelected :
530543 depositOutpoints = ctx .StringSlice ("utxo" )
531544
545+ case isAmountSelected :
546+ // If there's only a swap amount specified we'll coin-select
547+ // deposits to cover the swap amount.
548+ depositOutpoints , err = selectDeposits (
549+ allDeposits , selectedAmount ,
550+ )
551+ if err != nil {
552+ return err
553+ }
554+
532555 default :
533556 return fmt .Errorf ("unknown quote request" )
534557 }
@@ -538,6 +561,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
538561 }
539562
540563 quoteReq := & looprpc.QuoteRequest {
564+ Amt : selectedAmount ,
541565 LoopInRouteHints : hints ,
542566 LoopInLastHop : lastHop ,
543567 Private : ctx .Bool (privateFlag .Name ),
@@ -550,15 +574,6 @@ func staticAddressLoopIn(ctx *cli.Context) error {
550574
551575 limits := getInLimits (quote )
552576
553- // populate the quote request with the sum of selected deposits and
554- // prompt the user for acceptance.
555- quoteReq .Amt , err = sumDeposits (
556- depositOutpoints , depositList .FilteredDeposits ,
557- )
558- if err != nil {
559- return err
560- }
561-
562577 if ! (ctx .Bool ("force" ) || ctx .Bool ("f" )) {
563578 err = displayInDetails (quoteReq , quote , ctx .Bool ("verbose" ))
564579 if err != nil {
@@ -571,6 +586,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
571586 }
572587
573588 req := & looprpc.StaticAddressLoopInRequest {
589+ Amount : quoteReq .Amt ,
574590 Outpoints : depositOutpoints ,
575591 MaxSwapFeeSatoshis : int64 (limits .maxSwapFee ),
576592 LastHop : lastHop ,
@@ -591,36 +607,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
591607 return nil
592608}
593609
594- func containsDuplicates (outpoints []string ) bool {
595- found := make (map [string ]struct {})
596- for _ , outpoint := range outpoints {
597- if _ , ok := found [outpoint ]; ok {
598- return true
599- }
600- found [outpoint ] = struct {}{}
601- }
610+ // selectDeposits sorts the deposits by amount in descending order, then by
611+ // blocks-until-expiry in ascending order. It then selects the deposits that
612+ // are needed to cover the amount requested without leaving a dust change. It
613+ // returns an error if the sum of deposits minus dust is less than the requested
614+ // amount.
615+ func selectDeposits (deposits []* looprpc.Deposit , amount int64 ) ([]string ,
616+ error ) {
602617
603- return false
604- }
618+ // Check that sum of deposits covers the swap amount while leaving no
619+ // dust change.
620+ dustLimit := lnwallet .DustLimitForSize (input .P2TRSize )
621+ var depositSum int64
622+ for _ , deposit := range deposits {
623+ depositSum += deposit .Value
624+ }
625+ if depositSum - int64 (dustLimit ) < amount {
626+ return nil , fmt .Errorf ("insufficient funds to cover swap " +
627+ "amount" )
628+ }
605629
606- func sumDeposits (outpoints []string , deposits []* looprpc.Deposit ) (int64 ,
607- error ) {
630+ // Sort the deposits by amount in descending order, then by
631+ // blocks-until-expiry in ascending order.
632+ sort .Slice (deposits , func (i , j int ) bool {
633+ if deposits [i ].Value == deposits [j ].Value {
634+ return deposits [i ].BlocksUntilExpiry <
635+ deposits [j ].BlocksUntilExpiry
636+ }
637+ return deposits [i ].Value > deposits [j ].Value
638+ })
608639
609- var sum int64
610- depositMap := make (map [string ]* looprpc.Deposit )
640+ // Select the deposits that are needed to cover the swap amount without
641+ // leaving a dust change.
642+ var selectedDeposits []string
643+ var selectedAmount int64
611644 for _ , deposit := range deposits {
612- depositMap [deposit .Outpoint ] = deposit
645+ if selectedAmount >= amount + int64 (dustLimit ) {
646+ break
647+ }
648+ selectedDeposits = append (selectedDeposits , deposit .Outpoint )
649+ selectedAmount += deposit .Value
613650 }
614651
652+ return selectedDeposits , nil
653+ }
654+
655+ func containsDuplicates (outpoints []string ) bool {
656+ found := make (map [string ]struct {})
615657 for _ , outpoint := range outpoints {
616- if _ , ok := depositMap [outpoint ]; ! ok {
617- return 0 , fmt . Errorf ( "deposit %v not found" , outpoint )
658+ if _ , ok := found [outpoint ]; ok {
659+ return true
618660 }
619-
620- sum += depositMap [outpoint ].Value
661+ found [outpoint ] = struct {}{}
621662 }
622663
623- return sum , nil
664+ return false
624665}
625666
626667func depositsToOutpoints (deposits []* looprpc.Deposit ) []string {
0 commit comments