lnrpc: ability to spend unconfirmed coins

This commit is contained in:
Tom Kirkpatrick 2020-09-27 16:48:15 +01:00
parent fd962d322a
commit 73a5f325b6
No known key found for this signature in database
GPG Key ID: 72203A8EC5967EA8
15 changed files with 1030 additions and 764 deletions

View File

@ -21,8 +21,7 @@ import (
) )
const ( const (
defaultUtxoMinConf = 1 userMsgFund = `PSBT funding initiated with peer %x.
userMsgFund = `PSBT funding initiated with peer %x.
Please create a PSBT that sends %v (%d satoshi) to the funding address %s. Please create a PSBT that sends %v (%d satoshi) to the funding address %s.
Note: The whole process should be completed within 10 minutes, otherwise there Note: The whole process should be completed within 10 minutes, otherwise there
@ -43,7 +42,7 @@ Paste the funded PSBT here to continue the funding flow.
Base64 encoded PSBT: ` Base64 encoded PSBT: `
userMsgSign = ` userMsgSign = `
PSBT verified by lnd, please continue the funding flow by signing the PSBT by PSBT verified by lnd, please continue the funding flow by signing the PSBT by
all required parties/devices. Once the transaction is fully signed, paste it all required parties/devices. Once the transaction is fully signed, paste it
again here either in base64 PSBT or hex encoded raw wire TX format. again here either in base64 PSBT or hex encoded raw wire TX format.
@ -68,7 +67,7 @@ var openChannelCommand = cli.Command{
amount to the remote node as part of the channel opening. Once the channel is open, amount to the remote node as part of the channel opening. Once the channel is open,
a channelPoint (txid:vout) of the funding output is returned. a channelPoint (txid:vout) of the funding output is returned.
If the remote peer supports the option upfront shutdown feature bit (query If the remote peer supports the option upfront shutdown feature bit (query
listpeers to see their supported feature bits), an address to enforce listpeers to see their supported feature bits), an address to enforce
payout of funds on cooperative close can optionally be provided. Note that payout of funds on cooperative close can optionally be provided. Note that
if you set this value, you will not be able to cooperatively close out to if you set this value, you will not be able to cooperatively close out to

View File

@ -35,6 +35,10 @@ import (
const defaultRecoveryWindow int32 = 2500 const defaultRecoveryWindow int32 = 2500
const (
defaultUtxoMinConf = 1
)
func printJSON(resp interface{}) { func printJSON(resp interface{}) {
b, err := json.Marshal(resp) b, err := json.Marshal(resp)
if err != nil { if err != nil {
@ -238,6 +242,13 @@ var sendCoinsCommand = cli.Command{
"sat/byte that should be used when crafting " + "sat/byte that should be used when crafting " +
"the transaction", "the transaction",
}, },
cli.Uint64Flag{
Name: "min_confs",
Usage: "(optional) the minimum number of confirmations " +
"each one of your outputs used for the transaction " +
"must satisfy",
Value: defaultUtxoMinConf,
},
txLabelFlag, txLabelFlag,
}, },
Action: actionDecorator(sendCoins), Action: actionDecorator(sendCoins),
@ -292,13 +303,16 @@ func sendCoins(ctx *cli.Context) error {
client, cleanUp := getClient(ctx) client, cleanUp := getClient(ctx)
defer cleanUp() defer cleanUp()
minConfs := int32(ctx.Uint64("min_confs"))
req := &lnrpc.SendCoinsRequest{ req := &lnrpc.SendCoinsRequest{
Addr: addr, Addr: addr,
Amount: amt, Amount: amt,
TargetConf: int32(ctx.Int64("conf_target")), TargetConf: int32(ctx.Int64("conf_target")),
SatPerByte: ctx.Int64("sat_per_byte"), SatPerByte: ctx.Int64("sat_per_byte"),
SendAll: ctx.Bool("sweepall"), SendAll: ctx.Bool("sweepall"),
Label: ctx.String(txLabelFlag.Name), Label: ctx.String(txLabelFlag.Name),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
} }
txid, err := client.SendCoins(ctxb, req) txid, err := client.SendCoins(ctxb, req)
if err != nil { if err != nil {
@ -454,6 +468,13 @@ var sendManyCommand = cli.Command{
Usage: "(optional) a manual fee expressed in sat/byte that should be " + Usage: "(optional) a manual fee expressed in sat/byte that should be " +
"used when crafting the transaction", "used when crafting the transaction",
}, },
cli.Uint64Flag{
Name: "min_confs",
Usage: "(optional) the minimum number of confirmations " +
"each one of your outputs used for the transaction " +
"must satisfy",
Value: defaultUtxoMinConf,
},
txLabelFlag, txLabelFlag,
}, },
Action: actionDecorator(sendMany), Action: actionDecorator(sendMany),
@ -476,11 +497,14 @@ func sendMany(ctx *cli.Context) error {
client, cleanUp := getClient(ctx) client, cleanUp := getClient(ctx)
defer cleanUp() defer cleanUp()
minConfs := int32(ctx.Uint64("min_confs"))
txid, err := client.SendMany(ctxb, &lnrpc.SendManyRequest{ txid, err := client.SendMany(ctxb, &lnrpc.SendManyRequest{
AddrToAmount: amountToAddr, AddrToAmount: amountToAddr,
TargetConf: int32(ctx.Int64("conf_target")), TargetConf: int32(ctx.Int64("conf_target")),
SatPerByte: ctx.Int64("sat_per_byte"), SatPerByte: ctx.Int64("sat_per_byte"),
Label: ctx.String(txLabelFlag.Name), Label: ctx.String(txLabelFlag.Name),
MinConfs: minConfs,
SpendUnconfirmed: minConfs == 0,
}) })
if err != nil { if err != nil {
return err return err
@ -1814,7 +1838,7 @@ var listChainTxnsCommand = cli.Command{
To get all transactions until the chain tip, including unconfirmed To get all transactions until the chain tip, including unconfirmed
transactions (identifiable with BlockHeight=0), set end_height to -1. transactions (identifiable with BlockHeight=0), set end_height to -1.
By default, this call will get all transactions our wallet was involved By default, this call will get all transactions our wallet was involved
in, including unconfirmed transactions. in, including unconfirmed transactions.
`, `,
Action: actionDecorator(listChainTxns), Action: actionDecorator(listChainTxns),
} }

File diff suppressed because it is too large Load Diff

View File

@ -863,6 +863,13 @@ message SendManyRequest {
// An optional label for the transaction, limited to 500 characters. // An optional label for the transaction, limited to 500 characters.
string label = 6; string label = 6;
// The minimum number of confirmations each one of your outputs used for
// the transaction must satisfy.
int32 min_confs = 7;
// Whether unconfirmed outputs should be used as inputs for the transaction.
bool spend_unconfirmed = 8;
} }
message SendManyResponse { message SendManyResponse {
// The id of the transaction // The id of the transaction
@ -893,6 +900,13 @@ message SendCoinsRequest {
// An optional label for the transaction, limited to 500 characters. // An optional label for the transaction, limited to 500 characters.
string label = 7; string label = 7;
// The minimum number of confirmations each one of your outputs used for
// the transaction must satisfy.
int32 min_confs = 8;
// Whether unconfirmed outputs should be used as inputs for the transaction.
bool spend_unconfirmed = 9;
} }
message SendCoinsResponse { message SendCoinsResponse {
// The transaction ID of the transaction // The transaction ID of the transaction

View File

@ -5243,6 +5243,16 @@
"label": { "label": {
"type": "string", "type": "string",
"description": "An optional label for the transaction, limited to 500 characters." "description": "An optional label for the transaction, limited to 500 characters."
},
"min_confs": {
"type": "integer",
"format": "int32",
"description": "The minimum number of confirmations each one of your outputs used for\nthe transaction must satisfy."
},
"spend_unconfirmed": {
"type": "boolean",
"format": "boolean",
"description": "Whether unconfirmed outputs should be used as inputs for the transaction."
} }
} }
}, },
@ -5279,6 +5289,16 @@
"label": { "label": {
"type": "string", "type": "string",
"description": "An optional label for the transaction, limited to 500 characters." "description": "An optional label for the transaction, limited to 500 characters."
},
"min_confs": {
"type": "integer",
"format": "int32",
"description": "The minimum number of confirmations each one of your outputs used for\nthe transaction must satisfy."
},
"spend_unconfirmed": {
"type": "boolean",
"format": "boolean",
"description": "Whether unconfirmed outputs should be used as inputs for the transaction."
} }
} }
}, },

View File

@ -2,6 +2,7 @@ package lnrpc
import ( import (
"encoding/hex" "encoding/hex"
"errors"
"sort" "sort"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@ -52,3 +53,38 @@ func RPCTransactionDetails(txns []*lnwallet.TransactionDetail) *TransactionDetai
return txDetails return txDetails
} }
// ExtractMinConfs extracts the minimum number of confirmations that each
// output used to fund a transaction should satisfy.
func ExtractMinConfs(minConfs int32, spendUnconfirmed bool) (int32, error) {
switch {
// Ensure that the MinConfs parameter is non-negative.
case minConfs < 0:
return 0, errors.New("minimum number of confirmations must " +
"be a non-negative number")
// The transaction should not be funded with unconfirmed outputs
// unless explicitly specified by SpendUnconfirmed. We do this to
// provide sane defaults to the OpenChannel RPC, as otherwise, if the
// MinConfs field isn't explicitly set by the caller, we'll use
// unconfirmed outputs without the caller being aware.
case minConfs == 0 && !spendUnconfirmed:
return 1, nil
// In the event that the caller set MinConfs > 0 and SpendUnconfirmed to
// true, we'll return an error to indicate the conflict.
case minConfs > 0 && spendUnconfirmed:
return 0, errors.New("SpendUnconfirmed set to true with " +
"MinConfs > 0")
// The funding transaction of the new channel to be created can be
// funded with unconfirmed outputs.
case spendUnconfirmed:
return 0, nil
// If none of the above cases matched, we'll return the value set
// explicitly by the caller.
default:
return minConfs, nil
}
}

View File

@ -626,7 +626,12 @@ type SendOutputsRequest struct {
//A slice of the outputs that should be created in the transaction produced. //A slice of the outputs that should be created in the transaction produced.
Outputs []*signrpc.TxOut `protobuf:"bytes,2,rep,name=outputs,proto3" json:"outputs,omitempty"` Outputs []*signrpc.TxOut `protobuf:"bytes,2,rep,name=outputs,proto3" json:"outputs,omitempty"`
// An optional label for the transaction, limited to 500 characters. // An optional label for the transaction, limited to 500 characters.
Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"` Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"`
// The minimum number of confirmations each one of your outputs used for
// the transaction must satisfy.
MinConfs int32 `protobuf:"varint,4,opt,name=min_confs,json=minConfs,proto3" json:"min_confs,omitempty"`
// Whether unconfirmed outputs should be used as inputs for the transaction.
SpendUnconfirmed bool `protobuf:"varint,5,opt,name=spend_unconfirmed,json=spendUnconfirmed,proto3" json:"spend_unconfirmed,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"` XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"` XXX_sizecache int32 `json:"-"`
@ -678,6 +683,20 @@ func (m *SendOutputsRequest) GetLabel() string {
return "" return ""
} }
func (m *SendOutputsRequest) GetMinConfs() int32 {
if m != nil {
return m.MinConfs
}
return 0
}
func (m *SendOutputsRequest) GetSpendUnconfirmed() bool {
if m != nil {
return m.SpendUnconfirmed
}
return false
}
type SendOutputsResponse struct { type SendOutputsResponse struct {
// //
//The serialized transaction sent out on the network. //The serialized transaction sent out on the network.
@ -1381,97 +1400,99 @@ func init() {
func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) } func init() { proto.RegisterFile("walletrpc/walletkit.proto", fileDescriptor_6cc6942ac78249e5) }
var fileDescriptor_6cc6942ac78249e5 = []byte{ var fileDescriptor_6cc6942ac78249e5 = []byte{
// 1428 bytes of a gzipped FileDescriptorProto // 1460 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0xff, 0x6f, 0xda, 0xc8, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x57, 0x6f, 0x6f, 0x1a, 0x47,
0x12, 0x6f, 0xbe, 0x40, 0x60, 0x0c, 0x84, 0x2c, 0x24, 0xa1, 0x34, 0x6d, 0x52, 0x57, 0xef, 0xbd, 0x13, 0x8f, 0xff, 0x61, 0x98, 0x03, 0x8c, 0x17, 0x6c, 0x13, 0xe2, 0xc4, 0xce, 0x45, 0xcf, 0x53,
0xe8, 0xbd, 0x96, 0xe8, 0xa5, 0xea, 0xa9, 0xed, 0x49, 0xa7, 0x4b, 0xc0, 0x11, 0x11, 0x04, 0x52, 0xab, 0x49, 0xb0, 0xea, 0x28, 0x55, 0x92, 0x4a, 0x55, 0x6d, 0x38, 0x0b, 0x0b, 0x0c, 0xce, 0x81,
0x43, 0x1a, 0xf5, 0xee, 0x07, 0xcb, 0xe0, 0x2d, 0xb1, 0x02, 0xb6, 0xbb, 0x5e, 0x8a, 0xf9, 0xed, 0x63, 0xa5, 0x7d, 0x71, 0x3a, 0xb8, 0x0d, 0x3e, 0x19, 0xee, 0x2e, 0x7b, 0x4b, 0x38, 0xde, 0xf5,
0xfe, 0x8a, 0x93, 0xfa, 0x6f, 0xdd, 0x5f, 0x74, 0xda, 0x5d, 0x63, 0xd6, 0x40, 0x7a, 0x3a, 0xe9, 0x53, 0x54, 0xca, 0x77, 0xe9, 0xa7, 0xe8, 0x27, 0xaa, 0x76, 0xf7, 0x38, 0xf6, 0x00, 0xa7, 0xaa,
0x7e, 0x8a, 0x77, 0x3e, 0x33, 0x9f, 0x99, 0x9d, 0x99, 0xcc, 0x0e, 0xf0, 0x78, 0x62, 0x0e, 0x87, 0xd4, 0x57, 0xbe, 0x9d, 0xdf, 0xcc, 0x6f, 0x67, 0x67, 0xc6, 0x33, 0x03, 0x3c, 0x1c, 0x9b, 0x83,
0x98, 0x12, 0xaf, 0x7f, 0x22, 0xbe, 0xee, 0x6d, 0x5a, 0xf1, 0x88, 0x4b, 0x5d, 0x94, 0x8e, 0xa0, 0x01, 0xa6, 0xc4, 0xeb, 0x1d, 0x8b, 0xaf, 0x3b, 0x9b, 0x96, 0x3d, 0xe2, 0x52, 0x17, 0xa5, 0x22,
0x72, 0x9a, 0x78, 0x7d, 0x21, 0x2d, 0x17, 0x7d, 0x7b, 0xe0, 0x30, 0x75, 0xf6, 0x17, 0x13, 0x21, 0xa8, 0x94, 0x22, 0x5e, 0x4f, 0x48, 0x4b, 0x05, 0xdf, 0xee, 0x3b, 0x4c, 0x9d, 0xfd, 0xc5, 0x44,
0x55, 0x5b, 0x80, 0x9a, 0xb6, 0x4f, 0x6f, 0x1c, 0xdf, 0xc3, 0x0e, 0xd5, 0xf1, 0x97, 0x31, 0xf6, 0x48, 0xd5, 0x26, 0xa0, 0x86, 0xed, 0xd3, 0x6b, 0xc7, 0xf7, 0xb0, 0x43, 0x75, 0xfc, 0x79, 0x84,
0x29, 0x7a, 0x02, 0xe9, 0x91, 0xed, 0x18, 0x7d, 0xd7, 0xf9, 0xec, 0x97, 0xd6, 0x8e, 0xd6, 0x8e, 0x7d, 0x8a, 0x1e, 0x41, 0x6a, 0x68, 0x3b, 0x46, 0xcf, 0x75, 0x3e, 0xf9, 0xc5, 0x95, 0xc3, 0x95,
0x13, 0x7a, 0x6a, 0x64, 0x3b, 0x55, 0x76, 0xe6, 0xa0, 0x19, 0x84, 0xe0, 0x7a, 0x08, 0x9a, 0x01, 0xa3, 0x0d, 0x3d, 0x39, 0xb4, 0x9d, 0x0a, 0x3b, 0x73, 0xd0, 0x0c, 0x42, 0x70, 0x35, 0x04, 0xcd,
0x07, 0xd5, 0xb7, 0x50, 0x88, 0xf1, 0xf9, 0x9e, 0xeb, 0xf8, 0x18, 0x3d, 0x87, 0xc4, 0x98, 0x06, 0x80, 0x83, 0xea, 0x1b, 0xc8, 0xc7, 0xf8, 0x7c, 0xcf, 0x75, 0x7c, 0x8c, 0x9e, 0xc2, 0xc6, 0x88,
0x2e, 0x23, 0xdb, 0x38, 0x56, 0x4e, 0x95, 0xca, 0x90, 0x85, 0x52, 0xb9, 0xa1, 0x81, 0xab, 0x0b, 0x06, 0x2e, 0x23, 0x5b, 0x3b, 0x52, 0x4e, 0x94, 0xf2, 0x80, 0xb9, 0x52, 0xbe, 0xa6, 0x81, 0xab,
0x44, 0xfd, 0x00, 0xa8, 0x89, 0x4d, 0x1f, 0xb7, 0xc7, 0xd4, 0x1b, 0x47, 0x91, 0xe4, 0x60, 0xdd, 0x0b, 0x44, 0x7d, 0x0f, 0xa8, 0x81, 0x4d, 0x1f, 0xb7, 0x46, 0xd4, 0x1b, 0x45, 0x9e, 0x64, 0x61,
0xb6, 0x78, 0x08, 0x19, 0x7d, 0xdd, 0xb6, 0xd0, 0xff, 0x20, 0xe5, 0x8e, 0xa9, 0xe7, 0xda, 0x0e, 0xd5, 0xb6, 0xb8, 0x0b, 0x69, 0x7d, 0xd5, 0xb6, 0xd0, 0x73, 0x48, 0xba, 0x23, 0xea, 0xb9, 0xb6,
0xe5, 0xbe, 0x95, 0xd3, 0xed, 0x90, 0xab, 0x3d, 0xa6, 0xd7, 0x4c, 0xac, 0x47, 0x0a, 0xea, 0x1b, 0x43, 0xf9, 0xdd, 0xca, 0xc9, 0x56, 0xc8, 0xd5, 0x1a, 0xd1, 0x2b, 0x26, 0xd6, 0x23, 0x05, 0xf5,
0x28, 0xc4, 0x28, 0xc3, 0x60, 0x9e, 0x01, 0xe0, 0xc0, 0xb3, 0x89, 0x49, 0x6d, 0xd7, 0xe1, 0xdc, 0x35, 0xe4, 0x63, 0x94, 0xa1, 0x33, 0x4f, 0x00, 0x70, 0xe0, 0xd9, 0xc4, 0xa4, 0xb6, 0xeb, 0x70,
0x9b, 0xba, 0x24, 0x51, 0x3b, 0x50, 0xd4, 0xf1, 0xf0, 0x1f, 0x8e, 0x65, 0x1f, 0x76, 0x17, 0x48, 0xee, 0x75, 0x5d, 0x92, 0xa8, 0x6d, 0x28, 0xe8, 0x78, 0xf0, 0x1f, 0xfb, 0xb2, 0x07, 0x3b, 0x73,
0x45, 0x34, 0xea, 0x07, 0x48, 0x36, 0xf0, 0x54, 0xc7, 0x5f, 0xd0, 0x31, 0xe4, 0xef, 0xf1, 0xd4, 0xa4, 0xc2, 0x1b, 0xf5, 0x3d, 0x24, 0xea, 0x78, 0xa2, 0xe3, 0xcf, 0xe8, 0x08, 0x72, 0x77, 0x78,
0xf8, 0x6c, 0x3b, 0x03, 0x4c, 0x0c, 0x8f, 0x30, 0x5e, 0x91, 0xfc, 0xdc, 0x3d, 0x9e, 0x5e, 0x70, 0x62, 0x7c, 0xb2, 0x9d, 0x3e, 0x26, 0x86, 0x47, 0x18, 0xaf, 0x08, 0x7e, 0xf6, 0x0e, 0x4f, 0xce,
0xf1, 0x35, 0x93, 0xa2, 0xa7, 0x00, 0x5c, 0xd3, 0x1c, 0xd9, 0xc3, 0x69, 0x58, 0x83, 0x34, 0xd3, 0xb9, 0xf8, 0x8a, 0x49, 0xd1, 0x63, 0x00, 0xae, 0x69, 0x0e, 0xed, 0xc1, 0x24, 0xcc, 0x41, 0x8a,
0xe1, 0x02, 0x35, 0x0b, 0xca, 0x99, 0x65, 0x91, 0x30, 0x6e, 0x55, 0x85, 0x8c, 0x38, 0x86, 0xf7, 0xe9, 0x70, 0x81, 0x9a, 0x01, 0xe5, 0xd4, 0xb2, 0x48, 0xe8, 0xb7, 0xaa, 0x42, 0x5a, 0x1c, 0xc3,
0x47, 0xb0, 0x69, 0x5a, 0x16, 0xe1, 0xdc, 0x69, 0x9d, 0x7f, 0xab, 0xef, 0x41, 0xe9, 0x12, 0xd3, 0xf7, 0x23, 0x58, 0x37, 0x2d, 0x8b, 0x70, 0xee, 0x94, 0xce, 0xbf, 0xd5, 0x77, 0xa0, 0x74, 0x88,
0xf1, 0xcd, 0x3e, 0x4b, 0x01, 0xda, 0x85, 0x24, 0x0d, 0x8c, 0x3b, 0x1c, 0x84, 0xd7, 0x4d, 0xd0, 0xe9, 0xf8, 0x66, 0x8f, 0x85, 0x00, 0xed, 0x40, 0x82, 0x06, 0xc6, 0x2d, 0x0e, 0xc2, 0xe7, 0x6e,
0xa0, 0x8e, 0x03, 0x54, 0x84, 0xc4, 0xd0, 0xec, 0xe1, 0x21, 0x77, 0x99, 0xd6, 0xc5, 0x41, 0xfd, 0xd0, 0xa0, 0x86, 0x03, 0x54, 0x80, 0x8d, 0x81, 0xd9, 0xc5, 0x03, 0x7e, 0x65, 0x4a, 0x17, 0x07,
0x01, 0xb6, 0xaf, 0xc7, 0xbd, 0xa1, 0xed, 0xdf, 0x45, 0x2e, 0x5e, 0x40, 0xd6, 0x13, 0x22, 0x03, 0xf5, 0x47, 0xd8, 0xba, 0x1a, 0x75, 0x07, 0xb6, 0x7f, 0x1b, 0x5d, 0xf1, 0x0c, 0x32, 0x9e, 0x10,
0x13, 0xe2, 0xce, 0x7c, 0x65, 0x42, 0xa1, 0xc6, 0x64, 0x2a, 0x01, 0xd4, 0xc1, 0x8e, 0x25, 0xf2, 0x19, 0x98, 0x10, 0x77, 0x7a, 0x57, 0x3a, 0x14, 0x6a, 0x4c, 0xa6, 0xfe, 0xb9, 0x02, 0xa8, 0x8d,
0xe1, 0xcf, 0xb2, 0x7c, 0x00, 0xe0, 0x9b, 0xd4, 0xf0, 0x30, 0x31, 0xee, 0x27, 0xdc, 0x6e, 0x43, 0x1d, 0x4b, 0x04, 0xc4, 0x9f, 0x86, 0x79, 0x1f, 0xc0, 0x37, 0xa9, 0xe1, 0x61, 0x62, 0xdc, 0x8d,
0x4f, 0xf9, 0x26, 0xbd, 0xc6, 0xa4, 0x31, 0x41, 0xc7, 0xb0, 0xe5, 0x0a, 0xfd, 0xd2, 0x3a, 0x6f, 0xb9, 0xe1, 0x9a, 0x9e, 0xf4, 0x4d, 0x7a, 0x85, 0x49, 0x7d, 0x8c, 0x8e, 0x60, 0xd3, 0x15, 0xfa,
0xa5, 0x5c, 0x25, 0xec, 0xeb, 0x4a, 0x37, 0x68, 0x8f, 0xa9, 0x3e, 0x83, 0xe7, 0xb1, 0x6e, 0xc8, 0xc5, 0x55, 0x5e, 0x4b, 0xd9, 0x72, 0x58, 0xd8, 0xe5, 0x4e, 0xd0, 0x1a, 0x51, 0x7d, 0x0a, 0xcf,
0xb1, 0xbe, 0x84, 0x42, 0xcc, 0x67, 0x18, 0xef, 0x2e, 0x24, 0x89, 0x39, 0x31, 0x68, 0x74, 0x5f, 0x9c, 0x5d, 0x93, 0x9c, 0x8d, 0x97, 0xf6, 0xfa, 0x5c, 0x69, 0x3f, 0x87, 0x6d, 0x56, 0xb7, 0x96,
0x62, 0x4e, 0xba, 0x81, 0xfa, 0x06, 0x90, 0xe6, 0x53, 0x7b, 0x64, 0x52, 0x7c, 0x81, 0xf1, 0x2c, 0x31, 0x72, 0x98, 0x82, 0x4d, 0x86, 0xd8, 0x2a, 0x6e, 0x1c, 0xae, 0x1c, 0x25, 0xf5, 0x1c, 0x07,
0xc2, 0x43, 0x50, 0x58, 0xf3, 0x1b, 0xd4, 0x24, 0x03, 0x3c, 0x2b, 0x11, 0x30, 0x51, 0x97, 0x4b, 0xae, 0x67, 0x72, 0xf5, 0x05, 0xe4, 0x63, 0xde, 0x87, 0x4f, 0xdf, 0x81, 0x04, 0x31, 0xc7, 0x06,
0xd4, 0xd7, 0x50, 0x88, 0x99, 0x85, 0x4e, 0xbe, 0x7b, 0x33, 0xf5, 0xdb, 0x06, 0x64, 0xae, 0xb1, 0x8d, 0x42, 0x47, 0xcc, 0x71, 0x27, 0x50, 0x5f, 0x03, 0xd2, 0x7c, 0x6a, 0x0f, 0x4d, 0x8a, 0xcf,
0x63, 0xd9, 0xce, 0xa0, 0x33, 0xc1, 0xd8, 0x8b, 0xb5, 0xd7, 0xda, 0x5f, 0xb4, 0x17, 0x7a, 0x07, 0x31, 0x9e, 0xbe, 0xf5, 0x00, 0x14, 0x46, 0x68, 0x50, 0x93, 0xf4, 0xf1, 0x34, 0xdb, 0xc0, 0x44,
0x99, 0x89, 0x4d, 0x1d, 0xec, 0xfb, 0x06, 0x9d, 0x7a, 0x98, 0x17, 0x28, 0x77, 0xba, 0x57, 0x89, 0x1d, 0x2e, 0x51, 0x5f, 0x41, 0x3e, 0x66, 0x16, 0x5e, 0xf2, 0xcd, 0x18, 0xa9, 0x5f, 0xd7, 0x20,
0x46, 0x41, 0xe5, 0x56, 0xc0, 0xdd, 0xa9, 0x87, 0x75, 0x65, 0x32, 0x3f, 0xb0, 0x66, 0x32, 0x47, 0x7d, 0x85, 0x1d, 0xcb, 0x76, 0xfa, 0xed, 0x31, 0xc6, 0x5e, 0xac, 0x52, 0x57, 0xfe, 0xa1, 0x52,
0xee, 0xd8, 0xa1, 0x86, 0x6f, 0x52, 0x9e, 0xad, 0xac, 0x9e, 0x16, 0x92, 0x8e, 0x49, 0xd1, 0x11, 0xd1, 0x5b, 0x48, 0x8f, 0x6d, 0xea, 0x60, 0xdf, 0x37, 0xe8, 0xc4, 0xc3, 0x3c, 0xd7, 0xd9, 0x93,
0x64, 0x66, 0x51, 0xf7, 0xa6, 0x14, 0x97, 0x36, 0xb9, 0x02, 0x88, 0xb8, 0xcf, 0xa7, 0x14, 0xa3, 0xdd, 0x72, 0xd4, 0x55, 0xca, 0x37, 0x02, 0xee, 0x4c, 0x3c, 0xac, 0x2b, 0xe3, 0xd9, 0x81, 0xd5,
0x57, 0x80, 0x7a, 0xc4, 0x35, 0xad, 0xbe, 0xe9, 0x53, 0xc3, 0xa4, 0x14, 0x8f, 0x3c, 0xea, 0x97, 0xa5, 0x39, 0x74, 0x47, 0x0e, 0x35, 0x7c, 0x93, 0xf2, 0xb8, 0x67, 0xf4, 0x94, 0x90, 0xb4, 0x4d,
0x12, 0x5c, 0x6f, 0x27, 0x42, 0xce, 0x42, 0x00, 0x9d, 0xc2, 0xae, 0x83, 0x03, 0x6a, 0xcc, 0x6d, 0x8a, 0x0e, 0x21, 0x3d, 0xf5, 0xba, 0x3b, 0xa1, 0x98, 0x87, 0x3f, 0xa3, 0x83, 0xf0, 0xfb, 0x6c,
0xee, 0xb0, 0x3d, 0xb8, 0xa3, 0xa5, 0x24, 0xb7, 0x28, 0x30, 0xf0, 0x7c, 0x86, 0xd5, 0x39, 0xc4, 0x42, 0x31, 0x7a, 0x09, 0xa8, 0x4b, 0x5c, 0xd3, 0xea, 0x99, 0x3e, 0x35, 0x4c, 0x4a, 0xf1, 0xd0,
0x6c, 0x88, 0xc8, 0x3e, 0xb6, 0x0c, 0x39, 0xf9, 0x29, 0x61, 0x13, 0x81, 0xd5, 0xa8, 0x0a, 0xe8, 0xa3, 0x3e, 0xcf, 0x40, 0x46, 0xdf, 0x8e, 0x90, 0xd3, 0x10, 0x40, 0x27, 0xb0, 0xe3, 0xe0, 0x80,
0x35, 0xec, 0xcd, 0x6d, 0x62, 0x57, 0x48, 0x2f, 0x18, 0x75, 0xe6, 0x77, 0x29, 0x42, 0xe2, 0xb3, 0x1a, 0x33, 0x9b, 0x5b, 0x6c, 0xf7, 0x6f, 0x69, 0x31, 0xc1, 0x2d, 0xf2, 0x0c, 0x3c, 0x9b, 0x62,
0x4b, 0xfa, 0xb8, 0xb4, 0x75, 0xb4, 0x76, 0x9c, 0xd2, 0xc5, 0x41, 0xdd, 0x83, 0xa2, 0x5c, 0x9a, 0x35, 0x0e, 0x31, 0x1b, 0x22, 0xa2, 0x8f, 0x2d, 0x43, 0x0e, 0x7e, 0x52, 0xd8, 0x44, 0x60, 0x25,
0x59, 0xaf, 0xaa, 0xb7, 0xb0, 0xbb, 0x20, 0x0f, 0x4b, 0xfd, 0x13, 0xe4, 0x3c, 0x01, 0x18, 0x3e, 0xca, 0x02, 0x7a, 0x05, 0xbb, 0x33, 0x9b, 0xd8, 0x13, 0x52, 0x73, 0x46, 0xed, 0xd9, 0x5b, 0x0a,
0x47, 0xc2, 0xc1, 0xb7, 0x2f, 0x15, 0x44, 0xb6, 0xd4, 0xb3, 0x9e, 0xcc, 0xa3, 0xfe, 0xbe, 0x06, 0xb0, 0xf1, 0xc9, 0x25, 0x3d, 0x5c, 0xdc, 0xe4, 0x05, 0x24, 0x0e, 0xea, 0x2e, 0x14, 0xe4, 0xd4,
0xb9, 0xf3, 0xf1, 0xc8, 0x93, 0xba, 0xee, 0x6f, 0xb5, 0xc3, 0x21, 0x28, 0x22, 0x41, 0x3c, 0x59, 0x4c, 0xab, 0x5e, 0xbd, 0x81, 0x9d, 0x39, 0x79, 0x98, 0xea, 0x9f, 0x21, 0xeb, 0x09, 0xc0, 0xf0,
0xbc, 0x1b, 0xb2, 0x3a, 0x08, 0x11, 0x4b, 0xd1, 0x52, 0x55, 0x37, 0x96, 0xaa, 0x1a, 0x65, 0x62, 0x39, 0x12, 0xf6, 0xd0, 0x3d, 0x29, 0x21, 0xb2, 0xa5, 0x9e, 0xf1, 0x64, 0x1e, 0xf5, 0x8f, 0x15,
0x53, 0xce, 0xc4, 0x0e, 0x6c, 0x47, 0x71, 0x85, 0x03, 0xec, 0x15, 0xec, 0xb0, 0x91, 0x1f, 0xcb, 0xc8, 0x9e, 0x8d, 0x86, 0x9e, 0x54, 0x75, 0xff, 0xaa, 0x1c, 0x0e, 0x40, 0x11, 0x01, 0xe2, 0xc1,
0x0c, 0x2a, 0xc1, 0xd6, 0x57, 0x4c, 0x7a, 0xae, 0x8f, 0x79, 0xb0, 0x29, 0x7d, 0x76, 0x54, 0x7f, 0xe2, 0xd5, 0x90, 0xd1, 0x41, 0x88, 0x58, 0x88, 0x16, 0xb2, 0xba, 0xb6, 0x90, 0xd5, 0x28, 0x12,
0x5b, 0x17, 0x4f, 0xce, 0x42, 0xc6, 0x9a, 0x50, 0xa0, 0xf3, 0x01, 0x64, 0x58, 0x98, 0x9a, 0xf6, 0xeb, 0x72, 0x24, 0xb6, 0x61, 0x2b, 0xf2, 0x2b, 0xec, 0x85, 0x2f, 0x61, 0x9b, 0x4d, 0x8f, 0x58,
0xd0, 0x0f, 0x6f, 0xfa, 0x38, 0xbc, 0xa9, 0x34, 0xa2, 0x6a, 0x42, 0xa1, 0xfe, 0x48, 0x47, 0x74, 0x64, 0x50, 0x11, 0x36, 0xbf, 0x60, 0xd2, 0x75, 0x7d, 0xcc, 0x9d, 0x4d, 0xea, 0xd3, 0xa3, 0xfa,
0x49, 0x8a, 0x6e, 0x61, 0x5b, 0x66, 0xb3, 0x2d, 0x3f, 0x9c, 0xd0, 0x2f, 0xa5, 0x02, 0x2c, 0x47, 0xfb, 0xaa, 0x98, 0x5e, 0x73, 0x11, 0x6b, 0x40, 0x9e, 0xce, 0x7a, 0x99, 0x61, 0x61, 0x6a, 0xda,
0x21, 0x3b, 0xb8, 0xac, 0x31, 0xf2, 0x9c, 0x44, 0x73, 0x69, 0xf9, 0xe5, 0x77, 0x90, 0x8b, 0xeb, 0x03, 0x3f, 0x7c, 0xe9, 0xc3, 0xf0, 0xa5, 0x52, 0xb7, 0xab, 0x0a, 0x85, 0xda, 0x03, 0x1d, 0xd1,
0xa0, 0xff, 0x2c, 0xbb, 0x62, 0xb5, 0x4e, 0x2f, 0x9a, 0x9e, 0xa7, 0x20, 0x29, 0x7a, 0x41, 0x35, 0x05, 0x29, 0xba, 0x81, 0x2d, 0x99, 0xcd, 0xb6, 0xfc, 0xb0, 0xd9, 0xbf, 0x90, 0x12, 0xb0, 0xe8,
0x61, 0xbf, 0xc9, 0xa6, 0x91, 0xc4, 0x34, 0xcb, 0x1b, 0x82, 0x4d, 0x1a, 0x44, 0xaf, 0x0c, 0xff, 0x85, 0x7c, 0xc1, 0x45, 0x95, 0x91, 0x67, 0x25, 0x9a, 0x0b, 0xcb, 0x2f, 0xbd, 0x85, 0x6c, 0x5c,
0x5e, 0x3d, 0x75, 0xd1, 0x01, 0xa4, 0xdd, 0xaf, 0x98, 0x4c, 0x88, 0x1d, 0x96, 0x2f, 0xa5, 0xcf, 0x07, 0x7d, 0xb7, 0x78, 0x15, 0xcb, 0x75, 0x6a, 0xde, 0xf4, 0x2c, 0x09, 0x09, 0x51, 0x0b, 0xaa,
0x05, 0x6a, 0x19, 0x4a, 0xcb, 0x2e, 0xc4, 0x25, 0xff, 0xfb, 0x6d, 0x03, 0x14, 0x69, 0x1a, 0xa0, 0x09, 0x7b, 0x0d, 0xd6, 0xd7, 0x24, 0xa6, 0x69, 0xdc, 0x10, 0xac, 0xd3, 0x20, 0x1a, 0x58, 0xfc,
0x02, 0x6c, 0xdf, 0xb4, 0x1a, 0xad, 0xf6, 0x6d, 0xcb, 0xb8, 0xbd, 0xec, 0xb6, 0xb4, 0x4e, 0x27, 0x7b, 0x79, 0x03, 0x47, 0xfb, 0x90, 0x72, 0xbf, 0x60, 0x32, 0x26, 0x76, 0x98, 0xbe, 0xa4, 0x3e,
0xff, 0x08, 0x95, 0xa0, 0x58, 0x6d, 0x5f, 0x5d, 0x5d, 0x76, 0xaf, 0xb4, 0x56, 0xd7, 0xe8, 0x5e, 0x13, 0xa8, 0x25, 0x28, 0x2e, 0x5e, 0x21, 0x1e, 0xf9, 0xfd, 0xd7, 0x35, 0x50, 0xa4, 0x6e, 0x80,
0x5e, 0x69, 0x46, 0xb3, 0x5d, 0x6d, 0xe4, 0xd7, 0xd0, 0x3e, 0x14, 0x24, 0xa4, 0xd5, 0x36, 0x6a, 0xf2, 0xb0, 0x75, 0xdd, 0xac, 0x37, 0x5b, 0x37, 0x4d, 0xe3, 0xe6, 0xa2, 0xd3, 0xd4, 0xda, 0xed,
0x5a, 0xf3, 0xec, 0x53, 0x7e, 0x1d, 0xed, 0xc2, 0x8e, 0x04, 0xe8, 0xda, 0xc7, 0x76, 0x43, 0xcb, 0xdc, 0x03, 0x54, 0x84, 0x42, 0xa5, 0x75, 0x79, 0x79, 0xd1, 0xb9, 0xd4, 0x9a, 0x1d, 0xa3, 0x73,
0x6f, 0x30, 0xfd, 0x7a, 0xb7, 0x59, 0x35, 0xda, 0x17, 0x17, 0x9a, 0xae, 0xd5, 0x66, 0xc0, 0x26, 0x71, 0xa9, 0x19, 0x8d, 0x56, 0xa5, 0x9e, 0x5b, 0x41, 0x7b, 0x90, 0x97, 0x90, 0x66, 0xcb, 0xa8,
0x73, 0xc1, 0x81, 0xb3, 0x6a, 0x55, 0xbb, 0xee, 0xce, 0x91, 0x04, 0xfa, 0x17, 0x3c, 0x8f, 0x99, 0x6a, 0x8d, 0xd3, 0x8f, 0xb9, 0x55, 0xb4, 0x03, 0xdb, 0x12, 0xa0, 0x6b, 0x1f, 0x5a, 0x75, 0x2d,
0x30, 0xf7, 0xed, 0x9b, 0xae, 0xd1, 0xd1, 0xaa, 0xed, 0x56, 0xcd, 0x68, 0x6a, 0x1f, 0xb5, 0x66, 0xb7, 0xc6, 0xf4, 0x6b, 0x9d, 0x46, 0xc5, 0x68, 0x9d, 0x9f, 0x6b, 0xba, 0x56, 0x9d, 0x02, 0xeb,
0x3e, 0x89, 0xfe, 0x0d, 0x6a, 0x9c, 0xa0, 0x73, 0x53, 0xad, 0x6a, 0x9d, 0x4e, 0x5c, 0x6f, 0x0b, 0xec, 0x0a, 0x0e, 0x9c, 0x56, 0x2a, 0xda, 0x55, 0x67, 0x86, 0x6c, 0xa0, 0xff, 0xc1, 0xd3, 0x98,
0x1d, 0xc2, 0x93, 0x85, 0x08, 0xae, 0xda, 0x5d, 0x6d, 0xc6, 0x9a, 0x4f, 0xa1, 0x23, 0x38, 0x58, 0x09, 0xbb, 0xbe, 0x75, 0xdd, 0x31, 0xda, 0x5a, 0xa5, 0xd5, 0xac, 0x1a, 0x0d, 0xed, 0x83, 0xd6,
0x8c, 0x84, 0x6b, 0x84, 0x7c, 0xf9, 0x34, 0x3a, 0x80, 0x12, 0xd7, 0x90, 0x99, 0x67, 0xf1, 0x02, 0xc8, 0x25, 0xd0, 0xff, 0x41, 0x8d, 0x13, 0xb4, 0xaf, 0x2b, 0x15, 0xad, 0xdd, 0x8e, 0xeb, 0x6d,
0x2a, 0x42, 0x3e, 0xcc, 0x9c, 0xd1, 0xd0, 0x3e, 0x19, 0xf5, 0xb3, 0x4e, 0x3d, 0xaf, 0xa0, 0x27, 0xa2, 0x03, 0x78, 0x34, 0xe7, 0xc1, 0x65, 0xab, 0xa3, 0x4d, 0x59, 0x73, 0x49, 0x74, 0x08, 0xfb,
0xb0, 0xdf, 0xd2, 0x3a, 0x8c, 0x6e, 0x09, 0xcc, 0x2c, 0x24, 0xeb, 0xac, 0x55, 0xad, 0xb7, 0xf5, 0xf3, 0x9e, 0x70, 0x8d, 0x90, 0x2f, 0x97, 0x42, 0xfb, 0x50, 0xe4, 0x1a, 0x32, 0xf3, 0xd4, 0x5f,
0x7c, 0xf6, 0xf4, 0x8f, 0x2d, 0x48, 0xdf, 0xf2, 0x0e, 0x6d, 0xd8, 0x14, 0x35, 0x41, 0x91, 0xb6, 0x40, 0x05, 0xc8, 0x85, 0x91, 0x33, 0xea, 0xda, 0x47, 0xa3, 0x76, 0xda, 0xae, 0xe5, 0x14, 0xf4,
0x29, 0xf4, 0x74, 0xa1, 0x79, 0xe3, 0x5b, 0x5b, 0xf9, 0xd9, 0x43, 0x70, 0xf4, 0x2f, 0xa6, 0x48, 0x08, 0xf6, 0x9a, 0x5a, 0x9b, 0xd1, 0x2d, 0x80, 0xe9, 0xb9, 0x60, 0x9d, 0x36, 0x2b, 0xb5, 0x96,
0xeb, 0x50, 0x9c, 0x6d, 0x69, 0xdb, 0x89, 0xb3, 0xad, 0xd8, 0xa2, 0x74, 0xc8, 0xc6, 0x16, 0x1a, 0x9e, 0xcb, 0x9c, 0xfc, 0xb5, 0x09, 0xa9, 0x1b, 0x5e, 0xa1, 0x75, 0x9b, 0xa2, 0x06, 0x28, 0xd2,
0x74, 0x28, 0x19, 0xac, 0xda, 0x9f, 0xca, 0x47, 0x0f, 0x2b, 0x84, 0x9c, 0xef, 0x21, 0x5b, 0xc3, 0x62, 0x86, 0x1e, 0xcf, 0x15, 0x6f, 0x7c, 0x01, 0x2c, 0x3d, 0xb9, 0x0f, 0x8e, 0xfe, 0xc5, 0x14,
0xc4, 0xfe, 0x8a, 0x5b, 0x38, 0xa0, 0x0d, 0x3c, 0x45, 0x3b, 0x92, 0x89, 0xd8, 0x92, 0xca, 0x7b, 0x69, 0xb3, 0x8a, 0xb3, 0x2d, 0x2c, 0x4e, 0x71, 0xb6, 0x25, 0x0b, 0x99, 0x0e, 0x99, 0xd8, 0x6e,
0xd1, 0x83, 0xdf, 0xc0, 0xd3, 0x1a, 0xf6, 0xfb, 0xc4, 0xf6, 0xa8, 0x4b, 0xd0, 0x5b, 0x48, 0x0b, 0x84, 0x0e, 0x24, 0x83, 0x65, 0xab, 0x58, 0xe9, 0xf0, 0x7e, 0x85, 0x90, 0xf3, 0x1d, 0x64, 0xaa,
0x5b, 0x66, 0x57, 0x90, 0x95, 0x9a, 0x6e, 0xdf, 0xa4, 0x2e, 0x79, 0xd0, 0xf2, 0x47, 0x48, 0x31, 0x98, 0xd8, 0x5f, 0x70, 0x13, 0x07, 0xb4, 0x8e, 0x27, 0x68, 0x5b, 0x32, 0x11, 0x0b, 0x57, 0x69,
0x7f, 0x6c, 0x47, 0x42, 0xf2, 0x8b, 0x29, 0xed, 0x50, 0xe5, 0xfd, 0x25, 0x79, 0x18, 0x72, 0x1d, 0x37, 0x5a, 0x1d, 0xea, 0x78, 0x52, 0xc5, 0x7e, 0x8f, 0xd8, 0x1e, 0x75, 0x09, 0x7a, 0x03, 0x29,
0x50, 0xb8, 0xfc, 0xc8, 0xfb, 0x93, 0x4c, 0x23, 0xc9, 0xcb, 0x65, 0x79, 0xfe, 0x2f, 0xec, 0x4c, 0x61, 0xcb, 0xec, 0xf2, 0xb2, 0x52, 0xc3, 0xed, 0x99, 0xd4, 0x25, 0xf7, 0x5a, 0xfe, 0x04, 0x49,
0x4d, 0x50, 0xa4, 0xd5, 0x24, 0x56, 0x9e, 0xe5, 0x35, 0x29, 0x56, 0x9e, 0x55, 0x1b, 0x4d, 0x13, 0x76, 0x1f, 0x5b, 0xb7, 0x90, 0x3c, 0x31, 0xa5, 0x75, 0xac, 0xb4, 0xb7, 0x20, 0x0f, 0x5d, 0xae,
0x14, 0x69, 0x07, 0x89, 0xb1, 0x2d, 0xaf, 0x34, 0x31, 0xb6, 0x55, 0xab, 0x8b, 0x0e, 0xd9, 0xd8, 0x01, 0x0a, 0xf7, 0x28, 0x79, 0x15, 0x93, 0x69, 0x24, 0x79, 0xa9, 0x24, 0xf7, 0xff, 0xb9, 0xf5,
0x43, 0x17, 0x2b, 0xf6, 0xaa, 0xa7, 0x31, 0x56, 0xec, 0xd5, 0x6f, 0xe4, 0xcf, 0xb0, 0x15, 0x3e, 0xab, 0x01, 0x8a, 0xb4, 0x9a, 0xc4, 0xd2, 0xb3, 0xb8, 0x70, 0xc5, 0xd2, 0xb3, 0x6c, 0xa3, 0x69,
0x25, 0xe8, 0xb1, 0xa4, 0x1c, 0x7f, 0xf6, 0x62, 0x19, 0x5b, 0x78, 0x79, 0xd0, 0x25, 0xc0, 0x7c, 0x80, 0x22, 0xed, 0x20, 0x31, 0xb6, 0xc5, 0x95, 0x26, 0xc6, 0xb6, 0x6c, 0x75, 0xd1, 0x21, 0x13,
0x86, 0xa3, 0x83, 0x07, 0x46, 0xbb, 0xe0, 0x79, 0xfa, 0xdd, 0xc1, 0x8f, 0x7e, 0x85, 0xfc, 0xe2, 0x1b, 0x74, 0xb1, 0x64, 0x2f, 0x1b, 0x8d, 0xb1, 0x64, 0x2f, 0x9f, 0x91, 0xbf, 0xc0, 0x66, 0x38,
0xbc, 0x44, 0xaa, 0x6c, 0xb2, 0x7a, 0x5e, 0x97, 0x5f, 0x7c, 0x57, 0x47, 0x90, 0x9f, 0xff, 0xff, 0x4a, 0xd0, 0x43, 0x49, 0x39, 0x3e, 0xf6, 0x62, 0x11, 0x9b, 0x9b, 0x3c, 0xe8, 0x02, 0x60, 0xd6,
0x97, 0x93, 0x81, 0x4d, 0xef, 0xc6, 0xbd, 0x4a, 0xdf, 0x1d, 0x9d, 0x0c, 0xd9, 0x46, 0xe3, 0xd8, 0xc3, 0xd1, 0xfe, 0x3d, 0xad, 0x5d, 0xf0, 0x3c, 0xfe, 0x66, 0xe3, 0x47, 0xbf, 0x41, 0x6e, 0xbe,
0xce, 0xc0, 0xc1, 0x74, 0xe2, 0x92, 0xfb, 0x93, 0xa1, 0x63, 0x9d, 0xf0, 0xf7, 0xed, 0x24, 0xe2, 0x5f, 0x22, 0x55, 0x36, 0x59, 0xde, 0xaf, 0x4b, 0xcf, 0xbe, 0xa9, 0x23, 0xc8, 0xcf, 0x7e, 0xf8,
0xea, 0x25, 0xf9, 0xcf, 0xb3, 0xd7, 0x7f, 0x06, 0x00, 0x00, 0xff, 0xff, 0x01, 0xb2, 0xa4, 0x25, 0xf5, 0xb8, 0x6f, 0xd3, 0xdb, 0x51, 0xb7, 0xdc, 0x73, 0x87, 0xc7, 0x03, 0xb6, 0xd1, 0x38, 0xb6,
0xe7, 0x0d, 0x00, 0x00, 0xd3, 0x77, 0x30, 0x1d, 0xbb, 0xe4, 0xee, 0x78, 0xe0, 0x58, 0xc7, 0x7c, 0xbe, 0x1d, 0x47, 0x5c,
0xdd, 0x04, 0xff, 0xa5, 0xf7, 0xea, 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x49, 0x6e, 0x8a, 0xc5,
0x32, 0x0e, 0x00, 0x00,
} }
// Reference imports to suppress errors if they are not otherwise used. // Reference imports to suppress errors if they are not otherwise used.

View File

@ -233,6 +233,13 @@ message SendOutputsRequest {
// An optional label for the transaction, limited to 500 characters. // An optional label for the transaction, limited to 500 characters.
string label = 3; string label = 3;
// The minimum number of confirmations each one of your outputs used for
// the transaction must satisfy.
int32 min_confs = 4;
// Whether unconfirmed outputs should be used as inputs for the transaction.
bool spend_unconfirmed = 5;
} }
message SendOutputsResponse { message SendOutputsResponse {
/* /*

View File

@ -877,6 +877,16 @@
"label": { "label": {
"type": "string", "type": "string",
"description": "An optional label for the transaction, limited to 500 characters." "description": "An optional label for the transaction, limited to 500 characters."
},
"min_confs": {
"type": "integer",
"format": "int32",
"description": "The minimum number of confirmations each one of your outputs used for\nthe transaction must satisfy."
},
"spend_unconfirmed": {
"type": "boolean",
"format": "boolean",
"description": "Whether unconfirmed outputs should be used as inputs for the transaction."
} }
} }
}, },

View File

@ -473,6 +473,13 @@ func (w *WalletKit) SendOutputs(ctx context.Context,
}) })
} }
// Then, we'll extract the minimum number of confirmations that each
// output we use to fund the transaction should satisfy.
minConfs, err := lnrpc.ExtractMinConfs(req.MinConfs, req.SpendUnconfirmed)
if err != nil {
return nil, err
}
label, err := labels.ValidateAPI(req.Label) label, err := labels.ValidateAPI(req.Label)
if err != nil { if err != nil {
return nil, err return nil, err
@ -481,7 +488,7 @@ func (w *WalletKit) SendOutputs(ctx context.Context,
// Now that we have the outputs mapped, we can request that the wallet // Now that we have the outputs mapped, we can request that the wallet
// attempt to create this transaction. // attempt to create this transaction.
tx, err := w.cfg.Wallet.SendOutputs( tx, err := w.cfg.Wallet.SendOutputs(
outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), label, outputsToCreate, chainfee.SatPerKWeight(req.SatPerKw), minConfs, label,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -78,7 +78,7 @@ func (w *WalletController) IsOurAddress(a btcutil.Address) bool {
// SendOutputs currently returns dummy values. // SendOutputs currently returns dummy values.
func (w *WalletController) SendOutputs(outputs []*wire.TxOut, func (w *WalletController) SendOutputs(outputs []*wire.TxOut,
_ chainfee.SatPerKWeight, _ string) (*wire.MsgTx, error) { _ chainfee.SatPerKWeight, _ int32, _ string) (*wire.MsgTx, error) {
return nil, nil return nil, nil
} }

View File

@ -297,7 +297,7 @@ func (b *BtcWallet) IsOurAddress(a btcutil.Address) bool {
// //
// This is a part of the WalletController interface. // This is a part of the WalletController interface.
func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut, func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight, label string) (*wire.MsgTx, error) { feeRate chainfee.SatPerKWeight, minconf int32, label string) (*wire.MsgTx, error) {
// Convert our fee rate from sat/kw to sat/kb since it's required by // Convert our fee rate from sat/kw to sat/kb since it's required by
// SendOutputs. // SendOutputs.
@ -308,8 +308,13 @@ func (b *BtcWallet) SendOutputs(outputs []*wire.TxOut,
return nil, lnwallet.ErrNoOutputs return nil, lnwallet.ErrNoOutputs
} }
// Sanity check minconf.
if minconf < 0 {
return nil, lnwallet.ErrInvalidMinconf
}
return b.wallet.SendOutputs( return b.wallet.SendOutputs(
outputs, defaultAccount, 1, feeSatPerKB, label, outputs, defaultAccount, minconf, feeSatPerKB, label,
) )
} }

View File

@ -56,6 +56,11 @@ var (
// or send coins to a set of outputs that is empty. // or send coins to a set of outputs that is empty.
var ErrNoOutputs = errors.New("no outputs") var ErrNoOutputs = errors.New("no outputs")
// ErrInvalidMinconf is returned if we try to create a transaction with
// invalid minconf value.
var ErrInvalidMinconf = errors.New("minimum number of confirmations must " +
"be a non-negative number")
// Utxo is an unspent output denoted by its outpoint, and output value of the // Utxo is an unspent output denoted by its outpoint, and output value of the
// original output. // original output.
type Utxo struct { type Utxo struct {
@ -181,7 +186,7 @@ type WalletController interface {
// //
// NOTE: This method requires the global coin selection lock to be held. // NOTE: This method requires the global coin selection lock to be held.
SendOutputs(outputs []*wire.TxOut, SendOutputs(outputs []*wire.TxOut,
feeRate chainfee.SatPerKWeight, label string) (*wire.MsgTx, error) feeRate chainfee.SatPerKWeight, minconf int32, label string) (*wire.MsgTx, error)
// CreateSimpleTx creates a Bitcoin transaction paying to the specified // CreateSimpleTx creates a Bitcoin transaction paying to the specified
// outputs. The transaction is not broadcasted to the network. In the // outputs. The transaction is not broadcasted to the network. In the

View File

@ -171,18 +171,20 @@ func newPkScript(t *testing.T, w *lnwallet.LightningWallet,
// parties to send on-chain funds to each other. // parties to send on-chain funds to each other.
func sendCoins(t *testing.T, miner *rpctest.Harness, func sendCoins(t *testing.T, miner *rpctest.Harness,
sender, receiver *lnwallet.LightningWallet, output *wire.TxOut, sender, receiver *lnwallet.LightningWallet, output *wire.TxOut,
feeRate chainfee.SatPerKWeight) *wire.MsgTx { //nolint:unparam feeRate chainfee.SatPerKWeight, mineBlock bool, minConf int32) *wire.MsgTx { //nolint:unparam
t.Helper() t.Helper()
tx, err := sender.SendOutputs( tx, err := sender.SendOutputs(
[]*wire.TxOut{output}, 2500, labels.External, []*wire.TxOut{output}, feeRate, minConf, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to send transaction: %v", err) t.Fatalf("unable to send transaction: %v", err)
} }
mineAndAssertTxInBlock(t, miner, tx.TxHash()) if mineBlock {
mineAndAssertTxInBlock(t, miner, tx.TxHash())
}
if err := waitForWalletSync(miner, sender); err != nil { if err := waitForWalletSync(miner, sender); err != nil {
t.Fatalf("unable to sync alice: %v", err) t.Fatalf("unable to sync alice: %v", err)
@ -201,12 +203,6 @@ func assertTxInWallet(t *testing.T, w *lnwallet.LightningWallet,
t.Helper() t.Helper()
// If the backend is Neutrino, then we can't determine unconfirmed
// transactions since it's not aware of the mempool.
if !confirmed && w.BackEnd() == "neutrino" {
return
}
// We'll fetch all of our transaction and go through each one until // We'll fetch all of our transaction and go through each one until
// finding the expected transaction with its expected confirmation // finding the expected transaction with its expected confirmation
// status. // status.
@ -1269,7 +1265,7 @@ func testListTransactionDetails(miner *rpctest.Harness,
} }
burnOutput := wire.NewTxOut(outputAmt, outputScript) burnOutput := wire.NewTxOut(outputAmt, outputScript)
burnTX, err := alice.SendOutputs( burnTX, err := alice.SendOutputs(
[]*wire.TxOut{burnOutput}, 2500, labels.External, []*wire.TxOut{burnOutput}, 2500, 1, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create burn tx: %v", err) t.Fatalf("unable to create burn tx: %v", err)
@ -1544,7 +1540,7 @@ func testTransactionSubscriptions(miner *rpctest.Harness,
} }
burnOutput := wire.NewTxOut(outputAmt, outputScript) burnOutput := wire.NewTxOut(outputAmt, outputScript)
tx, err := alice.SendOutputs( tx, err := alice.SendOutputs(
[]*wire.TxOut{burnOutput}, 2500, labels.External, []*wire.TxOut{burnOutput}, 2500, 1, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create burn tx: %v", err) t.Fatalf("unable to create burn tx: %v", err)
@ -1737,7 +1733,7 @@ func newTx(t *testing.T, r *rpctest.Harness, pubKey *btcec.PublicKey,
PkScript: keyScript, PkScript: keyScript,
} }
tx, err := alice.SendOutputs( tx, err := alice.SendOutputs(
[]*wire.TxOut{newOutput}, 2500, labels.External, []*wire.TxOut{newOutput}, 2500, 1, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create output: %v", err) t.Fatalf("unable to create output: %v", err)
@ -2028,7 +2024,7 @@ func testSignOutputUsingTweaks(r *rpctest.Harness,
PkScript: keyScript, PkScript: keyScript,
} }
tx, err := alice.SendOutputs( tx, err := alice.SendOutputs(
[]*wire.TxOut{newOutput}, 2500, labels.External, []*wire.TxOut{newOutput}, 2500, 1, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create output: %v", err) t.Fatalf("unable to create output: %v", err)
@ -2152,7 +2148,7 @@ func testReorgWalletBalance(r *rpctest.Harness, w *lnwallet.LightningWallet,
PkScript: script, PkScript: script,
} }
tx, err := w.SendOutputs( tx, err := w.SendOutputs(
[]*wire.TxOut{output}, 2500, labels.External, []*wire.TxOut{output}, 2500, 1, labels.External,
) )
if err != nil { if err != nil {
t.Fatalf("unable to send outputs: %v", err) t.Fatalf("unable to send outputs: %v", err)
@ -2311,7 +2307,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
} }
bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey) bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey)
// We'll use a transaction fee of 13020 satoshis, which will allow us to // We'll use a transaction fee of 14380 satoshis, which will allow us to
// sweep all of Alice's balance in one transaction containing 1 input // sweep all of Alice's balance in one transaction containing 1 input
// and 1 output. // and 1 output.
// //
@ -2323,7 +2319,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: int64(aliceBalance - txFee), Value: int64(aliceBalance - txFee),
PkScript: bobPkScript, PkScript: bobPkScript,
} }
tx := sendCoins(t, r, alice, bob, output, txFeeRate) tx := sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash := tx.TxHash() txHash := tx.TxHash()
assertTxInWallet(t, alice, txHash, true) assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true) assertTxInWallet(t, bob, txHash, true)
@ -2345,7 +2341,7 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: btcutil.SatoshiPerBitcoin, Value: btcutil.SatoshiPerBitcoin,
PkScript: alicePkScript, PkScript: alicePkScript,
} }
tx = sendCoins(t, r, bob, alice, output, txFeeRate) tx = sendCoins(t, r, bob, alice, output, txFeeRate, true, 1)
txHash = tx.TxHash() txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true) assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true) assertTxInWallet(t, bob, txHash, true)
@ -2357,14 +2353,14 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
Value: btcutil.SatoshiPerBitcent, Value: btcutil.SatoshiPerBitcent,
PkScript: bobPkScript, PkScript: bobPkScript,
} }
tx = sendCoins(t, r, alice, bob, output, txFeeRate) tx = sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash() txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true) assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true) assertTxInWallet(t, bob, txHash, true)
// Then, we'll spend the change output and ensure we see its // Then, we'll spend the change output and ensure we see its
// confirmation come in. // confirmation come in.
tx = sendCoins(t, r, alice, bob, output, txFeeRate) tx = sendCoins(t, r, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash() txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true) assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true) assertTxInWallet(t, bob, txHash, true)
@ -2376,6 +2372,101 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
} }
} }
// testSpendUnconfirmed ensures that when can spend unconfirmed outputs.
func testSpendUnconfirmed(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) {
bobPkScript := newPkScript(t, bob, lnwallet.WitnessPubKey)
alicePkScript := newPkScript(t, alice, lnwallet.WitnessPubKey)
txFeeRate := chainfee.SatPerKWeight(2500)
// First we will empty out bob's wallet, sending the entire balance
// to alice.
bobBalance, err := bob.ConfirmedBalance(0)
if err != nil {
t.Fatalf("unable to retrieve bob's balance: %v", err)
}
txFee := btcutil.Amount(28760)
output := &wire.TxOut{
Value: int64(bobBalance - txFee),
PkScript: alicePkScript,
}
tx := sendCoins(t, miner, bob, alice, output, txFeeRate, true, 1)
txHash := tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
// Verify that bob doesn't have enough balance to send coins.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin * 0.5,
PkScript: alicePkScript,
}
_, err = bob.SendOutputs(
[]*wire.TxOut{output}, txFeeRate, 0, labels.External,
)
if err == nil {
t.Fatalf("should have not been able to pay due to insufficient balance: %v", err)
}
// Next we will send a transaction to bob but leave it in an
// unconfirmed state.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin,
PkScript: bobPkScript,
}
tx = sendCoins(t, miner, alice, bob, output, txFeeRate, false, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, false)
assertTxInWallet(t, bob, txHash, false)
// Now, try to spend some of the unconfirmed funds from bob's wallet.
output = &wire.TxOut{
Value: btcutil.SatoshiPerBitcoin * 0.5,
PkScript: alicePkScript,
}
// First, verify that we don't have enough balance to send the coins
// using confirmed outputs only.
_, err = bob.SendOutputs(
[]*wire.TxOut{output}, txFeeRate, 1, labels.External,
)
if err == nil {
t.Fatalf("should have not been able to pay due to insufficient balance: %v", err)
}
// Now try the send again using unconfirmed outputs.
tx = sendCoins(t, miner, bob, alice, output, txFeeRate, false, 0)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, false)
assertTxInWallet(t, bob, txHash, false)
// Mine the unconfirmed transactions.
err = waitForMempoolTx(miner, &txHash)
if err != nil {
t.Fatalf("tx not relayed to miner: %v", err)
}
if _, err := miner.Node.Generate(1); err != nil {
t.Fatalf("unable to generate block: %v", err)
}
if err := waitForWalletSync(miner, alice); err != nil {
t.Fatalf("unable to sync alice: %v", err)
}
if err := waitForWalletSync(miner, bob); err != nil {
t.Fatalf("unable to sync bob: %v", err)
}
// Finally, send the remainder of bob's wallet balance back to him so
// that these money movements dont mess up later tests.
output = &wire.TxOut{
Value: int64(bobBalance) - (btcutil.SatoshiPerBitcoin * 0.4),
PkScript: bobPkScript,
}
tx = sendCoins(t, miner, alice, bob, output, txFeeRate, true, 1)
txHash = tx.TxHash()
assertTxInWallet(t, alice, txHash, true)
assertTxInWallet(t, bob, txHash, true)
}
// testLastUnusedAddr tests that the LastUnusedAddress returns the address if // testLastUnusedAddr tests that the LastUnusedAddress returns the address if
// it isn't used, and also that once the address becomes used, then it's // it isn't used, and also that once the address becomes used, then it's
// properly rotated. // properly rotated.
@ -2418,7 +2509,7 @@ func testLastUnusedAddr(miner *rpctest.Harness,
Value: 1000000, Value: 1000000,
PkScript: addrScript, PkScript: addrScript,
} }
sendCoins(t, miner, bob, alice, output, feeRate) sendCoins(t, miner, bob, alice, output, feeRate, true, 1)
// If we make a new address, then it should be brand new, as // If we make a new address, then it should be brand new, as
// the prior address has been used. // the prior address has been used.
@ -2534,7 +2625,7 @@ func testCreateSimpleTx(r *rpctest.Harness, w *lnwallet.LightningWallet,
// _very_ similar to the one we just created being sent. The // _very_ similar to the one we just created being sent. The
// only difference is that the dry run tx is not signed, and // only difference is that the dry run tx is not signed, and
// that the change output position might be different. // that the change output position might be different.
tx, sendErr := w.SendOutputs(outputs, feeRate, labels.External) tx, sendErr := w.SendOutputs(outputs, feeRate, 1, labels.External)
switch { switch {
case test.valid && sendErr != nil: case test.valid && sendErr != nil:
t.Fatalf("got unexpected error when sending tx: %v", t.Fatalf("got unexpected error when sending tx: %v",
@ -2678,6 +2769,10 @@ var walletTests = []walletTestCase{
name: "change output spend confirmation", name: "change output spend confirmation",
test: testChangeOutputSpendConfirmation, test: testChangeOutputSpendConfirmation,
}, },
{
name: "spend unconfirmed outputs",
test: testSpendUnconfirmed,
},
{ {
name: "insane fee reject", name: "insane fee reject",
test: testReservationInitiatorBalanceBelowDustCancel, test: testReservationInitiatorBalanceBelowDustCancel,
@ -3310,6 +3405,10 @@ func runTests(t *testing.T, walletDriver *lnwallet.WalletDriver,
strings.Contains(walletTest.name, "dual funder") { strings.Contains(walletTest.name, "dual funder") {
t.Skip("skipping dual funder tests for neutrino") t.Skip("skipping dual funder tests for neutrino")
} }
if backEnd == "neutrino" &&
strings.Contains(walletTest.name, "spend unconfirmed") {
t.Skip("skipping spend unconfirmed tests for neutrino")
}
walletTest.test(miningNode, alice, bob, t) walletTest.test(miningNode, alice, bob, t)
}) })

View File

@ -1042,14 +1042,15 @@ func allowCORS(handler http.Handler, origins []string) http.Handler {
// more addresses specified in the passed payment map. The payment map maps an // more addresses specified in the passed payment map. The payment map maps an
// address to a specified output value to be sent to that address. // address to a specified output value to be sent to that address.
func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64, func (r *rpcServer) sendCoinsOnChain(paymentMap map[string]int64,
feeRate chainfee.SatPerKWeight, label string) (*chainhash.Hash, error) { feeRate chainfee.SatPerKWeight, minconf int32,
label string) (*chainhash.Hash, error) {
outputs, err := addrPairsToOutputs(paymentMap, r.cfg.ActiveNetParams.Params) outputs, err := addrPairsToOutputs(paymentMap, r.cfg.ActiveNetParams.Params)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tx, err := r.server.cc.wallet.SendOutputs(outputs, feeRate, label) tx, err := r.server.cc.wallet.SendOutputs(outputs, feeRate, minconf, label)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1180,8 +1181,16 @@ func (r *rpcServer) SendCoins(ctx context.Context,
return nil, err return nil, err
} }
rpcsLog.Infof("[sendcoins] addr=%v, amt=%v, sat/kw=%v, sweep_all=%v", // Then, we'll extract the minimum number of confirmations that each
in.Addr, btcutil.Amount(in.Amount), int64(feePerKw), // output we use to fund the transaction should satisfy.
minConfs, err := lnrpc.ExtractMinConfs(in.MinConfs, in.SpendUnconfirmed)
if err != nil {
return nil, err
}
rpcsLog.Infof("[sendcoins] addr=%v, amt=%v, sat/kw=%v, min_confs=%v, "+
"sweep_all=%v",
in.Addr, btcutil.Amount(in.Amount), int64(feePerKw), minConfs,
in.SendAll) in.SendAll)
// Decode the address receiving the coins, we need to check whether the // Decode the address receiving the coins, we need to check whether the
@ -1273,7 +1282,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
paymentMap := map[string]int64{targetAddr.String(): in.Amount} paymentMap := map[string]int64{targetAddr.String(): in.Amount}
err := wallet.WithCoinSelectLock(func() error { err := wallet.WithCoinSelectLock(func() error {
newTXID, err := r.sendCoinsOnChain( newTXID, err := r.sendCoinsOnChain(
paymentMap, feePerKw, label, paymentMap, feePerKw, minConfs, label,
) )
if err != nil { if err != nil {
return err return err
@ -1311,6 +1320,13 @@ func (r *rpcServer) SendMany(ctx context.Context,
return nil, err return nil, err
} }
// Then, we'll extract the minimum number of confirmations that each
// output we use to fund the transaction should satisfy.
minConfs, err := lnrpc.ExtractMinConfs(in.MinConfs, in.SpendUnconfirmed)
if err != nil {
return nil, err
}
label, err := labels.ValidateAPI(in.Label) label, err := labels.ValidateAPI(in.Label)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1327,7 +1343,7 @@ func (r *rpcServer) SendMany(ctx context.Context,
wallet := r.server.cc.wallet wallet := r.server.cc.wallet
err = wallet.WithCoinSelectLock(func() error { err = wallet.WithCoinSelectLock(func() error {
sendManyTXID, err := r.sendCoinsOnChain( sendManyTXID, err := r.sendCoinsOnChain(
in.AddrToAmount, feePerKw, label, in.AddrToAmount, feePerKw, minConfs, label,
) )
if err != nil { if err != nil {
return err return err
@ -1587,42 +1603,6 @@ func (r *rpcServer) DisconnectPeer(ctx context.Context,
return &lnrpc.DisconnectPeerResponse{}, nil return &lnrpc.DisconnectPeerResponse{}, nil
} }
// extractOpenChannelMinConfs extracts the minimum number of confirmations from
// the OpenChannelRequest that each output used to fund the channel's funding
// transaction should satisfy.
func extractOpenChannelMinConfs(in *lnrpc.OpenChannelRequest) (int32, error) {
switch {
// Ensure that the MinConfs parameter is non-negative.
case in.MinConfs < 0:
return 0, errors.New("minimum number of confirmations must " +
"be a non-negative number")
// The funding transaction should not be funded with unconfirmed outputs
// unless explicitly specified by SpendUnconfirmed. We do this to
// provide sane defaults to the OpenChannel RPC, as otherwise, if the
// MinConfs field isn't explicitly set by the caller, we'll use
// unconfirmed outputs without the caller being aware.
case in.MinConfs == 0 && !in.SpendUnconfirmed:
return 1, nil
// In the event that the caller set MinConfs > 0 and SpendUnconfirmed to
// true, we'll return an error to indicate the conflict.
case in.MinConfs > 0 && in.SpendUnconfirmed:
return 0, errors.New("SpendUnconfirmed set to true with " +
"MinConfs > 0")
// The funding transaction of the new channel to be created can be
// funded with unconfirmed outputs.
case in.SpendUnconfirmed:
return 0, nil
// If none of the above cases matched, we'll return the value set
// explicitly by the caller.
default:
return in.MinConfs, nil
}
}
// newFundingShimAssembler returns a new fully populated // newFundingShimAssembler returns a new fully populated
// chanfunding.CannedAssembler using a FundingShim obtained from an RPC caller. // chanfunding.CannedAssembler using a FundingShim obtained from an RPC caller.
func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim, initiator bool, func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim, initiator bool,
@ -1832,7 +1812,7 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
// Then, we'll extract the minimum number of confirmations that each // Then, we'll extract the minimum number of confirmations that each
// output we use to fund the channel's funding transaction should // output we use to fund the channel's funding transaction should
// satisfy. // satisfy.
minConfs, err := extractOpenChannelMinConfs(in) minConfs, err := lnrpc.ExtractMinConfs(in.MinConfs, in.SpendUnconfirmed)
if err != nil { if err != nil {
return nil, err return nil, err
} }