Skip to content

Commit ddfeb3e

Browse files
committed
all open channel stuff
1 parent ed5fd80 commit ddfeb3e

File tree

22 files changed

+5831
-1575
lines changed

22 files changed

+5831
-1575
lines changed

cmd/loop/openchannel.go

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

0 commit comments

Comments
 (0)