mirror of https://github.com/lightninglabs/loop
Merge pull request #237 from joostjager/external-lndclient
lndclient: move to github.com/lightninglabs/lndclientpull/239/head lndclient-split
commit
fc8f3c9986
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -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
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
||||||
status, err := unmarshallPaymentStatus(payment)
|
|
||||||
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(rpcPayment *lnrpc.Payment) (
|
|
||||||
*PaymentStatus, error) {
|
|
||||||
|
|
||||||
status := PaymentStatus{
|
|
||||||
State: rpcPayment.Status,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch status.State {
|
|
||||||
case lnrpc.Payment_SUCCEEDED:
|
|
||||||
preimage, err := lntypes.MakePreimageFromStr(
|
|
||||||
rpcPayment.PaymentPreimage,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
status.Preimage = preimage
|
|
||||||
status.Fee = lnwire.MilliSatoshi(rpcPayment.FeeMsat)
|
|
||||||
status.Value = lnwire.MilliSatoshi(rpcPayment.ValueMsat)
|
|
||||||
|
|
||||||
case lnrpc.Payment_FAILED:
|
|
||||||
status.FailureReason = rpcPayment.FailureReason
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, htlc := range rpcPayment.Htlcs {
|
|
||||||
if htlc.Status != lnrpc.HTLCAttempt_IN_FLIGHT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
status.InFlightHtlcs++
|
|
||||||
|
|
||||||
lastHop := htlc.Route.Hops[len(htlc.Route.Hops)-1]
|
|
||||||
status.InFlightAmt += lnwire.MilliSatoshi(
|
|
||||||
lastHop.AmtToForwardMsat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshallRouteHints marshalls a list of route hints.
|
|
||||||
func marshallRouteHints(routeHints [][]zpay32.HopHint) (
|
|
||||||
[]*lnrpc.RouteHint, error) {
|
|
||||||
|
|
||||||
rpcRouteHints := make([]*lnrpc.RouteHint, 0, len(routeHints))
|
|
||||||
for _, routeHint := range routeHints {
|
|
||||||
rpcRouteHint := make(
|
|
||||||
[]*lnrpc.HopHint, 0, len(routeHint),
|
|
||||||
)
|
|
||||||
for _, hint := range routeHint {
|
|
||||||
rpcHint, err := marshallHopHint(hint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcRouteHint = append(rpcRouteHint, rpcHint)
|
|
||||||
}
|
|
||||||
rpcRouteHints = append(rpcRouteHints, &lnrpc.RouteHint{
|
|
||||||
HopHints: rpcRouteHint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return rpcRouteHints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// marshallHopHint marshalls a single hop hint.
|
|
||||||
func marshallHopHint(hint zpay32.HopHint) (*lnrpc.HopHint, error) {
|
|
||||||
nodeID, err := route.NewVertexFromBytes(
|
|
||||||
hint.NodeID.SerializeCompressed(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &lnrpc.HopHint{
|
|
||||||
ChanId: hint.ChannelID,
|
|
||||||
CltvExpiryDelta: uint32(hint.CLTVExpiryDelta),
|
|
||||||
FeeBaseMsat: hint.FeeBaseMSat,
|
|
||||||
FeeProportionalMillionths: hint.FeeProportionalMillionths,
|
|
||||||
NodeId: nodeID.String(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
@ -1,254 +0,0 @@
|
|||||||
package lndclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
|
||||||
"github.com/lightninglabs/loop/swap"
|
|
||||||
"github.com/lightningnetwork/lnd/input"
|
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SignerClient exposes sign functionality.
|
|
||||||
type SignerClient interface {
|
|
||||||
SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
|
|
||||||
signDescriptors []*input.SignDescriptor) ([][]byte, error)
|
|
||||||
|
|
||||||
// ComputeInputScript generates the proper input script for P2WPKH
|
|
||||||
// output and NP2WPKH outputs. This method only requires that the
|
|
||||||
// `Output`, `HashType`, `SigHashes` and `InputIndex` fields are
|
|
||||||
// populated within the sign descriptors.
|
|
||||||
ComputeInputScript(ctx context.Context, tx *wire.MsgTx,
|
|
||||||
signDescriptors []*input.SignDescriptor) ([]*input.Script, error)
|
|
||||||
|
|
||||||
// SignMessage signs a message with the key specified in the key
|
|
||||||
// locator. The returned signature is fixed-size LN wire format encoded.
|
|
||||||
SignMessage(ctx context.Context, msg []byte,
|
|
||||||
locator keychain.KeyLocator) ([]byte, error)
|
|
||||||
|
|
||||||
// VerifyMessage verifies a signature over a message using the public
|
|
||||||
// key provided. The signature must be fixed-size LN wire format
|
|
||||||
// encoded.
|
|
||||||
VerifyMessage(ctx context.Context, msg, sig []byte, pubkey [33]byte) (
|
|
||||||
bool, error)
|
|
||||||
|
|
||||||
// DeriveSharedKey returns a shared secret key by performing
|
|
||||||
// Diffie-Hellman key derivation between the ephemeral public key and
|
|
||||||
// the key specified by the key locator (or the node's identity private
|
|
||||||
// key if no key locator is specified):
|
|
||||||
//
|
|
||||||
// P_shared = privKeyNode * ephemeralPubkey
|
|
||||||
//
|
|
||||||
// The resulting shared public key is serialized in the compressed
|
|
||||||
// format and hashed with SHA256, resulting in a final key length of 256
|
|
||||||
// bits.
|
|
||||||
DeriveSharedKey(ctx context.Context, ephemeralPubKey *btcec.PublicKey,
|
|
||||||
keyLocator *keychain.KeyLocator) ([32]byte, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type signerClient struct {
|
|
||||||
client signrpc.SignerClient
|
|
||||||
signerMac serializedMacaroon
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSignerClient(conn *grpc.ClientConn,
|
|
||||||
signerMac serializedMacaroon) *signerClient {
|
|
||||||
|
|
||||||
return &signerClient{
|
|
||||||
client: signrpc.NewSignerClient(conn),
|
|
||||||
signerMac: signerMac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func marshallSignDescriptors(signDescriptors []*input.SignDescriptor,
|
|
||||||
) []*signrpc.SignDescriptor {
|
|
||||||
|
|
||||||
rpcSignDescs := make([]*signrpc.SignDescriptor, len(signDescriptors))
|
|
||||||
for i, signDesc := range signDescriptors {
|
|
||||||
var keyBytes []byte
|
|
||||||
var keyLocator *signrpc.KeyLocator
|
|
||||||
if signDesc.KeyDesc.PubKey != nil {
|
|
||||||
keyBytes = signDesc.KeyDesc.PubKey.SerializeCompressed()
|
|
||||||
} else {
|
|
||||||
keyLocator = &signrpc.KeyLocator{
|
|
||||||
KeyFamily: int32(
|
|
||||||
signDesc.KeyDesc.KeyLocator.Family,
|
|
||||||
),
|
|
||||||
KeyIndex: int32(
|
|
||||||
signDesc.KeyDesc.KeyLocator.Index,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var doubleTweak []byte
|
|
||||||
if signDesc.DoubleTweak != nil {
|
|
||||||
doubleTweak = signDesc.DoubleTweak.Serialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcSignDescs[i] = &signrpc.SignDescriptor{
|
|
||||||
WitnessScript: signDesc.WitnessScript,
|
|
||||||
Output: &signrpc.TxOut{
|
|
||||||
PkScript: signDesc.Output.PkScript,
|
|
||||||
Value: signDesc.Output.Value,
|
|
||||||
},
|
|
||||||
Sighash: uint32(signDesc.HashType),
|
|
||||||
InputIndex: int32(signDesc.InputIndex),
|
|
||||||
KeyDesc: &signrpc.KeyDescriptor{
|
|
||||||
RawKeyBytes: keyBytes,
|
|
||||||
KeyLoc: keyLocator,
|
|
||||||
},
|
|
||||||
SingleTweak: signDesc.SingleTweak,
|
|
||||||
DoubleTweak: doubleTweak,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rpcSignDescs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *signerClient) SignOutputRaw(ctx context.Context, tx *wire.MsgTx,
|
|
||||||
signDescriptors []*input.SignDescriptor) ([][]byte, error) {
|
|
||||||
|
|
||||||
txRaw, err := swap.EncodeTx(tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rpcSignDescs := marshallSignDescriptors(signDescriptors)
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := s.client.SignOutputRaw(rpcCtx,
|
|
||||||
&signrpc.SignReq{
|
|
||||||
RawTxBytes: txRaw,
|
|
||||||
SignDescs: rpcSignDescs,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.RawSigs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComputeInputScript generates the proper input script for P2WPKH output and
|
|
||||||
// NP2WPKH outputs. This method only requires that the `Output`, `HashType`,
|
|
||||||
// `SigHashes` and `InputIndex` fields are populated within the sign
|
|
||||||
// descriptors.
|
|
||||||
func (s *signerClient) ComputeInputScript(ctx context.Context, tx *wire.MsgTx,
|
|
||||||
signDescriptors []*input.SignDescriptor) ([]*input.Script, error) {
|
|
||||||
|
|
||||||
txRaw, err := swap.EncodeTx(tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rpcSignDescs := marshallSignDescriptors(signDescriptors)
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := s.client.ComputeInputScript(
|
|
||||||
rpcCtx, &signrpc.SignReq{
|
|
||||||
RawTxBytes: txRaw,
|
|
||||||
SignDescs: rpcSignDescs,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
inputScripts := make([]*input.Script, 0, len(resp.InputScripts))
|
|
||||||
for _, inputScript := range resp.InputScripts {
|
|
||||||
inputScripts = append(inputScripts, &input.Script{
|
|
||||||
SigScript: inputScript.SigScript,
|
|
||||||
Witness: inputScript.Witness,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return inputScripts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignMessage signs a message with the key specified in the key locator. The
|
|
||||||
// returned signature is fixed-size LN wire format encoded.
|
|
||||||
func (s *signerClient) SignMessage(ctx context.Context, msg []byte,
|
|
||||||
locator keychain.KeyLocator) ([]byte, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcIn := &signrpc.SignMessageReq{
|
|
||||||
Msg: msg,
|
|
||||||
KeyLoc: &signrpc.KeyLocator{
|
|
||||||
KeyFamily: int32(locator.Family),
|
|
||||||
KeyIndex: int32(locator.Index),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := s.client.SignMessage(rpcCtx, rpcIn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp.Signature, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// VerifyMessage verifies a signature over a message using the public key
|
|
||||||
// provided. The signature must be fixed-size LN wire format encoded.
|
|
||||||
func (s *signerClient) VerifyMessage(ctx context.Context, msg, sig []byte,
|
|
||||||
pubkey [33]byte) (bool, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcIn := &signrpc.VerifyMessageReq{
|
|
||||||
Msg: msg,
|
|
||||||
Signature: sig,
|
|
||||||
Pubkey: pubkey[:],
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := s.client.VerifyMessage(rpcCtx, rpcIn)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return resp.Valid, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key
|
|
||||||
// derivation between the ephemeral public key and the key specified by the key
|
|
||||||
// locator (or the node's identity private key if no key locator is specified):
|
|
||||||
//
|
|
||||||
// P_shared = privKeyNode * ephemeralPubkey
|
|
||||||
//
|
|
||||||
// The resulting shared public key is serialized in the compressed format and
|
|
||||||
// hashed with SHA256, resulting in a final key length of 256 bits.
|
|
||||||
func (s *signerClient) DeriveSharedKey(ctx context.Context,
|
|
||||||
ephemeralPubKey *btcec.PublicKey,
|
|
||||||
keyLocator *keychain.KeyLocator) ([32]byte, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcIn := &signrpc.SharedKeyRequest{
|
|
||||||
EphemeralPubkey: ephemeralPubKey.SerializeCompressed(),
|
|
||||||
KeyLoc: &signrpc.KeyLocator{
|
|
||||||
KeyFamily: int32(keyLocator.Family),
|
|
||||||
KeyIndex: int32(keyLocator.Index),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcCtx = s.signerMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := s.client.DeriveSharedKey(rpcCtx, rpcIn)
|
|
||||||
if err != nil {
|
|
||||||
return [32]byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var sharedKey [32]byte
|
|
||||||
copy(sharedKey[:], resp.SharedKey)
|
|
||||||
return sharedKey, nil
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package lndclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/verrpc"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VersionerClient exposes the version of lnd.
|
|
||||||
type VersionerClient interface {
|
|
||||||
// GetVersion returns the version and build information of the lnd
|
|
||||||
// daemon.
|
|
||||||
GetVersion(ctx context.Context) (*verrpc.Version, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type versionerClient struct {
|
|
||||||
client verrpc.VersionerClient
|
|
||||||
readonlyMac serializedMacaroon
|
|
||||||
}
|
|
||||||
|
|
||||||
func newVersionerClient(conn *grpc.ClientConn,
|
|
||||||
readonlyMac serializedMacaroon) *versionerClient {
|
|
||||||
|
|
||||||
return &versionerClient{
|
|
||||||
client: verrpc.NewVersionerClient(conn),
|
|
||||||
readonlyMac: readonlyMac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVersion returns the version and build information of the lnd
|
|
||||||
// daemon.
|
|
||||||
//
|
|
||||||
// NOTE: This method is part of the VersionerClient interface.
|
|
||||||
func (v *versionerClient) GetVersion(ctx context.Context) (*verrpc.Version,
|
|
||||||
error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(
|
|
||||||
v.readonlyMac.WithMacaroonAuth(ctx), rpcTimeout,
|
|
||||||
)
|
|
||||||
defer cancel()
|
|
||||||
return v.client.GetVersion(rpcCtx, &verrpc.VersionRequest{})
|
|
||||||
}
|
|
||||||
|
|
||||||
// VersionString returns a nice, human readable string of a version returned by
|
|
||||||
// the VersionerClient, including all build tags.
|
|
||||||
func VersionString(version *verrpc.Version) string {
|
|
||||||
short := VersionStringShort(version)
|
|
||||||
enabledTags := strings.Join(version.BuildTags, ",")
|
|
||||||
return fmt.Sprintf("%s, build tags '%s'", short, enabledTags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VersionStringShort returns a nice, human readable string of a version
|
|
||||||
// returned by the VersionerClient.
|
|
||||||
func VersionStringShort(version *verrpc.Version) string {
|
|
||||||
versionStr := fmt.Sprintf(
|
|
||||||
"v%d.%d.%d", version.AppMajor, version.AppMinor,
|
|
||||||
version.AppPatch,
|
|
||||||
)
|
|
||||||
if version.AppPreRelease != "" {
|
|
||||||
versionStr = fmt.Sprintf(
|
|
||||||
"%s-%s", versionStr, version.AppPreRelease,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return versionStr
|
|
||||||
}
|
|
@ -1,349 +0,0 @@
|
|||||||
package lndclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
|
||||||
"github.com/btcsuite/btcutil"
|
|
||||||
"github.com/btcsuite/btcwallet/wtxmgr"
|
|
||||||
"github.com/lightninglabs/loop/swap"
|
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/signrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WalletKitClient exposes wallet functionality.
|
|
||||||
type WalletKitClient interface {
|
|
||||||
// ListUnspent returns a list of all utxos spendable by the wallet with
|
|
||||||
// a number of confirmations between the specified minimum and maximum.
|
|
||||||
ListUnspent(ctx context.Context, minConfs, maxConfs int32) (
|
|
||||||
[]*lnwallet.Utxo, error)
|
|
||||||
|
|
||||||
// LeaseOutput locks an output to the given ID, preventing it from being
|
|
||||||
// available for any future coin selection attempts. The absolute time
|
|
||||||
// of the lock's expiration is returned. The expiration of the lock can
|
|
||||||
// be extended by successive invocations of this call. Outputs can be
|
|
||||||
// unlocked before their expiration through `ReleaseOutput`.
|
|
||||||
LeaseOutput(ctx context.Context, lockID wtxmgr.LockID,
|
|
||||||
op wire.OutPoint) (time.Time, error)
|
|
||||||
|
|
||||||
// ReleaseOutput unlocks an output, allowing it to be available for coin
|
|
||||||
// selection if it remains unspent. The ID should match the one used to
|
|
||||||
// originally lock the output.
|
|
||||||
ReleaseOutput(ctx context.Context, lockID wtxmgr.LockID,
|
|
||||||
op wire.OutPoint) error
|
|
||||||
|
|
||||||
DeriveNextKey(ctx context.Context, family int32) (
|
|
||||||
*keychain.KeyDescriptor, error)
|
|
||||||
|
|
||||||
DeriveKey(ctx context.Context, locator *keychain.KeyLocator) (
|
|
||||||
*keychain.KeyDescriptor, error)
|
|
||||||
|
|
||||||
NextAddr(ctx context.Context) (btcutil.Address, error)
|
|
||||||
|
|
||||||
PublishTransaction(ctx context.Context, tx *wire.MsgTx) error
|
|
||||||
|
|
||||||
SendOutputs(ctx context.Context, outputs []*wire.TxOut,
|
|
||||||
feeRate chainfee.SatPerKWeight) (*wire.MsgTx, error)
|
|
||||||
|
|
||||||
EstimateFee(ctx context.Context, confTarget int32) (chainfee.SatPerKWeight,
|
|
||||||
error)
|
|
||||||
|
|
||||||
// ListSweeps returns a list of sweep transaction ids known to our node.
|
|
||||||
// Note that this function only looks up transaction ids, and does not
|
|
||||||
// query our wallet for the full set of transactions.
|
|
||||||
ListSweeps(ctx context.Context) ([]string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type walletKitClient struct {
|
|
||||||
client walletrpc.WalletKitClient
|
|
||||||
walletKitMac serializedMacaroon
|
|
||||||
}
|
|
||||||
|
|
||||||
// A compile-time constraint to ensure walletKitclient satisfies the
|
|
||||||
// WalletKitClient interface.
|
|
||||||
var _ WalletKitClient = (*walletKitClient)(nil)
|
|
||||||
|
|
||||||
func newWalletKitClient(conn *grpc.ClientConn,
|
|
||||||
walletKitMac serializedMacaroon) *walletKitClient {
|
|
||||||
|
|
||||||
return &walletKitClient{
|
|
||||||
client: walletrpc.NewWalletKitClient(conn),
|
|
||||||
walletKitMac: walletKitMac,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListUnspent returns a list of all utxos spendable by the wallet with a number
|
|
||||||
// of confirmations between the specified minimum and maximum.
|
|
||||||
func (m *walletKitClient) ListUnspent(ctx context.Context, minConfs,
|
|
||||||
maxConfs int32) ([]*lnwallet.Utxo, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.ListUnspent(rpcCtx, &walletrpc.ListUnspentRequest{
|
|
||||||
MinConfs: minConfs,
|
|
||||||
MaxConfs: maxConfs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
utxos := make([]*lnwallet.Utxo, 0, len(resp.Utxos))
|
|
||||||
for _, utxo := range resp.Utxos {
|
|
||||||
var addrType lnwallet.AddressType
|
|
||||||
switch utxo.AddressType {
|
|
||||||
case lnrpc.AddressType_WITNESS_PUBKEY_HASH:
|
|
||||||
addrType = lnwallet.WitnessPubKey
|
|
||||||
case lnrpc.AddressType_NESTED_PUBKEY_HASH:
|
|
||||||
addrType = lnwallet.NestedWitnessPubKey
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("invalid utxo address type %v",
|
|
||||||
utxo.AddressType)
|
|
||||||
}
|
|
||||||
|
|
||||||
pkScript, err := hex.DecodeString(utxo.PkScript)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
opHash, err := chainhash.NewHash(utxo.Outpoint.TxidBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
utxos = append(utxos, &lnwallet.Utxo{
|
|
||||||
AddressType: addrType,
|
|
||||||
Value: btcutil.Amount(utxo.AmountSat),
|
|
||||||
Confirmations: utxo.Confirmations,
|
|
||||||
PkScript: pkScript,
|
|
||||||
OutPoint: wire.OutPoint{
|
|
||||||
Hash: *opHash,
|
|
||||||
Index: utxo.Outpoint.OutputIndex,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return utxos, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LeaseOutput locks an output to the given ID, preventing it from being
|
|
||||||
// available for any future coin selection attempts. The absolute time of the
|
|
||||||
// lock's expiration is returned. The expiration of the lock can be extended by
|
|
||||||
// successive invocations of this call. Outputs can be unlocked before their
|
|
||||||
// expiration through `ReleaseOutput`.
|
|
||||||
func (m *walletKitClient) LeaseOutput(ctx context.Context, lockID wtxmgr.LockID,
|
|
||||||
op wire.OutPoint) (time.Time, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.LeaseOutput(rpcCtx, &walletrpc.LeaseOutputRequest{
|
|
||||||
Id: lockID[:],
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: op.Hash[:],
|
|
||||||
OutputIndex: op.Index,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return time.Unix(int64(resp.Expiration), 0), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleaseOutput unlocks an output, allowing it to be available for coin
|
|
||||||
// selection if it remains unspent. The ID should match the one used to
|
|
||||||
// originally lock the output.
|
|
||||||
func (m *walletKitClient) ReleaseOutput(ctx context.Context,
|
|
||||||
lockID wtxmgr.LockID, op wire.OutPoint) error {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
_, err := m.client.ReleaseOutput(rpcCtx, &walletrpc.ReleaseOutputRequest{
|
|
||||||
Id: lockID[:],
|
|
||||||
Outpoint: &lnrpc.OutPoint{
|
|
||||||
TxidBytes: op.Hash[:],
|
|
||||||
OutputIndex: op.Index,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) DeriveNextKey(ctx context.Context, family int32) (
|
|
||||||
*keychain.KeyDescriptor, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.DeriveNextKey(rpcCtx, &walletrpc.KeyReq{
|
|
||||||
KeyFamily: family,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &keychain.KeyDescriptor{
|
|
||||||
KeyLocator: keychain.KeyLocator{
|
|
||||||
Family: keychain.KeyFamily(resp.KeyLoc.KeyFamily),
|
|
||||||
Index: uint32(resp.KeyLoc.KeyIndex),
|
|
||||||
},
|
|
||||||
PubKey: key,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) DeriveKey(ctx context.Context, in *keychain.KeyLocator) (
|
|
||||||
*keychain.KeyDescriptor, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.DeriveKey(rpcCtx, &signrpc.KeyLocator{
|
|
||||||
KeyFamily: int32(in.Family),
|
|
||||||
KeyIndex: int32(in.Index),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
key, err := btcec.ParsePubKey(resp.RawKeyBytes, btcec.S256())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &keychain.KeyDescriptor{
|
|
||||||
KeyLocator: *in,
|
|
||||||
PubKey: key,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) NextAddr(ctx context.Context) (
|
|
||||||
btcutil.Address, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.NextAddr(rpcCtx, &walletrpc.AddrRequest{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
addr, err := btcutil.DecodeAddress(resp.Addr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) PublishTransaction(ctx context.Context,
|
|
||||||
tx *wire.MsgTx) error {
|
|
||||||
|
|
||||||
txHex, err := swap.EncodeTx(tx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
_, err = m.client.PublishTransaction(rpcCtx, &walletrpc.Transaction{
|
|
||||||
TxHex: txHex,
|
|
||||||
})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) SendOutputs(ctx context.Context,
|
|
||||||
outputs []*wire.TxOut, feeRate chainfee.SatPerKWeight) (
|
|
||||||
*wire.MsgTx, error) {
|
|
||||||
|
|
||||||
rpcOutputs := make([]*signrpc.TxOut, len(outputs))
|
|
||||||
for i, output := range outputs {
|
|
||||||
rpcOutputs[i] = &signrpc.TxOut{
|
|
||||||
PkScript: output.PkScript,
|
|
||||||
Value: output.Value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.SendOutputs(rpcCtx, &walletrpc.SendOutputsRequest{
|
|
||||||
Outputs: rpcOutputs,
|
|
||||||
SatPerKw: int64(feeRate),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := swap.DecodeTx(resp.RawTx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *walletKitClient) EstimateFee(ctx context.Context, confTarget int32) (
|
|
||||||
chainfee.SatPerKWeight, error) {
|
|
||||||
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rpcCtx = m.walletKitMac.WithMacaroonAuth(rpcCtx)
|
|
||||||
resp, err := m.client.EstimateFee(rpcCtx, &walletrpc.EstimateFeeRequest{
|
|
||||||
ConfTarget: int32(confTarget),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return chainfee.SatPerKWeight(resp.SatPerKw), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListSweeps returns a list of sweep transaction ids known to our node.
|
|
||||||
// Note that this function only looks up transaction ids (Verbose=false), and
|
|
||||||
// does not query our wallet for the full set of transactions.
|
|
||||||
func (m *walletKitClient) ListSweeps(ctx context.Context) ([]string, error) {
|
|
||||||
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
resp, err := m.client.ListSweeps(
|
|
||||||
m.walletKitMac.WithMacaroonAuth(rpcCtx),
|
|
||||||
&walletrpc.ListSweepsRequest{
|
|
||||||
Verbose: false,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Since we have requested the abbreviated response from lnd, we can
|
|
||||||
// just get our response to a list of sweeps and return it.
|
|
||||||
sweeps := resp.GetTransactionIds()
|
|
||||||
return sweeps.TransactionIds, nil
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package swap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ChainParamsFromNetwork returns chain parameters based on a network name.
|
|
||||||
func ChainParamsFromNetwork(network string) (*chaincfg.Params, error) {
|
|
||||||
switch network {
|
|
||||||
case "mainnet":
|
|
||||||
return &chaincfg.MainNetParams, nil
|
|
||||||
|
|
||||||
case "testnet":
|
|
||||||
return &chaincfg.TestNet3Params, nil
|
|
||||||
|
|
||||||
case "regtest":
|
|
||||||
return &chaincfg.RegressionNetParams, nil
|
|
||||||
|
|
||||||
case "simnet":
|
|
||||||
return &chaincfg.SimNetParams, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unknown network")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue