Skip to content

Commit e5c5d49

Browse files
authored
Merge pull request #158 from guggero/max-quote-fix
Don't fail loop in quote if balance is insufficient for the miner fee estimation
2 parents a473ab5 + acdd2a2 commit e5c5d49

File tree

7 files changed

+132
-17
lines changed

7 files changed

+132
-17
lines changed

client.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/hex"
66
"errors"
77
"fmt"
8+
"strings"
89
"sync"
910
"sync/atomic"
1011
"time"
@@ -59,6 +60,11 @@ var (
5960
globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout
6061

6162
republishDelay = 10 * time.Second
63+
64+
// MinerFeeEstimationFailed is a magic number that is returned in a
65+
// quote call as the miner fee if the fee estimation in lnd's wallet
66+
// failed because of insufficient funds.
67+
MinerFeeEstimationFailed btcutil.Amount = -1
6268
)
6369

6470
// Client performs the client side part of swaps. This interface exists to be
@@ -505,10 +511,24 @@ func (s *Client) LoopInQuote(ctx context.Context,
505511
}, nil
506512
}
507513

508-
// Get estimate for miner fee.
514+
// Get estimate for miner fee. If estimating the miner fee for the
515+
// requested amount is not possible because lnd's wallet cannot
516+
// construct a sample TX, we just return zero instead of failing the
517+
// quote. The user interface should inform the user that fee estimation
518+
// was not possible.
519+
//
520+
// TODO(guggero): Thread through error code from lnd to avoid string
521+
// matching.
509522
minerFee, err := s.lndServices.Client.EstimateFeeToP2WSH(
510523
ctx, request.Amount, request.HtlcConfTarget,
511524
)
525+
if err != nil && strings.Contains(err.Error(), "insufficient funds") {
526+
return &LoopInQuote{
527+
SwapFee: swapFee,
528+
MinerFee: MinerFeeEstimationFailed,
529+
CltvDelta: quote.CltvDelta,
530+
}, nil
531+
}
512532
if err != nil {
513533
return nil, err
514534
}

cmd/loop/loopin.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66

77
"github.com/btcsuite/btcutil"
8+
"github.com/lightninglabs/loop"
89
"github.com/lightninglabs/loop/looprpc"
910
"github.com/lightninglabs/loop/swap"
1011
"github.com/lightningnetwork/lnd/routing/route"
@@ -76,6 +77,18 @@ func loopIn(ctx *cli.Context) error {
7677
return err
7778
}
7879

80+
// For loop in, the fee estimation is handed to lnd which tries to
81+
// construct a real transaction to sample realistic fees to pay to the
82+
// HTLC. If the wallet doesn't have enough funds to create this TX, we
83+
// know it won't have enough to pay the real transaction either. It
84+
// makes sense to abort the loop in this case.
85+
if !external && quote.MinerFee == int64(loop.MinerFeeEstimationFailed) {
86+
return fmt.Errorf("miner fee estimation not " +
87+
"possible, lnd has insufficient funds to " +
88+
"create a sample transaction for selected " +
89+
"amount")
90+
}
91+
7992
limits := getInLimits(amt, quote)
8093
err = displayLimits(swap.TypeIn, amt, limits, external, "")
8194
if err != nil {

cmd/loop/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,8 @@ func displayLimits(swapType swap.Type, amt btcutil.Amount, l *limits,
159159
"wallet.\n\n")
160160
}
161161

162-
fmt.Printf("Max swap fees for %d Loop %v: %d\n",
163-
amt, swapType, totalSuccessMax,
164-
)
162+
fmt.Printf("Max swap fees for %d Loop %v: %d\n", amt, swapType,
163+
totalSuccessMax)
165164

