cmd: open channel command

This commit is contained in:
Slyghtning 2025-05-21 14:08:54 +02:00
parent 6cb54edf06
commit 79120cc4bc
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 377 additions and 46 deletions

366
cmd/loop/openchannel.go Normal file
View file

@ -0,0 +1,366 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"strconv"
"github.com/lightninglabs/loop/looprpc"
lndcommands "github.com/lightningnetwork/lnd/cmd/commands"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/urfave/cli/v3"
)
const (
defaultUtxoMinConf = 1
)
var (
channelTypeTweakless = "tweakless"
channelTypeAnchors = "anchors"
channelTypeSimpleTaproot = "taproot"
)
var openChannelCommand = &cli.Command{
Name: "openchannel",
Usage: "Open a channel to a an existing peer.",
Description: `
Attempt to open a new channel to an existing peer with the key
node-key.
The channel will be initialized with local-amt satoshis locally and
push-amt satoshis for the remote node. Note that the push-amt is
deducted from the specified local-amt which implies that the local-amt
must be greater than the push-amt. Also note that specifying push-amt
means you give that amount to the remote node as part of the channel
opening. Once the channel is open, a channelPoint (txid:vout) of the
funding output is returned.
If the remote peer supports the option upfront shutdown feature bit
(query listpeers to see their supported feature bits), an address to
enforce payout of funds on cooperative close can optionally be provided.
Note that if you set this value, you will not be able to cooperatively
close out to another address.
One can also specify a short string memo to record some useful
information about the channel using the --memo argument. This is stored
locally only, and is purely for reference. It has no bearing on the
channel's operation. Max allowed length is 500 characters.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "node_key",
Usage: "the identity public key of the target " +
"node/peer serialized in compressed format",
},
&cli.IntFlag{
Name: "local_amt",
Usage: "the number of satoshis the wallet should " +
"commit to the channel",
},
&cli.Uint64Flag{
Name: "base_fee_msat",
Usage: "the base fee in milli-satoshis that will " +
"be charged for each forwarded HTLC, " +
"regardless of payment size",
},
&cli.Uint64Flag{
Name: "fee_rate_ppm",
Usage: "the fee rate ppm (parts per million) that " +
"will be charged proportionally based on the " +
"value of each forwarded HTLC, the lowest " +
"possible rate is 0 with a granularity of " +
"0.000001 (millionths)",
},
&cli.IntFlag{
Name: "push_amt",
Usage: "the number of satoshis to give the remote " +
"side as part of the initial commitment " +
"state, this is equivalent to first opening " +
"a channel and sending the remote party " +
"funds, but done all in one step",
},
&cli.Int64Flag{
Name: "sat_per_byte",
Usage: "Deprecated, use sat_per_vbyte instead.",
Hidden: true,
},
&cli.Uint64Flag{
Name: "sat_per_vbyte",
Usage: "(optional) a manual fee expressed in " +
"sat/vbyte that should be used when crafting " +
"the transaction",
},
&cli.BoolFlag{
Name: "private",
Usage: "make the channel private, such that it won't " +
"be announced to the greater network, and " +
"nodes other than the two channel endpoints " +
"must be explicitly told about it to be able " +
"to route through it",
},
&cli.Int64Flag{
Name: "min_htlc_msat",
Usage: "(optional) the minimum value we will require " +
"for incoming HTLCs on the channel",
},
&cli.Uint64Flag{
Name: "remote_csv_delay",
Usage: "(optional) the number of blocks we will " +
"require our channel counterparty to wait " +
"before accessing its funds in case of " +
"unilateral close. If this is not set, we " +
"will scale the value according to the " +
"channel size",
},
&cli.Uint64Flag{
Name: "max_local_csv",
Usage: "(optional) the maximum number of blocks that " +
"we will allow the remote peer to require we " +
"wait before accessing our funds in the case " +
"of a unilateral close.",
},
&cli.StringFlag{
Name: "close_address",
Usage: "(optional) an address to enforce payout of " +
"our funds to on cooperative close. Note " +
"that if this value is set on channel open, " +
"you will *not* be able to cooperatively " +
"close to a different address.",
},
&cli.Uint64Flag{
Name: "remote_max_value_in_flight_msat",
Usage: "(optional) the maximum value in msat that " +
"can be pending within the channel at any " +
"given time",
},
&cli.StringFlag{
Name: "channel_type",
Usage: fmt.Sprintf("(optional) the type of channel to "+
"propose to the remote peer (%q, %q, %q)",
channelTypeTweakless, channelTypeAnchors,
channelTypeSimpleTaproot),
},
&cli.BoolFlag{
Name: "zero_conf",
Usage: "(optional) whether a zero-conf channel open " +
"should be attempted.",
},
&cli.BoolFlag{
Name: "scid_alias",
Usage: "(optional) whether a scid-alias channel type" +
" should be negotiated.",
},
&cli.Uint64Flag{
Name: "remote_reserve_sats",
Usage: "(optional) the minimum number of satoshis we " +
"require the remote node to keep as a direct " +
"payment. If not specified, a default of 1% " +
"of the channel capacity will be used.",
},
&cli.StringFlag{
Name: "memo",
Usage: `(optional) a note-to-self containing some useful
information about the channel. This is stored
locally only, and is purely for reference. It
has no bearing on the channel's operation. Max
allowed length is 500 characters`,
},
&cli.BoolFlag{
Name: "fundmax",
Usage: "if set, the wallet will attempt to commit " +
"the maximum possible local amount to the " +
"channel. This must not be set at the same " +
"time as local_amt",
},
&cli.StringSliceFlag{
Name: "utxo",
Usage: "a utxo specified as outpoint(tx:idx) which " +
"will be used to fund a channel. This flag " +
"can be repeatedly used to fund a channel " +
"with a selection of utxos. The selected " +
"funds can either be entirely spent by " +
"specifying the fundmax flag or partially by " +
"selecting a fraction of the sum of the " +
"outpoints in local_amt",
},
},
Action: openChannel,
}
func openChannel(ctx context.Context, cmd *cli.Command) error {
var (
args = cmd.Args()
remaining []string
err error
)
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()
// Show command help if no arguments provided
if cmd.NArg() == 0 && cmd.NumFlags() == 0 {
_ = cli.ShowCommandHelp(ctx, cmd, "openchannel")
return nil
}
// Check that only the field sat_per_vbyte or the deprecated field
// sat_per_byte is used.
feeRateFlag, err := checkNotBothSet(
cmd, "sat_per_vbyte", "sat_per_byte",
)
if err != nil {
return err
}
var feeRateSatPerVbyte uint64
switch feeRateFlag {
case "sat_per_vbyte":
feeRateSatPerVbyte = cmd.Uint64(feeRateFlag)
case "sat_per_byte":
feeRateSatPerByte := cmd.Int64(feeRateFlag)
if feeRateSatPerByte < 0 {
return fmt.Errorf("%v must be non-negative", feeRateFlag)
}
feeRateSatPerVbyte = uint64(feeRateSatPerByte)
}
minConfs := defaultUtxoMinConf
req := &lnrpc.OpenChannelRequest{
SatPerVbyte: feeRateSatPerVbyte,
FundMax: cmd.Bool("fundmax"),
MinHtlcMsat: cmd.Int64("min_htlc_msat"),
RemoteCsvDelay: uint32(cmd.Uint64("remote_csv_delay")),
MinConfs: int32(minConfs),
SpendUnconfirmed: minConfs == 0,
CloseAddress: cmd.String("close_address"),
RemoteMaxValueInFlightMsat: cmd.Uint64("remote_max_value_in_flight_msat"),
MaxLocalCsv: uint32(cmd.Uint64("max_local_csv")),
ZeroConf: cmd.Bool("zero_conf"),
ScidAlias: cmd.Bool("scid_alias"),
RemoteChanReserveSat: cmd.Uint64("remote_reserve_sats"),
Memo: cmd.String("memo"),
}
switch {
case cmd.IsSet("node_key"):
nodePubHex, err := hex.DecodeString(cmd.String("node_key"))
if err != nil {
return fmt.Errorf("unable to decode node "+
"public key: %w", err)
}
req.NodePubkey = nodePubHex
case args.Present():
nodePubHex, err := hex.DecodeString(args.First())
if err != nil {
return fmt.Errorf("unable to decode node "+
"public key: %w", err)
}
remaining = args.Tail()
req.NodePubkey = nodePubHex
default:
return fmt.Errorf("node id argument missing")
}
if cmd.IsSet("utxo") {
utxos := cmd.StringSlice("utxo")
outpoints, err := lndcommands.UtxosToOutpoints(utxos)
if err != nil {
return fmt.Errorf("unable to decode utxos: %w", err)
}
req.Outpoints = outpoints
}
// The fundmax flag is NOT allowed to be combined with local_amt above.
// It is allowed to be combined with push_amt, but only if explicitly
// set.
if cmd.Bool("fundmax") && cmd.IsSet("local_amt") {
return fmt.Errorf("local_amt and fundmax are mutually " +
"exclusive")
}
switch {
case cmd.IsSet("local_amt"):
req.LocalFundingAmount = int64(cmd.Int("local_amt"))
case !cmd.Bool("fundmax"):
return fmt.Errorf("either local_amt or fundmax must be " +
"specified")
}
if cmd.IsSet("push_amt") {
req.PushSat = int64(cmd.Int("push_amt"))
} else if len(remaining) > 0 {
req.PushSat, err = strconv.ParseInt(remaining[0], 10, 64)
if err != nil {
return fmt.Errorf("unable to decode push amt: %w", err)
}
}
if cmd.IsSet("base_fee_msat") {
req.BaseFee = cmd.Uint64("base_fee_msat")
req.UseBaseFee = true
}
if cmd.IsSet("fee_rate_ppm") {
req.FeeRate = cmd.Uint64("fee_rate_ppm")
req.UseFeeRate = true
}
req.Private = cmd.Bool("private")
// Parse the channel type and map it to its RPC representation.
channelType := cmd.String("channel_type")
switch channelType {
case "":
break
case channelTypeTweakless:
req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
case channelTypeAnchors:
req.CommitmentType = lnrpc.CommitmentType_ANCHORS
case channelTypeSimpleTaproot:
req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
default:
return fmt.Errorf("unsupported channel type %v", channelType)
}
wrappedReq := &looprpc.StaticOpenChannelRequest{
OpenChannelRequest: req,
}
resp, err := client.StaticOpenChannel(ctx, wrappedReq)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
// or flag b can be set, but not both. It returns the name of the flag or an
// error.
func checkNotBothSet(cmd *cli.Command, a, b string) (string, error) {
if cmd.IsSet(a) && cmd.IsSet(b) {
return "", fmt.Errorf(
"either %s or %s should be set, but not both", a, b,
)
}
if cmd.IsSet(a) {
return a, nil
}
return b, nil
}

View file

@ -2,18 +2,15 @@ package main
import (
"context"
"encoding/hex"
"errors"
"fmt"
"strconv"
"strings"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc"
lndcommands "github.com/lightningnetwork/lnd/cmd/commands"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli/v3"
@ -36,6 +33,7 @@ var staticAddressCommands = &cli.Command{
withdrawalCommand,
summaryCommand,
staticAddressLoopInCommand,
openChannelCommand,
},
}
@ -194,7 +192,7 @@ func withdraw(ctx context.Context, cmd *cli.Command) error {
case isAllSelected:
case isUtxoSelected:
utxos := cmd.StringSlice("utxo")
outpoints, err = utxosToOutpoints(utxos)
outpoints, err = lndcommands.UtxosToOutpoints(utxos)
if err != nil {
return err
}
@ -239,6 +237,7 @@ var listDepositsCommand = &cli.Command{
"of the following: \n" +
"deposited\nwithdrawing\nwithdrawn\n" +
"looping_in\nlooped_in\n" +
"opening_channel\nchannel_published\n" +
"publish_expired_deposit\n" +
"sweep_htlc_timeout\nhtlc_timeout_swept\n" +
"wait_for_expiry_sweep\nexpired\nfailed\n.",
@ -278,6 +277,12 @@ func listDeposits(ctx context.Context, cmd *cli.Command) error {
case "looped_in":
filterState = looprpc.DepositState_LOOPED_IN
case "opening_channel":
filterState = looprpc.DepositState_OPENING_CHANNEL
case "channel_published":
filterState = looprpc.DepositState_CHANNEL_PUBLISHED
case "publish_expired_deposit":
filterState = looprpc.DepositState_PUBLISH_EXPIRED
@ -379,7 +384,7 @@ var summaryCommand = &cli.Command{
Usage: "Display a summary of static address related information.",
Description: `
Displays various static address related information about deposits,
withdrawals and swaps.
withdrawals, swaps and channel openings.
`,
Action: summary,
}
@ -407,46 +412,6 @@ func summary(ctx context.Context, cmd *cli.Command) error {
return nil
}
func utxosToOutpoints(utxos []string) ([]*lnrpc.OutPoint, error) {
outpoints := make([]*lnrpc.OutPoint, 0, len(utxos))
if len(utxos) == 0 {
return nil, fmt.Errorf("no utxos specified")
}
for _, utxo := range utxos {
outpoint, err := NewProtoOutPoint(utxo)
if err != nil {
return nil, err
}
outpoints = append(outpoints, outpoint)
}
return outpoints, nil
}
// NewProtoOutPoint parses an OutPoint into its corresponding lnrpc.OutPoint
// type.
func NewProtoOutPoint(op string) (*lnrpc.OutPoint, error) {
parts := strings.Split(op, ":")
if len(parts) != 2 {
return nil, errors.New("outpoint should be of the form " +
"txid:index")
}
txid := parts[0]
if hex.DecodedLen(len(txid)) != chainhash.HashSize {
return nil, fmt.Errorf("invalid hex-encoded txid %v", txid)
}
outputIndex, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid output index: %v", err)
}
return &lnrpc.OutPoint{
TxidStr: txid,
OutputIndex: uint32(outputIndex),
}, nil
}
var staticAddressLoopInCommand = &cli.Command{
Name: "in",
Usage: "Loop in funds from static address deposits.",