go-nostr/nip60/pay.go
2025-02-06 15:02:49 -03:00

146 lines
4.3 KiB
Go

package nip60
import (
"context"
"fmt"
"time"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut05"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip60/client"
)
func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOption) (string, error) {
if w.PublishUpdate == nil {
return "", fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
ss := &sendSettings{}
for _, opt := range opts {
opt(ss)
}
invoiceAmount, err := GetSatoshisAmountFromBolt11(invoice)
if err != nil {
return "", err
}
w.tokensMu.Lock()
defer w.tokensMu.Unlock()
var chosen chosenTokens
var meltQuote string
var meltAmountWithoutFeeReserve uint64
feeReservePct := uint64(1)
feeReserveAbs := uint64(1)
excludeMints := make([]string, 0, 1)
for range 5 {
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
var fee uint64
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint, excludeMints)
if err != nil {
return "", err
}
// we will only do this in mints that support nut08
if info, _ := client.GetMintInfo(ctx, chosen.mint); info == nil || !info.Nuts.Nut08.Supported {
excludeMints = append(excludeMints, chosen.mint)
continue
}
// request _melt_ quote (ask the mint how much will it cost to pay a bolt11 invoice)
meltResp, err := client.PostMeltQuoteBolt11(ctx, chosen.mint, nut05.PostMeltQuoteBolt11Request{
Request: invoice,
Unit: cashu.Sat.String(),
})
if err != nil {
return "", fmt.Errorf("error requesting melt quote from %s: %w", chosen.mint, err)
}
// if amount in proofs is not sufficient to pay for the melt request,
// increase the amount and get proofs again (because of lighting fees)
if meltResp.Amount+meltResp.FeeReserve+fee > chosen.proofs.Amount() {
feeReserveAbs++
} else {
meltQuote = meltResp.Quote
meltAmountWithoutFeeReserve = invoiceAmount + fee
goto meltworked
}
}
return "", fmt.Errorf("stopped trying to do the melt because all the mints are charging way too much")
meltworked:
activeKeyset, err := client.GetActiveKeyset(ctx, chosen.mint)
if err != nil {
return "", fmt.Errorf("failed to get active keyset for %s: %w", chosen.mint, err)
}
ksKeys, err := ParseKeysetKeys(activeKeyset.Keys)
if err != nil {
return "", fmt.Errorf("failed to parse keys for %s: %w", chosen.mint, err)
}
// since we rely on nut08 we will send all the proofs we've gathered and expect a change
// we do a split here and discard the principal, as we won't get it back from the mint
_, preChange, err := splitIntoPrincipalAndChange(
chosen.keysets,
chosen.proofs,
meltAmountWithoutFeeReserve,
activeKeyset.Id,
nil,
)
// request from mint to _melt_ into paying the invoice
delay := 200 * time.Millisecond
// this request will block until the invoice is paid or it fails
// (but the API also says it can return "pending" so we handle both)
meltStatus, err := client.PostMeltBolt11(ctx, chosen.mint, nut05.PostMeltBolt11Request{
Quote: meltQuote,
Inputs: chosen.proofs,
Outputs: preChange.bm,
})
inspectmeltstatusresponse:
if err != nil || meltStatus.State == nut05.Unpaid {
return "", fmt.Errorf("error melting token: %w", err)
} else if meltStatus.State == nut05.Unknown {
return "", fmt.Errorf("we don't know what happened with the melt at %s: %v", chosen.mint, meltStatus)
} else if meltStatus.State == nut05.Pending {
for {
time.Sleep(delay)
delay *= 2
meltStatus, err = client.GetMeltQuoteState(ctx, chosen.mint, meltStatus.Quote)
goto inspectmeltstatusresponse
}
}
// the invoice has been paid, now we save the change we got
changeProofs, err := constructProofs(preChange, meltStatus.Change, ksKeys)
if err != nil {
return "", fmt.Errorf("failed to construct principal proofs: %w", err)
}
he := HistoryEntry{
event: &nostr.Event{},
TokenReferences: make([]TokenRef, 0, 5),
createdAt: nostr.Now(),
In: false,
Amount: chosen.proofs.Amount() - changeProofs.Amount(),
}
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, changeProofs, chosen.tokenIndexes, &he); err != nil {
return "", err
}
w.Lock()
if err := he.toEvent(ctx, w.kr, he.event); err == nil {
w.PublishUpdate(*he.event, nil, nil, nil, true)
}
w.Unlock()
return meltStatus.Preimage, nil
}