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

MPP: change to msat #111

Merged
merged 2 commits into from
Feb 5, 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
2 changes: 1 addition & 1 deletion cashu/nuts/nut05/nut05.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type PostMeltQuoteBolt11Request struct {
}

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

type PostMeltQuoteBolt11Response struct {
Expand Down
3 changes: 2 additions & 1 deletion cmd/nutw/nutw.go
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,8 @@ func pay(ctx *cli.Context) error {
printErr(errmsg)
}

split[selectedMint] = uint64(amountToUse)
// amount needs to be in msat
split[selectedMint] = uint64(amountToUse * 1000)
amountStillToPay -= amountToUse

if amountStillToPay > 0 {
Expand Down
31 changes: 30 additions & 1 deletion mint/lightning/fakebackend.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,36 @@ func (fb *FakeBackend) InvoiceStatus(hash string) (Invoice, error) {
return fb.Invoices[invoiceIdx].ToInvoice(), nil
}

func (fb *FakeBackend) SendPayment(ctx context.Context, request string, amount uint64) (PaymentStatus, error) {
func (fb *FakeBackend) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
invoice, err := decodepay.Decodepay(request)
if err != nil {
return PaymentStatus{}, fmt.Errorf("error decoding invoice: %v", err)
}

status := Succeeded
if invoice.Description == FailPaymentDescription {
status = Failed
} else if fb.PaymentDelay > 0 {
if time.Now().Unix() < int64(invoice.CreatedAt)+fb.PaymentDelay {
status = Pending
}
}

outgoingPayment := FakeBackendInvoice{
PaymentHash: invoice.PaymentHash,
Preimage: FakePreimage,
Status: status,
Amount: uint64(invoice.MSatoshi) * 1000,
}
fb.Invoices = append(fb.Invoices, outgoingPayment)

return PaymentStatus{
Preimage: FakePreimage,
PaymentStatus: status,
}, nil
}

func (fb *FakeBackend) PayPartialAmount(ctx context.Context, request string, amountMsat, 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, amount uint64) (PaymentStatus, error)
SendPayment(ctx context.Context, request string, 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
SubscribeInvoice(paymentHash string) (InvoiceSubscriptionClient, error)
Expand Down
41 changes: 17 additions & 24 deletions mint/lightning/lnd.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,24 +112,11 @@ func (lnd *LndClient) InvoiceStatus(hash string) (Invoice, error) {
return invoice, nil
}

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

// if amount is less than amount in invoice, pay partially if supported by backend.
// not checking err because invoice has already been validated by the mint
req := lnrpc.PayReqString{PayReq: request}
payReq, err := lnd.grpcClient.DecodePayReq(ctx, &req)
if err != nil {
return PaymentStatus{PaymentStatus: Failed}, err
}
if amount < uint64(payReq.NumMsat) {
return lnd.payPartialInvoice(ctx, payReq, amount, &feeLimit)
}

func (lnd *LndClient) SendPayment(ctx context.Context, request string, maxFee uint64) (PaymentStatus, error) {
feeLimit := &lnrpc.FeeLimit{Limit: &lnrpc.FeeLimit_Fixed{Fixed: int64(maxFee)}}
sendPaymentRequest := lnrpc.SendRequest{
PaymentRequest: request,
FeeLimit: &feeLimit,
FeeLimit: feeLimit,
}
sendPaymentResponse, err := lnd.grpcClient.SendPaymentSync(ctx, &sendPaymentRequest)
if err != nil {
Expand All @@ -151,15 +138,21 @@ func (lnd *LndClient) SendPayment(ctx context.Context, request string, amount ui
return paymentResponse, nil
}

func (lnd *LndClient) payPartialInvoice(
func (lnd *LndClient) PayPartialAmount(
ctx context.Context,
req *lnrpc.PayReq,
partialAmountToPay uint64,
feeLimit *lnrpc.FeeLimit,
request string,
amountMsat uint64,
maxFee uint64,
) (PaymentStatus, error) {
payReq, err := lnd.grpcClient.DecodePayReq(ctx, &lnrpc.PayReqString{PayReq: request})
if err != nil {
return PaymentStatus{PaymentStatus: Failed}, err
}

feeLimit := &lnrpc.FeeLimit{Limit: &lnrpc.FeeLimit_Fixed{Fixed: int64(maxFee)}}
queryRoutesRequest := lnrpc.QueryRoutesRequest{
PubKey: req.Destination,
Amt: int64(partialAmountToPay),
PubKey: payReq.Destination,
AmtMsat: int64(amountMsat),
FeeLimit: feeLimit,
}

Expand All @@ -172,10 +165,10 @@ func (lnd *LndClient) payPartialInvoice(
}

route := queryRoutesResponse.Routes[0]
mppRecord := lnrpc.MPPRecord{PaymentAddr: req.PaymentAddr, TotalAmtMsat: req.NumMsat}
mppRecord := lnrpc.MPPRecord{PaymentAddr: payReq.PaymentAddr, TotalAmtMsat: payReq.NumMsat}
route.Hops[len(route.Hops)-1].MppRecord = &mppRecord

paymentHashBytes, err := hex.DecodeString(req.PaymentHash)
paymentHashBytes, err := hex.DecodeString(payReq.PaymentHash)
if err != nil {
return PaymentStatus{PaymentStatus: Failed}, err
}
Expand Down
65 changes: 45 additions & 20 deletions mint/mint.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,18 +530,35 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
invoiceSatAmount := uint64(bolt11.MSatoshi) / 1000
quoteAmount := invoiceSatAmount

// check if a mint quote exists with the same invoice.
_, err = m.db.GetMintQuoteByPaymentHash(bolt11.PaymentHash)
isInternal := false
if err == nil {
isInternal = true
}

isMpp := false
var amountMsat uint64 = 0
// check mpp option
if len(meltQuoteRequest.Options) > 0 {
mpp, ok := meltQuoteRequest.Options["mpp"]
if ok {
if m.mppEnabled {
// check mpp amount is less than invoice amount
if mpp.Amount >= invoiceSatAmount {
// 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)
}
quoteAmount = mpp.Amount
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 {
Expand Down Expand Up @@ -571,6 +588,13 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
}
// Fee reserve that is required by the mint
fee := m.lightningClient.FeeReserve(quoteAmount)
// if mint quote exists with same invoice, it can be
// settled internally so set the fee to 0
if isInternal {
m.logDebugf(`in melt quote request found mint quote with same invoice.
Setting fee reserve to 0 because quotes can be settled internally.`)
fee = 0
}
meltQuote := storage.MeltQuote{
Id: quoteId,
InvoiceRequest: request,
Expand All @@ -579,19 +603,8 @@ func (m *Mint) RequestMeltQuote(meltQuoteRequest nut05.PostMeltQuoteBolt11Reques
FeeReserve: fee,
State: nut05.Unpaid,
Expiry: uint64(time.Now().Add(time.Minute * QuoteExpiryMins).Unix()),
}

// check if a mint quote exists with the same invoice.
// if mint quote exists with same invoice, it can be
// settled internally so set the fee to 0
mintQuote, err := m.db.GetMintQuoteByPaymentHash(bolt11.PaymentHash)
if err == nil {
m.logDebugf(`in melt quote request found mint quote with same invoice.
Setting fee reserve to 0 because quotes can be settled internally.`)

meltQuote.InvoiceRequest = mintQuote.PaymentRequest
meltQuote.PaymentHash = mintQuote.PaymentHash
meltQuote.FeeReserve = 0
IsMpp: isMpp,
AmountMsat: amountMsat,
}

m.logInfof("got melt quote request for invoice of amount '%v'. Setting fee reserve to %v",
Expand Down Expand Up @@ -781,13 +794,25 @@ func (m *Mint) MeltTokens(ctx context.Context, meltTokensRequest nut05.PostMeltB
}
m.publishProofsStateChanges(proofs, nut07.Spent)
} else {
m.logInfof("attempting to pay invoice: %v", meltQuote.InvoiceRequest)
// if quote can't be settled internally, ask backend to make payment
sendPaymentResponse, err := m.lightningClient.SendPayment(ctx, meltQuote.InvoiceRequest, meltQuote.Amount)
var sendPaymentResponse lightning.PaymentStatus
// if melt is MPP, pay partial amount. If not, send full payment
if meltQuote.IsMpp {
m.logInfof("attempting MPP payment of amount '%v' for invoice '%v'",
meltQuote.Amount, meltQuote.InvoiceRequest)
sendPaymentResponse, err = m.lightningClient.PayPartialAmount(
ctx,
meltQuote.InvoiceRequest,
meltQuote.AmountMsat,
m.lightningClient.FeeReserve(meltQuote.AmountMsat/1000),
)
} else {
m.logInfof("attempting to pay invoice: %v", meltQuote.InvoiceRequest)
sendPaymentResponse, err = m.lightningClient.SendPayment(ctx, meltQuote.InvoiceRequest, meltQuote.Amount)
}
if err != nil {
// if SendPayment failed do not return yet, an extra check will be done
sendPaymentResponse.PaymentStatus = lightning.Failed
m.logDebugf("SendPayment failed with error: %v. Will do extra check", err)
m.logDebugf("Payment failed with error: %v. Will do extra check", err)
}

switch sendPaymentResponse.PaymentStatus {
Expand Down
30 changes: 24 additions & 6 deletions mint/mint_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -706,7 +706,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest := nut05.PostMeltQuoteBolt11Request{
Request: addInvoiceResponse.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 6000}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 6000 * 1000}},
}
meltQuote1, err := testMint.RequestMeltQuote(meltQuoteRequest)
if err != nil {
Expand All @@ -716,7 +716,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: addInvoiceResponse.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 4000}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 4000 * 1000}},
}
meltQuote2, err := testMppMint.RequestMeltQuote(meltQuoteRequest)
if err != nil {
Expand Down Expand Up @@ -771,7 +771,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: addInvoiceResponse.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 6000}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 6000 * 1000}},
}
meltQuote1, err = testMint.RequestMeltQuote(meltQuoteRequest)
if err != nil {
Expand All @@ -781,7 +781,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: addInvoiceResponse.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 4000}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 4000 * 1000}},
}
meltQuote2, err = testMppMint.RequestMeltQuote(meltQuoteRequest)
if err != nil {
Expand Down Expand Up @@ -828,7 +828,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: addInvoiceResponse.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 10100}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 10100 * 1000}},
}
meltQuote1, err = testMint.RequestMeltQuote(meltQuoteRequest)
if err == nil {
Expand All @@ -854,7 +854,7 @@ func TestMPPMelt(t *testing.T) {
meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: addHodlInvoiceRes.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {Amount: 2000}},
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 2000 * 1000}},
}
meltQuote, err := testMint.RequestMeltQuote(meltQuoteRequest)
if err != nil {
Expand Down Expand Up @@ -894,6 +894,24 @@ func TestMPPMelt(t *testing.T) {
t.Fatalf("expected pending proof but got '%s' instead", proofState.State)
}
}

