Skip to content

Commit de351a3

Browse files
committed
loopd: fractional static address swap amount
1 parent 328ccd9 commit de351a3

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

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

0 commit comments

Comments
 (0)