|  | 
|  | 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 | +} | 
0 commit comments