Skip to content

Commit cafd1eb

Browse files
committed
loopd: fractional static address swap amount
1 parent 1094c64 commit cafd1eb

File tree

1 file changed

+76
-35
lines changed

1 file changed

+76
-35
lines changed

cmd/loop/staticaddr.go

+76-35
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
)
@@ -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

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

0 commit comments

Comments
 (0)