Browse Source

lndclient: move to github.com/lightninglabs/lndclient

pull/237/head
Joost Jager 4 months ago
parent
commit
0c9fcd790e
No known key found for this signature in database GPG Key ID: A61B9D4C393C59C7
42 changed files with 44 additions and 3702 deletions
  1. +1
    -1
      client.go
  2. +1
    -1
      client_test.go
  3. +1
    -1
      config.go
  4. +1
    -1
      executor.go
  5. +1
    -2
      go.mod
  6. +4
    -5
      go.sum
  7. +0
    -136
      lndclient/basic_client.go
  8. +0
    -239
      lndclient/chainnotifier_client.go
  9. +0
    -166
      lndclient/invoices_client.go
  10. +0
    -1221
      lndclient/lightning_client.go
  11. +0
    -463
      lndclient/lnd_services.go
  12. +0
    -158
      lndclient/lnd_services_test.go
  13. +0
    -23
      lndclient/log.go
  14. +0
    -118
      lndclient/macaroon_pouch.go
  15. +0
    -416
      lndclient/router_client.go
  16. +0
    -254
      lndclient/signer_client.go
  17. +0
    -68
      lndclient/versioner_client.go
  18. +0
    -349
      lndclient/walletkit_client.go
  19. +6
    -3
      loopd/daemon.go
  20. +1
    -1
      loopd/log.go
  21. +2
    -2
      loopd/run.go
  22. +1
    -1
      loopd/swapclient_server.go
  23. +1
    -1
      loopd/utils.go
  24. +8
    -5
      loopd/view.go
  25. +1
    -1
      loopin.go
  26. +1
    -1
      loopin_test.go
  27. +1
    -1
      loopout.go
  28. +1
    -1
      loopout_test.go
  29. +1
    -1
      lsat/interceptor.go
  30. +1
    -1
      lsat/interceptor_test.go
  31. +1
    -1
      swap.go
  32. +0
    -27
      swap/net.go
  33. +0
    -24
      swap/tx.go
  34. +1
    -1
      sweep/sweeper.go
  35. +1
    -1
      test/context.go
  36. +1
    -1
      test/invoices_mock.go
  37. +1
    -1
      test/lightning_client_mock.go
  38. +1
    -1
      test/lnd_services_mock.go
  39. +1
    -1
      test/router_mock.go
  40. +1
    -1
      test/versioner_mock.go
  41. +1
    -1
      test/walletkit_mock.go
  42. +1
    -1
      testcontext_test.go

+ 1
- 1
client.go View File

