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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,15 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light

✅ `pay_invoice`
- ⚠️ amount not supported (for amountless invoices)
- ⚠️ PAYMENT_FAILED error code not supported

✅ `pay_keysend`
- ⚠️ PAYMENT_FAILED error code not supported

✅ `make_invoice`

✅ `lookup_invoice`
- ⚠️ NOT_FOUND error code not supported

✅ `list_transactions`
- ⚠️ from and until in request not supported
Expand All @@ -161,15 +164,18 @@ You can also contribute to our [bounty program](https://github.com/getAlby/light

✅ `pay_invoice`
- ⚠️ amount not supported (for amountless invoices)
- ⚠️ PAYMENT_FAILED error code not supported

✅ `pay_keysend`
- ⚠️ preimage in request not supported
- ⚠️ PAYMENT_FAILED error code not supported

✅ `make_invoice`
- ⚠️ expiry in request not supported

✅ `lookup_invoice`
- ⚠️ fees_paid in response not supported
- ⚠️ NOT_FOUND error code not supported

✅ `list_transactions`
- ⚠️ offset and unpaid in request not supported
Expand Down
7 changes: 3 additions & 4 deletions handle_balance_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req
Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}}, nostr.Tags{}, ss)
}

svc.Logger.WithFields(logrus.Fields{
Expand All @@ -63,7 +63,7 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while fetching balance: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}

responsePayload := &Nip47BalanceResponse{
Expand All @@ -84,6 +84,5 @@ func (svc *Service) HandleGetBalanceEvent(ctx context.Context, request *Nip47Req
return svc.createResponse(event, Nip47Response{
ResultType: NIP_47_GET_BALANCE_METHOD,
Result: responsePayload,
},
ss)
}, nostr.Tags{}, ss)
}
6 changes: 3 additions & 3 deletions handle_info_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques
Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}}, nostr.Tags{}, ss)
}

svc.Logger.WithFields(logrus.Fields{
Expand All @@ -59,7 +59,7 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while fetching node info: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}

responsePayload := &Nip47GetInfoResponse{
Expand All @@ -77,5 +77,5 @@ func (svc *Service) HandleGetInfoEvent(ctx context.Context, request *Nip47Reques
return svc.createResponse(event, Nip47Response{
ResultType: request.Method,
Result: responsePayload,
}, ss)
}, nostr.Tags{}, ss)
}
7 changes: 3 additions & 4 deletions handle_list_transactions_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni
Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}}, nostr.Tags{}, ss)
}

svc.Logger.WithFields(logrus.Fields{
Expand All @@ -74,7 +74,7 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while fetching transactions: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}

responsePayload := &Nip47ListTransactionsResponse{
Expand All @@ -87,6 +87,5 @@ func (svc *Service) HandleListTransactionsEvent(ctx context.Context, request *Ni
return svc.createResponse(event, Nip47Response{
ResultType: request.Method,
Result: responsePayload,
},
ss)
}, nostr.Tags{}, ss)
}
9 changes: 4 additions & 5 deletions handle_lookup_invoice_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47
Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}}, nostr.Tags{}, ss)
}

// TODO: move to a shared generic function
Expand Down Expand Up @@ -80,7 +80,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}
paymentHash = paymentRequest.PaymentHash
}
Expand All @@ -102,7 +102,7 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while looking up invoice: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}

responsePayload := &Nip47LookupInvoiceResponse{
Expand All @@ -114,6 +114,5 @@ func (svc *Service) HandleLookupInvoiceEvent(ctx context.Context, request *Nip47
return svc.createResponse(event, Nip47Response{
ResultType: NIP_47_LOOKUP_INVOICE_METHOD,
Result: responsePayload,
},
ss)
}, nostr.Tags{}, ss)
}
9 changes: 4 additions & 5 deletions handle_make_invoice_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re
Error: &Nip47Error{
Code: code,
Message: message,
}}, ss)
}}, nostr.Tags{}, ss)
}

// TODO: move to a shared generic function
Expand Down Expand Up @@ -68,7 +68,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re
Code: NIP_47_OTHER,
Message: "Only one of description, description_hash can be provided",
},
}, ss)
}, nostr.Tags{}, ss)
}

svc.Logger.WithFields(logrus.Fields{
Expand Down Expand Up @@ -100,7 +100,7 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while making invoice: %s", err.Error()),
},
}, ss)
}, nostr.Tags{}, ss)
}

responsePayload := &Nip47MakeInvoiceResponse{
Expand All @@ -112,6 +112,5 @@ func (svc *Service) HandleMakeInvoiceEvent(ctx context.Context, request *Nip47Re
return svc.createResponse(event, Nip47Response{
ResultType: NIP_47_MAKE_INVOICE_METHOD,
Result: responsePayload,
},
ss)
}, nostr.Tags{}, ss)
}
198 changes: 198 additions & 0 deletions handle_multi_pay_invoice_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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 Nip47MultiPayInvoiceElement) {
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)

// TODO: Decide what to do if id is empty
dTag := []string{"d", invoiceInfo.Id}
resp, err := svc.createResponse(event, Nip47Response{
ResultType: NIP_47_MULTI_PAY_INVOICE_METHOD,
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Failed to decode bolt11 invoice: %s", err.Error()),
},
}, nostr.Tags{dTag}, 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
}

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

invoiceDTagValue := invoiceInfo.Id
if invoiceDTagValue == "" {
invoiceDTagValue = paymentRequest.PaymentHash
}
dTag := []string{"d", invoiceDTagValue}

hasPermission, code, message := svc.hasPermission(&app, event, NIP_47_PAY_INVOICE_METHOD, paymentRequest.MSatoshi)

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_MULTI_PAY_INVOICE_METHOD,
Error: &Nip47Error{
Code: code,
Message: message,
},
}, nostr.Tags{dTag}, 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
}
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_MULTI_PAY_INVOICE_METHOD,
Error: &Nip47Error{
Code: NIP_47_ERROR_INTERNAL,
Message: fmt.Sprintf("Something went wrong while paying invoice: %s", err.Error()),
},
}, nostr.Tags{dTag}, 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
}
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_MULTI_PAY_INVOICE_METHOD,
Result: Nip47PayResponse{
Preimage: preimage,
},
}, nostr.Tags{dTag}, 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
}
svc.PublishEvent(ctx, sub, event, resp)
}(invoiceInfo)
}

wg.Wait()
return
}
Loading
Loading