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

mint - subscribe to invoice state changes #105

Merged
merged 1 commit into from
Jan 23, 2025
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
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
Loading