Add listunspent RPC call

Returns a brief json summary of each utxo found by calling
ListUnspentWitness in the wallet. The two arguments are the
minimum and maximum number of conrfirmations (0=include
unconfirmed)
This commit is contained in:
AdamISZ 2018-09-27 15:49:44 +02:00
parent 71444e74ac
commit 9bb2a26948
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
12 changed files with 1076 additions and 558 deletions

View File

@ -120,12 +120,12 @@ func newAddress(ctx *cli.Context) error {
// Map the string encoded address type, to the concrete typed address
// type enum. An unrecognized address type will result in an error.
var addrType lnrpc.NewAddressRequest_AddressType
var addrType lnrpc.AddressType
switch stringAddrType { // TODO(roasbeef): make them ints on the cli?
case "p2wkh":
addrType = lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH
addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
case "np2wkh":
addrType = lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH
addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
default:
return fmt.Errorf("invalid address type %v, support address type "+
"are: p2wkh and np2wkh", stringAddrType)
@ -242,6 +242,101 @@ func sendCoins(ctx *cli.Context) error {
return nil
}
var listUnspentCommand = cli.Command{
Name: "listunspent",
Category: "On-chain",
Usage: "List utxos available for spending.",
ArgsUsage: "min-confs max-confs",
Description: `
For each spendable utxo currently in the wallet, with at least min_confs
confirmations, and at most max_confs confirmations, lists the txid, index,
amount, address, address type, scriptPubkey and number of confirmations.
Use --min_confs=0 to include unconfirmed coins. To list all coins
with at least min_confs confirmations, omit the second argument or flag
'--max_confs'.
`,
Flags: []cli.Flag{
cli.Int64Flag{
Name: "min_confs",
Usage: "the minimum number of confirmations for a utxo",
},
cli.Int64Flag{
Name: "max_confs",
Usage: "the maximum number of confirmations for a utxo",
},
},
Action: actionDecorator(listUnspent),
}
func listUnspent(ctx *cli.Context) error {
var (
minConfirms int64
maxConfirms int64
err error
)
args := ctx.Args()
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
cli.ShowCommandHelp(ctx, "listunspent")
return nil
}
if ctx.IsSet("max_confs") && !ctx.IsSet("min_confs") {
return fmt.Errorf("max_confs cannot be set without " +
"min_confs being set")
}
switch {
case ctx.IsSet("min_confs"):
minConfirms = ctx.Int64("min_confs")
case args.Present():
minConfirms, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
cli.ShowCommandHelp(ctx, "listunspent")
return nil
}
args = args.Tail()
default:
return fmt.Errorf("minimum confirmations argument missing")
}
switch {
case ctx.IsSet("max_confs"):
maxConfirms = ctx.Int64("max_confs")
case args.Present():
maxConfirms, err = strconv.ParseInt(args.First(), 10, 64)
if err != nil {
cli.ShowCommandHelp(ctx, "listunspent")
return nil
}
args = args.Tail()
default:
// No maxconfs was specified; we use max as flag;
// the default for ctx.Int64 (0) is *not* appropriate here.
maxConfirms = math.MaxInt32
}
if minConfirms < 0 || maxConfirms < minConfirms {
return fmt.Errorf("maximum confirmations must be greater or " +
"equal to minimum confirmations")
}
ctxb := context.Background()
client, cleanUp := getClient(ctx)
defer cleanUp()
req := &lnrpc.ListUnspentRequest{
MinConfs: int32(minConfirms),
MaxConfs: int32(maxConfirms),
}
jsonResponse, err := client.ListUnspent(ctxb, req)
if err != nil {
return err
}
printRespJSON(jsonResponse)
return nil
}
var sendManyCommand = cli.Command{
Name: "sendmany",
Category: "On-chain",

View File

@ -256,6 +256,7 @@ func main() {
newAddressCommand,
sendManyCommand,
sendCoinsCommand,
listUnspentCommand,
connectCommand,
disconnectCommand,
openChannelCommand,

View File

@ -599,8 +599,8 @@ func getChanInfo(ctx context.Context, node *lntest.HarnessNode) (
}
const (
AddrTypeWitnessPubkeyHash = lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH
AddrTypeNestedPubkeyHash = lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH
AddrTypeWitnessPubkeyHash = lnrpc.AddressType_WITNESS_PUBKEY_HASH
AddrTypeNestedPubkeyHash = lnrpc.AddressType_NESTED_PUBKEY_HASH
)
// testOnchainFundRecovery checks lnd's ability to rescan for onchain outputs

View File

@ -29,6 +29,8 @@ description):
`lnd`.
* SendCoins
* Sends an amount of satoshis to a specific address.
* ListUnspent
* Lists available utxos within a range of confirmations.
* SubscribeTransactions
* Returns a stream which sends async notifications each time a transaction
is created or one is received that pays to us.

File diff suppressed because it is too large Load Diff

View File

@ -124,6 +124,23 @@ func request_Lightning_SendCoins_0(ctx context.Context, marshaler runtime.Marsha
}
var (
filter_Lightning_ListUnspent_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
func request_Lightning_ListUnspent_0(ctx context.Context, marshaler runtime.Marshaler, client LightningClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var protoReq ListUnspentRequest
var metadata runtime.ServerMetadata
if err := runtime.PopulateQueryParameters(&protoReq, req.URL.Query(), filter_Lightning_ListUnspent_0); err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := client.ListUnspent(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
var (
filter_Lightning_NewAddress_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)}
)
@ -1018,6 +1035,35 @@ func RegisterLightningHandler(ctx context.Context, mux *runtime.ServeMux, conn *
})
mux.Handle("GET", pattern_Lightning_ListUnspent_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if cn, ok := w.(http.CloseNotifier); ok {
go func(done <-chan struct{}, closed <-chan bool) {
select {
case <-done:
case <-closed:
cancel()
}
}(ctx.Done(), cn.CloseNotify())
}
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
rctx, err := runtime.AnnotateContext(ctx, mux, req)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_Lightning_ListUnspent_0(rctx, inboundMarshaler, client, req, pathParams)
ctx = runtime.NewServerMetadataContext(ctx, md)
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
forward_Lightning_ListUnspent_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle("GET", pattern_Lightning_NewAddress_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
@ -1900,6 +1946,8 @@ var (
pattern_Lightning_SendCoins_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "transactions"}, ""))
pattern_Lightning_ListUnspent_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "utxos"}, ""))
pattern_Lightning_NewAddress_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "newaddress"}, ""))
pattern_Lightning_SignMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"v1", "signmessage"}, ""))
@ -1970,6 +2018,8 @@ var (
forward_Lightning_SendCoins_0 = runtime.ForwardResponseMessage
forward_Lightning_ListUnspent_0 = runtime.ForwardResponseMessage
forward_Lightning_NewAddress_0 = runtime.ForwardResponseMessage
forward_Lightning_SignMessage_0 = runtime.ForwardResponseMessage

View File

@ -231,6 +231,16 @@ service Lightning {
};
}
/** lncli: `listunspent`
ListUnspent returns a list of all utxos spendable by the wallet with a
number of confirmations between the specified minimum and maximum.
*/
rpc ListUnspent (ListUnspentRequest) returns (ListUnspentResponse) {
option (google.api.http) = {
get: "/v1/utxos"
};
}
/**
SubscribeTransactions creates a uni-directional stream from the server to
the client in which any newly discovered transactions relevant to the
@ -657,6 +667,28 @@ service Lightning {
};
}
message Utxo {
/// The type of address
AddressType type = 1 [json_name = "address_type"];
/// The address
string address = 2 [json_name = "address"];
/// The value of the unspent coin in satoshis
int64 amount_sat = 3 [json_name = "amount_sat"];
/// The scriptpubkey in hex
string script_pubkey = 4 [json_name = "script_pubkey"];
/// The outpoint in format txid:n
/// Note that this reuses the `ChannelPoint` message but
/// is not actually a channel related outpoint, of course
ChannelPoint outpoint = 5 [json_name = "outpoint"];
/// The number of confirmations for the Utxo
int64 confirmations = 6 [json_name = "confirmations"];
}
message Transaction {
/// The transaction hash
string tx_hash = 1 [ json_name = "tx_hash" ];
@ -808,18 +840,31 @@ message SendCoinsResponse {
string txid = 1 [json_name = "txid"];
}
message ListUnspentRequest {
/// The minimum number of confirmations to be included.
int32 min_confs = 1;
/// The maximum number of confirmations to be included.
int32 max_confs = 2;
}
message ListUnspentResponse {
/// A list of utxos
repeated Utxo utxos = 1 [json_name = "utxos"];
}
/**
`AddressType` has to be one of:
- `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0)
- `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1)
*/
message NewAddressRequest {
enum AddressType {
enum AddressType {
WITNESS_PUBKEY_HASH = 0;
NESTED_PUBKEY_HASH = 1;
}
}
message NewAddressRequest {
/// The address type
AddressType type = 1;
}

View File

@ -1051,6 +1051,41 @@
]
}
},
"/v1/utxos": {
"get": {
"summary": "* lncli: `listunspent`\nListUnspent returns a list of all utxos spendable by the wallet with a\nnumber of confirmations between the specified minimum and maximum.",
"operationId": "ListUnspent",
"responses": {
"200": {
"description": "",
"schema": {
"$ref": "#/definitions/lnrpcListUnspentResponse"
}
}
},
"parameters": [
{
"name": "min_confs",
"description": "/ The minimum number of confirmations to be included.",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
},
{
"name": "max_confs",
"description": "/ The maximum number of confirmations to be included.",
"in": "query",
"required": false,
"type": "integer",
"format": "int32"
}
],
"tags": [
"Lightning"
]
}
},
"/v1/verifymessage": {
"post": {
"summary": "* lncli: `verifymessage`\nVerifyMessage verifies a signature over a msg. The signature must be\nzbase32 encoded and signed by an active node in the resident node's\nchannel database. In addition to returning the validity of the signature,\nVerifyMessage also returns the recovered pubkey from the signature.",
@ -1231,6 +1266,16 @@
}
}
},
"lnrpcAddressType": {
"type": "string",
"enum": [
"WITNESS_PUBKEY_HASH",
"NESTED_PUBKEY_HASH"
],
"default": "WITNESS_PUBKEY_HASH",
"description": "- `p2wkh`: Pay to witness key hash (`WITNESS_PUBKEY_HASH` = 0)\n- `np2wkh`: Pay to nested witness key hash (`NESTED_PUBKEY_HASH` = 1)",
"title": "* \n`AddressType` has to be one of:"
},
"lnrpcChangePasswordRequest": {
"type": "object",
"properties": {
@ -2194,6 +2239,18 @@
}
}
},
"lnrpcListUnspentResponse": {
"type": "object",
"properties": {
"utxos": {
"type": "array",
"items": {
"$ref": "#/definitions/lnrpcUtxo"
},
"title": "/ A list of utxos"
}
}
},
"lnrpcNetworkInfo": {
"type": "object",
"properties": {
@ -2919,6 +2976,37 @@
"lnrpcUnlockWalletResponse": {
"type": "object"
},
"lnrpcUtxo": {
"type": "object",
"properties": {
"type": {
"$ref": "#/definitions/lnrpcAddressType",
"title": "/ The type of address"
},
"address": {
"type": "string",
"title": "/ The address"
},
"amount_sat": {
"type": "string",
"format": "int64",
"title": "/ The value of the unspent coin in satoshis"
},
"script_pubkey": {
"type": "string",
"title": "/ The scriptpubkey in hex"
},
"outpoint": {
"$ref": "#/definitions/lnrpcChannelPoint",
"title": "/ The outpoint in format txid:n\n/ Note that this reuses the `ChannelPoint` message but\n/ is not actually a channel related outpoint, of course"
},
"confirmations": {
"type": "string",
"format": "int64",
"title": "/ The number of confirmations for the Utxo"
}
}
},
"lnrpcVerifyMessageRequest": {
"type": "object",
"properties": {

View File

@ -173,7 +173,7 @@ func (n *NetworkHarness) SetUp(lndArgs []string) error {
// each.
ctxb := context.Background()
addrReq := &lnrpc.NewAddressRequest{
Type: lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH,
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
}
clients := []lnrpc.LightningClient{n.Alice, n.Bob}
for _, client := range clients {
@ -1200,7 +1200,7 @@ func (n *NetworkHarness) SendCoins(ctx context.Context, amt btcutil.Amount,
target *HarnessNode) error {
return n.sendCoins(
ctx, amt, target, lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH,
ctx, amt, target, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
true,
)
}
@ -1212,7 +1212,7 @@ func (n *NetworkHarness) SendCoinsUnconfirmed(ctx context.Context,
amt btcutil.Amount, target *HarnessNode) error {
return n.sendCoins(
ctx, amt, target, lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH,
ctx, amt, target, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
false,
)
}
@ -1223,7 +1223,7 @@ func (n *NetworkHarness) SendCoinsNP2WKH(ctx context.Context,
amt btcutil.Amount, target *HarnessNode) error {
return n.sendCoins(
ctx, amt, target, lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH,
ctx, amt, target, lnrpc.AddressType_NESTED_PUBKEY_HASH,
true,
)
}
@ -1232,7 +1232,7 @@ func (n *NetworkHarness) SendCoinsNP2WKH(ctx context.Context,
// targeted lightning node. The confirmed boolean indicates whether the
// transaction that pays to the target should confirm.
func (n *NetworkHarness) sendCoins(ctx context.Context, amt btcutil.Amount,
target *HarnessNode, addrType lnrpc.NewAddressRequest_AddressType,
target *HarnessNode, addrType lnrpc.AddressType,
confirmed bool) error {
balReq := &lnrpc.WalletBalanceRequest{}

View File

@ -349,6 +349,7 @@ func (b *BtcWallet) ListUnspentWitness(minConfs, maxConfs int32) (
Hash: *txid,
Index: output.Vout,
},
Confirmations: output.Confirmations,
}
witnessOutputs = append(witnessOutputs, utxo)
}

View File

@ -52,6 +52,7 @@ var (
type Utxo struct {
AddressType AddressType
Value btcutil.Amount
Confirmations int64
PkScript []byte
RedeemScript []byte
WitnessScript []byte

View File

@ -163,6 +163,10 @@ var (
Entity: "onchain",
Action: "write",
}},
"/lnrpc.Lightning/ListUnspent": {{
Entity: "onchain",
Action: "read",
}},
"/lnrpc.Lightning/SendMany": {{
Entity: "onchain",
Action: "write",
@ -680,6 +684,87 @@ func determineFeePerKw(feeEstimator lnwallet.FeeEstimator, targetConf int32,
}
}
// ListUnspent returns useful information about each unspent output
// owned by the wallet, as reported by the underlying `ListUnspentWitness`;
// the information returned is: outpoint, amount in satoshis, address,
// address type, scriptPubKey in hex and number of confirmations.
// The result is filtered to contain outputs whose number of confirmations
// is between a minimum and maximum number of confirmations specified by the
// user, with 0 meaning unconfirmed.
func (r *rpcServer) ListUnspent(ctx context.Context,
in *lnrpc.ListUnspentRequest) (*lnrpc.ListUnspentResponse, error) {
minConfs := in.MinConfs
maxConfs := in.MaxConfs
if minConfs < 0 {
return nil, fmt.Errorf("min confirmations must be >= 0")
}
if minConfs > maxConfs {
return nil, fmt.Errorf("max confirmations must be >= min " +
"confirmations")
}
utxos, err := r.server.cc.wallet.ListUnspentWitness(minConfs, maxConfs)
if err != nil {
return nil, err
}
resp := &lnrpc.ListUnspentResponse{Utxos: []*lnrpc.Utxo{}}
for _, utxo := range utxos {
// Translate lnwallet address type to gRPC proto address type:
var addrType lnrpc.AddressType
switch utxo.AddressType {
case lnwallet.WitnessPubKey:
addrType = lnrpc.AddressType_WITNESS_PUBKEY_HASH
case lnwallet.NestedWitnessPubKey:
addrType = lnrpc.AddressType_NESTED_PUBKEY_HASH
case lnwallet.UnknownAddressType:
rpcsLog.Warnf("[listunspent] utxo with address of unknown "+
"type ignored: %v", utxo.OutPoint.String())
continue
default:
return nil, fmt.Errorf("invalid utxo address type")
}
outpoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{
FundingTxidStr: utxo.OutPoint.Hash.String(),
},
OutputIndex: utxo.OutPoint.Index,
}
utxoResp := lnrpc.Utxo{
Type: addrType,
AmountSat: int64(utxo.Value),
ScriptPubkey: hex.EncodeToString(utxo.PkScript),
Outpoint: outpoint,
Confirmations: utxo.Confirmations,
}
_, outAddresses, _, err := txscript.ExtractPkScriptAddrs(
utxo.PkScript, activeNetParams.Params)
if err != nil {
return nil, err
}
if len(outAddresses) != 1 {
return nil, fmt.Errorf("an output was unexpectedly multisig")
}
utxoResp.Address = outAddresses[0].String()
resp.Utxos = append(resp.Utxos, &utxoResp)
}
maxStr := ""
if maxConfs != math.MaxInt32 {
maxStr = " max=" + fmt.Sprintf("%d", maxConfs)
}
rpcsLog.Debugf("[listunspent] min=%v%v, generated utxos: %v",
minConfs, maxStr, utxos)
return resp, nil
}
// SendCoins executes a request to send coins to a particular address. Unlike
// SendMany, this RPC call only allows creating a single output at a time.
func (r *rpcServer) SendCoins(ctx context.Context,
@ -743,9 +828,9 @@ func (r *rpcServer) NewAddress(ctx context.Context,
// available address types.
var addrType lnwallet.AddressType
switch in.Type {
case lnrpc.NewAddressRequest_WITNESS_PUBKEY_HASH:
case lnrpc.AddressType_WITNESS_PUBKEY_HASH:
addrType = lnwallet.WitnessPubKey
case lnrpc.NewAddressRequest_NESTED_PUBKEY_HASH:
case lnrpc.AddressType_NESTED_PUBKEY_HASH:
addrType = lnwallet.NestedWitnessPubKey
}