Skip to content
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
3 changes: 3 additions & 0 deletions cmd/loop/loopout.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ var loopOutCommand = &cli.Command{
verboseFlag,
channelFlag,
},
Commands: []*cli.Command{
sweepHtlcCommand,
},
Action: loopOut,
}

Expand Down
119 changes: 119 additions & 0 deletions cmd/loop/sweephtlc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"bytes"
"context"
"encoding/hex"
"fmt"

"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli/v3"
)

// sweepHtlcCommand exposes HTLC success-path sweeping over loop CLI.
var sweepHtlcCommand = &cli.Command{
Name: "sweephtlc",
Usage: "sweep an HTLC output using the preimage success path",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "outpoint",
Usage: "htlc outpoint to sweep (format: txid:vout)",
Required: true,
},
&cli.StringFlag{
Name: "htlcaddr",
Usage: "htlc address corresponding to the outpoint",
Required: true,
},
&cli.UintFlag{
Name: "feerate",
Usage: "fee rate to use in sat/vbyte",
Required: true,
},
&cli.StringFlag{
Name: "destaddr",
Usage: "optional destination address; defaults to a " +
"new wallet address",
},
&cli.StringFlag{
Name: "preimage",
Usage: "optional preimage hex to override stored " +
"swap preimage",
},
&cli.BoolFlag{
Name: "publish",
Usage: "publish the sweep transaction immediately",
Value: false,
},
},
Hidden: true,
Action: sweepHtlc,
}

// sweepHtlc executes the SweepHtlc RPC and prints the sweep transaction hex.
func sweepHtlc(ctx context.Context, cmd *cli.Command) error {
// Loopd connecting client.
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()

// Find the preimage if the user passed it.
var preimage []byte
if cmd.IsSet("preimage") {
preimage, err = hex.DecodeString(cmd.String("preimage"))
if err != nil {
return fmt.Errorf("invalid preimage: %w", err)
}
}

// Call SweepHtlc on loopd trying to sweep the HTLC.
resp, err := client.SweepHtlc(ctx, &looprpc.SweepHtlcRequest{
Outpoint: cmd.String("outpoint"),
DestAddress: cmd.String("destaddr"),
HtlcAddress: cmd.String("htlcaddr"),
SatPerVbyte: uint32(cmd.Uint("feerate")),
Preimage: preimage,
Publish: cmd.Bool("publish"),
})
if err != nil {
return err
}

// Always display the raw sweep transaction.
fmt.Printf("sweep_tx_hex: %x\n", resp.SweepTx)

// Report publish status in a user-friendly way based on response.
switch {
case resp.GetNotRequested() != nil:
fmt.Println("publish: not requested (pass --publish to " +
"broadcast)")

case resp.GetPublished() != nil:
fmt.Println("publish: success")

case resp.GetFailed() != nil:
errMsg := resp.GetFailed().GetError()
fmt.Printf("publish: failed: %s\n", errMsg)

return fmt.Errorf("publish failed: %s", errMsg)

default:
fmt.Println("publish: unknown status")
}

// Print txid if the transaction is valid.
var tx wire.MsgTx
if err := tx.Deserialize(bytes.NewReader(resp.SweepTx)); err == nil {
fmt.Printf("sweep_txid: %s\n", tx.TxHash().String())
} else {
fmt.Printf("sweep_txid: could not decode tx: %v\n", err)
}

// Print the fee-rate.
fmt.Printf("fee_sats: %d\n", resp.FeeSats)

return nil
}
22 changes: 22 additions & 0 deletions docs/loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ The following flags are supported:
| `--channel="…"` | the comma-separated list of short channel IDs of the channels to loop out | string |
| `--help` (`-h`) | show help | bool | `false` |

### `out sweephtlc` subcommand

sweep an HTLC output using the preimage success path.

Usage:

```bash
$ loop [GLOBAL FLAGS] out sweephtlc [COMMAND FLAGS] [ARGUMENTS...]
```

The following flags are supported:

| Name | Description | Type | Default value |
|------------------|----------------------------------------------------------------|--------|:-------------:|
| `--outpoint="…"` | htlc outpoint to sweep (format: txid:vout) | string |
| `--htlcaddr="…"` | htlc address corresponding to the outpoint | string |
| `--feerate="…"` | fee rate to use in sat/vbyte | uint | `0` |
| `--destaddr="…"` | optional destination address; defaults to a new wallet address | string |
| `--preimage="…"` | optional preimage hex to override stored swap preimage | string |
| `--publish` | publish the sweep transaction immediately | bool | `false` |
| `--help` (`-h`) | show help | bool | `false` |

