Browse Source

lndclient: add router sub server

This commit exposes router sub server functionality to loop. This is a
preparation for using reliable payments in loop out.
pull/60/head
Joost Jager 2 years ago
parent
commit
f559120565
No known key found for this signature in database GPG Key ID: A61B9D4C393C59C7
5 changed files with 319 additions and 0 deletions
  1. +4
    -0
      lndclient/lnd_services.go
  2. +10
    -0
      lndclient/macaroon_pouch.go
  3. +238
    -0
      lndclient/router_client.go
  4. +24
    -0
      test/lnd_services_mock.go
  5. +43
    -0
      test/router_mock.go

+ 4
- 0
lndclient/lnd_services.go View File

@ -24,6 +24,7 @@ type LndServices struct {
ChainNotifier ChainNotifierClient
Signer SignerClient
Invoices InvoicesClient
Router RouterClient
ChainParams *chaincfg.Params
@ -121,6 +122,7 @@ func NewLndServices(lndAddress, application, network, macaroonDir,
signerClient := newSignerClient(conn, macaroons.signerMac)
walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac)
invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac)
routerClient := newRouterClient(conn, macaroons.routerMac)
cleanup := func() {
logger.Debugf("Closing lnd connection")
@ -145,6 +147,7 @@ func NewLndServices(lndAddress, application, network, macaroonDir,
ChainNotifier: notifierClient,
Signer: signerClient,
Invoices: invoicesClient,
Router: routerClient,
ChainParams: chainParams,
macaroons: macaroons,
},
@ -178,6 +181,7 @@ var (
defaultInvoiceMacaroonFilename = "invoices.macaroon"
defaultChainMacaroonFilename = "chainnotifier.macaroon"
defaultWalletKitMacaroonFilename = "walletkit.macaroon"
defaultRouterMacaroonFilename = "router.macaroon"
defaultSignerFilename = "signer.macaroon"
// maxMsgRecvSize is the largest gRPC message our client will receive.

+ 10
- 0
lndclient/macaroon_pouch.go View File

@ -48,6 +48,9 @@ type macaroonPouch struct {
// walletKitMac is the macaroon for the WalletKit sub-server.
walletKitMac serializedMacaroon
// routerMac is the macaroon for the router sub-server.
routerMac serializedMacaroon
// adminMac is the primary admin macaroon for lnd.
adminMac serializedMacaroon
}
@ -87,6 +90,13 @@ func newMacaroonPouch(macaroonDir string) (*macaroonPouch, error) {
return nil, err
}
m.routerMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultRouterMacaroonFilename),
)
if err != nil {
return nil, err
}
m.adminMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultAdminMacaroonFilename),
)

+ 238
- 0
lndclient/router_client.go View File

