|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/hex" |
| 6 | + "fmt" |
| 7 | + "github.com/lightninglabs/loop/looprpc" |
| 8 | + "strconv" |
| 9 | + |
| 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.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 *cli.Context) error { |
| 192 | + args := ctx.Args() |
| 193 | + ctxb := context.Background() |
| 194 | + var err error |
| 195 | + |
| 196 | + client, cleanup, err := getClient(ctx) |
| 197 | + if err != nil { |
| 198 | + return err |
| 199 | + } |
| 200 | + defer cleanup() |
| 201 | + |
| 202 | + // Show command help if no arguments provided |
| 203 | + if ctx.NArg() == 0 && ctx.NumFlags() == 0 { |
| 204 | + _ = cli.ShowCommandHelp(ctx, "openchannel") |
| 205 | + return nil |
| 206 | + } |
| 207 | + |
| 208 | + // Check that only the field sat_per_vbyte or the deprecated field |
| 209 | + // sat_per_byte is used. |
| 210 | + feeRateFlag, err := checkNotBothSet( |
| 211 | + ctx, "sat_per_vbyte", "sat_per_byte", |
| 212 | + ) |
| 213 | + if err != nil { |
| 214 | + return err |
| 215 | + } |
| 216 | + |
| 217 | + minConfs := defaultUtxoMinConf |
| 218 | + req := &looprpc.OpenChannelRequest{ |
| 219 | + SatPerVbyte: ctx.Uint64(feeRateFlag), |
| 220 | + MinHtlcMsat: ctx.Int64("min_htlc_msat"), |
| 221 | + RemoteCsvDelay: uint32(ctx.Uint64("remote_csv_delay")), |
| 222 | + MinConfs: int32(minConfs), |
| 223 | + SpendUnconfirmed: minConfs == 0, |
| 224 | + CloseAddress: ctx.String("close_address"), |
| 225 | + RemoteMaxValueInFlightMsat: ctx.Uint64("remote_max_value_in_flight_msat"), |
| 226 | + MaxLocalCsv: uint32(ctx.Uint64("max_local_csv")), |
| 227 | + ZeroConf: ctx.Bool("zero_conf"), |
| 228 | + ScidAlias: ctx.Bool("scid_alias"), |
| 229 | + RemoteChanReserveSat: ctx.Uint64("remote_reserve_sats"), |
| 230 | + Memo: ctx.String("memo"), |
| 231 | + } |
| 232 | + |
| 233 | + switch { |
| 234 | + case ctx.IsSet("node_key"): |
| 235 | + nodePubHex, err := hex.DecodeString(ctx.String("node_key")) |
| 236 | + if err != nil { |
| 237 | + return fmt.Errorf("unable to decode node public key: "+ |
| 238 | + "%v", err) |
| 239 | + } |
| 240 | + req.NodePubkey = nodePubHex |
| 241 | + |
| 242 | + case args.Present(): |
| 243 | + nodePubHex, err := hex.DecodeString(args.First()) |
| 244 | + if err != nil { |
| 245 | + return fmt.Errorf("unable to decode node public key: "+ |
| 246 | + "%v", err) |
| 247 | + } |
| 248 | + args = args.Tail() |
| 249 | + req.NodePubkey = nodePubHex |
| 250 | + |
| 251 | + default: |
| 252 | + return fmt.Errorf("node id argument missing") |
| 253 | + } |
| 254 | + |
| 255 | + if ctx.IsSet("utxo") { |
| 256 | + utxos := ctx.StringSlice("utxo") |
| 257 | + |
| 258 | + outpoints, err := UtxosToOutpoints(utxos) |
| 259 | + if err != nil { |
| 260 | + return fmt.Errorf("unable to decode utxos: %w", err) |
| 261 | + } |
| 262 | + |
| 263 | + req.Outpoints = outpoints |
| 264 | + } else { |
| 265 | + return fmt.Errorf("must specify at least one static address " + |
| 266 | + "utxo") |
| 267 | + } |
| 268 | + |
| 269 | + if !ctx.IsSet("local_amt") { |
| 270 | + return fmt.Errorf("local_amt must be set") |
| 271 | + } |
| 272 | + |
| 273 | + // todo(hieblmi): check if the selected utxos cover the local_amt |
| 274 | + // and fees/dust requirement. |
| 275 | + req.LocalFundingAmount = int64(ctx.Int("local_amt")) |
| 276 | + |
| 277 | + if ctx.IsSet("push_amt") { |
| 278 | + req.PushSat = int64(ctx.Int("push_amt")) |
| 279 | + } else if args.Present() { |
| 280 | + req.PushSat, err = strconv.ParseInt(args.First(), 10, 64) |
| 281 | + if err != nil { |
| 282 | + return fmt.Errorf("unable to decode push amt: %w", err) |
| 283 | + } |
| 284 | + } |
| 285 | + |
| 286 | + if ctx.IsSet("base_fee_msat") { |
| 287 | + req.BaseFee = ctx.Uint64("base_fee_msat") |
| 288 | + req.UseBaseFee = true |
| 289 | + } |
| 290 | + |
| 291 | + if ctx.IsSet("fee_rate_ppm") { |
| 292 | + req.FeeRate = ctx.Uint64("fee_rate_ppm") |
| 293 | + req.UseFeeRate = true |
| 294 | + } |
| 295 | + |
| 296 | + req.Private = ctx.Bool("private") |
| 297 | + |
| 298 | + // Parse the channel type and map it to its RPC representation. |
| 299 | + channelType := ctx.String("channel_type") |
| 300 | + switch channelType { |
| 301 | + case "": |
| 302 | + break |
| 303 | + case channelTypeTweakless: |
| 304 | + req.CommitmentType = looprpc.CommitmentType_STATIC_REMOTE_KEY |
| 305 | + |
| 306 | + case channelTypeAnchors: |
| 307 | + req.CommitmentType = looprpc.CommitmentType_ANCHORS |
| 308 | + |
| 309 | + case channelTypeSimpleTaproot: |
| 310 | + req.CommitmentType = looprpc.CommitmentType_SIMPLE_TAPROOT |
| 311 | + default: |
| 312 | + return fmt.Errorf("unsupported channel type %v", channelType) |
| 313 | + } |
| 314 | + |
| 315 | + _, err = client.StaticOpenChannel(ctxb, req) |
| 316 | + |
| 317 | + return err |
| 318 | +} |
| 319 | + |
| 320 | +// UtxosToOutpoints converts a slice of UTXO strings into a slice of OutPoint |
| 321 | +// protobuf objects. It returns an error if no UTXOs are specified or if any |
| 322 | +// UTXO string cannot be parsed into an OutPoint. |
| 323 | +func UtxosToOutpoints(utxos []string) ([]*looprpc.OutPoint, error) { |
| 324 | + var outpoints []*looprpc.OutPoint |
| 325 | + if len(utxos) == 0 { |
| 326 | + return nil, fmt.Errorf("no utxos specified") |
| 327 | + } |
| 328 | + for _, utxo := range utxos { |
| 329 | + outpoint, err := NewProtoOutPoint(utxo) |
| 330 | + if err != nil { |
| 331 | + return nil, err |
| 332 | + } |
| 333 | + outpoints = append(outpoints, outpoint) |
| 334 | + } |
| 335 | + |
| 336 | + return outpoints, nil |
| 337 | +} |
| 338 | + |
| 339 | +// checkNotBothSet accepts two flag names, a and b, and checks that only flag a |
| 340 | +// or flag b can be set, but not both. It returns the name of the flag or an |
| 341 | +// error. |
| 342 | +func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) { |
| 343 | + if ctx.IsSet(a) && ctx.IsSet(b) { |
| 344 | + return "", fmt.Errorf( |
| 345 | + "either %s or %s should be set, but not both", a, b, |
| 346 | + ) |
| 347 | + } |
| 348 | + |
| 349 | + if ctx.IsSet(a) { |
| 350 | + return a, nil |
| 351 | + } |
| 352 | + |
| 353 | + return b, nil |
| 354 | +} |
0 commit comments