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: handle pending mint quotes #86

Merged
merged 1 commit into from
Nov 26, 2024
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
2 changes: 1 addition & 1 deletion cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ var (
NoProofsProvided = Error{Detail: "no proofs provided", Code: InvalidProofErrCode}
DuplicateProofs = Error{Detail: "duplicate proofs", Code: InvalidProofErrCode}
QuoteNotExistErr = Error{Detail: "quote does not exist", Code: MeltQuoteErrCode}
MeltQuotePending = Error{Detail: "quote is pending", Code: MeltQuotePendingErrCode}
QuotePending = Error{Detail: "quote is pending", Code: MeltQuotePendingErrCode}
MeltQuoteAlreadyPaid = Error{Detail: "quote already paid", Code: MeltQuoteAlreadyPaidErrCode}
MeltAmountExceededErr = Error{Detail: "max amount for melting exceeded", Code: AmountLimitExceeded}
MeltQuoteForRequestExists = Error{Detail: "melt quote for payment request already exists", Code: MeltQuoteErrCode}
Expand Down
5 changes: 5 additions & 0 deletions cashu/nuts/nut04/nut04.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
Unpaid State = iota
Paid
Issued
Pending
Unknown
)

Expand All @@ -26,6 +27,8 @@ func (state State) String() string {
return "PAID"
case Issued:
return "ISSUED"
case Pending:
return "PENDING"
default:
return "unknown"
}
Expand All @@ -39,6 +42,8 @@ func StringToState(state string) State {
return Paid
case "ISSUED":
return Issued
case "PENDING":
return Pending
}
return Unknown
}
Expand Down
121 changes: 62 additions & 59 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,6 @@ func (m *Mint) RequestMintQuote(mintQuoteRequest nut04.PostMintQuoteBolt11Reques
}