@ -0,0 +1,238 @@
package lndclient
import (
"context"
"encoding/hex"
"fmt"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/routing/route"
"time"
"github.com/lightningnetwork/lnd/channeldb"
"google.golang.org/grpc/codes"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnwire"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/lntypes"
)
// RouterClient exposes payment functionality.
type RouterClient interface {
// SendPayment attempts to route a payment to the final destination. The
// call returns a payment update stream and an error stream.
SendPayment(ctx context.Context, request SendPaymentRequest) (
chan PaymentStatus, chan error, error)
// TrackPayment picks up a previously started payment and returns a
// payment update stream and an error stream.
TrackPayment(ctx context.Context, hash lntypes.Hash) (
chan PaymentStatus, chan error, error)
}
// PaymentStatus describe the state of a payment.
type PaymentStatus struct {
State routerrpc.PaymentState
Preimage lntypes.Preimage
Fee lnwire.MilliSatoshi
Route *route.Route
}
// SendPaymentRequest defines the payment parameters for a new payment.
type SendPaymentRequest struct {
Invoice string
MaxFee btcutil.Amount
MaxCltv *int32
OutgoingChannel *uint64
Timeout time.Duration
}
// routerClient is a wrapper around the generated routerrpc proxy.
type routerClient struct {
client routerrpc.RouterClient
routerKitMac serializedMacaroon
}
func newRouterClient(conn *grpc.ClientConn,
routerKitMac serializedMacaroon) *routerClient {
return &routerClient{
client: routerrpc.NewRouterClient(conn),
routerKitMac: routerKitMac,
}
}
// SendPayment attempts to route a payment to the final destination. The call
// returns a payment update stream and an error stream.
func (r *routerClient) SendPayment(ctx context.Context,
request SendPaymentRequest) (chan PaymentStatus, chan error, error) {
rpcCtx := r.routerKitMac.WithMacaroonAuth(ctx)
rpcReq := &routerrpc.SendPaymentRequest{
FeeLimitSat: int64(request.MaxFee),
PaymentRequest: request.Invoice,
TimeoutSeconds: int32(request.Timeout.Seconds()),
}
if request.MaxCltv != nil {
rpcReq.CltvLimit = *request.MaxCltv
}
if request.OutgoingChannel != nil {
rpcReq.OutgoingChanId = *request.OutgoingChannel
}
stream, err := r.client.SendPayment(rpcCtx, rpcReq)
if err != nil {
return nil, nil, err
}
return r.trackPayment(ctx, stream)
}
// TrackPayment picks up a previously started payment and returns a payment
// update stream and an error stream.
func (r *routerClient) TrackPayment(ctx context.Context,
hash lntypes.Hash) (chan PaymentStatus, chan error, error) {
ctx = r.routerKitMac.WithMacaroonAuth(ctx)
stream, err := r.client.TrackPayment(
ctx, &routerrpc.TrackPaymentRequest{
PaymentHash: hash[:],
},
)
if err != nil {
return nil, nil, err
}
return r.trackPayment(ctx, stream)
}
// trackPayment takes an update stream from either a SendPayment or a
// TrackPayment rpc call and converts it into distinct update and error streams.
func (r *routerClient) trackPayment(ctx context.Context,
stream routerrpc.Router_TrackPaymentClient) (chan PaymentStatus,
chan error, error) {
statusChan := make(chan PaymentStatus)
errorChan := make(chan error, 1)
go func() {
for {
rpcStatus, err := stream.Recv()
if err != nil {
switch status.Convert(err).Code() {
// NotFound is only expected as a response to
// TrackPayment.
case codes.NotFound:
err = channeldb.ErrPaymentNotInitiated
// NotFound is only expected as a response to
// SendPayment.
case codes.AlreadyExists:
err = channeldb.ErrAlreadyPaid
}
errorChan <- err
return
}
status, err := unmarshallPaymentStatus(rpcStatus)
if err != nil {
errorChan <- err
return
}
select {
case statusChan <- *status:
case <-ctx.Done():
return
}
}
}()
return statusChan, errorChan, nil
}
// unmarshallPaymentStatus converts an rpc status update to the PaymentStatus
// type that is used throughout the application.
func unmarshallPaymentStatus(rpcStatus *routerrpc.PaymentStatus) (
*PaymentStatus, error) {
status := PaymentStatus{
State: rpcStatus.State,
}
if status.State == routerrpc.PaymentState_SUCCEEDED {
preimage, err := lntypes.MakePreimage(
rpcStatus.Preimage,
)
if err != nil {
return nil, err
}
status.Preimage = preimage
status.Fee = lnwire.MilliSatoshi(
rpcStatus.Route.TotalFeesMsat,
)
if rpcStatus.Route != nil {
route, err := unmarshallRoute(rpcStatus.Route)
if err != nil {
return nil, err
}
status.Route = route
}
}
return &status, nil
}
// unmarshallRoute unmarshalls an rpc route.
func unmarshallRoute(rpcroute *lnrpc.Route) (
*route.Route, error) {
hops := make([]*route.Hop, len(rpcroute.Hops))
for i, hop := range rpcroute.Hops {
routeHop, err := unmarshallHop(hop)
if err != nil {
return nil, err
}
hops[i] = routeHop
}
// TODO(joostjager): Fetch self node from lnd.
selfNode := route.Vertex{}
route, err := route.NewRouteFromHops(
lnwire.MilliSatoshi(rpcroute.TotalAmtMsat),
rpcroute.TotalTimeLock,
selfNode,
hops,
)
if err != nil {
return nil, err
}
return route, nil
}
// unmarshallKnownPubkeyHop unmarshalls an rpc hop.
func unmarshallHop(hop *lnrpc.Hop) (*route.Hop, error) {
pubKey, err := hex.DecodeString(hop.PubKey)
if err != nil {
return nil, fmt.Errorf("cannot decode pubkey %s", hop.PubKey)
}
var pubKeyBytes [33]byte
copy(pubKeyBytes[:], pubKey)
return &route.Hop{
OutgoingTimeLock: hop.Expiry,
AmtToForward: lnwire.MilliSatoshi(hop.AmtToForwardMsat),
PubKeyBytes: pubKeyBytes,
ChannelID: hop.ChanId,
}, nil
}

