Skip to content
Open
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
6 changes: 4 additions & 2 deletions cashu/cashu.go
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ const (
AmountLimitExceeded CashuErrCode = 11006
DuplicateInputErrCode CashuErrCode = 11007
DuplicateOutputErrCode CashuErrCode = 11008
AmountlessMismatchErrCode CashuErrCode = 11012

UnknownKeysetErrCode CashuErrCode = 12001
InactiveKeysetErrCode CashuErrCode = 12002
Expand All @@ -514,8 +515,7 @@ const (
LightningPaymentErrCode CashuErrCode = 20004
MeltQuotePendingErrCode CashuErrCode = 20005
MeltQuoteAlreadyPaidErrCode CashuErrCode = 20006

MeltQuoteErrCode CashuErrCode = 20009
MeltQuoteErrCode CashuErrCode = 20010
)

var (
Expand All @@ -531,6 +531,8 @@ var (
MintQuoteAlreadyIssued = Error{Detail: "quote already issued", Code: MintQuoteAlreadyIssuedErrCode}
MintingDisabled = Error{Detail: "minting is disabled", Code: MintingDisabledErrCode}
MintAmountExceededErr = Error{Detail: "max amount for minting exceeded", Code: AmountLimitExceeded}
MeltAmountlessMismatchErr = Error{Detail: "invoice amount and amountless option do not match", Code: AmountlessMismatchErrCode}
InvoiceAmountMissingErr = Error{Detail: "amountless option not specified for amountless invoice request", Code: AmountlessMismatchErrCode}
MintQuoteInvalidSigErr = Error{Detail: "Mint quote with pubkey but no valid signature provided.", Code: MintQuoteInvalidSigErrCode}
OutputsOverQuoteAmountErr = Error{Detail: "sum of the output amounts is greater than quote amount", Code: StandardErrCode}
ProofAlreadyUsedErr = Error{Detail: "proof already used", Code: ProofAlreadyUsedErrCode}
Expand Down
19 changes: 14 additions & 5 deletions cashu/nuts/nut05/nut05.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,22 @@ func StringToState(state string) State {
}

type PostMeltQuoteBolt11Request struct {
Request string `json:"request"`
Unit string `json:"unit"`
Options map[string]MppOption `json:"options,omitempty"`
Request string `json:"request"`
Unit string `json:"unit"`
Options MeltOptions `json:"options,omitempty"`
}

type MppOption struct {
AmountMsat uint64 `json:"amount"`
type MeltOptions struct {
MppOption *Amount `json:"mpp,omitempty"`
AmountlessOption *AmountMsat `json:"amountless,omitempty"`
}

type Amount struct {
Amount uint64 `json:"amount"`
}

type AmountMsat struct {
AmountMsat uint64 `json:"amount_msat"`
}

type PostMeltQuoteBolt11Response struct {
Expand Down
9 changes: 5 additions & 4 deletions cashu/nuts/nut06/nut06.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,11 @@ type NutSetting struct {
}

type MethodSetting struct {
Method string `json:"method"`
Unit string `json:"unit"`
MinAmount uint64 `json:"min_amount,omitempty"`
MaxAmount uint64 `json:"max_amount,omitempty"`
Method string `json:"method"`
Unit string `json:"unit"`
MinAmount uint64 `json:"min_amount,omitempty"`
MaxAmount uint64 `json:"max_amount,omitempty"`
Amountless bool `json:"amountless,omitempty"`
}

type Supported struct {
Expand Down
7 changes: 6 additions & 1 deletion mint/lightning/fakebackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ func (fb *FakeBackend) InvoiceStatus(hash string) (Invoice, error) {
return fb.Invoices[invoiceIdx].ToInvoice(), nil
}

func (fb *FakeBackend) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
func (fb *FakeBackend) SendPayment(
ctx context.Context,
request string,
amount uint64,
maxFee uint64,
) (PaymentStatus, error) {
invoice, err := decodepay.Decodepay(request)
if err != nil {
return PaymentStatus{}, fmt.Errorf("error decoding invoice: %v", err)
Expand Down
3 changes: 2 additions & 1 deletion mint/lightning/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ type Client interface {
ConnectionStatus() error
CreateInvoice(amount uint64) (Invoice, error)
InvoiceStatus(hash string) (Invoice, error)
SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error)
// amount used only for supporting amountless invoices in melt requests
SendPayment(ctx context.Context, request string, amount uint64, maxFee uint64) (PaymentStatus, error)
PayPartialAmount(ctx context.Context, request string, amountMsat uint64, maxFee uint64) (PaymentStatus, error)
OutgoingPaymentStatus(ctx context.Context, hash string) (PaymentStatus, error)
FeeReserve(amount uint64) uint64
Expand Down
18 changes: 17 additions & 1 deletion mint/lightning/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/macaroons"
decodepay "github.com/nbd-wtf/ln-decodepay"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
Expand Down Expand Up @@ -112,12 +113,27 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) {
return invoice, nil
}

func (lnd *LndClient) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
func (lnd *LndClient) SendPayment(
ctx context.Context,
request string,
amount uint64,
maxFee uint64,
) (PaymentStatus, error) {
feeLimit := &lnrpc.FeeLimit{Limit: &lnrpc.FeeLimit_Fixed{Fixed: int64(maxFee)}}
sendPaymentRequest := lnrpc.SendRequest{
PaymentRequest: request,
FeeLimit: feeLimit,
}

bolt11, err := decodepay.Decodepay(request)
if err != nil {
return PaymentStatus{}, err
}
// if this is an amountless invoice, pay the amount specified
if bolt11.MSatoshi == 0 {
sendPaymentRequest.Amt = int64(amount)
}

sendPaymentResponse, err := lnd.grpcClient.SendPaymentSync(ctx, &sendPaymentRequest)
if err != nil {
// if context deadline is exceeded (1 min), mark payment as pending
Expand Down
84 changes: 52 additions & 32 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
return storage.MeltQuote{}, cashu.BuildCashuError(errmsg, cashu.UnitErrCode)
}

var quoteAmount uint64
// check invoice passed is valid
request := meltQuoteRequest.Request
bolt11, err := decodepay.Decodepay(request)
Expand All @@ -579,10 +580,23 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
return storage.MeltQuote{}, cashu.BuildCashuError(errmsg, cashu.MeltQuoteErrCode)
}
if bolt11.MSatoshi == 0 {
return storage.MeltQuote{}, cashu.BuildCashuError("invoice has no amount", cashu.MeltQuoteErrCode)
// if amountless invoice, check amount specified in options
if meltQuoteRequest.Options.AmountlessOption == nil {
return storage.MeltQuote{}, cashu.InvoiceAmountMissingErr
} else {
amountless := meltQuoteRequest.Options.AmountlessOption
quoteAmount = amountless.AmountMsat / 1000
}
} else {
quoteAmount = uint64(bolt11.MSatoshi) / 1000

// if amountless option passed, check that amounts matched
if meltQuoteRequest.Options.AmountlessOption != nil {
if meltQuoteRequest.Options.AmountlessOption.AmountMsat != uint64(bolt11.MSatoshi) {
return storage.MeltQuote{}, cashu.MeltAmountlessMismatchErr
}
}
}
invoiceSatAmount := uint64(bolt11.MSatoshi) / 1000
quoteAmount := invoiceSatAmount

// check if a mint quote exists with the same invoice.
_, err = m.db.GetMintQuoteByPaymentHash(bolt11.PaymentHash)
Expand All @@ -594,31 +608,31 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
isMpp := false
var amountMsat uint64 = 0
// check mpp option
if len(meltQuoteRequest.Options) > 0 {
mpp, ok := meltQuoteRequest.Options["mpp"]
if ok {
if m.mppEnabled {
// if this is an internal invoice, reject MPP request
if isInternal {
return storage.MeltQuote{},
cashu.BuildCashuError("mpp for internal invoice is not allowed", cashu.MeltQuoteErrCode)
}
if meltQuoteRequest.Options.MppOption != nil {
if m.mppEnabled {
// if this is an internal invoice, reject MPP request
if isInternal {
return storage.MeltQuote{},
cashu.BuildCashuError("mpp for internal invoice is not allowed", cashu.MeltQuoteErrCode)
}

// check mpp msat amount is less than invoice amount
if mpp.AmountMsat >= uint64(bolt11.MSatoshi) {
return storage.MeltQuote{},
cashu.BuildCashuError("mpp amount is not less than amount in invoice",
cashu.MeltQuoteErrCode)
}
isMpp = true
amountMsat = mpp.AmountMsat
quoteAmount = amountMsat / 1000
m.logInfof("got melt quote request to pay partial amount '%v' of invoice with amount '%v'",
quoteAmount, invoiceSatAmount)
} else {
// check mpp msat amount is less than invoice amount
mppAmount := meltQuoteRequest.Options.MppOption.Amount
if mppAmount >= uint64(bolt11.MSatoshi) {
return storage.MeltQuote{},
cashu.BuildCashuError("MPP is not supported", cashu.MeltQuoteErrCode)
cashu.BuildCashuError("mpp amount is not less than amount in invoice", cashu.MeltQuoteErrCode)
}
if mppAmount > 0 && bolt11.MSatoshi == 0 {
return storage.MeltQuote{}, cashu.BuildCashuError("invalid invoice for MPP option", cashu.MeltQuoteErrCode)
}
isMpp = true
amountMsat = mppAmount
quoteAmount = amountMsat / 1000
m.logInfof("got melt quote request to pay partial amount '%v' of invoice with amount '%v'",
quoteAmount, bolt11.MSatoshi/1000)
} else {
return storage.MeltQuote{},
cashu.BuildCashuError("MPP is not supported", cashu.MeltQuoteErrCode)
}
}

Expand Down Expand Up @@ -661,8 +675,8 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
AmountMsat: amountMsat,
}

m.logInfof("got melt quote request for invoice of amount '%v'. Setting fee reserve to %v",
invoiceSatAmount, meltQuote.FeeReserve)
m.logInfof("got melt quote request for amount '%v'. Setting fee reserve to %v",
quoteAmount, meltQuote.FeeReserve)

if err := m.db.SaveMeltQuote(meltQuote); err != nil {
errmsg := fmt.Sprintf("error saving melt quote to db: %v", err)
Expand Down Expand Up @@ -861,7 +875,12 @@ func (m *Mint) MeltTokens(ctx context.Context, meltTokensRequest nut05.PostMeltB
)
} else {
m.logInfof("attempting to pay invoice: %v", meltQuote.InvoiceRequest)
sendPaymentResponse, err = m.lightningClient.SendPayment(ctx, meltQuote.InvoiceRequest, meltQuote.Amount)
sendPaymentResponse, err = m.lightningClient.SendPayment(
ctx,
meltQuote.InvoiceRequest,
meltQuote.Amount,
meltQuote.FeeReserve,
)
}
if err != nil {
// if SendPayment failed do not return yet, an extra check will be done
Expand Down Expand Up @@ -1625,10 +1644,11 @@ func (m *Mint) SetMintInfo(mintInfo MintInfo) {
Nut05: nut06.NutSetting{
Methods: []nut06.MethodSetting{
{
Method: cashu.BOLT11_METHOD,
Unit: cashu.Sat.String(),
MinAmount: m.limits.MeltingSettings.MinAmount,
MaxAmount: m.limits.MeltingSettings.MaxAmount,
Method: cashu.BOLT11_METHOD,
Unit: cashu.Sat.String(),
MinAmount: m.limits.MeltingSettings.MinAmount,
MaxAmount: m.limits.MeltingSettings.MaxAmount,
Amountless: true,
},
},
Disabled: false,
Expand Down
Loading