Skip to content

Commit

Permalink
support eclair.
Browse files Browse the repository at this point in the history
  • Loading branch information
fiatjaf committed Mar 14, 2022
1 parent c5ea7f9 commit 44046a5
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 80 deletions.
10 changes: 9 additions & 1 deletion connect/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/kelseyhightower/envconfig"
"github.com/lnbits/relampago"
"github.com/lnbits/relampago/eclair"
"github.com/lnbits/relampago/lnd"
"github.com/lnbits/relampago/sparko"
"github.com/lnbits/relampago/void"
Expand All @@ -22,6 +23,9 @@ type LightningBackendSettings struct {
LNDHost string `envconfig:"LND_HOST"`
LNDCertPath string `envconfig:"LND_CERT_PATH"`
LNDMacaroonPath string `envconfig:"LND_MACAROON_PATH"`

EclairHost string `envconfig:"ECLAIR_HOST"`
EclairPassword string `envconfig:"ECLAIR_PASSWORD"`
}

func Connect() (relampago.Wallet, error) {
Expand All @@ -39,14 +43,18 @@ func Connect() (relampago.Wallet, error) {
// start lightning backend
switch lbs.BackendType {
case "lndrest":
case "lndgrpc":
case "lnd", "lndgrpc":
return lnd.Start(lnd.Params{
Host: lbs.LNDHost,
CertPath: lbs.LNDCertPath,
MacaroonPath: lbs.LNDMacaroonPath,
ConnectTimeout: time.Duration(connectTimeout) * time.Second,
})
case "eclair":
return eclair.Start(eclair.Params{
Host: lbs.EclairHost,
Password: lbs.EclairPassword,
})
case "clightning":
case "sparko":
return sparko.Start(sparko.Params{
Expand Down
215 changes: 215 additions & 0 deletions eclair/eclair.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package eclair

import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"strings"
"time"

"github.com/fiatjaf/eclair-go"
rp "github.com/lnbits/relampago"
)

type Params struct {
Host string
Password string
}

type EclairWallet struct {
Params

client *eclair.Client
}

func Start(params Params) (*EclairWallet, error) {
if !strings.HasPrefix(params.Host, "http") {
params.Host = "http://" + params.Host
}

e := &EclairWallet{
Params: params,
client: &eclair.Client{
Host: params.Host,
Password: params.Password,
},
}

if ws, err := e.client.Websocket(); err != nil {
panic(err)
} else {
go func() {
for message := range ws {
typ := message.Get("type").String()
if typ == "channel-state-changed" {
continue
}

log.Printf("[%s] %s", typ, message.String())
}
}()
}

return e, nil
}

// Compile time check to ensure that EclairWallet fully implements rp.Wallet
var _ rp.Wallet = (*EclairWallet)(nil)

func (e *EclairWallet) Kind() string {
return "eclair"
}

func (e *EclairWallet) GetInfo() (rp.WalletInfo, error) {
res, err := e.client.Call("channels", map[string]interface{}{})
if err != nil {
return rp.WalletInfo{}, fmt.Errorf("error calling 'channels': %w", err)
}

var balance int64
for _, channel := range res.Array() {
balance += channel.Get("data.commitments.localCommit.spec.toLocal").Int()
}

return rp.WalletInfo{Balance: balance}, nil
}

func (e *EclairWallet) CreateInvoice(params rp.InvoiceParams) (rp.InvoiceData, error) {
args := map[string]interface{}{
"amountMsat": params.Msatoshi,
}

if params.DescriptionHash == nil {
args["description"] = params.Description
} else {
args["descriptionHash"] = hex.EncodeToString(params.DescriptionHash)
}

preimage := make([]byte, 32)
if _, err := rand.Read(preimage); err != nil {
return rp.InvoiceData{}, fmt.Errorf("failed to make random preimage: %w", err)
} else {
args["paymentPreimage"] = hex.EncodeToString(preimage)
}

if params.Expiry != nil {
args["expireIn"] = *params.Expiry / time.Second
}

inv, err := e.client.Call("createinvoice", args)
if err != nil {
return rp.InvoiceData{}, fmt.Errorf("'createinvoice' call failed: %w", err)
}
return rp.InvoiceData{
Invoice: inv.Get("serialized").String(),
Preimage: args["paymentPreimage"].(string),
CheckingID: inv.Get("paymentHash").String(),
}, nil
}

func (e *EclairWallet) GetInvoiceStatus(checkingID string) (rp.InvoiceStatus, error) {
res, err := e.client.Call("getreceivedinfo", map[string]interface{}{
"paymentHash": checkingID,
})
if err != nil {
if strings.Contains(err.Error(), "Not found") {
return rp.InvoiceStatus{
CheckingID: checkingID,
Exists: false,
}, nil
}

return rp.InvoiceStatus{},
fmt.Errorf("error on 'getreceivedinfo' hash=%s: %w", checkingID, err)
}

return rp.InvoiceStatus{
CheckingID: checkingID,
Exists: true,
Paid: res.Get("status.type").String() == "received",
MSatoshiReceived: res.Get("status.amount").Int(),
}, nil
}

func (e *EclairWallet) PaidInvoicesStream() (<-chan rp.InvoiceStatus, error) {
listener := make(chan rp.InvoiceStatus)
return listener, nil
}

func (e *EclairWallet) MakePayment(params rp.PaymentParams) (rp.PaymentData, error) {
args := map[string]interface{}{
"invoice": params.Invoice,
"blocking": false,
"maxFeePct": 1,
}
if params.CustomAmount != 0 {
args["amountMsat"] = params.CustomAmount
}

id, err := e.client.Call("payinvoice", args)
if err != nil {
return rp.PaymentData{}, fmt.Errorf("error calling 'payinvoice' with '%s': %w",
params.Invoice, err)
}

return rp.PaymentData{
CheckingID: id.Value().(string),
}, nil
}

func (e *EclairWallet) GetPaymentStatus(checkingID string) (rp.PaymentStatus, error) {
res, err := e.client.Call("getsentinfo", map[string]interface{}{
"id": checkingID,
})
if err != nil {
return rp.PaymentStatus{},
fmt.Errorf("error getting payment %s: %w", checkingID, err)
}

if res.Get("#").Int() == 0 {
return rp.PaymentStatus{
CheckingID: checkingID,
Status: rp.NeverTried,
}, nil
} else {
for _, attempt := range res.Array() {
status := attempt.Get("status")

switch status.Get("type").String() {
case "sent":
return rp.PaymentStatus{
CheckingID: checkingID,
Status: rp.Complete,
FeePaid: status.Get("feesPaid").Int(),
Preimage: status.Get("paymentPreimage").String(),
}, nil
case "pending":
return rp.PaymentStatus{
CheckingID: checkingID,
Status: rp.Pending,
}, nil
default:
// what is this?
return rp.PaymentStatus{
CheckingID: checkingID,
Status: rp.Unknown,
}, nil
case "failed":
// this one failed, but keep checking the others
continue
}
}

// if we reached here that's because all attempts are failed
return rp.PaymentStatus{
CheckingID: checkingID,
Status: rp.Failed,
}, nil
}
}

func (e *EclairWallet) PaymentsStream() (<-chan rp.PaymentStatus, error) {
listener := make(chan rp.PaymentStatus)
return listener, nil
}
79 changes: 0 additions & 79 deletions relampago.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,85 +65,6 @@ type PaymentStatus struct {
Preimage string `json:"preimage"`
}

// // description hash?
// var hexh, b64h string
// if params.DescriptionHash != nil {
// hexh = hex.EncodeToString(params.DescriptionHash)
// b64h = base64.StdEncoding.EncodeToString(params.DescriptionHash)
// }
//
// switch backend := params.Backend.(type) {
// case SparkoParams:
// spark := &lightning.Client{
// SparkURL: backend.Host,
// SparkToken: backend.Key,
// CallTimeout: time.Second * 3,
// }
//
// var method, desc string
// if params.DescriptionHash == nil {
// method = "invoice"
// desc = params.Description
// } else {
// method = "invoicewithdescriptionhash"
// desc = hexh
// }
//
// label := params.Label
// if label == "" {
// label = "makeinvoice/" + strconv.FormatInt(time.Now().Unix(), 16)
// }
//
// inv, err := spark.Call(method, params.Msatoshi, label, desc)
// if err != nil {
// return "", fmt.Errorf(method+" call failed: %w", err)
// }
// return inv.Get("bolt11").String(), nil
//
// case LNDParams:
// body, _ := sjson.Set("{}", "value_msat", params.Msatoshi)
//
// if params.DescriptionHash == nil {
// body, _ = sjson.Set(body, "memo", params.Description)
// } else {
// body, _ = sjson.Set(body, "description_hash", b64h)
// }
//
// req, err := http.NewRequest("POST",
// backend.Host+"/v1/invoices",
// bytes.NewBufferString(body),
// )
// if err != nil {
// return "", err
// }
//
// // macaroon must be hex, so if it is on base64 we adjust that
// if b, err := base64.StdEncoding.DecodeString(backend.Macaroon); err == nil {
// backend.Macaroon = hex.EncodeToString(b)
// }
//
// req.Header.Set("Grpc-Metadata-macaroon", backend.Macaroon)
// resp, err := http.DefaultClient.Do(req)
// if err != nil {
// return "", err
// }
// defer resp.Body.Close()
// if resp.StatusCode >= 300 {
// body, _ := ioutil.ReadAll(resp.Body)
// text := string(body)
// if len(text) > 300 {
// text = text[:300]
// }
// return "", fmt.Errorf("call to lnd failed (%d): %s", resp.StatusCode, text)
// }
//
// b, err := ioutil.ReadAll(resp.Body)
// if err != nil {
// return "", err
// }
//
// return gjson.ParseBytes(b).Get("payment_request").String(), nil
//
// case LNBitsParams:
// body, _ := sjson.Set("{}", "amount", params.Msatoshi/1000)
// body, _ = sjson.Set(body, "out", false)
Expand Down

0 comments on commit 44046a5

Please sign in to comment.