Skip to content

Commit

Permalink
Merge pull request #105 from elnosh/invoice-sub
Browse files Browse the repository at this point in the history
mint - subscribe to invoice state changes
  • Loading branch information
elnosh authored Jan 23, 2025
2 parents 50859aa + cacb6fa commit 7c0381a
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 8 deletions.
61 changes: 61 additions & 0 deletions mint/invoicesub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package mint

import (
"time"

"github.com/elnosh/gonuts/cashu/nuts/nut04"
"github.com/elnosh/gonuts/mint/lightning"
)

// checkInvoicePaid should be called in a different goroutine to check in the background
// if the invoice for the quoteId gets paid and update it in the db.
func (m *Mint) checkInvoicePaid(quoteId string) {
mintQuote, err := m.db.GetMintQuote(quoteId)
if err != nil {
m.logErrorf("could not get mint quote '%v' from db: %v", quoteId, err)
return
}

invoiceSub, err := m.lightningClient.SubscribeInvoice(mintQuote.PaymentHash)
if err != nil {
m.logErrorf("could not subscribe to invoice changes for mint quote '%v': %v", quoteId, err)
return
}

updateChan := make(chan lightning.Invoice)
errChan := make(chan error)

go func() {
for {
invoice, err := invoiceSub.Recv()
if err != nil {
errChan <- err
return
}

// only send on channel if invoice gets settled
if invoice.Settled {
updateChan <- invoice
return
}
}

}()

timeUntilExpiry := int64(mintQuote.Expiry) - time.Now().Unix()

select {
case invoice := <-updateChan:
if invoice.Settled {
m.logInfof("received update from invoice sub. Invoice for mint quote '%v' is PAID", mintQuote.Id)
if err := m.db.UpdateMintQuoteState(mintQuote.Id, nut04.Paid); err != nil {
m.logErrorf("could not mark mint quote '%v' as PAID in db: %v", mintQuote.Id, err)
}
}
case err := <-errChan:
m.logErrorf("error reading from invoice subscription: %v", err)
case <-time.After(time.Second * time.Duration(timeUntilExpiry)):
// cancel when quote reaches expiry time
m.logDebugf("canceling invoice subscription for quote '%v'. Reached deadline", mintQuote.Id)
}
}
23 changes: 23 additions & 0 deletions mint/lightning/fakebackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ func (fb *FakeBackend) FeeReserve(amount uint64) uint64 {
return 0
}

func (fb *FakeBackend) SubscribeInvoice(paymentHash string) (InvoiceSubscriptionClient, error) {
return &FakeInvoiceSub{
paymentHash: paymentHash,
fb: fb,
}, nil
}

type FakeInvoiceSub struct {
paymentHash string
fb *FakeBackend
}

func (fakeSub *FakeInvoiceSub) Recv() (Invoice, error) {
invoiceIdx := slices.IndexFunc(fakeSub.fb.Invoices, func(i FakeBackendInvoice) bool {
return i.PaymentHash == fakeSub.paymentHash
})
if invoiceIdx == -1 {
return Invoice{}, errors.New("invoice does not exist")
}

return fakeSub.fb.Invoices[invoiceIdx].ToInvoice(), nil
}