@ -9,7 +9,7 @@ import (
"time"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/lsat"
"github.com/lightninglabs/loop/swap"

+ 1
- 1
client_test.go View File

@ -9,7 +9,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lnrpc"

+ 1
- 1
config.go View File

@ -3,7 +3,7 @@ package loop
import (
"time"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/lsat"
)

+ 1
- 1
executor.go View File

@ -7,7 +7,7 @@ import (
"sync/atomic"
"time"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep"
"github.com/lightningnetwork/lnd/queue"

+ 1
- 2
go.mod View File

@ -8,16 +8,15 @@ require (
github.com/coreos/bbolt v1.3.3
github.com/fortytw2/leaktest v1.3.0
github.com/golang/protobuf v1.3.2
github.com/google/go-cmp v0.3.1 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.14.3
github.com/jessevdk/go-flags v1.4.0
github.com/lightninglabs/lndclient v0.0.0-20200618122423-5d815058a719
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d
github.com/lightningnetwork/lnd v0.10.0-beta.rc6.0.20200615174244-103c59a4889f
github.com/lightningnetwork/lnd/queue v1.0.4
github.com/stretchr/testify v1.5.1
github.com/urfave/cli v1.20.0
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0
golang.org/x/text v0.3.2 // indirect
google.golang.org/genproto v0.0.0-20190927181202-20e1ac93f88c
google.golang.org/grpc v1.24.0
gopkg.in/macaroon.v2 v2.1.0

+ 4
- 5
go.sum View File

@ -110,9 +110,8 @@ github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
@ -168,6 +167,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/lndclient v0.0.0-20200618122423-5d815058a719 h1:RzyO65pK78xxSaekERq2V3g32GMBPc0MfxmM8MDMVmQ=
github.com/lightninglabs/lndclient v0.0.0-20200618122423-5d815058a719/go.mod h1:WQU2oE0eJIp7jtBOUnL7CO5/YoWrAqyH23A6M1r31i8=
github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg=
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 h1:j4iZ1XlUAPQmW6oSzMcJGILYsRHNs+4O3Gk+2Ms5Dww=
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200/go.mod h1:MlZmoKa7CJP3eR1s5yB7Rm5aSyadpKkxqAwLQmog7N0=
@ -311,13 +312,11 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=

+ 0
- 136
lndclient/basic_client.go View File

@ -1,136 +0,0 @@
package lndclient
import (
"fmt"
"io/ioutil"
"path/filepath"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
macaroon "gopkg.in/macaroon.v2"
)
// BasicClientOption is a functional option argument that allows adding arbitrary
// lnd basic client configuration overrides, without forcing existing users of
// NewBasicClient to update their invocation. These are always processed in
// order, with later options overriding earlier ones.
type BasicClientOption func(*basicClientOptions)
// basicClientOptions is a set of options that can configure the lnd client
// returned by NewBasicClient.
type basicClientOptions struct {
macFilename string
}
// defaultBasicClientOptions returns a basicClientOptions set to lnd basic client
// defaults.
func defaultBasicClientOptions() *basicClientOptions {
return &basicClientOptions{
macFilename: defaultAdminMacaroonFilename,
}
}
// MacFilename is a basic client option that sets the name of the macaroon file
// to use.
func MacFilename(macFilename string) BasicClientOption {
return func(bc *basicClientOptions) {
bc.macFilename = macFilename
}
}
// applyBasicClientOptions updates a basicClientOptions set with functional
// options.
func (bc *basicClientOptions) applyBasicClientOptions(options ...BasicClientOption) {
for _, option := range options {
option(bc)
}
}
// NewBasicClient creates a new basic gRPC client to lnd. We call this client
// "basic" as it falls back to expected defaults if the arguments aren't
// provided.
func NewBasicClient(lndHost, tlsPath, macDir, network string,
basicOptions ...BasicClientOption) (
lnrpc.LightningClient, error) {
conn, err := NewBasicConn(
lndHost, tlsPath, macDir, network, basicOptions...,
)
if err != nil {
return nil, err
}
return lnrpc.NewLightningClient(conn), nil
}
// NewBasicConn creates a new basic gRPC connection to lnd. We call this
// connection "basic" as it falls back to expected defaults if the arguments
// aren't provided.
func NewBasicConn(lndHost, tlsPath, macDir, network string,
basicOptions ...BasicClientOption) (
*grpc.ClientConn, error) {
if tlsPath == "" {
tlsPath = defaultTLSCertPath
}
// Load the specified TLS certificate and build transport credentials
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
if err != nil {
return nil, err
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
if macDir == "" {
macDir = filepath.Join(
defaultLndDir, defaultDataDir, defaultChainSubDir,
"bitcoin", network,
)
}
// Starting with the set of default options, we'll apply any specified
// functional options to the basic client.
bco := defaultBasicClientOptions()
bco.applyBasicClientOptions(basicOptions...)
macPath := filepath.Join(macDir, bco.macFilename)
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err == nil {
// Only if file is found
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v",
err)
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(mac)
opts = append(opts, grpc.WithPerRPCCredentials(cred))
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
}
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
opts = append(
opts, grpc.WithContextDialer(
lncfg.ClientAddressDialer(defaultRPCPort),
),
)
conn, err := grpc.Dial(lndHost, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v", err)
}
return conn, nil
}

+ 0
- 239
lndclient/chainnotifier_client.go View File

@ -1,239 +0,0 @@
package lndclient
import (
"context"
"fmt"
"sync"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"google.golang.org/grpc"
)
// ChainNotifierClient exposes base lightning functionality.
type ChainNotifierClient interface {
RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error)
RegisterConfirmationsNtfn(ctx context.Context, txid *chainhash.Hash,
pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error)
RegisterSpendNtfn(ctx context.Context,
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
chan *chainntnfs.SpendDetail, chan error, error)
}
type chainNotifierClient struct {
client chainrpc.ChainNotifierClient
chainMac serializedMacaroon
wg sync.WaitGroup
}
func newChainNotifierClient(conn *grpc.ClientConn, chainMac serializedMacaroon) *chainNotifierClient {
return &chainNotifierClient{
client: chainrpc.NewChainNotifierClient(conn),
chainMac: chainMac,
}
}
func (s *chainNotifierClient) WaitForFinished() {
s.wg.Wait()
}
func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
chan *chainntnfs.SpendDetail, chan error, error) {
var rpcOutpoint *chainrpc.Outpoint
if outpoint != nil {
rpcOutpoint = &chainrpc.Outpoint{
Hash: outpoint.Hash[:],
Index: outpoint.Index,
}
}
macaroonAuth := s.chainMac.WithMacaroonAuth(ctx)
resp, err := s.client.RegisterSpendNtfn(macaroonAuth, &chainrpc.SpendRequest{
HeightHint: uint32(heightHint),
Outpoint: rpcOutpoint,
Script: pkScript,
})
if err != nil {
return nil, nil, err
}
spendChan := make(chan *chainntnfs.SpendDetail, 1)
errChan := make(chan error, 1)
processSpendDetail := func(d *chainrpc.SpendDetails) error {
outpointHash, err := chainhash.NewHash(d.SpendingOutpoint.Hash)
if err != nil {
return err
}
txHash, err := chainhash.NewHash(d.SpendingTxHash)
if err != nil {
return err
}
tx, err := swap.DecodeTx(d.RawSpendingTx)
if err != nil {
return err
}
spendChan <- &chainntnfs.SpendDetail{
SpentOutPoint: &wire.OutPoint{
Hash: *outpointHash,
Index: d.SpendingOutpoint.Index,
},
SpenderTxHash: txHash,
SpenderInputIndex: d.SpendingInputIndex,
SpendingTx: tx,
SpendingHeight: int32(d.SpendingHeight),
}
return nil
}
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
spendEvent, err := resp.Recv()
if err != nil {
errChan <- err
return
}
switch c := spendEvent.Event.(type) {
case *chainrpc.SpendEvent_Spend:
err := processSpendDetail(c.Spend)
if err != nil {
errChan <- err
}
return
}
}
}()
return spendChan, errChan, nil
}
func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error) {
var txidSlice []byte
if txid != nil {
txidSlice = txid[:]
}
confStream, err := s.client.RegisterConfirmationsNtfn(
s.chainMac.WithMacaroonAuth(ctx),
&chainrpc.ConfRequest{
Script: pkScript,
NumConfs: uint32(numConfs),
HeightHint: uint32(heightHint),
Txid: txidSlice,
},
)
if err != nil {
return nil, nil, err
}
confChan := make(chan *chainntnfs.TxConfirmation, 1)
errChan := make(chan error, 1)
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
var confEvent *chainrpc.ConfEvent
confEvent, err := confStream.Recv()
if err != nil {
errChan <- err
return
}
switch c := confEvent.Event.(type) {
// Script confirmed
case *chainrpc.ConfEvent_Conf:
tx, err := swap.DecodeTx(c.Conf.RawTx)
if err != nil {
errChan <- err
return
}
blockHash, err := chainhash.NewHash(
c.Conf.BlockHash,
)
if err != nil {
errChan <- err
return
}
confChan <- &chainntnfs.TxConfirmation{
BlockHeight: c.Conf.BlockHeight,
BlockHash: blockHash,
Tx: tx,
TxIndex: c.Conf.TxIndex,
}
return
// Ignore reorg events, not supported.
case *chainrpc.ConfEvent_Reorg:
continue
// Nil event, should never happen.
case nil:
errChan <- fmt.Errorf("conf event empty")
return
// Unexpected type.
default:
errChan <- fmt.Errorf(
"conf event has unexpected type",
)
return
}
}
}()
return confChan, errChan, nil
}
func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) (
chan int32, chan error, error) {
blockEpochClient, err := s.client.RegisterBlockEpochNtfn(
s.chainMac.WithMacaroonAuth(ctx), &chainrpc.BlockEpoch{},
)
if err != nil {
return nil, nil, err
}
blockErrorChan := make(chan error, 1)
blockEpochChan := make(chan int32)
// Start block epoch goroutine.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
epoch, err := blockEpochClient.Recv()
if err != nil {
blockErrorChan <- err
return
}
select {
case blockEpochChan <- int32(epoch.Height):
case <-ctx.Done():
return
}
}
}()
return blockEpochChan, blockErrorChan, nil
}