### `in` command

perform an on-chain to off-chain swap (loop in).
Expand Down
18 changes: 16 additions & 2 deletions instantout/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,16 @@ func (f *FSM) BuildHTLCAction(ctx context.Context,
return f.handleErrorAndUnlockReservations(ctx, err)
}

minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
if err != nil {
return f.handleErrorAndUnlockReservations(ctx, err)
}

// Now that our nonces are set, we can create and sign the htlc
// transaction.
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
htlcTx, err := f.InstantOut.createHtlcTransaction(
f.cfg.Network, minRelayFee,
)
if err != nil {
return f.handleErrorAndUnlockReservations(ctx, err)
}
Expand Down Expand Up @@ -382,6 +389,11 @@ func (f *FSM) PushPreimageAction(ctx context.Context,
return f.handleErrorAndUnlockReservations(ctx, err)
}

minRelayFee, err := f.cfg.Wallet.MinRelayFee(ctx)
if err != nil {
return f.handleErrorAndUnlockReservations(ctx, err)
}

pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
ctx,
&swapserverrpc.PushPreimageRequest{
Expand All @@ -400,7 +412,9 @@ func (f *FSM) PushPreimageAction(ctx context.Context,

// Now that we have the sweepless sweep signatures we can build and
// publish the sweepless sweep transaction.
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
sweepTx, err := f.InstantOut.createSweeplessSweepTx(
feeRate, minRelayFee,
)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
Expand Down
29 changes: 23 additions & 6 deletions instantout/instantout.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
Expand Down Expand Up @@ -145,8 +146,8 @@ func (i *InstantOut) getInputReservations() (InputReservations, error) {
}

// createHtlcTransaction creates the htlc transaction for the instant out.
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
*wire.MsgTx, error) {
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params,
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {

if network == nil {
return nil, errors.New("no network provided")
Expand All @@ -170,7 +171,16 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
// Estimate the fee
weight := htlcWeight(len(inputReservations))
fee := i.htlcFeeRate.FeeForWeight(weight)
if fee > i.Value/5 {

// We cap the fee at 20% of the deposit value.
_, clamped, err := utils.ClampSweepFee(
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
weight,
)
if err != nil {
return nil, err
}
if clamped {
return nil, errors.New("fee is higher than 20% of " +
"sweep value")
}
Expand All @@ -193,8 +203,8 @@ func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (

// createSweeplessSweepTx creates the sweepless sweep transaction for the
// instant out.
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
*wire.MsgTx, error) {
func (i *InstantOut) createSweeplessSweepTx(feerate,
minRelayFeeRate chainfee.SatPerKWeight) (*wire.MsgTx, error) {

inputReservations, err := i.getInputReservations()
if err != nil {
Expand All @@ -214,7 +224,14 @@ func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
// Estimate the fee
weight := sweeplessSweepWeight(len(inputReservations))
fee := feerate.FeeForWeight(weight)
if fee > i.Value/5 {
_, clamped, err := utils.ClampSweepFee(
fee, i.Value, utils.MaxFeeToAmountRatio, minRelayFeeRate,
weight,
)
if err != nil {
return nil, err
}
if clamped {
return nil, errors.New("fee is higher than 20% of " +
"sweep value")
}
Expand Down
11 changes: 11 additions & 0 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,17 @@ func (s *swapClientServer) StopDaemon(ctx context.Context,
return &looprpc.StopDaemonResponse{}, nil
}

// SweepHtlc spends a Loop HTLC output using the success path and a known
// preimage.
func (s *swapClientServer) SweepHtlc(ctx context.Context,
req *looprpc.SweepHtlcRequest) (*looprpc.SweepHtlcResponse, error) {

return sweepHtlc(
ctx, req, s.lnd.ChainParams, s.impl.Store,
s.lnd.ChainNotifier, s.lnd.WalletKit, s.lnd.Signer,
)
}

// GetLiquidityParams gets our current liquidity manager's parameters.
func (s *swapClientServer) GetLiquidityParams(_ context.Context,
_ *looprpc.GetLiquidityParamsRequest) (*looprpc.LiquidityParameters,
Expand Down
Loading
Loading