Skip to content

Commit c06c15c

Browse files
committed
mint: support paying amountless invoices
1 parent 0237731 commit c06c15c

10 files changed

Lines changed: 205 additions & 58 deletions

File tree

cashu/cashu.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,7 @@ const (
502502
AmountLimitExceeded CashuErrCode = 11006
503503
DuplicateInputErrCode CashuErrCode = 11007
504504
DuplicateOutputErrCode CashuErrCode = 11008
505+
AmountlessMismatchErrCode CashuErrCode = 11011
505506

506507
UnknownKeysetErrCode CashuErrCode = 12001
507508
InactiveKeysetErrCode CashuErrCode = 12002
@@ -514,8 +515,7 @@ const (
514515
LightningPaymentErrCode CashuErrCode = 20004
515516
MeltQuotePendingErrCode CashuErrCode = 20005
516517
MeltQuoteAlreadyPaidErrCode CashuErrCode = 20006
517-
518-
MeltQuoteErrCode CashuErrCode = 20009
518+
MeltQuoteErrCode CashuErrCode = 20010
519519
)
520520

521521
var (
@@ -531,6 +531,8 @@ var (
531531
MintQuoteAlreadyIssued = Error{Detail: "quote already issued", Code: MintQuoteAlreadyIssuedErrCode}
532532
MintingDisabled = Error{Detail: "minting is disabled", Code: MintingDisabledErrCode}
533533
MintAmountExceededErr = Error{Detail: "max amount for minting exceeded", Code: AmountLimitExceeded}
534+
MeltAmountlessMismatchErr = Error{Detail: "invoice amount and amountless option do not match", Code: AmountlessMismatchErrCode}
535+
InvoiceAmountMissingErr = Error{Detail: "amountless option not specified for amountless invoice request", Code: AmountlessMismatchErrCode}
534536
MintQuoteInvalidSigErr = Error{Detail: "Mint quote with pubkey but no valid signature provided.", Code: MintQuoteInvalidSigErrCode}
535537
OutputsOverQuoteAmountErr = Error{Detail: "sum of the output amounts is greater than quote amount", Code: StandardErrCode}
536538
ProofAlreadyUsedErr = Error{Detail: "proof already used", Code: ProofAlreadyUsedErrCode}

cashu/nuts/nut05/nut05.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,22 @@ func StringToState(state string) State {
4444
}
4545

4646
type PostMeltQuoteBolt11Request struct {
47-
Request string `json:"request"`
48-
Unit string `json:"unit"`
49-
Options map[string]MppOption `json:"options,omitempty"`
47+
Request string `json:"request"`
48+
Unit string `json:"unit"`
49+
Options MeltOptions `json:"options,omitempty"`
5050
}
5151

52-
type MppOption struct {
53-
AmountMsat uint64 `json:"amount"`
52+
type MeltOptions struct {
53+
MppOption *Amount `json:"mpp,omitempty"`
54+
AmountlessOption *AmountMsat `json:"amountless,omitempty"`
55+
}
56+
57+
type Amount struct {
58+
Amount uint64 `json:"amount"`
59+
}
60+
61+
type AmountMsat struct {
62+
AmountMsat uint64 `json:"amount_msat"`
5463
}
5564

5665
type PostMeltQuoteBolt11Response struct {

cashu/nuts/nut06/nut06.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ type NutSetting struct {
6969
}
7070

7171
type MethodSetting struct {
72-
Method string `json:"method"`
73-
Unit string `json:"unit"`
74-
MinAmount uint64 `json:"min_amount,omitempty"`
75-
MaxAmount uint64 `json:"max_amount,omitempty"`
72+
Method string `json:"method"`
73+
Unit string `json:"unit"`
74+
MinAmount uint64 `json:"min_amount,omitempty"`
75+
MaxAmount uint64 `json:"max_amount,omitempty"`
76+
Amountless bool `json:"amountless,omitempty"`
7677
}
7778

7879
type Supported struct {

mint/lightning/fakebackend.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,12 @@ func (fb *FakeBackend) InvoiceStatus(hash string) (Invoice, error) {
8181
return fb.Invoices[invoiceIdx].ToInvoice(), nil
8282
}
8383

84-
func (fb *FakeBackend) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
84+
func (fb *FakeBackend) SendPayment(
85+
ctx context.Context,
86+
request string,
87+
amount uint64,
88+
maxFee uint64,
89+
) (PaymentStatus, error) {
8590
invoice, err := decodepay.Decodepay(request)
8691
if err != nil {
8792
return PaymentStatus{}, fmt.Errorf("error decoding invoice: %v", err)

mint/lightning/lightning.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ type Client interface {
77
ConnectionStatus() error
88
CreateInvoice(amount uint64) (Invoice, error)
99
InvoiceStatus(hash string) (Invoice, error)
10-
SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error)
10+
// amount used only for supporting amountless invoices in melt requests
11+
SendPayment(ctx context.Context, request string, amount uint64, maxFee uint64) (PaymentStatus, error)
1112
PayPartialAmount(ctx context.Context, request string, amountMsat uint64, maxFee uint64) (PaymentStatus, error)
1213
OutgoingPaymentStatus(ctx context.Context, hash string) (PaymentStatus, error)
1314
FeeReserve(amount uint64) uint64

mint/lightning/lnd.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
1313
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1414
"github.com/lightningnetwork/lnd/macaroons"
15+
decodepay "github.com/nbd-wtf/ln-decodepay"
1516
"google.golang.org/grpc"
1617
"google.golang.org/grpc/credentials"
1718
)
@@ -112,12 +113,27 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) {
112113
return invoice, nil
113114
}
114115

115-
func (lnd *LndClient) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
116+
func (lnd *LndClient) SendPayment(
117+
ctx context.Context,
118+
request string,
119+
amount uint64,
120+
maxFee uint64,
121+
) (PaymentStatus, error) {
116122
feeLimit := &lnrpc.FeeLimit{Limit: &lnrpc.FeeLimit_Fixed{Fixed: int64(maxFee)}}
117123
sendPaymentRequest := lnrpc.SendRequest{
118124
PaymentRequest: request,
119125
FeeLimit: feeLimit,
120126
}
127+
128+
bolt11, err := decodepay.Decodepay(request)
129+
if err != nil {
130+
return PaymentStatus{}, err
131+
}
132+
// if this is an amountless invoice, pay the amount specified
133+
if bolt11.MSatoshi == 0 {
134+
sendPaymentRequest.Amt = int64(amount)
135+
}
136+
121137
sendPaymentResponse, err := lnd.grpcClient.SendPaymentSync(ctx, &sendPaymentRequest)
122138
if err != nil {
123139
// if context deadline is exceeded (1 min), mark payment as pending

mint/mint.go

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
571571
return storage.MeltQuote{}, cashu.BuildCashuError(errmsg, cashu.UnitErrCode)
572572
}
573573

574+
var quoteAmount uint64
574575
// check invoice passed is valid
575576
request := meltQuoteRequest.Request
576577
bolt11, err := decodepay.Decodepay(request)
@@ -579,10 +580,23 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
579580
return storage.MeltQuote{}, cashu.BuildCashuError(errmsg, cashu.MeltQuoteErrCode)
580581
}
581582
if bolt11.MSatoshi == 0 {
582-
return storage.MeltQuote{}, cashu.BuildCashuError("invoice has no amount", cashu.MeltQuoteErrCode)
583+
// if amountless invoice, check amount specified in options
584+
if meltQuoteRequest.Options.AmountlessOption == nil {
585+
return storage.MeltQuote{}, cashu.InvoiceAmountMissingErr
586+
} else {
587+
amountless := meltQuoteRequest.Options.AmountlessOption
588+
quoteAmount = amountless.AmountMsat / 1000
589+
}
590+
} else {
591+
quoteAmount = uint64(bolt11.MSatoshi) / 1000
592+
593+
// if amountless option passed, check that amounts matched
594+
if meltQuoteRequest.Options.AmountlessOption != nil {
595+
if meltQuoteRequest.Options.AmountlessOption.AmountMsat != uint64(bolt11.MSatoshi) {
596+
return storage.MeltQuote{}, cashu.MeltAmountlessMismatchErr
597+
}
598+
}
583599
}
584-
invoiceSatAmount := uint64(bolt11.MSatoshi) / 1000
585-
quoteAmount := invoiceSatAmount
586600

587601
// check if a mint quote exists with the same invoice.
588602
_, err = m.db.GetMintQuoteByPaymentHash(bolt11.PaymentHash)
@@ -594,31 +608,31 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
594608
isMpp := false
595609
var amountMsat uint64 = 0
596610
// check mpp option
597-
if len(meltQuoteRequest.Options) > 0 {
598-
mpp, ok := meltQuoteRequest.Options["mpp"]
599-
if ok {
600-
if m.mppEnabled {
601-
// if this is an internal invoice, reject MPP request
602-
if isInternal {
603-
return storage.MeltQuote{},
604-
cashu.BuildCashuError("mpp for internal invoice is not allowed", cashu.MeltQuoteErrCode)
605-
}
611+
if meltQuoteRequest.Options.MppOption != nil {
612+
if m.mppEnabled {
613+
// if this is an internal invoice, reject MPP request
614+
if isInternal {
615+
return storage.MeltQuote{},
616+
cashu.BuildCashuError("mpp for internal invoice is not allowed", cashu.MeltQuoteErrCode)
617+
}
606618

607-
// check mpp msat amount is less than invoice amount
608-
if mpp.AmountMsat >= uint64(bolt11.MSatoshi) {
609-
return storage.MeltQuote{},
610-
cashu.BuildCashuError("mpp amount is not less than amount in invoice",
611-
cashu.MeltQuoteErrCode)
612-
}
613-
isMpp = true
614-
amountMsat = mpp.AmountMsat
615-
quoteAmount = amountMsat / 1000
616-
m.logInfof("got melt quote request to pay partial amount '%v' of invoice with amount '%v'",
617-
quoteAmount, invoiceSatAmount)
618-
} else {
619+
// check mpp msat amount is less than invoice amount
620+
mppAmount := meltQuoteRequest.Options.MppOption.Amount
621+
if mppAmount >= uint64(bolt11.MSatoshi) {
619622
return storage.MeltQuote{},
620-
cashu.BuildCashuError("MPP is not supported", cashu.MeltQuoteErrCode)
623+
cashu.BuildCashuError("mpp amount is not less than amount in invoice", cashu.MeltQuoteErrCode)
624+
}
625+
if mppAmount > 0 && bolt11.MSatoshi == 0 {
626+
return storage.MeltQuote{}, cashu.BuildCashuError("invalid invoice for MPP option", cashu.MeltQuoteErrCode)
621627
}
628+
isMpp = true
629+
amountMsat = mppAmount
630+
quoteAmount = amountMsat / 1000
631+
m.logInfof("got melt quote request to pay partial amount '%v' of invoice with amount '%v'",
632+
quoteAmount, bolt11.MSatoshi/1000)
633+
} else {
634+
return storage.MeltQuote{},
635+
cashu.BuildCashuError("MPP is not supported", cashu.MeltQuoteErrCode)
622636
}
623637
}
624638

@@ -661,8 +675,8 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
661675
AmountMsat: amountMsat,
662676
}
663677

664-
m.logInfof("got melt quote request for invoice of amount '%v'. Setting fee reserve to %v",
665-
invoiceSatAmount, meltQuote.FeeReserve)
678+
m.logInfof("got melt quote request for amount '%v'. Setting fee reserve to %v",
679+
quoteAmount, meltQuote.FeeReserve)
666680

667681
if err := m.db.SaveMeltQuote(meltQuote); err != nil {
668682
errmsg := fmt.Sprintf("error saving melt quote to db: %v", err)
@@ -861,7 +875,12 @@ func (m *Mint) MeltTokens(ctx context.Context, meltTokensRequest nut05.PostMeltB
861875
)
862876
} else {
863877
m.logInfof("attempting to pay invoice: %v", meltQuote.InvoiceRequest)
864-
sendPaymentResponse, err = m.lightningClient.SendPayment(ctx, meltQuote.InvoiceRequest, meltQuote.Amount)
878+
sendPaymentResponse, err = m.lightningClient.SendPayment(
879+
ctx,
880+
meltQuote.InvoiceRequest,
881+
meltQuote.Amount,
882+
meltQuote.FeeReserve,
883+
)
865884
}
866885
if err != nil {
867886
// if SendPayment failed do not return yet, an extra check will be done
@@ -1625,10 +1644,11 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) {
16251644
Nut05: nut06.NutSetting{
16261645
Methods: []nut06.MethodSetting{
16271646
{
1628-
Method: cashu.BOLT11_METHOD,
1629-
Unit: cashu.Sat.String(),
1630-
MinAmount: m.limits.MeltingSettings.MinAmount,
1631-
MaxAmount: m.limits.MeltingSettings.MaxAmount,
1647+
Method: cashu.BOLT11_METHOD,
1648+
Unit: cashu.Sat.String(),
1649+
MinAmount: m.limits.MeltingSettings.MinAmount,
1650+
MaxAmount: m.limits.MeltingSettings.MaxAmount,
1651+
Amountless: true,
16321652
},
16331653
},
16341654
Disabled: false,

0 commit comments

Comments
 (0)