166165
if warning != "" {
167166
fmt.Println(warning)

cmd/loop/quote.go

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,85 @@ package main
22

33
import (
44
"context"
5+
"fmt"
6+
"os"
57
"time"
68

9+
"github.com/lightninglabs/loop"
710
"github.com/lightninglabs/loop/looprpc"
811
"github.com/urfave/cli"
912
)
1013

1114
var quoteCommand = cli.Command{
1215
Name: "quote",
1316
Usage: "get a quote for the cost of a swap",
17+
Subcommands: []cli.Command{quoteInCommand, quoteOutCommand},
18+
}
19+
20+
var quoteInCommand = cli.Command{
21+
Name: "in",
22+
Usage: "get a quote for the cost of a loop in swap",
23+
ArgsUsage: "amt",
24+
Description: "Allows to determine the cost of a swap up front",
25+
Flags: []cli.Flag{
26+
cli.Uint64Flag{
27+
Name: "conf_target",
28+
Usage: "the number of blocks from the swap " +
29+
"initiation height that the on-chain HTLC " +
30+
"should be swept within in a Loop Out",
31+
Value: 6,
32+
},
33+
},
34+
Action: quoteIn,
35+
}
36+
37+
func quoteIn(ctx *cli.Context) error {
38+
// Show command help if the incorrect number arguments was provided.
39+
if ctx.NArg() != 1 {
40+
return cli.ShowCommandHelp(ctx, "in")
41+
}
42+
43+
args := ctx.Args()
44+
amt, err := parseAmt(args[0])
45+
if err != nil {
46+
return err
47+
}
48+
49+
client, cleanup, err := getClient(ctx)
50+
if err != nil {
51+
return err
52+
}
53+
defer cleanup()
54+
55+
ctxb := context.Background()
56+
quoteReq := &looprpc.QuoteRequest{
57+
Amt: int64(amt),
58+
ConfTarget: int32(ctx.Uint64("conf_target")),
59+
}
60+
quoteResp, err := client.GetLoopInQuote(ctxb, quoteReq)
61+
if err != nil {
62+
return err
63+
}
64+
65+
// For loop in, the fee estimation is handed to lnd which tries to
66+
// construct a real transaction to sample realistic fees to pay to the
67+
// HTLC. If the wallet doesn't have enough funds to create this TX, we
68+
// don't want to fail the quote. But the user should still be informed
69+
// why the fee shows as -1.
70+
if quoteResp.MinerFee == int64(loop.MinerFeeEstimationFailed) {
71+
_, _ = fmt.Fprintf(os.Stderr, "Warning: Miner fee estimation "+
72+
"not possible, lnd has insufficient funds to "+
73+
"create a sample transaction for selected "+
74+
"amount.\n")
75+
}
76+
77+
printRespJSON(quoteResp)
78+
return nil
79+
}
80+
81+
var quoteOutCommand = cli.Command{
82+
Name: "out",
83+
Usage: "get a quote for the cost of a loop out swap",
1484
ArgsUsage: "amt",
1585
Description: "Allows to determine the cost of a swap up front",
1686
Flags: []cli.Flag{
@@ -32,13 +102,13 @@ var quoteCommand = cli.Command{
32102
"swap fee.",
33103
},
34104
},
35-
Action: quote,
105+
Action: quoteOut,
36106
}
37107

38-
func quote(ctx *cli.Context) error {
108+
func quoteOut(ctx *cli.Context) error {
39109
// Show command help if the incorrect number arguments was provided.
40110
if ctx.NArg() != 1 {
41-
return cli.ShowCommandHelp(ctx, "quote")
111+
return cli.ShowCommandHelp(ctx, "out")
42112
}
43113

44114
args := ctx.Args()
@@ -60,15 +130,16 @@ func quote(ctx *cli.Context) error {
60130
}
61131

62132
ctxb := context.Background()
63-
resp, err := client.LoopOutQuote(ctxb, &looprpc.QuoteRequest{
133+
quoteReq := &looprpc.QuoteRequest{
64134
Amt: int64(amt),
65135
ConfTarget: int32(ctx.Uint64("conf_target")),
66136
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
67-
})
137+
}
138+
quoteResp, err := client.LoopOutQuote(ctxb, quoteReq)
68139
if err != nil {
69140
return err
70141
}
71142

72-
printRespJSON(resp)
143+
printRespJSON(quoteResp)
73144
return nil
74145
}

looprpc/client.pb.go

Lines changed: 8 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

looprpc/client.proto

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ message QuoteRequest {
412412
publishing the HTLC on chain. Setting this to a larger value will give the
413413
server the opportunity to batch multiple swaps together, and wait for
414414
low-fee periods before publishing the HTLC, potentially resulting in a
415-
lower total swap fee.
415+
lower total swap fee. This only has an effect on loop out quotes.
416416
*/
417417
uint64 swap_publication_deadline = 4;
418418
}
@@ -429,7 +429,13 @@ message QuoteResponse {
429429
int64 prepay_amt = 2;
430430

431431
/**
432-
An estimate of the on-chain fee that needs to be paid to sweep the HTLC.
432+
An estimate of the on-chain fee that needs to be paid to sweep the HTLC for
433+
a loop out or to pay to the HTLC for loop in. If a miner fee of 0 is
434+
returned, it means the external_htlc flag was set for a loop in and the fee
435+
estimation was skipped. If a miner fee of -1 is returned, it means lnd's
436+
wallet tried to estimate the fee but was unable to create a sample
437+
estimation transaction because not enough funds are available. An
438+
information message should be shown to the user in this case.
433439
*/
434440
int64 miner_fee = 3;
435441

looprpc/client.swagger.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
},
8282
{
8383
"name": "swap_publication_deadline",
84-
"description": "*\nThe latest time (in unix seconds) we allow the server to wait before\npublishing the HTLC on chain. Setting this to a larger value will give the\nserver the opportunity to batch multiple swaps together, and wait for\nlow-fee periods before publishing the HTLC, potentially resulting in a\nlower total swap fee.",
84+
"description": "*\nThe latest time (in unix seconds) we allow the server to wait before\npublishing the HTLC on chain. Setting this to a larger value will give the\nserver the opportunity to batch multiple swaps together, and wait for\nlow-fee periods before publishing the HTLC, potentially resulting in a\nlower total swap fee. This only has an effect on loop out quotes.",
8585
"in": "query",
8686
"required": false,
8787
"type": "string",
@@ -176,7 +176,7 @@
176176
},
177177
{
178178
"name": "swap_publication_deadline",
179-
"description": "*\nThe latest time (in unix seconds) we allow the server to wait before\npublishing the HTLC on chain. Setting this to a larger value will give the\nserver the opportunity to batch multiple swaps together, and wait for\nlow-fee periods before publishing the HTLC, potentially resulting in a\nlower total swap fee.",
179+
"description": "*\nThe latest time (in unix seconds) we allow the server to wait before\npublishing the HTLC on chain. Setting this to a larger value will give the\nserver the opportunity to batch multiple swaps together, and wait for\nlow-fee periods before publishing the HTLC, potentially resulting in a\nlower total swap fee. This only has an effect on loop out quotes.",
180180
"in": "query",
181181
"required": false,
182182
"type": "string",
@@ -424,7 +424,7 @@
424424
"miner_fee": {
425425
"type": "string",
426426
"format": "int64",
427-
"description": "*\nAn estimate of the on-chain fee that needs to be paid to sweep the HTLC."
427+
"description": "*\nAn estimate of the on-chain fee that needs to be paid to sweep the HTLC for\na loop out or to pay to the HTLC for loop in. If a miner fee of 0 is\nreturned, it means the external_htlc flag was set for a loop in and the fee\nestimation was skipped. If a miner fee of -1 is returned, it means lnd's\nwallet tried to estimate the fee but was unable to create a sample\nestimation transaction because not enough funds are available. An\ninformation message should be shown to the user in this case."
428428
},
429429
"swap_payment_dest": {
430430
"type": "string",

0 commit comments

Comments
 (0)