Skip to content

staticaddr: fractional swap amount #887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 76 additions & 35 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/hex"
"errors"
"fmt"
"sort"
"strconv"
"strings"

Expand All @@ -14,6 +15,8 @@ import (
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)
Expand Down Expand Up @@ -457,6 +460,13 @@ var staticAddressLoopInCommand = cli.Command{
"The client can retry the swap with adjusted " +
"parameters after the payment timed out.",
},
cli.IntFlag{
Name: "amount",
Usage: "the number of satoshis that should be " +
"swapped from the selected deposits. If there" +
"is change it is sent back to the static " +
"address.",
},
lastHopFlag,
labelFlag,
routeHintsFlag,
Expand All @@ -482,11 +492,14 @@ func staticAddressLoopIn(ctx *cli.Context) error {
ctxb = context.Background()
isAllSelected = ctx.IsSet("all")
isUtxoSelected = ctx.IsSet("utxo")
isAmountSelected bool
selectedAmount = ctx.Int64("amount")
label = ctx.String("static-loop-in")
hints []*swapserverrpc.RouteHint
lastHop []byte
paymentTimeoutSeconds = uint32(loopin.DefaultPaymentTimeoutSeconds)
)
isAmountSelected = selectedAmount > 0

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

if len(depositList.FilteredDeposits) == 0 {
allDeposits := depositList.FilteredDeposits

if len(allDeposits) == 0 {
errString := fmt.Sprintf("no confirmed deposits available, "+
"deposits need at least %v confirmations",
deposit.MinConfs)
Expand All @@ -531,17 +546,25 @@ func staticAddressLoopIn(ctx *cli.Context) error {

var depositOutpoints []string
switch {
case isAllSelected == isUtxoSelected:
return errors.New("must select either all or some utxos")
case isAllSelected && isUtxoSelected:
return errors.New("cannot select all and specific utxos")

case isAllSelected:
depositOutpoints = depositsToOutpoints(
depositList.FilteredDeposits,
)
depositOutpoints = depositsToOutpoints(allDeposits)

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

case isAmountSelected:
// If there's only a swap amount specified we'll coin-select
// deposits to cover the swap amount.
depositOutpoints, err = selectDeposits(
allDeposits, selectedAmount,
)
if err != nil {
return err
}

default:
return fmt.Errorf("unknown quote request")
}
Expand All @@ -551,6 +574,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
}

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

limits := getInLimits(quote)

// populate the quote request with the sum of selected deposits and
// prompt the user for acceptance.
quoteReq.Amt, err = sumDeposits(
depositOutpoints, depositList.FilteredDeposits,
)
if err != nil {
return err
}

if !(ctx.Bool("force") || ctx.Bool("f")) {
err = displayInDetails(quoteReq, quote, ctx.Bool("verbose"))
if err != nil {
Expand All @@ -584,6 +599,7 @@ func staticAddressLoopIn(ctx *cli.Context) error {
}

req := &looprpc.StaticAddressLoopInRequest{
Amount: quoteReq.Amt,
Outpoints: depositOutpoints,
MaxSwapFeeSatoshis: int64(limits.maxSwapFee),
LastHop: lastHop,
Expand All @@ -604,36 +620,61 @@ func staticAddressLoopIn(ctx *cli.Context) error {
return nil
}

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

return false
}
// Check that sum of deposits covers the swap amount while leaving no
// dust change.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
var depositSum int64
for _, deposit := range deposits {
depositSum += deposit.Value
}
if depositSum-int64(dustLimit) < amount {
return nil, fmt.Errorf("insufficient funds to cover swap " +
"amount")
}

func sumDeposits(outpoints []string, deposits []*looprpc.Deposit) (int64,
error) {
// Sort the deposits by amount in descending order, then by
// blocks-until-expiry in ascending order.
sort.Slice(deposits, func(i, j int) bool {
if deposits[i].Value == deposits[j].Value {
return deposits[i].BlocksUntilExpiry <
deposits[j].BlocksUntilExpiry
}
return deposits[i].Value > deposits[j].Value
})

var sum int64
depositMap := make(map[string]*looprpc.Deposit)
// Select the deposits that are needed to cover the swap amount without
// leaving a dust change.
var selectedDeposits []string
var selectedAmount int64
for _, deposit := range deposits {
depositMap[deposit.Outpoint] = deposit
if selectedAmount >= amount+int64(dustLimit) {
break
}
selectedDeposits = append(selectedDeposits, deposit.Outpoint)
selectedAmount += deposit.Value
}

return selectedDeposits, nil
}

func containsDuplicates(outpoints []string) bool {
found := make(map[string]struct{})
for _, outpoint := range outpoints {
if _, ok := depositMap[outpoint]; !ok {
return 0, fmt.Errorf("deposit %v not found", outpoint)
if _, ok := found[outpoint]; ok {
return true
}

sum += depositMap[outpoint].Value
found[outpoint] = struct{}{}
}

return sum, nil
return false
}

func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
Expand Down
5 changes: 5 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ type StaticAddressLoopInRequest struct {
// swap payment. If the timeout is reached the swap will be aborted and
// the client can retry the swap if desired with different parameters.
PaymentTimeoutSeconds uint32

// SelectedAmount is the amount that the user selected for the swap. If
// the user did not select an amount, the amount of all deposits is
// used.
SelectedAmount btcutil.Amount
}

// LoopInTerms are the server terms on which it executes loop in swaps.
Expand Down
76 changes: 57 additions & 19 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ import (
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/queue"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
Expand Down Expand Up @@ -884,21 +886,26 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
infof("Loop in quote request received")

var (
numDeposits = uint32(len(req.DepositOutpoints))
err error
selectedAmount = btcutil.Amount(req.Amt)
totalDepositAmount btcutil.Amount
numDeposits = len(req.DepositOutpoints)
err error
)

htlcConfTarget, err := validateLoopInRequest(
req.ConfTarget, req.ExternalHtlc, numDeposits, req.Amt,
req.ConfTarget, req.ExternalHtlc, uint32(numDeposits),
int64(selectedAmount),
)
if err != nil {
return nil, err
}

// Retrieve deposits to calculate their total value.
var depositList *looprpc.ListStaticAddressDepositsResponse
amount := btcutil.Amount(req.Amt)
if len(req.DepositOutpoints) > 0 {

// If deposits are selected, we need to retrieve them to calculate the
// total value which we request a quote for.
if numDeposits > 0 {
depositList, err = s.ListStaticAddressDeposits(
ctx, &looprpc.ListStaticAddressDepositsRequest{
Outpoints: req.DepositOutpoints,
Expand All @@ -913,20 +920,45 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
"deposit outpoints")
}

// The requested amount should be 0 here if the request
// contained deposit outpoints.
if amount != 0 && len(depositList.FilteredDeposits) > 0 {
return nil, fmt.Errorf("amount should be 0 for " +
"deposit quotes")
if numDeposits != len(depositList.FilteredDeposits) {
return nil, fmt.Errorf("expected %d deposits, got %d",
numDeposits, len(depositList.FilteredDeposits))
}

// In case we quote for deposits we send the server both the
// total value and the number of deposits. This is so the server
// can probe the total amount and calculate the per input fee.
if amount == 0 && len(depositList.FilteredDeposits) > 0 {
for _, deposit := range depositList.FilteredDeposits {
amount += btcutil.Amount(deposit.Value)
}
// selected value and the number of deposits. This is so the
// server can probe the selected value and calculate the per
// input fee.
for _, deposit := range depositList.FilteredDeposits {
totalDepositAmount += btcutil.Amount(
deposit.Value,
)
}

// If the selected amount would leave a dust change output or
// exceeds the total deposits value, we return an error.
dustLimit := lnwallet.DustLimitForSize(input.P2TRSize)
remainingAmount := totalDepositAmount - selectedAmount
switch {
case remainingAmount < 0:
return nil, fmt.Errorf("selected amount %v exceeds "+
"total deposit value %v", selectedAmount,
totalDepositAmount)

case remainingAmount > 0 && remainingAmount < dustLimit:
return nil, fmt.Errorf("selected amount %v leaves "+
"dust change %v", selectedAmount,
totalDepositAmount)

default:
// If the remaining amount is 0 or equal or greater than
// the dust limit, we can proceed with the swap.
}

// If the client didn't select an amount we quote for the total
// deposits value.
if selectedAmount == 0 {
selectedAmount = totalDepositAmount
}
}

Expand All @@ -953,14 +985,14 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
}

quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
Amount: amount,
Amount: selectedAmount,
HtlcConfTarget: htlcConfTarget,
ExternalHtlc: req.ExternalHtlc,
LastHop: lastHop,
RouteHints: routeHints,
Private: req.Private,
Initiator: defaultLoopdInitiator,
NumDeposits: numDeposits,
NumDeposits: uint32(numDeposits),
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -1692,13 +1724,18 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
return nil, fmt.Errorf("error decoding swap invoice: "+
"%v", err)
}

swapAmount := swp.TotalDepositAmount()
if swp.SelectedAmount > 0 {
swapAmount = swp.SelectedAmount
}
swap := &looprpc.StaticAddressLoopInSwap{
SwapHash: swp.SwapHash[:],
DepositOutpoints: swp.DepositOutpoints,
State: toClientStaticAddressLoopInState(
swp.GetState(),
),
SwapAmountSatoshis: int64(swp.TotalDepositAmount()),
SwapAmountSatoshis: int64(swapAmount),
PaymentRequestAmountSatoshis: int64(
swapPayReq.MilliSat.ToSatoshis(),
),
Expand Down Expand Up @@ -1805,6 +1842,7 @@ func (s *swapClientServer) StaticAddressLoopIn(ctx context.Context,
}

req := &loop.StaticAddressLoopInRequest{
SelectedAmount: btcutil.Amount(in.Amount),
DepositOutpoints: in.Outpoints,
MaxSwapFee: btcutil.Amount(in.MaxSwapFeeSatoshis),
Label: in.Label,
Expand Down
3 changes: 3 additions & 0 deletions loopdb/sqlc/migrations/000015_static_selected_amount.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- selected_amount is a fractinal amount amongst selected deposits of a static
-- address loop-in swap.
ALTER TABLE static_address_swaps DROP COLUMN selected_amount;
4 changes: 4 additions & 0 deletions loopdb/sqlc/migrations/000015_static_selected_amount.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- selected_amount is a fractional amount amongst selected deposits of a static
-- address loop-in swap. A selected amount of 0 indicates that the total amount
-- of deposits is selected for the swap.
ALTER TABLE static_address_swaps ADD selected_amount BIGINT NOT NULL DEFAULT 0;
1 change: 1 addition & 0 deletions loopdb/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions loopdb/sqlc/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading