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: add multi pay invoice method #218

Closed
wants to merge 13 commits into from
197 changes: 197 additions & 0 deletions handle_multi_pay_invoice_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
package main

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

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

func (svc *Service) HandleMultiPayInvoiceEvent(ctx context.Context, sub *nostr.Subscription, request *Nip47Request, event *nostr.Event, app App, ss []byte) {

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)
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
}).Errorf("Failed to process event: %v", err)
return
}

multiPayParams := &Nip47MultiPayParams{}
err = json.Unmarshal(request.Params, multiPayParams)
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)
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
}).Errorf("Failed to process event: %v", err)
return
}

var wg sync.WaitGroup
for _, invoiceInfo := range multiPayParams.Invoices {
wg.Add(1)
go func(invoiceInfo InvoiceInfo) {
defer wg.Done()
bolt11 := invoiceInfo.Invoice
// Convert invoice to lowercase string
bolt11 = strings.ToLower(bolt11)
paymentRequest, err := decodepay.Decodepay(bolt11)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"bolt11": bolt11,
}).Errorf("Failed to decode bolt11 invoice: %v", err)

resp, err := svc.createResponse(event, Nip47Response{
ResultType: NIP_47_PAY_INVOICE_METHOD,
rolznz marked this conversation as resolved.
Show resolved Hide resolved
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, ss)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"paymentRequest": invoiceInfo.Invoice,
"invoiceId": invoiceInfo.Id,
}).Errorf("Failed to process event: %v", err)
return
}
dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this d tag, what is the value? it doesn't seem to match the spec.

Also you construct it in 4 different places, can't you just do it once?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah that's also why createAndAppendResponse existed.

Then we should also remove createAndAppendResponse and not deal with returning an array of responses.

But I thought you weren't happy with it

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't understand this d tag, what is the value? it doesn't seem to match the spec.

I thought d tag here meant this, am I wrong?

Copy link
Contributor

@rolznz rolznz Jan 18, 2024

Choose a reason for hiding this comment

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

I thought d tag here meant this, am I wrong?

I think it should be a simple tag like ["d", "ID_OR_PAYMENT_HASH HERE"],

Yeah that's also why createAndAppendResponse existed.

I mean, can't you create a variable to store the d tag at the start of the loop and then use it wherever you need it, rather than declare it 4 times? just to keep it DRY (or have a function that does it, but createAndAppendResponse did something different, right? we do not append a response any more)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah that's also why createAndAppendResponse existed.

Sorry ^one of the reasons why

But changed with the new commit now (although could only dry up dTag declarations)

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it should be a simple tag like ["d", "ID_OR_PAYMENT_HASH HERE"],

Should we confirm with Ben?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's a d tag not an a tag. I think you got them mixed up. Can you check the spec again?

Copy link
Contributor

Choose a reason for hiding this comment

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

I also use d tags in the way I mentioned in the PoS app. I will fix this.

resp.Tags = append(resp.Tags, dTag)
svc.PublishEvent(ctx, sub, event, resp)
return
}

hasPermission, code, message := svc.hasPermission(&app, event, request.Method, paymentRequest.MSatoshi)
rolznz marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

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

using request.Method does not work here. We use the NIP_47_PAY_INVOICE_METHOD permission for any payment request


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)

resp, err := svc.createResponse(event, Nip47Response{
ResultType: NIP_47_PAY_INVOICE_METHOD,
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
Error: &Nip47Error{
Code: code,
Message: message,
},
}, ss)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"paymentRequest": invoiceInfo.Invoice,
"invoiceId": invoiceInfo.Id,
}).Errorf("Failed to process event: %v", err)
return
}
dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)}
resp.Tags = append(resp.Tags, dTag)
Copy link
Contributor

Choose a reason for hiding this comment

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

You cannot change the response after it has been signed. Setting tags like this does not work.

I am fixing it.

svc.PublishEvent(ctx, sub, event, resp)
return
}

payment := Payment{App: app, NostrEvent: nostrEvent, PaymentRequest: bolt11, Amount: uint(paymentRequest.MSatoshi / 1000)}
insertPaymentResult := svc.db.Create(&payment)
if insertPaymentResult.Error != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"paymentRequest": bolt11,
"invoiceId": invoiceInfo.Id,
}).Errorf("Failed to process event: %v", insertPaymentResult.Error)
return
}

svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"bolt11": bolt11,
}).Info("Sending payment")

preimage, err := svc.lnClient.SendPaymentSync(ctx, event.PubKey, bolt11)
rolznz marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"appId": app.ID,
"bolt11": bolt11,
}).Infof("Failed to send payment: %v", err)
// TODO: What to do here?
nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_ERROR
svc.db.Save(&nostrEvent)

resp, err := svc.createResponse(event, Nip47Response{
ResultType: NIP_47_PAY_INVOICE_METHOD,
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()),
},
}, ss)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"paymentRequest": invoiceInfo.Invoice,
"invoiceId": invoiceInfo.Id,
}).Errorf("Failed to process event: %v", err)
return
}
dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)}
resp.Tags = append(resp.Tags, dTag)
svc.PublishEvent(ctx, sub, event, resp)
return
}
payment.Preimage = &preimage
// TODO: What to do here?
nostrEvent.State = NOSTR_EVENT_STATE_HANDLER_EXECUTED
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, we need to review these. They do not apply well to the multi methods.

I would probably set it to executed if at least one payment succeeds, otherwise error. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

CC @bumi

Copy link
Member Author

Choose a reason for hiding this comment

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

I would probably set it to executed if at least one payment succeeds, otherwise error.

One thought I had was to set a counter to see how many were successful and update this according to that

svc.db.Save(&nostrEvent)
svc.db.Save(&payment)
resp, err := svc.createResponse(event, Nip47Response{
ResultType: NIP_47_PAY_INVOICE_METHOD,
Result: Nip47PayResponse{
Preimage: preimage,
},
}, ss)
if err != nil {
svc.Logger.WithFields(logrus.Fields{
"eventId": event.ID,
"eventKind": event.Kind,
"paymentRequest": invoiceInfo.Invoice,
"invoiceId": invoiceInfo.Id,
}).Errorf("Failed to process event: %v", err)
return
}
dTag := []string{"a", fmt.Sprintf("%d:%s:%d", NIP_47_RESPONSE_KIND, event.PubKey, invoiceInfo.Id)}
resp.Tags = append(resp.Tags, dTag)
svc.PublishEvent(ctx, sub, event, resp)
}(invoiceInfo)
}

wg.Wait()
return
}
3 changes: 1 addition & 2 deletions handle_payment_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req
return nil, err
}

var bolt11 string
payParams := &Nip47PayParams{}
err = json.Unmarshal(request.Params, payParams)
if err != nil {
Expand All @@ -36,7 +35,7 @@ func (svc *Service) HandlePayInvoiceEvent(ctx context.Context, request *Nip47Req
return nil, err
}

bolt11 = payParams.Invoice
bolt11 := payParams.Invoice
// Convert invoice to lowercase string
bolt11 = strings.ToLower(bolt11)
paymentRequest, err := decodepay.Decodepay(bolt11)
Expand Down
14 changes: 14 additions & 0 deletions models.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
NIP_47_LOOKUP_INVOICE_METHOD = "lookup_invoice"
NIP_47_LIST_TRANSACTIONS_METHOD = "list_transactions"
NIP_47_PAY_KEYSEND_METHOD = "pay_keysend"
NIP_47_MULTI_PAY_INVOICE_METHOD = "multi_pay_invoice"
NIP_47_ERROR_INTERNAL = "INTERNAL"
NIP_47_ERROR_NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
NIP_47_ERROR_QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
Expand Down Expand Up @@ -251,6 +252,19 @@ type Nip47PayResponse struct {
Preimage string `json:"preimage"`
}

type Nip47MultiPayParams struct {
Invoices []InvoiceInfo `json:"invoices"`
}

type InvoiceInfo struct {
Id uint64 `json:"id"`
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
Invoice string `json:"invoice"`
}

type Nip47MultiPayResponse struct {
Invoice string `json:"invoice"`
}

type Nip47KeysendParams struct {
Amount int64 `json:"amount"`
Pubkey string `json:"pubkey"`
Expand Down
Loading
Loading