Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/eclair #351

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions init_lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ func InitLNClient(c *service.Config, logger *lecho.Logger, ctx context.Context)
return InitSingleLNDClient(c, ctx)
case service.LND_CLUSTER_CLIENT_TYPE:
return InitLNDCluster(c, logger, ctx)
case service.ECLAIR_CLIENT_TYPE:
return lnd.NewEclairClient(c.LNDAddress, c.EclairPassword, ctx)
default:
return nil, fmt.Errorf("Did not recognize LN client type %s", c.LNClientType)
}
Expand Down
1 change: 1 addition & 0 deletions lib/service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Config struct {
LNDCertFile string `envconfig:"LND_CERT_FILE"`
LNDMacaroonHex string `envconfig:"LND_MACAROON_HEX"`
LNDCertHex string `envconfig:"LND_CERT_HEX"`
EclairPassword string `envconfig:"ECLAIR_PASSWORD"`
LNDClusterLivenessPeriod int `envconfig:"LND_CLUSTER_LIVENESS_PERIOD" default:"10"`
LNDClusterActiveChannelRatio float64 `envconfig:"LND_CLUSTER_ACTIVE_CHANNEL_RATIO" default:"0.5"`
CustomName string `envconfig:"CUSTOM_NAME"`
Expand Down
259 changes: 259 additions & 0 deletions lnd/eclair.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package lnd

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"google.golang.org/grpc"
)

type EclairClient struct {
host string
password string
IdentityPubkey string
}

type EclairInvoicesSubscriber struct {
ctx context.Context
}

func (eis *EclairInvoicesSubscriber) Recv() (*lnrpc.Invoice, error) {
//placeholder
//block indefinitely
<-eis.ctx.Done()
return nil, fmt.Errorf("context canceled")
}

type EclairPaymentsTracker struct {
ctx context.Context
}

func (ept *EclairPaymentsTracker) Recv() (*lnrpc.Payment, error) {
//placeholder
//block indefinitely
<-ept.ctx.Done()
return nil, fmt.Errorf("context canceled")
}

func NewEclairClient(host, password string, ctx context.Context) (result *EclairClient, err error) {
result = &EclairClient{
host: host,
password: password,
}
info, err := result.GetInfo(ctx, &lnrpc.GetInfoRequest{})
if err != nil {
return nil, err
}
result.IdentityPubkey = info.IdentityPubkey
return result, nil
}

func (eclair *EclairClient) ListChannels(ctx context.Context, req *lnrpc.ListChannelsRequest, options ...grpc.CallOption) (*lnrpc.ListChannelsResponse, error) {
channels := []EclairChannel{}
err := eclair.Request(ctx, http.MethodPost, "/channels", "", nil, &channels)
if err != nil {
return nil, err
}
convertedChannels := []*lnrpc.Channel{}
for _, ch := range channels {
convertedChannels = append(convertedChannels, &lnrpc.Channel{
Active: ch.State == "NORMAL",
RemotePubkey: ch.NodeID,
ChannelPoint: "",
ChanId: 0,
Capacity: int64(ch.Data.Commitments.LocalCommit.Spec.ToLocal)/1000 + int64(ch.Data.Commitments.LocalCommit.Spec.ToRemote)/1000,
LocalBalance: int64(ch.Data.Commitments.LocalCommit.Spec.ToLocal) / 1000,
RemoteBalance: int64(ch.Data.Commitments.LocalCommit.Spec.ToRemote) / 1000,
CommitFee: 0,
CommitWeight: 0,
FeePerKw: 0,
UnsettledBalance: 0,
TotalSatoshisSent: 0,
TotalSatoshisReceived: 0,
NumUpdates: 0,
PendingHtlcs: []*lnrpc.HTLC{},
CsvDelay: 0,
Private: false,
Initiator: false,
ChanStatusFlags: "",
LocalChanReserveSat: 0,
RemoteChanReserveSat: 0,
StaticRemoteKey: false,
CommitmentType: 0,
Lifetime: 0,
Uptime: 0,
CloseAddress: "",
PushAmountSat: 0,
ThawHeight: 0,
LocalConstraints: &lnrpc.ChannelConstraints{},
RemoteConstraints: &lnrpc.ChannelConstraints{},
AliasScids: []uint64{},
ZeroConf: false,
ZeroConfConfirmedScid: 0,
})
}
return &lnrpc.ListChannelsResponse{
Channels: convertedChannels,
}, nil
}

