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

Feat: lookup invoice (Alby + LND) #124

Merged
merged 8 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 111 additions & 33 deletions alby.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
}).Errorf("Value must be 1 sat or greater")
return "", "", errors.New("Value must be 1 sat or greater")
}).Errorf("amount must be 1000 msat or greater");
return "", "", errors.New("amount must be 1000 msat or greater")
}

svc.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -174,21 +174,99 @@ func (svc *AlbyOAuthService) MakeInvoice(ctx context.Context, senderPubkey strin
"paymentHash": responsePayload.PaymentHash,
}).Info("Make invoice successful")
return responsePayload.PaymentRequest, responsePayload.PaymentHash, nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"amount": amount,
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Make invoice failed %s", string(errorPayload.Message))
return "", "", errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) {
// TODO: move to a shared function
app := App{}
err = svc.db.Preload("User").First(&app, &App{
NostrPubkey: senderPubkey,
}).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"amount": amount,
"description": description,
"descriptionHash": descriptionHash,
"expiry": expiry,
"paymentHash": paymentHash,
}).Errorf("App not found: %v", err)
return "", false, err
}

svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
}).Info("Processing lookup invoice request")
tok, err := svc.FetchUserToken(ctx, app)
if err != nil {
return "", false, err
}
client := svc.oauthConf.Client(ctx, tok)

body := bytes.NewBuffer([]byte{})

// TODO: move to a shared function
req, err := http.NewRequest("GET", fmt.Sprintf("%s/invoices/%s", svc.cfg.AlbyAPIURL, paymentHash), body)
if err != nil {
svc.Logger.WithError(err).Errorf("Error creating request /invoices/%s", paymentHash)
return "", false, err
}

req.Header.Set("User-Agent", "NWC")
req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
}).Errorf("Failed to lookup invoice: %v", err)
return "", false, err
}

if resp.StatusCode < 300 {
responsePayload := &LookupInvoiceResponse{}
err = json.NewDecoder(resp.Body).Decode(responsePayload)
if err != nil {
return "", false, err
}
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Make invoice failed %s", string(errorPayload.Message))
return "", "", errors.New(errorPayload.Message)
"paymentRequest": responsePayload.PaymentRequest,
"settled": responsePayload.Settled,
}).Info("Lookup invoice successful")
return responsePayload.PaymentRequest, responsePayload.Settled, nil
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"paymentHash": paymentHash,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Lookup invoice failed %s", string(errorPayload.Message))
return "", false, errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error) {
Expand Down Expand Up @@ -238,17 +316,17 @@ func (svc *AlbyOAuthService) GetBalance(ctx context.Context, senderPubkey string
"userId": app.User.ID,
}).Info("Balance fetch successful")
return int64(responsePayload.Balance), nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Balance fetch failed %s", string(errorPayload.Message))
return 0, errors.New(errorPayload.Message)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Balance fetch failed %s", string(errorPayload.Message))
return 0, errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) {
Expand Down Expand Up @@ -314,18 +392,18 @@ func (svc *AlbyOAuthService) SendPaymentSync(ctx context.Context, senderPubkey,
"userId": app.User.ID,
}).Info("Payment successful")
return responsePayload.Preimage, nil
} else {
errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"bolt11": payReq,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Payment failed %s", string(errorPayload.Message))
return "", errors.New(errorPayload.Message)
}

errorPayload := &ErrorResponse{}
err = json.NewDecoder(resp.Body).Decode(errorPayload)
svc.Logger.WithFields(logrus.Fields{
"senderPubkey": senderPubkey,
"bolt11": payReq,
"appId": app.ID,
"userId": app.User.ID,
"APIHttpStatus": resp.StatusCode,
}).Errorf("Payment failed %s", string(errorPayload.Message))
return "", errors.New(errorPayload.Message)
}

func (svc *AlbyOAuthService) AuthHandler(c echo.Context) error {
Expand Down
8 changes: 4 additions & 4 deletions handle_balance_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ const (
func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) {

nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"}
insertNostrEventResult := svc.db.Create(&nostrEvent)
if insertNostrEventResult.Error != nil {
err = svc.db.Create(&nostrEvent).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("Failed to save nostr event: %v", insertNostrEventResult.Error)
return nil, insertNostrEventResult.Error
}).Errorf("Failed to save nostr event: %v", err)
return nil, err
}

hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil)
Expand Down
115 changes: 115 additions & 0 deletions handle_lookup_invoice_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"context"
"encoding/json"
"fmt"

"github.com/nbd-wtf/go-nostr"
decodepay "github.com/nbd-wtf/ln-decodepay"
"github.com/sirupsen/logrus"
)

func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) {
// TODO: move to a shared function
nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"}
err = svc.db.Create(&nostrEvent).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("Failed to save nostr event: %v", err)
return nil, err
}

// TODO: move to a shared function
hasPermission, code, message := svc.hasPermission(&app, event, request.Method, nil)

if !hasPermission {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("App does not have permission: %s %s", code, message)

return svc.createResponse(event, Nip47Response{Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}

// TODO: move to a shared generic function
lookupInvoiceParams := &Nip47LookupInvoiceParams{}
err = json.Unmarshal(request.Params, lookupInvoiceParams)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("Failed to decode nostr event: %v", err)
return nil, err
}

svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"invoice": lookupInvoiceParams.Invoice,
"paymentHash": lookupInvoiceParams.PaymentHash,
}).Info("Looking up invoice")

paymentHash := lookupInvoiceParams.PaymentHash