+ 0
- 166
lndclient/invoices_client.go View File

@ -1,166 +0,0 @@
package lndclient
import (
"context"
"errors"
"sync"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc"
)
// InvoicesClient exposes invoice functionality.
type InvoicesClient interface {
SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) (
<-chan InvoiceUpdate, <-chan error, error)
SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error
CancelInvoice(ctx context.Context, hash lntypes.Hash) error
AddHoldInvoice(ctx context.Context, in *invoicesrpc.AddInvoiceData) (
string, error)
}
// InvoiceUpdate contains a state update for an invoice.
type InvoiceUpdate struct {
State channeldb.ContractState
AmtPaid btcutil.Amount
}
type invoicesClient struct {
client invoicesrpc.InvoicesClient
invoiceMac serializedMacaroon
wg sync.WaitGroup
}
func newInvoicesClient(conn *grpc.ClientConn, invoiceMac serializedMacaroon) *invoicesClient {
return &invoicesClient{
client: invoicesrpc.NewInvoicesClient(conn),
invoiceMac: invoiceMac,
}
}
func (s *invoicesClient) WaitForFinished() {
s.wg.Wait()
}
func (s *invoicesClient) SettleInvoice(ctx context.Context,
preimage lntypes.Preimage) error {
timeoutCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx := s.invoiceMac.WithMacaroonAuth(timeoutCtx)
_, err := s.client.SettleInvoice(rpcCtx, &invoicesrpc.SettleInvoiceMsg{
Preimage: preimage[:],
})
return err
}
func (s *invoicesClient) CancelInvoice(ctx context.Context,
hash lntypes.Hash) error {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx)
_, err := s.client.CancelInvoice(rpcCtx, &invoicesrpc.CancelInvoiceMsg{
PaymentHash: hash[:],
})
return err
}
func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan InvoiceUpdate,
<-chan error, error) {
invoiceStream, err := s.client.SubscribeSingleInvoice(
s.invoiceMac.WithMacaroonAuth(ctx),
&invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: hash[:],
},
)
if err != nil {
return nil, nil, err
}
updateChan := make(chan InvoiceUpdate)
errChan := make(chan error, 1)
// Invoice updates goroutine.
s.wg.Add(1)
go func() {
defer s.wg.Done()
for {
invoice, err := invoiceStream.Recv()
if err != nil {
errChan <- err
return
}
state, err := fromRPCInvoiceState(invoice.State)
if err != nil {
errChan <- err
return
}
select {
case updateChan <- InvoiceUpdate{
State: state,
AmtPaid: btcutil.Amount(invoice.AmtPaidSat),
}:
case <-ctx.Done():
return
}
}
}()
return updateChan, errChan, nil
}
func (s *invoicesClient) AddHoldInvoice(ctx context.Context,
in *invoicesrpc.AddInvoiceData) (string, error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
rpcIn := &invoicesrpc.AddHoldInvoiceRequest{
Memo: in.Memo,
Hash: in.Hash[:],
Value: int64(in.Value.ToSatoshis()),
Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry,
Private: true,
}
rpcCtx = s.invoiceMac.WithMacaroonAuth(rpcCtx)
resp, err := s.client.AddHoldInvoice(rpcCtx, rpcIn)
if err != nil {
return "", err
}
return resp.PaymentRequest, nil
}
func fromRPCInvoiceState(state lnrpc.Invoice_InvoiceState) (
channeldb.ContractState, error) {
switch state {
case lnrpc.Invoice_OPEN:
return channeldb.ContractOpen, nil
case lnrpc.Invoice_ACCEPTED:
return channeldb.ContractAccepted, nil
case lnrpc.Invoice_SETTLED:
return channeldb.ContractSettled, nil
case lnrpc.Invoice_CANCELED:
return channeldb.ContractCanceled, nil
}
return 0, errors.New("unknown state")
}