func (eclair *EclairClient) SendPaymentSync(ctx context.Context, req *lnrpc.SendRequest, options ...grpc.CallOption) (*lnrpc.SendResponse, error) {
payload := url.Values{}
payload.Add("invoice", req.PaymentRequest)
payload.Add("amountMsat", strconv.Itoa(int(req.Amt)*1000))
payload.Add("maxFeeFlatSat", strconv.Itoa(int(req.FeeLimit.GetFixed())))
payload.Add("blocking", "true") //wtf
resp := &EclairPayResponse{}
err := eclair.Request(ctx, http.MethodPost, "/payinvoice", "application/x-www-form-urlencoded", payload, resp)
if err != nil {
return nil, err
}
errString := ""
if resp.Type == "payment-failed" && len(resp.Failures) > 0 {
errString = resp.Failures[0].T
}
totalFees := 0
for _, part := range resp.Parts {
totalFees += part.FeesPaid / 1000
}
preimage, err := hex.DecodeString(resp.PaymentPreimage)
if err != nil {
return nil, err
}
return &lnrpc.SendResponse{
PaymentError: errString,
PaymentPreimage: preimage,
PaymentRoute: &lnrpc.Route{
TotalFees: int64(totalFees),
TotalAmt: int64(resp.RecipientAmount)/1000 + int64(totalFees),
},
PaymentHash: []byte(resp.PaymentHash),
}, nil
}

func (eclair *EclairClient) AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error) {
payload := url.Values{}
if req.Memo != "" {
payload.Add("description", req.Memo)
}
if len(req.DescriptionHash) != 0 {
payload.Add("descriptionHash", string(req.DescriptionHash))
}
payload.Add("amountMsat", strconv.Itoa(int(req.Value*1000)))
payload.Add("paymentPreimage", hex.EncodeToString(req.RPreimage))
payload.Add("expireIn", strconv.Itoa(int(req.Expiry)))
invoice := &EclairInvoice{}
err := eclair.Request(ctx, http.MethodPost, "/createinvoice", "application/x-www-form-urlencoded", payload, invoice)
if err != nil {
return nil, err
}
rHash, err := hex.DecodeString(invoice.PaymentHash)
if err != nil {
return nil, err
}
return &lnrpc.AddInvoiceResponse{
RHash: rHash,
PaymentRequest: invoice.Serialized,
AddIndex: uint64(invoice.Timestamp),
}, nil
}

func (eclair *EclairClient) SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error) {
return &EclairInvoicesSubscriber{
ctx: ctx,
}, nil
}

func (eclair *EclairClient) SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error) {
return &EclairPaymentsTracker{
ctx: ctx,
}, nil
}

func (eclair *EclairClient) GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error) {
info := EclairInfoResponse{}
err := eclair.Request(ctx, http.MethodPost, "/getinfo", "", nil, &info)
if err != nil {
return nil, err
}
addresses := []string{}
for _, addr := range info.PublicAddresses {
addresses = append(addresses, fmt.Sprintf("%s@%s", info.NodeID, addr))
}
return &lnrpc.GetInfoResponse{
Version: info.Version,
CommitHash: "",
IdentityPubkey: info.NodeID,
Alias: info.Alias,
Color: info.Color,
NumPendingChannels: 0,
NumActiveChannels: 0,
NumInactiveChannels: 0,
NumPeers: 0,
BlockHeight: uint32(info.BlockHeight),
BlockHash: "",
BestHeaderTimestamp: 0,
SyncedToChain: true,
SyncedToGraph: true,
Testnet: info.Network == "testnet",
Chains: []*lnrpc.Chain{{
Chain: "bitcoin",
Network: info.Network,
}},
Uris: addresses,
Features: map[uint32]*lnrpc.Feature{},
RequireHtlcInterceptor: false,
}, nil
}

func (eclair *EclairClient) Request(ctx context.Context, method, endpoint, contentType string, body url.Values, response interface{}) error {
httpReq, err := http.NewRequestWithContext(ctx, method, fmt.Sprintf("%s%s", eclair.host, endpoint), strings.NewReader(body.Encode()))
httpReq.Header.Set("Content-type", contentType)
httpReq.SetBasicAuth("", eclair.password)
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
response := map[string]interface{}{}
json.NewDecoder(resp.Body).Decode(&response)
return fmt.Errorf("Got a bad http response status code from Eclair %d for request %s. Body: %s", resp.StatusCode, httpReq.URL, response)
}
return json.NewDecoder(resp.Body).Decode(response)
}

func (eclair *EclairClient) DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error) {
invoice := &EclairInvoice{}
payload := url.Values{}
payload.Add("invoice", bolt11)
err := eclair.Request(ctx, http.MethodPost, "/parseinvoice", "application/x-www-form-urlencoded", payload, invoice)
if err != nil {
return nil, err
}
return &lnrpc.PayReq{
Destination: invoice.NodeID,
PaymentHash: invoice.PaymentHash,
NumSatoshis: int64(invoice.Amount) / 1000,
Timestamp: int64(invoice.Timestamp),
Expiry: int64(invoice.Expiry),
Description: invoice.Description,
DescriptionHash: invoice.DescriptionHash,
NumMsat: int64(invoice.Amount),
}, nil
}

func (eclair *EclairClient) IsIdentityPubkey(pubkey string) (isOurPubkey bool) {
return pubkey == eclair.IdentityPubkey
}

func (eclair *EclairClient) GetMainPubkey() (pubkey string) {
return eclair.IdentityPubkey
}
Loading