lntip/ln/lnd.go

197 lines
5.1 KiB
Go

package ln
import (
"context"
"crypto/x509"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"strings"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"gopkg.in/macaroon.v2"
"github.com/cretz/bine/tor"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
var stdOutLogger = log.New(os.Stdout, "", log.LstdFlags)
// thanks https://github.com/philippgille/ln-paywall/
// Invoice is a Lightning Network invoice and contains the typical invoice string and the payment hash.
type Invoice struct {
PaymentHash string `json:"payment_hash"`
PaymentRequest string `json:"payment_request"`
Settled bool `json:"settled"`
}
// LNDoptions are the options for the connection to the lnd node.
type LNDoptions struct {
Address string
CertFile string
CertHex string
MacaroonFile string
MacaroonHex string
TorExePath string // used when connecting through tor to LND
}
type LNDclient struct {
lndClient lnrpc.LightningClient
ctx context.Context
conn *grpc.ClientConn
}
// AddInvoice generates an invoice with the given price and memo.
func (c LNDclient) AddInvoice(value int64, memo string, descriptionHash []byte) (Invoice, error) {
result := Invoice{}
stdOutLogger.Printf("Adding invoice: memo=%s value=%v", memo, value)
invoice := lnrpc.Invoice{
Memo: memo,
DescriptionHash: descriptionHash,
Value: value,
}
res, err := c.lndClient.AddInvoice(c.ctx, &invoice)
if err != nil {
return result, err
}
result.PaymentHash = hex.EncodeToString(res.RHash)
result.PaymentRequest = res.PaymentRequest
return result, nil
}
// NewAddress gets the next BTC onchain address.
func (c LNDclient) NewAddress() (string, error) {
stdOutLogger.Printf("Getting a new BTC address")
request := lnrpc.NewAddressRequest{
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
}
res, err := c.lndClient.NewAddress(c.ctx, &request)
if err != nil {
return "", err
}
return res.Address, nil
}
// GetInvoice takes an invoice ID and returns the invoice details including settlement details
// An error is returned if no corresponding invoice was found.
func (c LNDclient) GetInvoice(paymentHashStr string) (Invoice, error) {
var invoice Invoice
stdOutLogger.Printf("Getting invoice: hash=%s\n", paymentHashStr)
plainHash, err := hex.DecodeString(paymentHashStr)
if err != nil {
return invoice, err
}
// Get the invoice for that hash
paymentHash := lnrpc.PaymentHash{
RHash: plainHash,
// Hex encoded, must be exactly 32 byte
RHashStr: paymentHashStr,
}
result, err := c.lndClient.LookupInvoice(c.ctx, &paymentHash)
if err != nil {
return invoice, err
}
invoice = Invoice{}
invoice.PaymentHash = hex.EncodeToString(result.RHash)
invoice.PaymentRequest = result.PaymentRequest
invoice.Settled = result.GetSettled()
return invoice, nil
}
func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
result := LNDclient{}
// Get credentials either from a hex string or a file
var creds credentials.TransportCredentials
// if a hex string is provided
if lndOptions.CertHex != "" {
cp := x509.NewCertPool()
cert, err := hex.DecodeString(lndOptions.CertHex)
if err != nil {
return result, err
}
cp.AppendCertsFromPEM(cert)
creds = credentials.NewClientTLSFromCert(cp, "")
// if a path to a cert file is provided
} else if lndOptions.CertFile != "" {
credsFromFile, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
if err != nil {
return result, err
}
creds = credsFromFile // make it available outside of the else if block
} else {
return result, fmt.Errorf("LND credential is missing")
}
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
if strings.Contains(lndOptions.Address, ".onion") {
// Start Tor
t, err := tor.Start(nil, &tor.StartConf{ExePath: lndOptions.TorExePath})
if err != nil {
return result, err
}
torDialer, err := t.Dialer(context.Background(), nil)
if err != nil {
return result, err
}
opts = append(opts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return torDialer.DialContext(ctx, "tcp", addr)
}))
}
var macaroonData []byte
if lndOptions.MacaroonHex != "" {
macBytes, err := hex.DecodeString(lndOptions.MacaroonHex)
if err != nil {
return result, err
}
macaroonData = macBytes
} else if lndOptions.MacaroonFile != "" {
macBytes, err := ioutil.ReadFile(lndOptions.MacaroonFile)
if err != nil {
return result, err
}
macaroonData = macBytes // make it available outside of the else if block
} else {
return result, fmt.Errorf("LND macaroon is missing")
}
mac := &macaroon.Macaroon{}
if err := mac.UnmarshalBinary(macaroonData); err != nil {
return result, err
}
macCred, err := macaroons.NewMacaroonCredential(mac)
if err != nil {
return result, err
}
opts = append(opts, grpc.WithPerRPCCredentials(macCred))
conn, err := grpc.Dial(lndOptions.Address, opts...)
if err != nil {
return result, err
}
c := lnrpc.NewLightningClient(conn)
result = LNDclient{
conn: conn,
ctx: context.Background(),
lndClient: c,
}
return result, nil
}