Skip to content

Commit fddc3f4

Browse files
committed
loopd: fractional static address swap amount
1 parent ee832ef commit fddc3f4

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
)
@@ -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

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

0 commit comments

Comments
 (0)