Skip to content

[6/?] StaticAddr: Quoting #750

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

Merged
merged 4 commits into from
Jun 25, 2024
Merged
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
12 changes: 7 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,9 +645,9 @@ func (s *Client) LoopIn(globalCtx context.Context,
return swapInfo, nil
}

// LoopInQuote takes an amount and returns a break down of estimated
// costs for the client. Both the swap server and the on-chain fee estimator are
// queried to get to build the quote response.
// LoopInQuote takes an amount and returns a breakdown of estimated costs for
// the client. Both the swap server and the on-chain fee estimator are queried
// to get to build the quote response.
func (s *Client) LoopInQuote(ctx context.Context,
request *LoopInQuoteRequest) (*LoopInQuote, error) {

Expand Down Expand Up @@ -693,7 +693,7 @@ func (s *Client) LoopInQuote(ctx context.Context,

quote, err := s.Server.GetLoopInQuote(
ctx, request.Amount, s.lndServices.NodePubkey, request.LastHop,
request.RouteHints, request.Initiator,
request.RouteHints, request.Initiator, request.NumDeposits,
)
if err != nil {
return nil, err
Expand All @@ -703,7 +703,9 @@ func (s *Client) LoopInQuote(ctx context.Context,

// We don't calculate the on-chain fee if the HTLC is going to be
// published externally.
if request.ExternalHtlc {
// We also don't calculate the on-chain fee if the loop in is funded by
// static address deposits because we don't publish the HTLC on-chain.
if request.ExternalHtlc || request.NumDeposits > 0 {
return &LoopInQuote{
SwapFee: swapFee,
MinerFee: 0,
Expand Down
76 changes: 65 additions & 11 deletions cmd/loop/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/routing/route"
Expand All @@ -19,10 +20,11 @@ var quoteCommand = cli.Command{
}

var quoteInCommand = cli.Command{
Name: "in",
Usage: "get a quote for the cost of a loop in swap",
ArgsUsage: "amt",
Description: "Allows to determine the cost of a swap up front",
Name: "in",
Usage: "get a quote for the cost of a loop in swap",
ArgsUsage: "amt",
Description: "Allows to determine the cost of a swap up front." +
"Either specify an amount or deposit outpoints.",
Flags: []cli.Flag{
cli.StringFlag{
Name: lastHopFlag.Name,
Expand All @@ -33,20 +35,38 @@ var quoteInCommand = cli.Command{
verboseFlag,
privateFlag,
routeHintsFlag,
cli.StringSliceFlag{
Name: "deposit_outpoint",
Usage: "one or more static address deposit outpoints " +
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one or more

Can you specify how to pass multiple outpoints, please?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

"to quote for. Deposit outpoints are not to " +
"be used in combination with an amount. Each" +
"additional outpoint can be added by " +
"specifying --deposit_outpoint tx_id:idx",
},
},
Action: quoteIn,
}

func quoteIn(ctx *cli.Context) error {
// Show command help if the incorrect number arguments was provided.
if ctx.NArg() != 1 {
if ctx.NArg() != 1 && !ctx.IsSet("deposit_outpoint") {
return cli.ShowCommandHelp(ctx, "in")
}

args := ctx.Args()
amt, err := parseAmt(args[0])
if err != nil {
return err
var (
manualAmt btcutil.Amount
depositAmt btcutil.Amount
depositOutpoints []string
err error
ctxb = context.Background()
)

if ctx.NArg() == 1 {
args := ctx.Args()
manualAmt, err = parseAmt(args[0])
if err != nil {
return err
}
}

client, cleanup, err := getClient(ctx)
Expand All @@ -62,11 +82,20 @@ func quoteIn(ctx *cli.Context) error {
return err
}

if ctx.IsSet("deposit_outpoint") {
depositOutpoints = ctx.StringSlice("deposit_outpoint")
depositAmt, err = depositAmount(ctxb, client, depositOutpoints)
if err != nil {
return err
}
}

quoteReq := &looprpc.QuoteRequest{
Amt: int64(amt),
Amt: int64(manualAmt),
ConfTarget: int32(ctx.Uint64("conf_target")),
LoopInRouteHints: hints,
Private: ctx.Bool(privateFlag.Name),
DepositOutpoints: depositOutpoints,
}

if ctx.IsSet(lastHopFlag.Name) {
Expand All @@ -80,7 +109,6 @@ func quoteIn(ctx *cli.Context) error {
quoteReq.LoopInLastHop = lastHopVertex[:]
}

ctxb := context.Background()
quoteResp, err := client.GetLoopInQuote(ctxb, quoteReq)
if err != nil {
return err
Expand All @@ -98,10 +126,36 @@ func quoteIn(ctx *cli.Context) error {
"amount.\n")
}

// If the user specified static address deposits, we quoted for their
// total value and need to display that value instead of the manually
// selected one.
if manualAmt == 0 {
quoteReq.Amt = int64(depositAmt)
}
printQuoteInResp(quoteReq, quoteResp, ctx.Bool("verbose"))
return nil
}

func depositAmount(ctx context.Context, client looprpc.SwapClientClient,
depositOutpoints []string) (btcutil.Amount, error) {

addressSummary, err := client.GetStaticAddressSummary(
ctx, &looprpc.StaticAddressSummaryRequest{
Outpoints: depositOutpoints,
},
)
if err != nil {
return 0, err
}

var depositAmt btcutil.Amount
for _, deposit := range addressSummary.FilteredDeposits {
depositAmt += btcutil.Amount(deposit.Value)
}

return depositAmt, nil
}

var quoteOutCommand = cli.Command{
Name: "out",
Usage: "get a quote for the cost of a loop out swap",
Expand Down
6 changes: 6 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ type LoopInQuoteRequest struct {
// initiated the swap (loop CLI, autolooper, LiT UI and so on) and is
// appended to the user agent string.
Initiator string

// The number of static address deposits the client wants to quote for.
// If the number of deposits exceeds one the server will apply a
// per-input service fee. This is to cover for the increased on-chain
// fee the server has to pay when the sweeping transaction is broadcast.
NumDeposits uint32
}

// LoopInQuote contains estimates for the fees making up the total swap cost
Expand Down
56 changes: 52 additions & 4 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -748,13 +748,31 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,

log.Infof("Loop in quote request received")

var (
numDeposits = uint32(len(req.DepositOutpoints))
err error
)

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

// Retrieve deposits to calculate their total value.
var summary *clientrpc.StaticAddressSummaryResponse
if len(req.DepositOutpoints) > 0 {
summary, err = s.GetStaticAddressSummary(
ctx, &clientrpc.StaticAddressSummaryRequest{
Outpoints: req.DepositOutpoints,
},
)
if err != nil {
return nil, err
}
}

var (
routeHints [][]zpay32.HopHint
lastHop *route.Vertex
Expand All @@ -777,14 +795,32 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
}
}

// The requested amount should be 0 here if the request contained
// deposit outpoints.
amount := btcutil.Amount(req.Amt)
if amount != 0 && len(summary.FilteredDeposits) > 0 {
return nil, fmt.Errorf("amount should be 0 for deposit " +
"quotes")
}

// 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(summary.FilteredDeposits) > 0 {
for _, deposit := range summary.FilteredDeposits {
amount += btcutil.Amount(deposit.Value)
}
}

quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
Amount: btcutil.Amount(req.Amt),
Amount: amount,
HtlcConfTarget: htlcConfTarget,
ExternalHtlc: req.ExternalHtlc,
LastHop: lastHop,
RouteHints: routeHints,
Private: req.Private,
Initiator: defaultLoopdInitiator,
NumDeposits: numDeposits,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -881,7 +917,7 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
log.Infof("Loop in request received")

htlcConfTarget, err := validateLoopInRequest(
in.HtlcConfTarget, in.ExternalHtlc,
in.HtlcConfTarget, in.ExternalHtlc, 0, in.Amt,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -1710,7 +1746,13 @@ func validateConfTarget(target, defaultTarget int32) (int32, error) {

// validateLoopInRequest fails if the mutually exclusive conf target and
// external parameters are both set.
func validateLoopInRequest(htlcConfTarget int32, external bool) (int32, error) {
func validateLoopInRequest(htlcConfTarget int32, external bool,
numDeposits uint32, amount int64) (int32, error) {

if amount == 0 && numDeposits == 0 {
return 0, errors.New("either amount or deposits must be set")
}

// If the htlc is going to be externally set, the htlcConfTarget should
// not be set, because it has no relevance when the htlc is external.
if external && htlcConfTarget != 0 {
Expand All @@ -1724,6 +1766,12 @@ func validateLoopInRequest(htlcConfTarget int32, external bool) (int32, error) {
return 0, nil
}

// If the loop in uses static address deposits, we do not need to set a
// confirmation target since the HTLC won't be published by the client.
if numDeposits > 0 {
return 0, nil
}

return validateConfTarget(htlcConfTarget, loop.DefaultHtlcConfTarget)
}

Expand Down
27 changes: 24 additions & 3 deletions loopd/swapclient_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,55 +145,76 @@ func TestValidateConfTarget(t *testing.T) {
func TestValidateLoopInRequest(t *testing.T) {
tests := []struct {
name string
amount int64
numDeposits uint32
external bool
confTarget int32
expectErr bool
expectedTarget int32
}{
{
name: "external and htlc conf set",
amount: 100_000,
external: true,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
name: "external and no conf",
amount: 100_000,
external: true,
confTarget: 0,
expectErr: false,
expectedTarget: 0,
},
{
name: "not external, zero conf",
amount: 100_000,
external: false,
confTarget: 0,
expectErr: false,
expectedTarget: loop.DefaultHtlcConfTarget,
},
{
name: "not external, bad conf",
amount: 100_000,
external: false,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
name: "not external, ok conf",
amount: 100_000,
external: false,
confTarget: 5,
expectErr: false,
expectedTarget: 5,
},
{
name: "not external, amount no deposit",
amount: 100_000,
numDeposits: 0,
external: false,
expectErr: false,
expectedTarget: loop.DefaultHtlcConfTarget,
},
{
name: "not external, deposit no amount",
amount: 100_000,
numDeposits: 1,
external: false,
expectErr: false,
},
}

for _, test := range tests {
test := test

t.Run(test.name, func(t *testing.T) {
external := test.external
conf, err := validateLoopInRequest(
test.confTarget, external,
test.confTarget, external, test.numDeposits,
test.amount,
)

if test.expectErr {
Expand Down
2 changes: 1 addition & 1 deletion loopin.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
// hints.
quote, err := cfg.server.GetLoopInQuote(
globalCtx, request.Amount, cfg.lnd.NodePubkey, request.LastHop,
request.RouteHints, request.Initiator,
request.RouteHints, request.Initiator, 0,
)
if err != nil {
return nil, wrapGrpcError("loop in terms", err)
Expand Down
Loading
Loading