+ 0
- 1221
lndclient/lightning_client.go
File diff suppressed because it is too large
View File


+ 0
- 463
lndclient/lnd_services.go View File

@ -1,463 +0,0 @@
package lndclient
import (
"context"
"errors"
"fmt"
"net"
"path/filepath"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
)
var (
rpcTimeout = 30 * time.Second
// minimalCompatibleVersion is the minimum version and build tags
// required in lnd to get all functionality implemented in lndclient.
// Users can provide their own, specific version if needed. If only a
// subset of the lndclient functionality is needed, the required build
// tags can be adjusted accordingly. This default will be used as a fall
// back version if none is specified in the configuration.
minimalCompatibleVersion = &verrpc.Version{
AppMajor: 0,
AppMinor: 10,
AppPatch: 1,
BuildTags: []string{
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
},
}
// ErrVersionCheckNotImplemented is the error that is returned if the
// version RPC is not implemented in lnd. This means the version of lnd
// is lower than v0.10.0-beta.
ErrVersionCheckNotImplemented = errors.New("version check not " +
"implemented, need minimum lnd version of v0.10.0-beta")
// ErrVersionIncompatible is the error that is returned if the connected
// lnd instance is not supported.
ErrVersionIncompatible = errors.New("version incompatible")
// ErrBuildTagsMissing is the error that is returned if the
// connected lnd instance does not have all built tags activated that
// are required.
ErrBuildTagsMissing = errors.New("build tags missing")
)
// LndServicesConfig holds all configuration settings that are needed to connect
// to an lnd node.
type LndServicesConfig struct {
// LndAddress is the network address (host:port) of the lnd node to
// connect to.
LndAddress string
// Network is the bitcoin network we expect the lnd node to operate on.
Network string
// MacaroonDir is the directory where all lnd macaroons can be found.
MacaroonDir string
// TLSPath is the path to lnd's TLS certificate file.
TLSPath string
// CheckVersion is the minimum version the connected lnd node needs to
// be in order to be compatible. The node will be checked against this
// when connecting. If no version is supplied, the default minimum
// version will be used.
CheckVersion *verrpc.Version
// Dialer is an optional dial function that can be passed in if the
// default lncfg.ClientAddressDialer should not be used.
Dialer DialerFunc
}
// DialerFunc is a function that is used as grpc.WithContextDialer().
type DialerFunc func(context.Context, string) (net.Conn, error)
// LndServices constitutes a set of required services.
type LndServices struct {
Client LightningClient
WalletKit WalletKitClient
ChainNotifier ChainNotifierClient
Signer SignerClient
Invoices InvoicesClient
Router RouterClient
Versioner VersionerClient
ChainParams *chaincfg.Params
NodeAlias string
NodePubkey [33]byte
Version *verrpc.Version
macaroons *macaroonPouch
}
// GrpcLndServices constitutes a set of required RPC services.
type GrpcLndServices struct {
LndServices
cleanup func()
}
// NewLndServices creates creates a connection to the given lnd instance and
// creates a set of required RPC services.
func NewLndServices(cfg *LndServicesConfig) (*GrpcLndServices, error) {
// We need to use a custom dialer so we can also connect to unix
// sockets and not just TCP addresses.
if cfg.Dialer == nil {
cfg.Dialer = lncfg.ClientAddressDialer(defaultRPCPort)
}
// Fall back to minimal compatible version if none if specified.
if cfg.CheckVersion == nil {
cfg.CheckVersion = minimalCompatibleVersion
}
// Based on the network, if the macaroon directory isn't set, then
// we'll use the expected default locations.
macaroonDir := cfg.MacaroonDir
if macaroonDir == "" {
switch cfg.Network {
case "testnet":
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "testnet",
)
case "mainnet":
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "mainnet",
)
case "simnet":
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "simnet",
)
case "regtest":
macaroonDir = filepath.Join(
defaultLndDir, defaultDataDir,
defaultChainSubDir, "bitcoin", "regtest",
)
default:
return nil, fmt.Errorf("unsupported network: %v",
cfg.Network)
}
}
// Setup connection with lnd
log.Infof("Creating lnd connection to %v", cfg.LndAddress)
conn, err := getClientConn(cfg)
if err != nil {
return nil, err
}
log.Infof("Connected to lnd")
chainParams, err := swap.ChainParamsFromNetwork(cfg.Network)
if err != nil {
return nil, err
}
// We are going to check that the connected lnd is on the same network
// and is a compatible version with all the required subservers enabled.
// For this, we make two calls, both of which only need the readonly
// macaroon. We don't use the pouch yet because if not all subservers
// are enabled, then not all macaroons might be there and the user would
// get a more cryptic error message.
readonlyMac, err := newSerializedMacaroon(
filepath.Join(macaroonDir, defaultReadonlyFilename),
)
if err != nil {
return nil, err
}
nodeAlias, nodeKey, version, err := checkLndCompatibility(
conn, chainParams, readonlyMac, cfg.Network, cfg.CheckVersion,
)
if err != nil {
return nil, err
}
// Now that we've ensured our macaroon directory is set properly, we
// can retrieve our full macaroon pouch from the directory.
macaroons, err := newMacaroonPouch(macaroonDir)
if err != nil {
return nil, fmt.Errorf("unable to obtain macaroons: %v", err)
}
// With the macaroons loaded and the version checked, we can now create
// the real lightning client which uses the admin macaroon.
lightningClient := newLightningClient(
conn, chainParams, macaroons.adminMac,
)
// With the network check passed, we'll now initialize the rest of the
// sub-server connections, giving each of them their specific macaroon.
notifierClient := newChainNotifierClient(conn, macaroons.chainMac)
signerClient := newSignerClient(conn, macaroons.signerMac)
walletKitClient := newWalletKitClient(conn, macaroons.walletKitMac)
invoicesClient := newInvoicesClient(conn, macaroons.invoiceMac)
routerClient := newRouterClient(conn, macaroons.routerMac)
versionerClient := newVersionerClient(conn, macaroons.readonlyMac)
cleanup := func() {
log.Debugf("Closing lnd connection")
err := conn.Close()
if err != nil {
log.Errorf("Error closing client connection: %v", err)
}
log.Debugf("Wait for client to finish")
lightningClient.WaitForFinished()
log.Debugf("Wait for chain notifier to finish")
notifierClient.WaitForFinished()
log.Debugf("Wait for invoices to finish")
invoicesClient.WaitForFinished()
log.Debugf("Lnd services finished")
}
services := &GrpcLndServices{
LndServices: LndServices{
Client: lightningClient,
WalletKit: walletKitClient,
ChainNotifier: notifierClient,
Signer: signerClient,
Invoices: invoicesClient,
Router: routerClient,
Versioner: versionerClient,
ChainParams: chainParams,
NodeAlias: nodeAlias,
NodePubkey: nodeKey,
Version: version,
macaroons: macaroons,
},
cleanup: cleanup,
}
log.Infof("Using network %v", cfg.Network)
return services, nil
}
// Close closes the lnd connection and waits for all sub server clients to
// finish their goroutines.
func (s *GrpcLndServices) Close() {
s.cleanup()
log.Debugf("Lnd services finished")
}
// checkLndCompatibility makes sure the connected lnd instance is running on the
// correct network, has the version RPC implemented, is the correct minimal
// version and supports all required build tags/subservers.
func checkLndCompatibility(conn *grpc.ClientConn, chainParams *chaincfg.Params,
readonlyMac serializedMacaroon, network string,
minVersion *verrpc.Version) (string, [33]byte, *verrpc.Version, error) {
// onErr is a closure that simplifies returning multiple values in the
// error case.
onErr := func(err error) (string, [33]byte, *verrpc.Version, error) {
closeErr := conn.Close()
if closeErr != nil {
log.Errorf("Error closing lnd connection: %v", closeErr)
}
// Make static error messages a bit less cryptic by adding the
// version or build tag that we expect.
newErr := fmt.Errorf("lnd compatibility check failed: %v", err)
if err == ErrVersionIncompatible || err == ErrBuildTagsMissing {
newErr = fmt.Errorf("error checking connected lnd "+
"version. at least version \"%s\" is "+
"required", VersionString(minVersion))
}
return "", [33]byte{}, nil, newErr
}
// We use our own clients with a readonly macaroon here, because we know
// that's all we need for the checks.
lightningClient := newLightningClient(conn, chainParams, readonlyMac)
versionerClient := newVersionerClient(conn, readonlyMac)
// With our readonly macaroon obtained, we'll ensure that the network
// for lnd matches our expected network.
info, err := lightningClient.GetInfo(context.Background())
if err != nil {
err := fmt.Errorf("unable to get info for lnd node: %v", err)
return onErr(err)
}
if network != info.Network {
err := fmt.Errorf("network mismatch with connected lnd node, "+
"wanted '%s', got '%s'", network, info.Network)
return onErr(err)
}
// Now let's also check the version of the connected lnd node.
version, err := checkVersionCompatibility(versionerClient, minVersion)
if err != nil {
return onErr(err)
}
// Return the static part of the info we just queried from the node so
// it can be cached for later use.
return info.Alias, info.IdentityPubkey, version, nil
}
// checkVersionCompatibility makes sure the connected lnd node has the correct
// version and required build tags enabled.
//
// NOTE: This check will **never** return a non-nil error for a version of
// lnd < 0.10.0 because any version previous to 0.10.0 doesn't have the version
// endpoint implemented!
func checkVersionCompatibility(client VersionerClient,
expected *verrpc.Version) (*verrpc.Version, error) {
// First, test that the version RPC is even implemented.
version, err := client.GetVersion(context.Background())
if err != nil {
// The version service has only been added in lnd v0.10.0. If
// we get an unimplemented error, it means the lnd version is
// definitely older than that.
s, ok := status.FromError(err)
if ok && s.Code() == codes.Unimplemented {
return nil, ErrVersionCheckNotImplemented
}
return nil, fmt.Errorf("GetVersion error: %v", err)
}
log.Infof("lnd version: %v", VersionString(version))
// Now check the version and make sure all required build tags are set.
err = assertVersionCompatible(version, expected)
if err != nil {
return nil, err
}
err = assertBuildTagsEnabled(version, expected.BuildTags)
if err != nil {
return nil, err
}
// All check positive, version is fully compatible.
return version, nil
}
// assertVersionCompatible makes sure the detected lnd version is compatible
// with our current version requirements.
func assertVersionCompatible(actual *verrpc.Version,
expected *verrpc.Version) error {
// We need to check the versions parts sequentially as they are
// hierarchical.
if actual.AppMajor != expected.AppMajor {
if actual.AppMajor > expected.AppMajor {
return nil
}
return ErrVersionIncompatible
}
if actual.AppMinor != expected.AppMinor {
if actual.AppMinor > expected.AppMinor {
return nil
}
return ErrVersionIncompatible
}
if actual.AppPatch != expected.AppPatch {
if actual.AppPatch > expected.AppPatch {
return nil
}
return ErrVersionIncompatible
}
// The actual version and expected version are identical.
return nil
}
// assertBuildTagsEnabled makes sure all required build tags are set.
func assertBuildTagsEnabled(actual *verrpc.Version,
requiredTags []string) error {
tagMap := make(map[string]struct{})
for _, tag := range actual.BuildTags {
tagMap[tag] = struct{}{}
}
for _, required := range requiredTags {
if _, ok := tagMap[required]; !ok {
return ErrBuildTagsMissing
}
}
// All tags found.
return nil
}
var (
defaultRPCPort = "10009"
defaultLndDir = btcutil.AppDataDir("lnd", false)
defaultTLSCertFilename = "tls.cert"
defaultTLSCertPath = filepath.Join(
defaultLndDir, defaultTLSCertFilename,
)
defaultDataDir = "data"
defaultChainSubDir = "chain"
defaultAdminMacaroonFilename = "admin.macaroon"
defaultInvoiceMacaroonFilename = "invoices.macaroon"
defaultChainMacaroonFilename = "chainnotifier.macaroon"
defaultWalletKitMacaroonFilename = "walletkit.macaroon"
defaultRouterMacaroonFilename = "router.macaroon"
defaultSignerFilename = "signer.macaroon"
defaultReadonlyFilename = "readonly.macaroon"
// maxMsgRecvSize is the largest gRPC message our client will receive.
// We set this to 200MiB.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
)
func getClientConn(cfg *LndServicesConfig) (*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials
// with it.
tlsPath := cfg.TLSPath
if tlsPath == "" {
tlsPath = defaultTLSCertPath
}
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
if err != nil {
return nil, err
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
// Use a custom dialer, to allow connections to unix sockets,
// in-memory listeners etc, and not just TCP addresses.
grpc.WithContextDialer(cfg.Dialer),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
}
conn, err := grpc.Dial(cfg.LndAddress, opts...)
if err != nil {
return nil, fmt.Errorf("unable to connect to RPC server: %v",
err)
}
return conn, nil
}

