diff --git a/cmd/http-relay/main.go b/cmd/http-relay/main.go index fa8f607..5d28c8c 100644 --- a/cmd/http-relay/main.go +++ b/cmd/http-relay/main.go @@ -16,6 +16,7 @@ import ( "strconv" "strings" + "github.com/lnproxy/lnc" "github.com/lnproxy/lnproxy" ) @@ -38,7 +39,7 @@ var ( PaymentTimePreference: 0.9, } - lnd *lnproxy.Lnd + lnd *lnc.Lnd validPath = regexp.MustCompile("^/api/(lnbc.*1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)") ) @@ -180,7 +181,7 @@ func main() { }, } - lnd = &lnproxy.Lnd{ + lnd = &lnc.Lnd{ Host: lndHost, Client: lndClient, TlsConfig: lndTlsConfig, diff --git a/go.mod b/go.mod index b9dae4a..dd8d736 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,6 @@ module github.com/lnproxy/lnproxy go 1.20 -require golang.org/x/net v0.9.0 +require github.com/lnproxy/lnc v0.0.0-20230624032429-0bf0de5096b5 + +require golang.org/x/net v0.11.0 // indirect diff --git a/go.sum b/go.sum index c2b1af1..eae76d0 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ -golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +github.com/lnproxy/lnc v0.0.0-20230624032429-0bf0de5096b5 h1:sFZQOgPMCWhIwMaXA/ePAeX+mNSi5Jp/w8a50rAfK4U= +github.com/lnproxy/lnc v0.0.0-20230624032429-0bf0de5096b5/go.mod h1:iR/TmFmHkd5E8EhrqjUbkFdrr9mawoSFyntciGRTVWM= +golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= +golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= diff --git a/ln.go b/ln.go deleted file mode 100644 index dd1657b..0000000 --- a/ln.go +++ /dev/null @@ -1,46 +0,0 @@ -package lnproxy - -import "errors" - -var PaymentHashExists = errors.New("invoice with that payment hash already exists") - -type LN interface { - DecodeInvoice(string) (*DecodedInvoice, error) - AddInvoice(InvoiceParameters) (string, error) - WatchInvoice([]byte) (uint64, error) - CancelInvoice([]byte) error - PayInvoice(p PaymentParameters) ([]byte, error) - SettleInvoice([]byte) error -} - -type DecodedInvoice struct { - PaymentHash string `json:"payment_hash"` - Timestamp uint64 `json:"timestamp,string"` - Expiry uint64 `json:"expiry,string"` - Description string `json:"description"` - DescriptionHash string `json:"description_hash"` - NumMsat uint64 `json:"num_msat,string"` - CltvExpiry uint64 `json:"cltv_expiry,string"` - Features map[string]struct { - Name string `json:"name"` - IsRequired bool `json:"is_required"` - IsKnown bool `json:"is_known"` - } `json:"features"` -} - -type InvoiceParameters struct { - Memo string `json:"memo,omitempty"` - Hash []byte `json:"hash"` - ValueMsat uint64 `json:"value_msat,string"` - DescriptionHash []byte `json:"description_hash,omitempty"` - Expiry uint64 `json:"expiry,string"` - CltvExpiry uint64 `json:"cltv_expiry,string"` -} - -type PaymentParameters struct { - Invoice string `json:"payment_request"` - AmtMsat uint64 `json:"amt_msat,omitempty,string"` - TimeoutSeconds uint64 `json:"timeout_seconds"` - FeeLimitMsat uint64 `json:"fee_limit_msat,string"` - CltvLimit uint64 `json:"cltv_limit"` -} diff --git a/lnd.go b/lnd.go deleted file mode 100644 index 28da8fd..0000000 --- a/lnd.go +++ /dev/null @@ -1,327 +0,0 @@ -package lnproxy - -import ( - "bytes" - "crypto/tls" - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/url" - "time" - - "golang.org/x/net/websocket" -) - -type Lnd struct { - Host *url.URL - Client *http.Client - TlsConfig *tls.Config - Macaroon string -} - -func (lnd *Lnd) DecodeInvoice(invoice string) (*DecodedInvoice, error) { - req, err := http.NewRequest( - "GET", - lnd.Host.JoinPath("v1/payreq", invoice).String(), - nil, - ) - if err != nil { - return nil, err - } - req.Header.Add("Grpc-Metadata-macaroon", lnd.Macaroon) - - resp, err := lnd.Client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - var x interface{} - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&x) - if err != nil { - return nil, err - } - return nil, fmt.Errorf("v1/payreq response: %#v", x) - } - - dec := json.NewDecoder(resp.Body) - p := DecodedInvoice{} - err = dec.Decode(&p) - if err != nil && err != io.EOF { - return nil, err - } - return &p, nil -} - -func (lnd *Lnd) AddInvoice(p InvoiceParameters) (string, error) { - params, err := json.Marshal(p) - if err != nil { - return "", err - } - buf := bytes.NewBuffer(params) - req, err := http.NewRequest( - "POST", - lnd.Host.JoinPath("v2/invoices/hodl").String(), - buf, - ) - if err != nil { - return "", err - } - req.Header.Add("Grpc-Metadata-macaroon", lnd.Macaroon) - resp, err := lnd.Client.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - var x interface{} - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&x) - if err != nil { - return "", err - } - if x, ok := x.(map[string]interface{}); ok { - if x["message"] == "invoice with payment hash already exists" { - return "", PaymentHashExists - } - } - return "", fmt.Errorf("v2/invoices/hodl response: %#v", x) - } - dec := json.NewDecoder(resp.Body) - pr := struct { - PaymentRequest string `json:"payment_request"` - }{} - err = dec.Decode(&pr) - if err != nil && err != io.EOF { - return "", err - } - return pr.PaymentRequest, nil -} - -func (lnd *Lnd) WatchInvoice(hash []byte) (uint64, error) { - header := http.Header(make(map[string][]string, 1)) - header.Add("Grpc-Metadata-Macaroon", lnd.Macaroon) - loc := *lnd.Host - if loc.Scheme == "https" { - loc.Scheme = "wss" - } else { - loc.Scheme = "ws" - } - origin := *lnd.Host - origin.Scheme = "http" - - ws, err := websocket.DialConfig(&websocket.Config{ - Location: loc.JoinPath("v2/invoices/subscribe", base64.URLEncoding.EncodeToString(hash)), - Origin: &origin, - TlsConfig: lnd.TlsConfig, - Header: header, - Version: 13, - }) - if err != nil { - return 0, err - } - defer ws.Close() - err = websocket.JSON.Send(ws, struct{}{}) - if err != nil { - return 0, err - } - for { - message := struct { - Result struct { - State string `json:"state"` - AmtPaidMsat uint64 `json:"amt_paid_msat,string"` - } `json:"result"` - Error struct { - Message string `json:"message"` - } `json:"error"` - }{} - err = websocket.JSON.Receive(ws, &message) - if err != nil && err != io.EOF { - return 0, err - } - if message.Error.Message != "" { - return 0, fmt.Errorf("v2/invoices/subscribe response: %s", message.Error.Message) - } - - switch message.Result.State { - case "OPEN": - time.Sleep(500 * time.Millisecond) - case "ACCEPTED": - return message.Result.AmtPaidMsat, nil - case "SETTLED", "CANCELED": - return message.Result.AmtPaidMsat, fmt.Errorf("invoice %s before payment", message.Result.State) - default: - return 0, fmt.Errorf("v2/invoices/subscribe unhandled state: %s", message.Result.State) - } - - if err == io.EOF { - return 0, err - } - } -} - -func (lnd *Lnd) CancelInvoice(hash []byte) error { - params, _ := json.Marshal( - struct { - PaymentHash []byte `json:"payment_hash"` - }{ - PaymentHash: hash, - }, - ) - buf := bytes.NewBuffer(params) - req, err := http.NewRequest( - "POST", - lnd.Host.JoinPath("v2/invoices/cancel").String(), - buf, - ) - if err != nil { - return err - } - req.Header.Add("Grpc-Metadata-macaroon", lnd.Macaroon) - resp, err := lnd.Client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - var x interface{} - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&x) - if err != nil && err != io.EOF { - return err - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("v2/invoices/cancel response: %v\n", x) - } - if xmap, ok := x.(map[string]interface{}); !ok || len(xmap) != 0 { - return fmt.Errorf("v2/invoices/cancel unhandled response: %v\n", x) - } - - return nil -} - -func (lnd *Lnd) PayInvoice(params PaymentParameters) ([]byte, error) { - header := http.Header(make(map[string][]string, 1)) - header.Add("Grpc-Metadata-Macaroon", lnd.Macaroon) - loc := *lnd.Host - if loc.Scheme == "https" { - loc.Scheme = "wss" - } else { - loc.Scheme = "ws" - } - q := url.Values{} - q.Set("method", "POST") - loc.RawQuery = q.Encode() - origin := *lnd.Host - origin.Scheme = "http" - - ws, err := websocket.DialConfig(&websocket.Config{ - Location: loc.JoinPath("v2/router/send"), - Origin: &origin, - TlsConfig: lnd.TlsConfig, - Header: header, - Version: 13, - }) - if err != nil { - return nil, err - } - defer ws.Close() - err = websocket.JSON.Send(ws, struct { - PaymentParameters - NoInflightUpdates bool `json:"no_inflight_updates"` - Amp bool `json:"amp"` - TimePref float64 `json:"time_pref"` - }{ - PaymentParameters: params, - NoInflightUpdates: true, - Amp: false, - TimePref: 0.9, - }) - if err != nil { - return nil, err - } - for { - message := struct { - Result struct { - Status string `json:"status"` - PreImage string `json:"payment_preimage"` - FailureReason string `json:"failure_reason"` - } `json:"result"` - Error struct { - Message string `json:"message"` - } `json:"error"` - }{} - err = websocket.JSON.Receive(ws, &message) - if err != nil && err != io.EOF { - return nil, err - } - if message.Error.Message != "" { - return nil, fmt.Errorf("v2/router/send response: %s", message.Error.Message) - } - - switch message.Result.Status { - case "FAILED": - return nil, fmt.Errorf("Payment failed\n") - case "UNKNOWN", "IN_FLIGHT", "": - time.Sleep(500 * time.Millisecond) - case "SUCCEEDED": - log.Printf("preimage: %s\n", message.Result.PreImage) - preimage, err := hex.DecodeString(message.Result.PreImage) - if err != nil { - log.Panicln(err) - } - return preimage, nil - default: - return nil, fmt.Errorf("v2/router/send unhandled status: %s", message.Result.Status) - } - - if err == io.EOF { - return nil, err - } - } -} - -func (lnd *Lnd) SettleInvoice(preimage []byte) error { - params, err := json.Marshal(struct { - PreImage []byte `json:"preimage"` - }{ - PreImage: preimage, - }) - if err != nil { - return err - } - buf := bytes.NewBuffer(params) - req, err := http.NewRequest( - "POST", - lnd.Host.JoinPath("v2/invoices/settle").String(), - buf, - ) - if err != nil { - return err - } - req.Header.Add("Grpc-Metadata-macaroon", lnd.Macaroon) - resp, err := lnd.Client.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - var x interface{} - dec := json.NewDecoder(resp.Body) - err = dec.Decode(&x) - if err != nil && err != io.EOF { - return err - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("v2/invoices/settle response: %#v", x) - } - if xmap, ok := x.(map[string]interface{}); !ok || len(xmap) != 0 { - return fmt.Errorf("v2/invoices/settle unhandled response: %#v", x) - } - return nil -} diff --git a/lnproxy.go b/lnproxy.go index d73792f..01e9b93 100644 --- a/lnproxy.go +++ b/lnproxy.go @@ -8,6 +8,8 @@ import ( "log" "strconv" "time" + + "github.com/lnproxy/lnc" ) var ClientFacing = errors.New("") @@ -84,7 +86,7 @@ func (ms *MaybeString) UnmarshalJSON(data []byte) error { return nil } -func Wrap(r RelayParameters, x ProxyParameters, p DecodedInvoice) (*InvoiceParameters, uint64, error) { +func Wrap(r RelayParameters, x ProxyParameters, p lnc.DecodedInvoice) (*lnc.InvoiceParameters, uint64, error) { for flag, _ := range p.Features { switch flag { case "8", "9", "14", "15", "16", "17", "25", "48", "49", "149", "151": @@ -105,7 +107,7 @@ func Wrap(r RelayParameters, x ProxyParameters, p DecodedInvoice) (*InvoiceParam return nil, 0, errors.Join(ClientFacing, errors.New("invoice amount too low")) } - q := InvoiceParameters{} + q := lnc.InvoiceParameters{} hash, err := hex.DecodeString(p.PaymentHash) if err != nil { @@ -133,7 +135,7 @@ func Wrap(r RelayParameters, x ProxyParameters, p DecodedInvoice) (*InvoiceParam q.Memo = p.Description } - if p.Timestamp + p.Expiry < uint64(time.Now().Unix()) + r.ExpiryBuffer { + if p.Timestamp+p.Expiry < uint64(time.Now().Unix())+r.ExpiryBuffer { return nil, 0, errors.Join(ClientFacing, errors.New("payment request expiration is too close.")) } q.Expiry = p.Timestamp + p.Expiry - uint64(time.Now().Unix()) - r.ExpiryBuffer @@ -158,7 +160,7 @@ func Wrap(r RelayParameters, x ProxyParameters, p DecodedInvoice) (*InvoiceParam return &q, fee_budget_msat, nil } -func Relay(ln LN, r RelayParameters, x ProxyParameters) (string, error) { +func Relay(ln lnc.LN, r RelayParameters, x ProxyParameters) (string, error) { p, err := ln.DecodeInvoice(x.Invoice) if err != nil { return "", err @@ -168,8 +170,8 @@ func Relay(ln LN, r RelayParameters, x ProxyParameters) (string, error) { return "", err } proxy_invoice, err := ln.AddInvoice(*q) - if errors.Is(err, PaymentHashExists) { - return "", errors.Join(ClientFacing, PaymentHashExists) + if errors.Is(err, lnc.PaymentHashExists) { + return "", errors.Join(ClientFacing, lnc.PaymentHashExists) } else if err != nil { return "", err } @@ -183,7 +185,7 @@ func Relay(ln LN, r RelayParameters, x ProxyParameters) (string, error) { } return } - preimage, err := ln.PayInvoice(PaymentParameters{ + preimage, err := ln.PayInvoice(lnc.PaymentParameters{ Invoice: x.Invoice, TimeoutSeconds: r.PaymentTimeout, FeeLimitMsat: fee_budget_msat,