Skip to content

Commit 8f721fd

Browse files
committed
cmd: open channel command
1 parent 9110959 commit 8f721fd

File tree

2 files changed

+364
-47
lines changed

2 files changed

+364
-47
lines changed

cmd/loop/openchannel.go

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"strconv"
8+
9+
"github.com/lightninglabs/loop/looprpc"
10+
lndcommands "github.com/lightningnetwork/lnd/cmd/commands"
11+
"github.com/lightningnetwork/lnd/lnrpc"
12+
"github.com/urfave/cli/v3"
13+
)
14+
15+
const (
16+
defaultUtxoMinConf = 1
17+
)
18+
19+
var (
20+
channelTypeTweakless = "tweakless"
21+
channelTypeAnchors = "anchors"
22+
channelTypeSimpleTaproot = "taproot"
23+
)
24+
25+
var openChannelCommand = &cli.Command{
26+
Name: "openchannel",
27+
Usage: "Open a channel to a an existing peer.",
28+
Description: `
29+
Attempt to open a new channel to an existing peer with the key
30+
node-key.
31+
32+
The channel will be initialized with local-amt satoshis locally and
33+
push-amt satoshis for the remote node. Note that the push-amt is
34+
deducted from the specified local-amt which implies that the local-amt
35+
must be greater than the push-amt. Also note that specifying push-amt
36+
means you give that amount to the remote node as part of the channel
37+
opening. Once the channel is open, a channelPoint (txid:vout) of the
38+
funding output is returned.
39+
40+
If the remote peer supports the option upfront shutdown feature bit
41+
(query listpeers to see their supported feature bits), an address to
42+
enforce payout of funds on cooperative close can optionally be provided.
43+
Note that if you set this value, you will not be able to cooperatively
44+
close out to another address.
45+
46+
One can also specify a short string memo to record some useful
47+
information about the channel using the --memo argument. This is stored
48+
locally only, and is purely for reference. It has no bearing on the
49+
channel's operation. Max allowed length is 500 characters.`,
50+
Flags: []cli.Flag{
51+
&cli.StringFlag{
52+
Name: "node_key",
53+
Usage: "the identity public key of the target " +
54+
"node/peer serialized in compressed format",
55+
},
56+
&cli.IntFlag{
57+
Name: "local_amt",
58+
Usage: "the number of satoshis the wallet should " +
59+
"commit to the channel",
60+
},
61+
&cli.Uint64Flag{
62+
Name: "base_fee_msat",
63+
Usage: "the base fee in milli-satoshis that will " +
64+
"be charged for each forwarded HTLC, " +
65+
"regardless of payment size",
66+
},
67+
&cli.Uint64Flag{
68+
Name: "fee_rate_ppm",
69+
Usage: "the fee rate ppm (parts per million) that " +
70+
"will be charged proportionally based on the " +
71+
"value of each forwarded HTLC, the lowest " +
72+
"possible rate is 0 with a granularity of " +
73+
"0.000001 (millionths)",
74+
},
75+
&cli.IntFlag{
76+
Name: "push_amt",
77+
Usage: "the number of satoshis to give the remote " +
78+
"side as part of the initial commitment " +
79+
"state, this is equivalent to first opening " +
80+
"a channel and sending the remote party " +
81+
"funds, but done all in one step",
82+
},
83+
&cli.Int64Flag{
84+
Name: "sat_per_byte",
85+
Usage: "Deprecated, use sat_per_vbyte instead.",
86+
Hidden: true,
87+
},
88+
&cli.Int64Flag{
89+
Name: "sat_per_vbyte",
90+
Usage: "(optional) a manual fee expressed in " +
91+
"sat/vbyte that should be used when crafting " +
92+
"the transaction",
93+
},
94+
&cli.BoolFlag{
95+
Name: "private",
96+
Usage: "make the channel private, such that it won't " +
97+
"be announced to the greater network, and " +
98+
"nodes other than the two channel endpoints " +
99+
"must be explicitly told about it to be able " +
100+
"to route through it",
101+
},
102+
&cli.Int64Flag{
103+
Name: "min_htlc_msat",
104+
Usage: "(optional) the minimum value we will require " +
105+
"for incoming HTLCs on the channel",
106+
},
107+
&cli.Uint64Flag{
108+
Name: "remote_csv_delay",
109+
Usage: "(optional) the number of blocks we will " +
110+
"require our channel counterparty to wait " +
111+
"before accessing its funds in case of " +
112+
"unilateral close. If this is not set, we " +
113+
"will scale the value according to the " +
114+
"channel size",
115+
},
116+
&cli.Uint64Flag{
117+
Name: "max_local_csv",
118+
Usage: "(optional) the maximum number of blocks that " +
119+
"we will allow the remote peer to require we " +
120+
"wait before accessing our funds in the case " +
121+
"of a unilateral close.",
122+
},
123+
&cli.StringFlag{
124+
Name: "close_address",
125+
Usage: "(optional) an address to enforce payout of " +
126+
"our funds to on cooperative close. Note " +
127+
"that if this value is set on channel open, " +
128+
"you will *not* be able to cooperatively " +
129+
"close to a different address.",
130+
},
131+
&cli.Uint64Flag{
132+
Name: "remote_max_value_in_flight_msat",
133+
Usage: "(optional) the maximum value in msat that " +
134+
"can be pending within the channel at any " +
135+
"given time",
136+
},
137+
&cli.StringFlag{
138+
Name: "channel_type",
139+
Usage: fmt.Sprintf("(optional) the type of channel to "+
140+
"propose to the remote peer (%q, %q, %q)",
141+
channelTypeTweakless, channelTypeAnchors,
142+
channelTypeSimpleTaproot),
143+
},
144+
&cli.BoolFlag{
145+
Name: "zero_conf",
146+
Usage: "(optional) whether a zero-conf channel open " +
147+
"should be attempted.",
148+
},
149+
&cli.BoolFlag{
150+
Name: "scid_alias",
151+
Usage: "(optional) whether a scid-alias channel type" +
152+
" should be negotiated.",
153+
},
154+
&cli.Uint64Flag{
155+
Name: "remote_reserve_sats",
156+
Usage: "(optional) the minimum number of satoshis we " +
157+
"require the remote node to keep as a direct " +
158+
"payment. If not specified, a default of 1% " +
159+
"of the channel capacity will be used.",
160+
},
161+
&cli.StringFlag{
162+
Name: "memo",
163+
Usage: `(optional) a note-to-self containing some useful
164+
information about the channel. This is stored
165+
locally only, and is purely for reference. It
166+
has no bearing on the channel's operation. Max
167+
allowed length is 500 characters`,
168+
},
169+
&cli.BoolFlag{
170+
Name: "fundmax",
171+
Usage: "if set, the wallet will attempt to commit " +
172+
"the maximum possible local amount to the " +
173+
"channel. This must not be set at the same " +
174+
"time as local_amt",
175+
},
176+
&cli.StringSliceFlag{
177+
Name: "utxo",
178+
Usage: "a utxo specified as outpoint(tx:idx) which " +
179+
"will be used to fund a channel. This flag " +
180+
"can be repeatedly used to fund a channel " +
181+
"with a selection of utxos. The selected " +
182+
"funds can either be entirely spent by " +
183+
"specifying the fundmax flag or partially by " +
184+
"selecting a fraction of the sum of the " +
185+
"outpoints in local_amt",
186+
},
187+
},
188+
Action: openChannel,
189+
}
190+
191+
func openChannel(ctx context.Context, cmd *cli.Command) error {
192+
var (
193+
args = cmd.Args()
194+
remaining []string
195+
ctxb = context.Background()
196+
err error
197+
)
198+
199+
client, cleanup, err := getClient(ctx, cmd)
200+
if err != nil {
201+
return err
202+
}
203+
defer cleanup()
204+
205+
// Show command help if no arguments provided
206+
if cmd.NArg() == 0 && cmd.NumFlags() == 0 {
207+
_ = cli.ShowCommandHelp(ctx, cmd, "openchannel")
208+
return nil
209+
}
210+
211+
// Check that only the field sat_per_vbyte or the deprecated field
212+
// sat_per_byte is used.
213+
feeRateFlag, err := checkNotBothSet(
214+
cmd, "sat_per_vbyte", "sat_per_byte",
215+
)
216+
if err != nil {
217+
return err
218+
}
219+
220+
minConfs := defaultUtxoMinConf
221+
req := &lnrpc.OpenChannelRequest{
222+
SatPerVbyte: cmd.Uint64(feeRateFlag),
223+
FundMax: cmd.Bool("fundmax"),
224+
MinHtlcMsat: cmd.Int64("min_htlc_msat"),
225+
RemoteCsvDelay: uint32(cmd.Uint64("remote_csv_delay")),
226+
MinConfs: int32(minConfs),
227+
SpendUnconfirmed: minConfs == 0,
228+
CloseAddress: cmd.String("close_address"),
229+
RemoteMaxValueInFlightMsat: cmd.Uint64("remote_max_value_in_flight_msat"),
230+
MaxLocalCsv: uint32(cmd.Uint64("max_local_csv")),
231+
ZeroConf: cmd.Bool("zero_conf"),
232+
ScidAlias: cmd.Bool("scid_alias"),
233+
RemoteChanReserveSat: cmd.Uint64("remote_reserve_sats"),
234+
Memo: cmd.String("memo"),
235+
}
236+
237+
switch {
238+
case cmd.IsSet("node_key"):
239+
nodePubHex, err := hex.DecodeString(cmd.String("node_key"))
240+
if err != nil {
241+
return fmt.Errorf("unable to decode node public key: "+
242+
"%v", err)
243+
}
244+
req.NodePubkey = nodePubHex
245+
246+
case args.Present():
247+
nodePubHex, err := hex.DecodeString(args.First())
248+
if err != nil {
249+
return fmt.Errorf("unable to decode node public key: "+
250+
"%v", err)
251+
}
252+
remaining = args.Tail()
253+
req.NodePubkey = nodePubHex
254+
255+
default:
256+
return fmt.Errorf("node id argument missing")
257+
}
258+
259+
if cmd.IsSet("utxo") {
260+
utxos := cmd.StringSlice("utxo")
261+
262+
outpoints, err := lndcommands.UtxosToOutpoints(utxos)
263+
if err != nil {
264+
return fmt.Errorf("unable to decode utxos: %w", err)
265+
}
266+
267+
req.Outpoints = outpoints
268+
}
269+
270+
// The fundmax flag is NOT allowed to be combined with local_amt above.
271+
// It is allowed to be combined with push_amt, but only if explicitly
272+
// set.
273+
if cmd.Bool("fundmax") && req.LocalFundingAmount != 0 {
274+
return fmt.Errorf("local amount cannot be set if attempting " +
275+
"to commit the maximum amount out of the wallet")
276+
}
277+
278+
switch {
279+
case cmd.IsSet("local_amt"):
280+
req.LocalFundingAmount = int64(cmd.Int("local_amt"))
281+
282+
case !cmd.Bool("fundmax"):
283+
return fmt.Errorf("either local_amt or fundmax must be " +
284+
"specified")
285+
}
286+
287+
if cmd.IsSet("push_amt") {
288+
req.PushSat = int64(cmd.Int("push_amt"))
289+
} else if len(remaining) > 0 {
290+
req.PushSat, err = strconv.ParseInt(remaining[0], 10, 64)
291+
if err != nil {
292+
return fmt.Errorf("unable to decode push amt: %w", err)
293+
}
294+
}
295+
296+
if cmd.IsSet("base_fee_msat") {
297+
req.BaseFee = cmd.Uint64("base_fee_msat")
298+
req.UseBaseFee = true
299+
}
300+
301+
if cmd.IsSet("fee_rate_ppm") {
302+
req.FeeRate = cmd.Uint64("fee_rate_ppm")
303+
req.UseFeeRate = true
304+
}
305+
306+
req.Private = cmd.Bool("private")
307+
308+
// Parse the channel type and map it to its RPC representation.
309+
channelType := cmd.String("channel_type")
310+
switch channelType {
311+
case "":
312+
break
313+
case channelTypeTweakless:
314+
req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY
315+
316+
case channelTypeAnchors:
317+
req.CommitmentType = lnrpc.CommitmentType_ANCHORS
318+
319+
case channelTypeSimpleTaproot:
320+
req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
321+
default:
322+
return fmt.Errorf("unsupported channel type %v", channelType)
323+
}
324+
325+
wrappedReq := &looprpc.StaticOpenChannelRequest{
326+
OpenChannelRequest: req,
327+
}
328+
329+
resp, err := client.StaticOpenChannel(ctxb, wrappedReq)
330+
331+
printRespJSON(resp)
332+
333+
return err
334+
}
335+
336+
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
337+
// or flag b can be set, but not both. It returns the name of the flag or an
338+
// error.
339+
func checkNotBothSet(cmd *cli.Command, a, b string) (string, error) {
340+
if cmd.IsSet(a) && cmd.IsSet(b) {
341+
return "", fmt.Errorf(
342+
"either %s or %s should be set, but not both", a, b,
343+
)
344+
}
345+
346+
if cmd.IsSet(a) {
347+
return a, nil
348+
}
349+
350+
return b, nil
351+
}

0 commit comments

Comments
 (0)