+ 24
- 0
test/lnd_services_mock.go View File

@ -24,6 +24,7 @@ func NewMockLnd() *LndMockServices {
chainNotifier := &mockChainNotifier{}
signer := &mockSigner{}
invoices := &mockInvoices{}
router := &mockRouter{}
lnd := LndMockServices{
LndServices: lndclient.LndServices{
@ -32,6 +33,7 @@ func NewMockLnd() *LndMockServices {
ChainNotifier: chainNotifier,
Signer: signer,
Invoices: invoices,
Router: router,
ChainParams: &chaincfg.TestNet3Params,
},
SendPaymentChannel: make(chan PaymentChannelMessage),
@ -44,6 +46,9 @@ func NewMockLnd() *LndMockServices {
SettleInvoiceChannel: make(chan lntypes.Preimage),
SingleInvoiceSubcribeChannel: make(chan *SingleInvoiceSubscription),
RouterSendPaymentChannel: make(chan RouterPaymentChannelMessage),
TrackPaymentChannel: make(chan TrackPaymentMessage),
FailInvoiceChannel: make(chan lntypes.Hash, 2),
epochChannel: make(chan int32),
Height: testStartingHeight,
@ -53,6 +58,7 @@ func NewMockLnd() *LndMockServices {
chainNotifier.lnd = &lnd
walletKit.lnd = &lnd
invoices.lnd = &lnd
router.lnd = &lnd
lnd.WaitForFinished = func() {
chainNotifier.WaitForFinished()
@ -69,6 +75,21 @@ type PaymentChannelMessage struct {
Done chan lndclient.PaymentResult
}
// TrackPaymentMessage is the data that passed through TrackPaymentChannel.
type TrackPaymentMessage struct {
Hash lntypes.Hash
Updates chan lndclient.PaymentStatus
Errors chan error
}
// RouterPaymentChannelMessage is the data that passed through RouterSendPaymentChannel.
type RouterPaymentChannelMessage struct {
lndclient.SendPaymentRequest
TrackPaymentMessage
}
// SingleInvoiceSubscription contains the single invoice subscribers
type SingleInvoiceSubscription struct {
Hash lntypes.Hash
@ -94,6 +115,9 @@ type LndMockServices struct {
SingleInvoiceSubcribeChannel chan *SingleInvoiceSubscription
RouterSendPaymentChannel chan RouterPaymentChannelMessage
TrackPaymentChannel chan TrackPaymentMessage
Height int32
WaitForFinished func()

+ 43
- 0
test/router_mock.go View File

@ -0,0 +1,43 @@
package test
import (
"github.com/lightninglabs/loop/lndclient"
"github.com/lightningnetwork/lnd/lntypes"
"golang.org/x/net/context"
)
type mockRouter struct {
lnd *LndMockServices
}
func (r *mockRouter) SendPayment(ctx context.Context,
request lndclient.SendPaymentRequest) (chan lndclient.PaymentStatus,
chan error, error) {
statusChan := make(chan lndclient.PaymentStatus)
errorChan := make(chan error)
r.lnd.RouterSendPaymentChannel <- RouterPaymentChannelMessage{
SendPaymentRequest: request,
TrackPaymentMessage: TrackPaymentMessage{
Updates: statusChan,
Errors: errorChan,
},
}
return statusChan, errorChan, nil
}
func (r *mockRouter) TrackPayment(ctx context.Context,
hash lntypes.Hash) (chan lndclient.PaymentStatus, chan error, error) {
statusChan := make(chan lndclient.PaymentStatus)
errorChan := make(chan error)
r.lnd.TrackPaymentChannel <- TrackPaymentMessage{
Hash: hash,
Updates: statusChan,
Errors: errorChan,
}
return statusChan, errorChan, nil
}

Loading…
Cancel
Save