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