Skip to content

Commit 4184f2d

Browse files
committed
cmd: open channel command
1 parent 1d067d8 commit 4184f2d

File tree

2 files changed

+370
-1
lines changed

2 files changed

+370
-1
lines changed

cmd/loop/openchannel.go

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

cmd/loop/staticaddr.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var staticAddressCommands = cli.Command{
3434
withdrawalCommand,
3535
summaryCommand,
3636
staticAddressLoopInCommand,
37+
openChannelCommand,
3738
},
3839
}
3940

@@ -239,6 +240,7 @@ var listDepositsCommand = cli.Command{
239240
"of the following: \n" +
240241
"deposited\nwithdrawing\nwithdrawn\n" +
241242
"looping_in\nlooped_in\n" +
243+
"opening_channel\nchannel_published\n" +
242244
"publish_expired_deposit\n" +
243245
"sweep_htlc_timeout\nhtlc_timeout_swept\n" +
244246
"wait_for_expiry_sweep\nexpired\nfailed\n.",
@@ -279,6 +281,12 @@ func listDeposits(ctx *cli.Context) error {
279281
case "looped_in":
280282
filterState = looprpc.DepositState_LOOPED_IN
281283

284+
case "opening_channel":
285+
filterState = looprpc.DepositState_OPENING_CHANNEL
286+
287+
case "channel_published":
288+
filterState = looprpc.DepositState_CHANNEL_PUBLISHED
289+
282290
case "publish_expired_deposit":
283291
filterState = looprpc.DepositState_PUBLISH_EXPIRED
284292

@@ -350,7 +358,7 @@ var summaryCommand = cli.Command{
350358
Usage: "Display a summary of static address related information.",
351359
Description: `
352360
Displays various static address related information about deposits,
353-
withdrawals and swaps.
361+
withdrawals, swaps and channel openings.
354362
`,
355363
Action: summary,
356364
}

0 commit comments

Comments
 (0)