// test reject MPP for internal quotes
mintQuoteRequest := nut04.PostMintQuoteBolt11Request{Amount: 10000, Unit: cashu.Sat.String()}
mintQuote, err := testMint.RequestMintQuote(mintQuoteRequest)
if err != nil {
t.Fatalf("error requesting mint quote: %v", err)
}

meltQuoteRequest = nut05.PostMeltQuoteBolt11Request{
Request: mintQuote.PaymentRequest,
Unit: cashu.Sat.String(),
Options: map[string]nut05.MppOption{"mpp": {AmountMsat: 6000 * 1000}},
}
meltQuote1, err = testMint.RequestMeltQuote(meltQuoteRequest)
expectedErrMsg = "mpp for internal invoice is not allowed"
if err.Error() != expectedErrMsg {
t.Fatalf("expected error '%v' but got '%v'", expectedErrMsg, err.Error())
}
}

func TestPendingProofs(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions mint/storage/sqlite/migrations/000008_mpp_melt_quote.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE melt_quotes DROP COLUMN is_mpp;
ALTER TABLE melt_quotes DROP COLUMN amount_msat;
2 changes: 2 additions & 0 deletions mint/storage/sqlite/migrations/000008_mpp_melt_quote.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE melt_quotes ADD COLUMN is_mpp BOOLEAN;
ALTER TABLE melt_quotes ADD COLUMN amount_msat INTEGER;
Loading