invoicesrpc: add SubscribeSingleInvoice rpc

This commit is contained in:
Joost Jager 2019-01-03 19:15:14 +01:00
parent 4c4536a488
commit 70c874be88
No known key found for this signature in database
GPG Key ID: A61B9D4C393C59C7
7 changed files with 241 additions and 19 deletions

15
lnrpc/file_utils.go Normal file

@ -0,0 +1,15 @@
package lnrpc
import (
"os"
)
// FileExists reports whether the named file or directory exists.
func FileExists(name string) bool {
if _, err := os.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}

@ -3,7 +3,9 @@
package invoicesrpc package invoicesrpc
import ( import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/macaroons"
) )
// Config is the primary configuration struct for the invoices RPC server. It // Config is the primary configuration struct for the invoices RPC server. It
@ -12,5 +14,19 @@ import (
// configuration options, while if able to be populated, the latter fields MUST // configuration options, while if able to be populated, the latter fields MUST
// also be specified. // also be specified.
type Config struct { type Config struct {
// NetworkDir is the main network directory wherein the invoices rpc
// server will find the macaroon named DefaultInvoicesMacFilename.
NetworkDir string
// MacService is the main macaroon service that we'll use to handle
// authentication for the invoices rpc server.
MacService *macaroons.Service
// InvoiceRegistry is a central registry of all the outstanding invoices
// created by the daemon.
InvoiceRegistry *invoices.InvoiceRegistry InvoiceRegistry *invoices.InvoiceRegistry
// ChainParams are required to properly decode invoice payment requests
// that are marshalled over rpc.
ChainParams *chaincfg.Params
} }

@ -6,6 +6,8 @@ package invoicesrpc // import "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc
import proto "github.com/golang/protobuf/proto" import proto "github.com/golang/protobuf/proto"
import fmt "fmt" import fmt "fmt"
import math "math" import math "math"
import lnrpc "github.com/lightningnetwork/lnd/lnrpc"
import _ "google.golang.org/genproto/googleapis/api/annotations"
import ( import (
context "golang.org/x/net/context" context "golang.org/x/net/context"
@ -35,6 +37,11 @@ const _ = grpc.SupportPackageIsVersion4
// //
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type InvoicesClient interface { type InvoicesClient interface {
// *
// SubscribeSingleInvoice returns a uni-directional stream (server -> client)
// to notify the client of state transitions of the specified invoice.
// Initially the current invoice state is always sent out.
SubscribeSingleInvoice(ctx context.Context, in *lnrpc.PaymentHash, opts ...grpc.CallOption) (Invoices_SubscribeSingleInvoiceClient, error)
} }
type invoicesClient struct { type invoicesClient struct {
@ -45,33 +52,102 @@ func NewInvoicesClient(cc *grpc.ClientConn) InvoicesClient {
return &invoicesClient{cc} return &invoicesClient{cc}
} }
func (c *invoicesClient) SubscribeSingleInvoice(ctx context.Context, in *lnrpc.PaymentHash, opts ...grpc.CallOption) (Invoices_SubscribeSingleInvoiceClient, error) {
stream, err := c.cc.NewStream(ctx, &_Invoices_serviceDesc.Streams[0], "/invoicesrpc.Invoices/SubscribeSingleInvoice", opts...)
if err != nil {
return nil, err
}
x := &invoicesSubscribeSingleInvoiceClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type Invoices_SubscribeSingleInvoiceClient interface {
Recv() (*lnrpc.Invoice, error)
grpc.ClientStream
}
type invoicesSubscribeSingleInvoiceClient struct {
grpc.ClientStream
}
func (x *invoicesSubscribeSingleInvoiceClient) Recv() (*lnrpc.Invoice, error) {
m := new(lnrpc.Invoice)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// InvoicesServer is the server API for Invoices service. // InvoicesServer is the server API for Invoices service.
type InvoicesServer interface { type InvoicesServer interface {
// *
// SubscribeSingleInvoice returns a uni-directional stream (server -> client)
// to notify the client of state transitions of the specified invoice.
// Initially the current invoice state is always sent out.
SubscribeSingleInvoice(*lnrpc.PaymentHash, Invoices_SubscribeSingleInvoiceServer) error
} }
func RegisterInvoicesServer(s *grpc.Server, srv InvoicesServer) { func RegisterInvoicesServer(s *grpc.Server, srv InvoicesServer) {
s.RegisterService(&_Invoices_serviceDesc, srv) s.RegisterService(&_Invoices_serviceDesc, srv)
} }
func _Invoices_SubscribeSingleInvoice_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(lnrpc.PaymentHash)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(InvoicesServer).SubscribeSingleInvoice(m, &invoicesSubscribeSingleInvoiceServer{stream})
}
type Invoices_SubscribeSingleInvoiceServer interface {
Send(*lnrpc.Invoice) error
grpc.ServerStream
}
type invoicesSubscribeSingleInvoiceServer struct {
grpc.ServerStream
}
func (x *invoicesSubscribeSingleInvoiceServer) Send(m *lnrpc.Invoice) error {
return x.ServerStream.SendMsg(m)
}
var _Invoices_serviceDesc = grpc.ServiceDesc{ var _Invoices_serviceDesc = grpc.ServiceDesc{
ServiceName: "invoicesrpc.Invoices", ServiceName: "invoicesrpc.Invoices",
HandlerType: (*InvoicesServer)(nil), HandlerType: (*InvoicesServer)(nil),
Methods: []grpc.MethodDesc{}, Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{
Metadata: "invoicesrpc/invoices.proto", {
StreamName: "SubscribeSingleInvoice",
Handler: _Invoices_SubscribeSingleInvoice_Handler,
ServerStreams: true,
},
},
Metadata: "invoicesrpc/invoices.proto",
} }
func init() { func init() {
proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_560fa62749d29606) proto.RegisterFile("invoicesrpc/invoices.proto", fileDescriptor_invoices_c6414974947f2940)
} }
var fileDescriptor_invoices_560fa62749d29606 = []byte{ var fileDescriptor_invoices_c6414974947f2940 = []byte{
// 105 bytes of a gzipped FileDescriptorProto // 177 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0xca, 0xcc, 0x2b, 0xcb, 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x4c, 0x8e, 0xb1, 0x8e, 0xc2, 0x30,
0xcf, 0x4c, 0x4e, 0x2d, 0x2e, 0x2a, 0x48, 0xd6, 0x87, 0xb1, 0xf5, 0x0a, 0x8a, 0xf2, 0x4b, 0xf2, 0x10, 0x44, 0x75, 0xcd, 0xe9, 0x2e, 0x27, 0x5d, 0xe1, 0x82, 0xc2, 0xe2, 0x1b, 0xb2, 0x40, 0x7a,
0x85, 0xb8, 0x91, 0xe4, 0x8c, 0xb8, 0xb8, 0x38, 0x3c, 0xa1, 0x5c, 0x27, 0xe3, 0x28, 0xc3, 0xf4, 0x0a, 0x2a, 0xa0, 0x42, 0x4a, 0x47, 0x67, 0x1b, 0xcb, 0x59, 0xe1, 0xec, 0x5a, 0xce, 0x06, 0xc4,
0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, 0xe4, 0xfc, 0x5c, 0xfd, 0x9c, 0xcc, 0xf4, 0x8c, 0x92, 0xbc, 0xdf, 0x23, 0x82, 0x91, 0xd2, 0x8d, 0x66, 0xe6, 0x49, 0xaf, 0xd2, 0x48, 0x37, 0x46, 0xe7, 0x87,
0xcc, 0xbc, 0xf4, 0xbc, 0xd4, 0x92, 0xf2, 0xfc, 0xa2, 0x6c, 0xfd, 0x9c, 0xbc, 0x14, 0xfd, 0x9c, 0x9c, 0x1c, 0x7c, 0x72, 0x9d, 0x32, 0x0b, 0xab, 0xbf, 0xd9, 0xa6, 0x97, 0x81, 0x39, 0x44, 0x0f,
0x3c, 0x64, 0x03, 0x8b, 0x0a, 0x92, 0x93, 0xd8, 0xc0, 0x86, 0x1a, 0x03, 0x02, 0x00, 0x00, 0xff, 0x26, 0x21, 0x18, 0x22, 0x16, 0x23, 0xc8, 0x54, 0xae, 0xfa, 0x37, 0x27, 0xf7, 0x8e, 0x9b, 0x63,
0xff, 0xaa, 0xe0, 0xa9, 0x52, 0x72, 0x00, 0x00, 0x00, 0xf5, 0x73, 0x28, 0x9c, 0xda, 0x56, 0x8b, 0x76, 0xb4, 0x83, 0xcb, 0x68, 0x7d, 0x8b, 0x14, 0xa2,
0x2f, 0x93, 0x52, 0x75, 0xa4, 0x17, 0x73, 0x32, 0x8f, 0xde, 0x93, 0xec, 0xcd, 0xd0, 0xe9, 0xff,
0xd2, 0x95, 0xcf, 0xea, 0x6b, 0xd7, 0x9c, 0xd7, 0x01, 0xa5, 0x1b, 0x6d, 0xed, 0xb8, 0x87, 0x88,
0xa1, 0x13, 0x42, 0x0a, 0xe4, 0xe5, 0xce, 0xf9, 0x0a, 0x91, 0x2e, 0x30, 0x21, 0x30, 0x33, 0xb5,
0xdf, 0x93, 0x47, 0xf3, 0x0c, 0x00, 0x00, 0xff, 0xff, 0xcf, 0x4e, 0x97, 0xf2, 0xdb, 0x00, 0x00,
0x00,
} }

@ -1,5 +1,8 @@
syntax = "proto3"; syntax = "proto3";
import "google/api/annotations.proto";
import "rpc.proto";
package invoicesrpc; package invoicesrpc;
option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"; option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc";
@ -7,5 +10,11 @@ option go_package = "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc";
// Invoices is a service that can be used to create, accept, settle and cancel // Invoices is a service that can be used to create, accept, settle and cancel
// invoices. // invoices.
service Invoices { service Invoices {
/**
SubscribeSingleInvoice returns a uni-directional stream (server -> client)
to notify the client of state transitions of the specified invoice.
Initially the current invoice state is always sent out.
*/
rpc SubscribeSingleInvoice (lnrpc.PaymentHash) returns (stream lnrpc.Invoice);
} }

@ -3,9 +3,15 @@
package invoicesrpc package invoicesrpc
import ( import (
"github.com/lightningnetwork/lnd/lnrpc" "context"
"google.golang.org/grpc" "google.golang.org/grpc"
"gopkg.in/macaroon-bakery.v2/bakery" "gopkg.in/macaroon-bakery.v2/bakery"
"io/ioutil"
"os"
"path/filepath"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
) )
const ( const (
@ -17,8 +23,27 @@ const (
) )
var ( var (
// macaroonOps are the set of capabilities that our minted macaroon (if
// it doesn't already exist) will have.
macaroonOps = []bakery.Op{
{
Entity: "invoices",
Action: "read",
},
}
// macPermissions maps RPC calls to the permissions they require. // macPermissions maps RPC calls to the permissions they require.
macPermissions = map[string][]bakery.Op{} macPermissions = map[string][]bakery.Op{
"/invoicesrpc.Invoices/SubscribeSingleInvoice": {{
Entity: "invoices",
Action: "read",
}},
}
// DefaultInvoicesMacFilename is the default name of the invoices
// macaroon that we expect to find via a file handle within the main
// configuration file in this package.
DefaultInvoicesMacFilename = "invoices.macaroon"
) )
// Server is a sub-server of the main RPC server: the invoices RPC. This sub // Server is a sub-server of the main RPC server: the invoices RPC. This sub
@ -28,6 +53,8 @@ type Server struct {
started int32 // To be used atomically. started int32 // To be used atomically.
shutdown int32 // To be used atomically. shutdown int32 // To be used atomically.
quit chan struct{}
cfg *Config cfg *Config
} }
@ -41,10 +68,42 @@ var _ InvoicesServer = (*Server)(nil)
// we'll create them on start up. If we're unable to locate, or create the // we'll create them on start up. If we're unable to locate, or create the
// macaroons we need, then we'll return with an error. // macaroons we need, then we'll return with an error.
func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) { func New(cfg *Config) (*Server, lnrpc.MacaroonPerms, error) {
// We don't create any new macaroons for this subserver, instead reuse // If the path of the invoices macaroon wasn't specified, then we'll
// existing onchain/offchain permissions. // assume that it's found at the default network directory.
macFilePath := filepath.Join(
cfg.NetworkDir, DefaultInvoicesMacFilename,
)
// Now that we know the full path of the invoices macaroon, we can
// check to see if we need to create it or not.
if !lnrpc.FileExists(macFilePath) && cfg.MacService != nil {
log.Infof("Baking macaroons for invoices RPC Server at: %v",
macFilePath)
// At this point, we know that the invoices macaroon doesn't
// yet, exist, so we need to create it with the help of the
// main macaroon service.
invoicesMac, err := cfg.MacService.Oven.NewMacaroon(
context.Background(), bakery.LatestVersion, nil,
macaroonOps...,
)
if err != nil {
return nil, nil, err
}
invoicesMacBytes, err := invoicesMac.M().MarshalBinary()
if err != nil {
return nil, nil, err
}
err = ioutil.WriteFile(macFilePath, invoicesMacBytes, 0644)
if err != nil {
os.Remove(macFilePath)
return nil, nil, err
}
}
server := &Server{ server := &Server{
cfg: cfg, cfg: cfg,
quit: make(chan struct{}, 1),
} }
return server, macPermissions, nil return server, macPermissions, nil
@ -61,6 +120,8 @@ func (s *Server) Start() error {
// //
// NOTE: This is part of the lnrpc.SubServer interface. // NOTE: This is part of the lnrpc.SubServer interface.
func (s *Server) Stop() error { func (s *Server) Stop() error {
close(s.quit)
return nil return nil
} }
@ -82,8 +143,41 @@ func (s *Server) RegisterWithRootServer(grpcServer *grpc.Server) error {
// all our methods are routed properly. // all our methods are routed properly.
RegisterInvoicesServer(grpcServer, s) RegisterInvoicesServer(grpcServer, s)
log.Debugf("Invoices RPC server successfully register with root " + log.Debugf("Invoices RPC server successfully registered with root " +
"gRPC server") "gRPC server")
return nil return nil
} }
// SubscribeInvoices returns a uni-directional stream (server -> client) for
// notifying the client of invoice state changes.
func (s *Server) SubscribeSingleInvoice(req *lnrpc.PaymentHash,
updateStream Invoices_SubscribeSingleInvoiceServer) error {
hash, err := lntypes.NewHash(req.RHash)
if err != nil {
return err
}
invoiceClient := s.cfg.InvoiceRegistry.SubscribeSingleInvoice(*hash)
defer invoiceClient.Cancel()
for {
select {
case newInvoice := <-invoiceClient.Updates:
rpcInvoice, err := CreateRPCInvoice(
newInvoice, s.cfg.ChainParams,
)
if err != nil {
return err
}
if err := updateStream.Send(rpcInvoice); err != nil {
return err
}
case <-s.quit:
return nil
}
}
}

@ -411,6 +411,7 @@ func newRPCServer(s *server, macService *macaroons.Service,
// server configuration struct. // server configuration struct.
err := subServerCgs.PopulateDependencies( err := subServerCgs.PopulateDependencies(
s.cc, networkDir, macService, atpl, invoiceRegistry, s.cc, networkDir, macService, atpl, invoiceRegistry,
activeNetParams.Params,
) )
if err != nil { if err != nil {
return nil, err return nil, err

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/autopilot" "github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/invoices" "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc" "github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
@ -54,7 +55,8 @@ type subRPCServerConfigs struct {
func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl, func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
networkDir string, macService *macaroons.Service, networkDir string, macService *macaroons.Service,
atpl *autopilot.Manager, atpl *autopilot.Manager,
invoiceRegistry *invoices.InvoiceRegistry) error { invoiceRegistry *invoices.InvoiceRegistry,
activeNetParams *chaincfg.Params) error {
// First, we'll use reflect to obtain a version of the config struct // First, we'll use reflect to obtain a version of the config struct
// that allows us to programmatically inspect its fields. // that allows us to programmatically inspect its fields.
@ -135,9 +137,18 @@ func (s *subRPCServerConfigs) PopulateDependencies(cc *chainControl,
case *invoicesrpc.Config: case *invoicesrpc.Config:
subCfgValue := extractReflectValue(cfg) subCfgValue := extractReflectValue(cfg)
subCfgValue.FieldByName("NetworkDir").Set(
reflect.ValueOf(networkDir),
)
subCfgValue.FieldByName("MacService").Set(
reflect.ValueOf(macService),
)
subCfgValue.FieldByName("InvoiceRegistry").Set( subCfgValue.FieldByName("InvoiceRegistry").Set(
reflect.ValueOf(invoiceRegistry), reflect.ValueOf(invoiceRegistry),
) )
subCfgValue.FieldByName("ChainParams").Set(
reflect.ValueOf(activeNetParams),
)
default: default:
return fmt.Errorf("unknown field: %v, %T", fieldName, return fmt.Errorf("unknown field: %v, %T", fieldName,