+ 0
- 158
lndclient/lnd_services_test.go View File

@ -1,158 +0,0 @@
package lndclient
import (
"context"
"testing"
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type mockVersioner struct {
version *verrpc.Version
err error
}
func (m *mockVersioner) GetVersion(_ context.Context) (*verrpc.Version, error) {
return m.version, m.err
}
// TestCheckVersionCompatibility makes sure the correct error is returned if an
// old lnd is connected that doesn't implement the version RPC, has an older
// version or if an lnd with not all subservers enabled is connected.
func TestCheckVersionCompatibility(t *testing.T) {
// Make sure a version check against a node that doesn't implement the
// version RPC always fails.
unimplemented := &mockVersioner{
err: status.Error(codes.Unimplemented, "missing"),
}
_, err := checkVersionCompatibility(unimplemented, &verrpc.Version{
AppMajor: 0,
AppMinor: 10,
AppPatch: 0,
})
if err != ErrVersionCheckNotImplemented {
t.Fatalf("unexpected error. got '%v' wanted '%v'", err,
ErrVersionCheckNotImplemented)
}
// Next, make sure an older version than what we want is rejected.
oldVersion := &mockVersioner{
version: &verrpc.Version{
AppMajor: 0,
AppMinor: 10,
AppPatch: 0,
},
}
_, err = checkVersionCompatibility(oldVersion, &verrpc.Version{
AppMajor: 0,
AppMinor: 11,
AppPatch: 0,
})
if err != ErrVersionIncompatible {
t.Fatalf("unexpected error. got '%v' wanted '%v'", err,
ErrVersionIncompatible)
}
// Finally, make sure we also get the correct error when trying to run
// against an lnd that doesn't have all required build tags enabled.
buildTagsMissing := &mockVersioner{
version: &verrpc.Version{
AppMajor: 0,
AppMinor: 10,
AppPatch: 0,
BuildTags: []string{"dev", "lntest", "btcd", "signrpc"},
},
}
_, err = checkVersionCompatibility(buildTagsMissing, &verrpc.Version{
AppMajor: 0,
AppMinor: 10,
AppPatch: 0,
BuildTags: []string{"signrpc", "walletrpc"},
})
if err != ErrBuildTagsMissing {
t.Fatalf("unexpected error. got '%v' wanted '%v'", err,
ErrVersionIncompatible)
}
}
// TestLndVersionCheckComparison makes sure the version check comparison works
// correctly and considers all three version levels.
func TestLndVersionCheckComparison(t *testing.T) {
actual := &verrpc.Version{
AppMajor: 1,
AppMinor: 2,
AppPatch: 3,
}
testCases := []struct {
name string
expectMajor uint32
expectMinor uint32
expectPatch uint32
actual *verrpc.Version
expectedErr error
}{
{
name: "no expectation",
expectMajor: 0,
expectMinor: 0,
expectPatch: 0,
actual: actual,
expectedErr: nil,
},
{
name: "expect exact same version",
expectMajor: 1,
expectMinor: 2,
expectPatch: 3,
actual: actual,
expectedErr: nil,
},
{
name: "ignore patch if minor is bigger",
expectMajor: 12,
expectMinor: 9,
expectPatch: 14,
actual: &verrpc.Version{
AppMajor: 12,
AppMinor: 22,
AppPatch: 0,
},
expectedErr: nil,
},
{
name: "all fields different",
expectMajor: 3,
expectMinor: 2,
expectPatch: 1,
actual: actual,
expectedErr: ErrVersionIncompatible,
},
{
name: "patch version different",
expectMajor: 1,
expectMinor: 2,
expectPatch: 4,
actual: actual,
expectedErr: ErrVersionIncompatible,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
err := assertVersionCompatible(
tc.actual, &verrpc.Version{
AppMajor: tc.expectMajor,
AppMinor: tc.expectMinor,
AppPatch: tc.expectPatch,
},
)
if err != tc.expectedErr {
t.Fatalf("unexpected error, got '%v' wanted "+
"'%v'", err, tc.expectedErr)
}
})
}
}