if (paymentHash == "") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I was the author of the NIP PR I would drop the payment_request parameter and mandate payment_hash. But not really related to this PR so OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we give feedback? this is the time to do it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the user always has access to the payment hash, as long as the invoice was generated through NWC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked in the PR.

paymentRequest, err := decodepay.Decodepay(lookupInvoiceParams.Invoice)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
rolznz marked this conversation as resolved.
Show resolved Hide resolved
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"invoice": lookupInvoiceParams.Invoice,
}).Errorf("Failed to decode bolt11 invoice: %v", err)

return svc.createResponse(event, Nip47Response{
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, ss)
}
paymentHash = paymentRequest.PaymentHash
}

invoice, paid, err := svc.lnClient.LookupInvoice(ctx, event.PubKey, paymentHash)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"invoice": lookupInvoiceParams.Invoice,
"paymentHash": lookupInvoiceParams.PaymentHash,
}).Infof("Failed to lookup invoice: %v", err)
nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR
svc.db.Save(&nostrEvent)
return svc.createResponse(event, Nip47Response{
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while looking up invoice: %s", err.Error()),
},
}, ss)
}

responsePayload := &Nip47LookupInvoiceResponse {
Invoice: invoice,
Paid: paid,
}

nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED
svc.db.Save(&nostrEvent)
return svc.createResponse(event, Nip47Response{
ResultType: NIP_47_LOOKUP_INVOICE_METHOD,
Result: responsePayload,
},
ss)
}
8 changes: 4 additions & 4 deletions handle_make_invoice_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re

// TODO: move to a shared function
nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"}
insertNostrEventResult := svc.db.Create(&nostrEvent)
if insertNostrEventResult.Error != nil {
err = svc.db.Create(&nostrEvent).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("Failed to save nostr event: %v", insertNostrEventResult.Error)
return nil, insertNostrEventResult.Error
}).Errorf("Failed to save nostr event: %v", err)
return nil, err
}

// TODO: move to a shared function
Expand Down
8 changes: 4 additions & 4 deletions handle_payment_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ import (
func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Request, event *nostr.Event, app App, ss []byte) (result *nostr.Event, err error) {

nostrEvent := NostrEvent{App: app, NostrId: event.ID, Content: event.Content, State: "received"}
insertNostrEventResult := svc.db.Create(&nostrEvent)
if insertNostrEventResult.Error != nil {
err = svc.db.Create(&nostrEvent).Error
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
}).Errorf("Failed to save nostr event: %v", insertNostrEventResult.Error)
return nil, insertNostrEventResult.Error
}).Errorf("Failed to save nostr event: %v", err)
return nil, err
}

var bolt11 string
Expand Down
19 changes: 19 additions & 0 deletions lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type LNClient interface {
SendPaymentSync(ctx context.Context, senderPubkey string, payReq string) (preimage string, err error)
GetBalance(ctx context.Context, senderPubkey string) (balance int64, err error)
MakeInvoice(ctx context.Context, senderPubkey string, amount int64, description string, descriptionHash string, expiry int64) (invoice string, paymentHash string, err error)
LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error)
}

// wrap it again :sweat_smile:
Expand Down Expand Up @@ -76,6 +77,24 @@ func (svc *LNDService) MakeInvoice(ctx context.Context, senderPubkey string, amo
return resp.GetPaymentRequest(), hex.EncodeToString(resp.GetRHash()), nil
}

func (svc *LNDService) LookupInvoice(ctx context.Context, senderPubkey string, paymentHash string) (invoice string, paid bool, err error) {
paymentHashBytes, err := hex.DecodeString(paymentHash)

if err != nil || len(paymentHashBytes) != 32 {
svc.Logger.WithFields(logrus.Fields{
"paymentHash": paymentHash,
}).Errorf("Invalid payment hash")
return "", false, errors.New("Payment hash must be 32 bytes hex")
}

lndInvoice, err := svc.client.LookupInvoice(ctx, &lnrpc.PaymentHash{ RHash: paymentHashBytes })
if err != nil {
return "", false, err
}

return lndInvoice.PaymentRequest, lndInvoice.State == *lnrpc.Invoice_SETTLED.Enum(), nil;
}

func (svc *LNDService) SendPaymentSync(ctx context.Context, senderPubkey, payReq string) (preimage string, err error) {
resp, err := svc.client.SendPaymentSync(ctx, &lnrpc.SendRequest{PaymentRequest: payReq})
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions lnd/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type LightningClientWrapper interface {
AddInvoice(ctx context.Context, req *lnrpc.Invoice, options ...grpc.CallOption) (*lnrpc.AddInvoiceResponse, error)
SubscribeInvoices(ctx context.Context, req *lnrpc.InvoiceSubscription, options ...grpc.CallOption) (SubscribeInvoicesWrapper, error)
SubscribePayment(ctx context.Context, req *routerrpc.TrackPaymentRequest, options ...grpc.CallOption) (SubscribePaymentWrapper, error)
LookupInvoice(ctx context.Context, req *lnrpc.PaymentHash, options ...grpc.CallOption) (*lnrpc.Invoice, error)
GetInfo(ctx context.Context, req *lnrpc.GetInfoRequest, options ...grpc.CallOption) (*lnrpc.GetInfoResponse, error)
DecodeBolt11(ctx context.Context, bolt11 string, options ...grpc.CallOption) (*lnrpc.PayReq, error)
IsIdentityPubkey(pubkey string) (isOurPubkey bool)
Expand Down
Loading
Loading