mirror of
https://github.com/edouardparis/lntop
synced 2024-11-13 13:10:34 +00:00
f72c5ca099
* config: add Views * refac views channels * fix config default * views: menu * config default: add comment * fix config default * README: enhance config section * fix README * controller: F2 Menu * views: transactions * fix views: set current in layout * fix menu * ft transactions * fix Help * fix controller Menu * view: transaction * refac controller * fix ui controller * try some bold * fix cursor * refac color * focus column transactions * controller: add keyBinding Menu m * help view: add menu * fix cursor: push to the right * refac remove current model * ui: txs and channels sortable * fix focus column * view transaction: transaction dest addresses * fix menu * refac controller * channels: sort * rename current column * refac cursor * refac currentColumnIndex * set cursor if view deleted * remove previous * clean view.View * controller: ToggleView * fix menu * view txs: add config * feat order * fix channels sort * feat transactions sort * feat help: add asc/desc * fix README * color: magenta * fix color * fix views menu * fix views help * network backend: SubscribeTransactions * pubsub: transactions * controller.Listen: refresh transactions * fix controller * fix controller pubsub: no need for wallet ticker * fix models transactions * views channels: column SENT and RECEIVED * update version * fix README * fix README * fix models sort * fix readme and default config * fix readme
435 lines
9.2 KiB
Go
435 lines
9.2 KiB
Go
package lnd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
|
"github.com/pkg/errors"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/edouardparis/lntop/config"
|
|
"github.com/edouardparis/lntop/logging"
|
|
"github.com/edouardparis/lntop/network/backend/pool"
|
|
"github.com/edouardparis/lntop/network/models"
|
|
"github.com/edouardparis/lntop/network/options"
|
|
)
|
|
|
|
const (
|
|
lndDefaultInvoiceExpiry = 3600
|
|
)
|
|
|
|
type Client struct {
|
|
lnrpc.LightningClient
|
|
conn *pool.Conn
|
|
}
|
|
|
|
func (c *Client) Close() error {
|
|
return c.conn.Close()
|
|
}
|
|
|
|
type Backend struct {
|
|
cfg *config.Network
|
|
logger logging.Logger
|
|
pool *pool.Pool
|
|
}
|
|
|
|
func (l Backend) NodeName() string {
|
|
return l.cfg.Name
|
|
}
|
|
|
|
func (l Backend) Ping() error {
|
|
clt, err := l.Client(context.Background())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clt.Close()
|
|
return nil
|
|
}
|
|
|
|
func (l Backend) Info(ctx context.Context) (*models.Info, error) {
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
resp, err := clt.GetInfo(ctx, &lnrpc.GetInfoRequest{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return infoProtoToInfo(resp), nil
|
|
}
|
|
|
|
func (l Backend) SubscribeInvoice(ctx context.Context, channelInvoice chan *models.Invoice) error {
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clt.Close()
|
|
|
|
cltInvoices, err := clt.SubscribeInvoices(ctx, &lnrpc.InvoiceSubscription{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break
|
|
default:
|
|
invoice, err := cltInvoices.Recv()
|
|
if err != nil {
|
|
st, ok := status.FromError(err)
|
|
if ok && st.Code() == codes.Canceled {
|
|
l.logger.Debug("stopping subscribe invoice: context canceled")
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
channelInvoice <- lookupInvoiceProtoToInvoice(invoice)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l Backend) SubscribeTransactions(ctx context.Context, channel chan *models.Transaction) error {
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clt.Close()
|
|
|
|
cltTransactions, err := clt.SubscribeTransactions(ctx, &lnrpc.GetTransactionsRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
break
|
|
default:
|
|
transaction, err := cltTransactions.Recv()
|
|
if err != nil {
|
|
st, ok := status.FromError(err)
|
|
if ok && st.Code() == codes.Canceled {
|
|
l.logger.Debug("stopping subscribe transactions: context canceled")
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
channel <- protoToTransaction(transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l Backend) SubscribeChannels(ctx context.Context, events chan *models.ChannelUpdate) error {
|
|
_, err := l.Client(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// events, err := clt.SubscribeChannelEvents(ctx, &lnrpc.ChannelEventSubscription{})
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// for {
|
|
// event, err := events.Recv()
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
// events <-
|
|
//}
|
|
return nil
|
|
}
|
|
|
|
func (l Backend) Client(ctx context.Context) (*Client, error) {
|
|
conn, err := l.pool.Get(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Client{
|
|
LightningClient: lnrpc.NewLightningClient(conn.ClientConn),
|
|
conn: conn,
|
|
}, nil
|
|
}
|
|
|
|
func (l Backend) NewClientConn() (*grpc.ClientConn, error) {
|
|
return newClientConn(l.cfg)
|
|
}
|
|
|
|
func (l Backend) GetTransactions(ctx context.Context) ([]*models.Transaction, error) {
|
|
l.logger.Debug("Get transactions...")
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.GetTransactionsRequest{}
|
|
resp, err := clt.GetTransactions(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return protoToTransactions(resp), nil
|
|
}
|
|
|
|
func (l Backend) GetWalletBalance(ctx context.Context) (*models.WalletBalance, error) {
|
|
l.logger.Debug("Retrieve wallet balance...")
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.WalletBalanceRequest{}
|
|
resp, err := clt.WalletBalance(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
balance := protoToWalletBalance(resp)
|
|
|
|
l.logger.Debug("Wallet balance retrieved", logging.Object("wallet", balance))
|
|
|
|
return balance, nil
|
|
}
|
|
|
|
func (l Backend) GetChannelsBalance(ctx context.Context) (*models.ChannelsBalance, error) {
|
|
l.logger.Debug("Retrieve channel balance...")
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.ChannelBalanceRequest{}
|
|
resp, err := clt.ChannelBalance(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
balance := protoToChannelsBalance(resp)
|
|
|
|
l.logger.Debug("Channel balance retrieved", logging.Object("balance", balance))
|
|
|
|
return balance, nil
|
|
}
|
|
|
|
func (l Backend) ListChannels(ctx context.Context, opt ...options.Channel) ([]*models.Channel, error) {
|
|
l.logger.Debug("List channels")
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
opts := options.NewChannelOptions(opt...)
|
|
req := &lnrpc.ListChannelsRequest{
|
|
ActiveOnly: opts.Active,
|
|
InactiveOnly: opts.Inactive,
|
|
PublicOnly: opts.Public,
|
|
PrivateOnly: opts.Private,
|
|
}
|
|
|
|
resp, err := clt.ListChannels(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
channels := listChannelsProtoToChannels(resp)
|
|
|
|
if opts.Pending {
|
|
req := &lnrpc.PendingChannelsRequest{}
|
|
resp, err := clt.PendingChannels(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
channels = append(channels, pendingChannelsProtoToChannels(resp)...)
|
|
}
|
|
|
|
fields := make([]logging.Field, len(channels))
|
|
for i := range channels {
|
|
fields[i] = logging.Object(fmt.Sprintf("channel_%d", i), channels[i])
|
|
}
|
|
|
|
l.logger.Debug("Channels retrieved", fields...)
|
|
|
|
return channels, nil
|
|
}
|
|
|
|
func (l Backend) GetChannelInfo(ctx context.Context, channel *models.Channel) error {
|
|
l.logger.Debug("GetChannelInfo")
|
|
|
|
// If channel does not have ID (pending), information cannot be retrieved
|
|
if channel.ID == 0 {
|
|
return nil
|
|
}
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.ChanInfoRequest{ChanId: channel.ID}
|
|
resp, err := clt.GetChanInfo(ctx, req)
|
|
if err != nil {
|
|
return errors.WithStack(err)
|
|
}
|
|
if resp == nil {
|
|
return nil
|
|
}
|
|
|
|
t := time.Unix(int64(uint64(resp.LastUpdate)), 0)
|
|
channel.LastUpdate = &t
|
|
channel.Policy1 = protoToRoutingPolicy(resp.Node1Policy)
|
|
channel.Policy2 = protoToRoutingPolicy(resp.Node2Policy)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (l Backend) GetNode(ctx context.Context, pubkey string) (*models.Node, error) {
|
|
l.logger.Debug("GetNode")
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.NodeInfoRequest{PubKey: pubkey}
|
|
resp, err := clt.GetNodeInfo(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return nodeProtoToNode(resp), nil
|
|
}
|
|
|
|
func (l Backend) CreateInvoice(ctx context.Context, amount int64, desc string) (*models.Invoice, error) {
|
|
l.logger.Debug("Create invoice...",
|
|
logging.Int64("amount", amount),
|
|
logging.String("desc", desc))
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.Invoice{
|
|
Value: amount,
|
|
Memo: desc,
|
|
CreationDate: time.Now().Unix(),
|
|
Expiry: lndDefaultInvoiceExpiry,
|
|
}
|
|
|
|
resp, err := clt.AddInvoice(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
invoice := addInvoiceProtoToInvoice(req, resp)
|
|
|
|
l.logger.Debug("Invoice retrieved", logging.Object("invoice", invoice))
|
|
|
|
return invoice, nil
|
|
}
|
|
|
|
func (l Backend) GetInvoice(ctx context.Context, RHash string) (*models.Invoice, error) {
|
|
l.logger.Debug("Retrieve invoice...", logging.String("r_hash", RHash))
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.PaymentHash{
|
|
RHashStr: RHash,
|
|
}
|
|
|
|
resp, err := clt.LookupInvoice(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
invoice := lookupInvoiceProtoToInvoice(resp)
|
|
|
|
l.logger.Debug("Invoice retrieved", logging.Object("invoice", invoice))
|
|
|
|
return invoice, nil
|
|
}
|
|
|
|
func (l Backend) SendPayment(ctx context.Context, payreq *models.PayReq) (*models.Payment, error) {
|
|
l.logger.Debug("Send payment...",
|
|
logging.String("destination", payreq.Destination),
|
|
logging.Int64("amount", payreq.Amount),
|
|
)
|
|
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
req := &lnrpc.SendRequest{PaymentRequest: payreq.String}
|
|
|
|
resp, err := clt.SendPaymentSync(ctx, req)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
payment := sendPaymentProtoToPayment(payreq, resp)
|
|
|
|
l.logger.Debug("Payment paid", logging.Object("payment", payment))
|
|
|
|
return payment, nil
|
|
}
|
|
|
|
func (l Backend) DecodePayReq(ctx context.Context, payreq string) (*models.PayReq, error) {
|
|
l.logger.Info("decode payreq", logging.String("payreq", payreq))
|
|
clt, err := l.Client(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer clt.Close()
|
|
|
|
resp, err := clt.DecodePayReq(ctx, &lnrpc.PayReqString{PayReq: payreq})
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
return payreqProtoToPayReq(resp, payreq), nil
|
|
}
|
|
|
|
func New(c *config.Network, logger logging.Logger) (*Backend, error) {
|
|
var err error
|
|
|
|
backend := &Backend{
|
|
cfg: c,
|
|
logger: logger.With(logging.String("name", c.Name)),
|
|
}
|
|
|
|
backend.pool, err = pool.New(backend.NewClientConn, c.PoolCapacity, time.Duration(c.ConnTimeout))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return backend, nil
|
|
}
|