+ 0
- 23
lndclient/log.go View File

@ -1,23 +0,0 @@
package lndclient
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// log is a logger that is initialized with no output filters. This
// means the package will not perform any logging by default until the
// caller requests it.
var log btclog.Logger
// The default amount of logging is none.
func init() {
UseLogger(build.NewSubLogger("LNDC", nil))
}
// UseLogger uses a specified Logger to output package logging info.
// This should be used in preference to SetLogWriter if the caller is also
// using btclog.
func UseLogger(logger btclog.Logger) {
log = logger
}

+ 0
- 118
lndclient/macaroon_pouch.go View File

@ -1,118 +0,0 @@
package lndclient
import (
"context"
"encoding/hex"
"io/ioutil"
"path/filepath"
"google.golang.org/grpc/metadata"
)
// serializedMacaroon is a type that represents a hex-encoded macaroon. We'll
// use this primarily vs the raw binary format as the gRPC metadata feature
// requires that all keys and values be strings.
type serializedMacaroon string
// newSerializedMacaroon reads a new serializedMacaroon from that target
// macaroon path. If the file can't be found, then an error is returned.
func newSerializedMacaroon(macaroonPath string) (serializedMacaroon, error) {
macBytes, err := ioutil.ReadFile(macaroonPath)
if err != nil {
return "", err
}
return serializedMacaroon(hex.EncodeToString(macBytes)), nil
}
// WithMacaroonAuth modifies the passed context to include the macaroon KV
// metadata of the target macaroon. This method can be used to add the macaroon
// at call time, rather than when the connection to the gRPC server is created.
func (s serializedMacaroon) WithMacaroonAuth(ctx context.Context) context.Context {
return metadata.AppendToOutgoingContext(ctx, "macaroon", string(s))
}
// macaroonPouch holds the set of macaroons we need to interact with lnd for
// Loop. Each sub-server has its own macaroon, and for the remaining temporary
// calls that directly hit lnd, we'll use the admin macaroon.
type macaroonPouch struct {
// invoiceMac is the macaroon for the invoices sub-server.
invoiceMac serializedMacaroon
// chainMac is the macaroon for the ChainNotifier sub-server.
chainMac serializedMacaroon
// signerMac is the macaroon for the Signer sub-server.
signerMac serializedMacaroon
// 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
// readonlyMac is the primary read-only macaroon for lnd.
readonlyMac serializedMacaroon
}
// newMacaroonPouch returns a new instance of a fully populated macaroonPouch
// given the directory where all the macaroons are stored.
func newMacaroonPouch(macaroonDir string) (*macaroonPouch, error) {
m := &macaroonPouch{}
var err error
m.invoiceMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultInvoiceMacaroonFilename),
)
if err != nil {
return nil, err
}
m.chainMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultChainMacaroonFilename),
)
if err != nil {
return nil, err
}
m.signerMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultSignerFilename),
)
if err != nil {
return nil, err
}
m.walletKitMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultWalletKitMacaroonFilename),
)
if err != nil {
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),
)
if err != nil {
return nil, err
}
m.readonlyMac, err = newSerializedMacaroon(
filepath.Join(macaroonDir, defaultReadonlyFilename),
)
if err != nil {
return nil, err
}
return m, nil
}

+ 0
- 416
lndclient/router_client.go View File

<
@ -1,416 +0,0 @@
package lndclient
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"time"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// 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 lnrpc.Payment_PaymentStatus
// FailureReason is the reason why the payment failed. Only set when
// State is Failed.
FailureReason lnrpc.PaymentFailureReason
Preimage lntypes.Preimage
Fee lnwire.MilliSatoshi
Value lnwire.MilliSatoshi
InFlightAmt lnwire.MilliSatoshi
InFlightHtlcs int
}
func (p PaymentStatus) String() string {
text := fmt.Sprintf("state=%v", p.State)
if p.State == lnrpc.Payment_IN_FLIGHT {
text += fmt.Sprintf(", inflight_htlcs=%v, inflight_amt=%v",
p.InFlightHtlcs, p.InFlightAmt)
}
return text
}
// SendPaymentRequest defines the payment parameters for a new payment.
type SendPaymentRequest struct {
// Invoice is an encoded payment request. The individual payment
// parameters Target, Amount, PaymentHash, FinalCLTVDelta and RouteHints
// are only processed when the Invoice field is empty.
Invoice string
// MaxFee is the fee limit for this payment.
MaxFee btcutil.Amount
// MaxCltv is the maximum timelock for this payment. If nil, there is no
// maximum.
MaxCltv *int32
// OutgoingChanIds is a restriction on the set of possible outgoing
// channels. If nil or empty, there is no restriction.
OutgoingChanIds []uint64
// Timeout is the payment loop timeout. After this time, no new payment
// attempts will be started.
Timeout time.Duration
// Target is the node in which the payment should be routed towards.
Target route.Vertex
// Amount is the value of the payment to send through the network in
// satoshis.
Amount btcutil.Amount
// PaymentHash is the r-hash value to use within the HTLC extended to
// the first hop.
PaymentHash *lntypes.Hash
// FinalCLTVDelta is the CTLV expiry delta to use for the _final_ hop
// in the route. This means that the final hop will have a CLTV delta
// of at least: currentHeight + FinalCLTVDelta.
FinalCLTVDelta uint16
// RouteHints represents the different routing hints that can be used to
// assist a payment in reaching its destination successfully. These
// hints will act as intermediate hops along the route.
//
// NOTE: This is optional unless required by the payment. When providing
// multiple routes, ensure the hop hints within each route are chained
// together and sorted in forward order in order to reach the
// destination successfully.
RouteHints [][]zpay32.HopHint
// LastHopPubkey is the pubkey of the last hop of the route taken
// for this payment. If empty, any hop may be used.
LastHopPubkey *route.Vertex
// MaxParts is the maximum number of partial payments that may be used
// to complete the full amount.
MaxParts uint32
// KeySend is set to true if the tlv payload will include the preimage.
KeySend bool
// CustomRecords holds the custom TLV records that will be added to the
// payment.
CustomRecords map[uint64][]byte
}
// 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()),
MaxParts: request.MaxParts,
OutgoingChanIds: request.OutgoingChanIds,
}
if request.MaxCltv != nil {
rpcReq.CltvLimit = *request.MaxCltv
}
if request.LastHopPubkey != nil {
rpcReq.LastHopPubkey = request.LastHopPubkey[:]
}
rpcReq.DestCustomRecords = request.CustomRecords
if request.KeySend {
if request.PaymentHash != nil {
return nil, nil, fmt.Errorf(
"keysend payment must not include a preset payment hash")
}
var preimage lntypes.Preimage
if _, err := rand.Read(preimage[:]); err != nil {
return nil, nil, err
}
if rpcReq.DestCustomRecords == nil {
rpcReq.DestCustomRecords = make(map[uint64][]byte)
}
// Override the payment hash.
rpcReq.DestCustomRecords[record.KeySendType] = preimage[:]
hash := preimage.Hash()
request.PaymentHash = &hash
}
// Only if there is no payment request set, we will parse the individual
// payment parameters.
if request.Invoice == "" {
rpcReq.Dest = request.Target[:]
rpcReq.Amt = int64(request.Amount)
rpcReq.PaymentHash = request.PaymentHash[:]
rpcReq.FinalCltvDelta = int32(request.FinalCLTVDelta)
routeHints, err := marshallRouteHints(request.RouteHints)
if err != nil {
return nil, nil, err
}
rpcReq.RouteHints = routeHints
}
stream, err := r.client.SendPaymentV2(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.TrackPaymentV2(
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.
// Once the payment reaches a final state, the status and error channels will
// be closed to signal that we are finished sending into them.
func (r *routerClient) trackPayment(ctx context.Context,
stream routerrpc.Router_TrackPaymentV2Client) (chan PaymentStatus,
chan error, error) {
statusChan := make(chan PaymentStatus)
errorChan := make(chan error, 1)
go func() {
for {
payment, err := stream.Recv()
if err != nil {
// If we get an EOF error, the payment has
// reached a final state and the server is
// finished sending us updates. We close both
// channels to signal that we are done sending
// values on them and return.
if err == io.EOF {
close(statusChan)
close(errorChan)
return
}
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
}