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")

Check warning on line 32 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L28-L32

Added lines #L28 - L32 were not covered by tests
}

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")

Check warning on line 43 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L39-L43

Added lines #L39 - L43 were not covered by tests
}

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

Check warning on line 56 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L46-L56

Added lines #L46 - L56 were not covered by tests
}

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

Check warning on line 105 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L59-L105

Added lines #L59 - L105 were not covered by tests
}

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

Check warning on line 139 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L108-L139

Added lines #L108 - L139 were not covered by tests
}

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

Check warning on line 166 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L142-L166

Added lines #L142 - L166 were not covered by tests
}

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

Check warning on line 172 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L169-L172

Added lines #L169 - L172 were not covered by tests
}

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

Check warning on line 178 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L175-L178

Added lines #L175 - L178 were not covered by tests
}

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

Check warning on line 214 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L181-L214

Added lines #L181 - L214 were not covered by tests
}

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)

Check warning on line 230 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L217-L230

Added lines #L217 - L230 were not covered by tests
}

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

Check warning on line 250 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L233-L250

Added lines #L233 - L250 were not covered by tests
}

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

Check warning on line 254 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L253-L254

Added lines #L253 - L254 were not covered by tests
}

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

Check warning on line 258 in lnd/eclair.go

View check run for this annotation

Codecov / codecov/patch

lnd/eclair.go#L257-L258

Added lines #L257 - L258 were not covered by tests
}
Loading
Loading