// GetMintQuoteState returns the state of a mint quote.
// Used to check whether a mint quote has been paid.
func (m *Mint) GetMintQuoteState(quoteId string) (storage.MintQuote, error) {
mintQuote, err := m.db.GetMintQuote(quoteId)
if err != nil {
Expand Down Expand Up @@ -335,78 +334,82 @@ func (m *Mint) GetMintQuoteState(quoteId string) (storage.MintQuote, error) {
// MintTokens verifies whether the mint quote with id has been paid and proceeds to
// sign the blindedMessages and return the BlindedSignatures if it was paid.
func (m *Mint) MintTokens(mintTokensRequest nut04.PostMintBolt11Request) (cashu.BlindedSignatures, error) {
mintQuote, err := m.db.GetMintQuote(mintTokensRequest.Quote)
mintQuote, err := m.GetMintQuoteState(mintTokensRequest.Quote)
if err != nil {
return nil, cashu.QuoteNotExistErr
return nil, err
}

blindedMessages := mintTokensRequest.Outputs
var blindedSignatures cashu.BlindedSignatures

invoicePaid := false
if mintQuote.State == nut04.Unpaid {
m.logDebugf("checking status of invoice with hash '%v'", mintQuote.PaymentHash)
invoiceStatus, err := m.lightningClient.InvoiceStatus(mintQuote.PaymentHash)
if err != nil {
errmsg := fmt.Sprintf("error getting invoice status: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.LightningBackendErrCode)
}
if invoiceStatus.Settled {
m.logInfof("mint quote '%v' with invoice payment hash '%v' was paid", mintQuote.Id, mintQuote.PaymentHash)
invoicePaid = true
}
} else {
invoicePaid = true
}

if invoicePaid {
if mintQuote.State == nut04.Issued {
return nil, cashu.MintQuoteAlreadyIssued
}
switch mintQuote.State {
case nut04.Unpaid:
return nil, cashu.MintQuoteRequestNotPaid
case nut04.Issued:
return nil, cashu.MintQuoteAlreadyIssued
case nut04.Pending:
return nil, cashu.QuotePending
case nut04.Paid:
err := func() error {
// set quote as pending while validating blinded messages and signing
err = m.db.UpdateMintQuoteState(mintQuote.Id, nut04.Pending)
if err != nil {
errmsg := fmt.Sprintf("error mint quote state: %v", err)
return cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

var blindedMessagesAmount uint64
B_s := make([]string, len(blindedMessages))
for i, bm := range blindedMessages {
blindedMessagesAmount += bm.Amount
B_s[i] = bm.B_
}
blindedMessages := mintTokensRequest.Outputs
var blindedMessagesAmount uint64
B_s := make([]string, len(blindedMessages))
for i, bm := range blindedMessages {
blindedMessagesAmount += bm.Amount
B_s[i] = bm.B_
}

if len(blindedMessages) > 0 {
for _, msg := range blindedMessages {
if blindedMessagesAmount < msg.Amount {
return nil, cashu.InvalidBlindedMessageAmount
if len(blindedMessages) > 0 {
for _, msg := range blindedMessages {
if blindedMessagesAmount < msg.Amount {
return cashu.InvalidBlindedMessageAmount
}
}
}
}

// verify that amount from blinded messages is less
// than quote amount
if blindedMessagesAmount > mintQuote.Amount {
return nil, cashu.OutputsOverQuoteAmountErr
}
// verify that amount from blinded messages is less
// than quote amount
if blindedMessagesAmount > mintQuote.Amount {
return cashu.OutputsOverQuoteAmountErr
}

sigs, err := m.db.GetBlindSignatures(B_s)
if err != nil {
errmsg := fmt.Sprintf("error getting blind signatures from db: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}
if len(sigs) > 0 {
return nil, cashu.BlindedMessageAlreadySigned
}
sigs, err := m.db.GetBlindSignatures(B_s)
if err != nil {
errmsg := fmt.Sprintf("error getting blind signatures from db: %v", err)
return cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}

blindedSignatures, err = m.signBlindedMessages(blindedMessages)
if err != nil {
return nil, err
}
if len(sigs) > 0 {
return cashu.BlindedMessageAlreadySigned
}

// mark quote as issued after signing the blinded messages
err = m.db.UpdateMintQuoteState(mintQuote.Id, nut04.Issued)
blindedSignatures, err = m.signBlindedMessages(blindedMessages)
if err != nil {
return err
}

// mark quote as issued after signing the blinded messages
err = m.db.UpdateMintQuoteState(mintQuote.Id, nut04.Issued)
if err != nil {
errmsg := fmt.Sprintf("error mint quote state: %v", err)
return cashu.BuildCashuError(errmsg, cashu.DBErrCode)
}
return nil
}()

// update mint quote to previous state if there was an error
if err != nil {
errmsg := fmt.Sprintf("error mint quote state: %v", err)
return nil, cashu.BuildCashuError(errmsg, cashu.DBErrCode)
if err := m.db.UpdateMintQuoteState(mintQuote.Id, mintQuote.State); err != nil {
return nil, err
}
return nil, err
}
} else {
return nil, cashu.MintQuoteRequestNotPaid
}

return blindedSignatures, nil
Expand Down Expand Up @@ -685,7 +688,7 @@ func (m *Mint) MeltTokens(ctx context.Context, meltTokensRequest nut05.PostMeltB
return storage.MeltQuote{}, cashu.MeltQuoteAlreadyPaid
}
if meltQuote.State == nut05.Pending {
return storage.MeltQuote{}, cashu.MeltQuotePending
return storage.MeltQuote{}, cashu.QuotePending
}

err = m.verifyProofs(proofs, Ys)
Expand Down
4 changes: 2 additions & 2 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -739,8 +739,8 @@ func TestPendingProofs(t *testing.T) {

meltTokensRequest = nut05.PostMeltBolt11Request{Quote: meltQuote.Id, Inputs: validProofs}
_, err = testMint.MeltTokens(ctx, meltTokensRequest)
if !errors.Is(err, cashu.MeltQuotePending) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.MeltQuotePending, err)
if !errors.Is(err, cashu.QuotePending) {
t.Fatalf("expected error '%v' but got '%v' instead", cashu.QuotePending, err)
}

// try to use currently pending proofs in another op.
Expand Down