Skip to content

Commit d6fb023

Browse files
committed
loopd: fractional static address swap amount
1 parent 23c08b6 commit d6fb023

File tree

1 file changed

+76
-35
lines changed

1 file changed

+76
-35
lines changed

cmd/loop/staticaddr.go

Lines changed: 76 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
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
)
@@ -457,6 +460,13 @@ var staticAddressLoopInCommand = cli.Command{
457460
"The client can retry the swap with adjusted " +
458461
"parameters after the payment timed out.",
459462
},
463+
cli.IntFlag{
464+
Name: "amount",
465+
Usage: "the number of satoshis that should be " +
466+
"swapped from the selected deposits. If there" +
467+
"is change it is sent back to the static " +
468+
"address.",
469+
},
460470
lastHopFlag,
461471
labelFlag,
462472
routeHintsFlag,
@@ -482,11 +492,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
482492
ctxb = context.Background()
483493
isAllSelected = ctx.IsSet("all")
484494
isUtxoSelected = ctx.IsSet("utxo")
495+
isAmountSelected bool
496+
selectedAmount = ctx.Int64("amount")
485497
label = ctx.String("static-loop-in")
486498
hints []*swapserverrpc.RouteHint
487499
lastHop []byte
488500
paymentTimeoutSeconds = uint32(loopin.DefaultPaymentTimeoutSeconds)
489501
)
502+
isAmountSelected = selectedAmount > 0
490503

491504
// Validate our label early so that we can fail before getting a quote.
492505
if err := labels.Validate(label); err != nil {
@@ -521,7 +534,9 @@ func staticAddressLoopIn(ctx *cli.Context) error {
521534
return err
522535
}
523536

524-
if len(depositList.FilteredDeposits) == 0 {
537+
allDeposits := depositList.FilteredDeposits
538+
539+
if len(allDeposits) == 0 {
525540
errString := fmt.Sprintf("no confirmed deposits available, "+
526541
"deposits need at least %v confirmations",
527542
deposit.MinConfs)
@@ -531,17 +546,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {
531546

532547
var depositOutpoints []string
533548
switch {
534-
case isAllSelected == isUtxoSelected:
535-
return errors.New("must select either all or some utxos")
549+
case isAllSelected && isUtxoSelected:
550+
return errors.New("cannot select all and specific utxos")
536551

537552
case isAllSelected:
538-
depositOutpoints = depositsToOutpoints(
539-
depositList.FilteredDeposits,
540-
)
553+
depositOutpoints = depositsToOutpoints(allDeposits)
541554

542555
case isUtxoSelected:
543556
depositOutpoints = ctx.StringSlice("utxo")
544557

558+
case isAmountSelected:
559+
// If there's only a swap amount specified we'll coin-select
560+
// deposits to cover the swap amount.
561+
depositOutpoints, err = selectDeposits(
562+
allDeposits, selectedAmount,
563+
)
564+
if err != nil {
565+
return err
566+
}
567+
545568
default:
546569
return fmt.Errorf("unknown quote request")
547570
}
@@ -551,6 +574,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
551574
}
552575

553576
quoteReq := &looprpc.QuoteRequest{
577+
Amt: selectedAmount,
554578
LoopInRouteHints: hints,
555579
LoopInLastHop: lastHop,
556580
Private: ctx.Bool(privateFlag.Name),
@@ -563,15 +587,6 @@ func staticAddressLoopIn(ctx *cli.Context) error {
563587

564588
limits := getInLimits(quote)
565589

566-
// populate the quote request with the sum of selected deposits and
567-
// prompt the user for acceptance.
568-
quoteReq.Amt, err = sumDeposits(
569-
depositOutpoints, depositList.FilteredDeposits,
570-
)
571-
if err != nil {
572-
return err
573-
}
574-
575590
if !(ctx.Bool("force") || ctx.Bool("f")) {
576591
err = displayInDetails(quoteReq, quote, ctx.Bool("verbose"))
577592
if err != nil {
@@ -584,6 +599,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
584599
}
585600

586601
req := &looprpc.StaticAddressLoopInRequest{
602+
Amount: quoteReq.Amt,
587603
Outpoints: depositOutpoints,
588604
MaxSwapFeeSatoshis: int64(limits.maxSwapFee),
589605
LastHop: lastHop,
@@ -604,36 +620,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
604620
return nil
605621
}
606622

607-
func containsDuplicates(outpoints []string) bool {
608-
found := make(map[string]struct{})
609-
for _, outpoint := range outpoints {
610-
if _, ok := found[outpoint]; ok {
611-
return true
612-
}
613-
found[outpoint] = struct{}{}
614-
}
623+
// selectDeposits sorts the deposits by amount in descending order, then by
624+
// blocks-until-expiry in ascending order. It then selects the deposits that
625+
// are needed to cover the amount requested without leaving a dust change. It
626+
// returns an error if the sum of deposits minus dust is less than the requested
627+
// amount.
628+
func selectDeposits(deposits []*looprpc.Deposit, amount int64) ([]string,
629+
error) {
615630

616-
return false
617-
}
631+
// Check that sum of deposits covers the swap amount while leaving no
632+
// dust change.
633+
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
634+
var depositSum int64
635+
for _, deposit := range deposits {
636+
depositSum += deposit.Value
637+
}
638+
if depositSum-int64(dustLimit) < amount {
639+
return nil, fmt.Errorf("insufficient funds to cover swap " +
640+
"amount")
641+
}
618642

619-
func sumDeposits(outpoints []string, deposits []*looprpc.Deposit) (int64,
620-
error) {
643+
// Sort the deposits by amount in descending order, then by
644+
// blocks-until-expiry in ascending order.
645+
sort.Slice(deposits, func(i, j int) bool {
646+
if deposits[i].Value == deposits[j].Value {
647+
return deposits[i].BlocksUntilExpiry <
648+
deposits[j].BlocksUntilExpiry
649+
}
650+
return deposits[i].Value > deposits[j].Value
651+
})
621652

622-
var sum int64
623-
depositMap := make(map[string]*looprpc.Deposit)
653+
// Select the deposits that are needed to cover the swap amount without
654+
// leaving a dust change.
655+
var selectedDeposits []string
656+
var selectedAmount int64
624657
for _, deposit := range deposits {
625-
depositMap[deposit.Outpoint] = deposit
658+
if selectedAmount >= amount+int64(dustLimit) {
659+
break
660+
}
661+
selectedDeposits = append(selectedDeposits, deposit.Outpoint)
662+
selectedAmount += deposit.Value
626663
}
627664

665+
return selectedDeposits, nil
666+
}
667+
668+
func containsDuplicates(outpoints []string) bool {
669+
found := make(map[string]struct{})
628670
for _, outpoint := range outpoints {
629-
if _, ok := depositMap[outpoint]; !ok {
630-
return 0, fmt.Errorf("deposit %v not found", outpoint)
671+
if _, ok := found[outpoint]; ok {
672+
return true
631673
}
632-
633-
sum += depositMap[outpoint].Value
674+
found[outpoint] = struct{}{}
634675
}
635676

636-
return sum, nil
677+
return false
637678
}
638679

639680
func depositsToOutpoints(deposits []*looprpc.Deposit) []string {

0 commit comments

Comments
 (0)