func (fb *FakeBackend) SetInvoiceStatus(hash string, status State) {
invoiceIdx := slices.IndexFunc(fb.Invoices, func(i FakeBackendInvoice) bool {
return i.PaymentHash == hash
Expand Down
7 changes: 7 additions & 0 deletions mint/lightning/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Client interface {
SendPayment(ctx context.Context, request string, amount uint64) (PaymentStatus, error)
OutgoingPaymentStatus(ctx context.Context, hash string) (PaymentStatus, error)
FeeReserve(amount uint64) uint64
SubscribeInvoice(paymentHash string) (InvoiceSubscriptionClient, error)
}

type Invoice struct {
Expand All @@ -34,3 +35,9 @@ type PaymentStatus struct {
PaymentStatus State
PaymentFailureReason string
}

// InvoiceSubscriptionClient subscribes to get updates on the status of an invoice
type InvoiceSubscriptionClient interface {
// This blocks until there is an update
Recv() (Invoice, error)
}
63 changes: 56 additions & 7 deletions mint/lightning/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ import (
"fmt"
"math"
"strings"
"time"

"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

const (
InvoiceExpiryMins = 10
// 1 hour
InvoiceExpiryTime = 3600
FeePercent float64 = 0.01
)

Expand All @@ -28,8 +29,9 @@ type LndConfig struct {
}

type LndClient struct {
grpcClient lnrpc.LightningClient
routerClient routerrpc.RouterClient
grpcClient lnrpc.LightningClient
routerClient routerrpc.RouterClient
invoicesClient invoicesrpc.InvoicesClient
}

func SetupLndClient(config LndConfig) (*LndClient, error) {
Expand All @@ -45,7 +47,13 @@ func SetupLndClient(config LndConfig) (*LndClient, error) {

grpcClient := lnrpc.NewLightningClient(conn)
routerClient := routerrpc.NewRouterClient(conn)
return &LndClient{grpcClient: grpcClient, routerClient: routerClient}, nil
invoicesClient := invoicesrpc.NewInvoicesClient(conn)

return &LndClient{
grpcClient: grpcClient,
routerClient: routerClient,
invoicesClient: invoicesClient,
}, nil
}

func (lnd *LndClient) ConnectionStatus() error {
Expand All @@ -61,7 +69,7 @@ func (lnd *LndClient) ConnectionStatus() error {
func (lnd *LndClient) CreateInvoice(amount uint64) (Invoice, error) {
invoiceRequest := lnrpc.Invoice{
Value: int64(amount),
Expiry: InvoiceExpiryMins * 60,
Expiry: InvoiceExpiryTime,
}

addInvoiceResponse, err := lnd.grpcClient.AddInvoice(context.Background(), &invoiceRequest)
Expand All @@ -74,7 +82,7 @@ func (lnd *LndClient) CreateInvoice(amount uint64) (Invoice, error) {
PaymentRequest: addInvoiceResponse.PaymentRequest,
PaymentHash: hash,
Amount: amount,
Expiry: uint64(time.Now().Add(time.Minute * InvoiceExpiryMins).Unix()),
Expiry: InvoiceExpiryTime,
}
return invoice, nil
}
Expand All @@ -98,6 +106,7 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) {
Preimage: hex.EncodeToString(lookupInvoiceResponse.RPreimage),
Settled: invoiceSettled,
Amount: uint64(lookupInvoiceResponse.Value),
Expiry: uint64(lookupInvoiceResponse.Expiry),
}

return invoice, nil
Expand Down Expand Up @@ -246,3 +255,43 @@ func (lnd *LndClient) FeeReserve(amount uint64) uint64 {
fee := math.Ceil(float64(amount) * FeePercent)
return uint64(fee)
}

func (lnd *LndClient) SubscribeInvoice(paymentHash string) (InvoiceSubscriptionClient, error) {
hash, err := hex.DecodeString(paymentHash)
if err != nil {
return nil, err
}
invoiceSubRequest := &invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: hash,
}
lndInvoiceClient, err := lnd.invoicesClient.SubscribeSingleInvoice(context.Background(), invoiceSubRequest)
if err != nil {
return nil, err
}
invoiceSub := &LndInvoiceSub{
paymentHash: paymentHash,
invoiceSubClient: lndInvoiceClient,
}
return invoiceSub, nil
}

type LndInvoiceSub struct {
paymentHash string
invoiceSubClient invoicesrpc.Invoices_SubscribeSingleInvoiceClient
}

func (lndSub *LndInvoiceSub) Recv() (Invoice, error) {
invoiceRes, err := lndSub.invoiceSubClient.Recv()
if err != nil {
return Invoice{}, err
}
invoiceSettled := invoiceRes.State == lnrpc.Invoice_SETTLED
invoice := Invoice{
PaymentRequest: invoiceRes.PaymentRequest,
PaymentHash: lndSub.paymentHash,
Preimage: hex.EncodeToString(invoiceRes.RPreimage),
Settled: invoiceSettled,
Amount: uint64(invoiceRes.Value),
}
return invoice, nil
}
5 changes: 4 additions & 1 deletion mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques
PaymentRequest: invoice.PaymentRequest,
PaymentHash: invoice.PaymentHash,
State: nut04.Unpaid,
Expiry: invoice.Expiry,
Expiry: uint64(time.Now().Add(time.Second * time.Duration(invoice.Expiry)).Unix()),
}

err = m.db.SaveMintQuote(mintQuote)
Expand All @@ -300,6 +300,9 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques
return storage.MintQuote{}, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

// goroutine to check in the background when invoice gets paid and update db if so
go m.checkInvoicePaid(quoteId)

return mintQuote, nil
}

Expand Down

0 comments on commit 7c0381a

Please sign in to comment.