nip60: just rely on nut08 overpaid fees to get change when melting, don't try to swap beforehand.

This commit is contained in:
fiatjaf 2025-01-30 09:45:00 -03:00
parent be65134354
commit f0054af4d8
5 changed files with 135 additions and 60 deletions

View File

@ -115,16 +115,12 @@ func signOutput(
// constructProofs unblinds the blindedSignatures and returns the proofs
func constructProofs(
prep preparedOutputs,
blindedSignatures cashu.BlindedSignatures,
blindedMessages cashu.BlindedMessages,
secrets []string,
rs []*secp256k1.PrivateKey,
keys map[uint64]*btcec.PublicKey,
) (cashu.Proofs, error) {
sigsLenght := len(blindedSignatures)
if sigsLenght != len(secrets) || sigsLenght != len(rs) {
return nil, errors.New("lengths do not match")
}
// blinded sigs might be less than slices in prep, but that is fine, we just ignore the last
// items in prep. it happens when we are building proofs from change sent by a mint after melt.
proofs := make(cashu.Proofs, len(blindedSignatures))
for i, blindedSignature := range blindedSignatures {
@ -139,7 +135,7 @@ func constructProofs(
if !nut12.VerifyBlindSignatureDLEQ(
*blindedSignature.DLEQ,
pubkey,
blindedMessages[i].B_,
prep.bm[i].B_,
blindedSignature.C_,
) {
return nil, errors.New("got blinded signature with invalid DLEQ proof")
@ -147,19 +143,19 @@ func constructProofs(
dleq = &cashu.DLEQProof{
E: blindedSignature.DLEQ.E,
S: blindedSignature.DLEQ.S,
R: hex.EncodeToString(rs[i].Serialize()),
R: hex.EncodeToString(prep.rs[i].Serialize()),
}
}
}
C, err := unblindSignature(blindedSignature.C_, rs[i], pubkey)
C, err := unblindSignature(blindedSignature.C_, prep.rs[i], pubkey)
if err != nil {
return nil, err
}
proof := cashu.Proof{
Amount: blindedSignature.Amount,
Secret: secrets[i],
Secret: prep.secrets[i],
C: C,
Id: blindedSignature.Id,
DLEQ: dleq,

View File

@ -139,7 +139,11 @@ inspectmeltstatusresponse:
return nil, fmt.Errorf("mint request to %s failed (%s): %w", to, mintQuote, err), manualActionRequired
}
proofs, err = constructProofs(mintResponse.Signatures, blindedMessages, secrets, rs, keysetKeys)
proofs, err = constructProofs(preparedOutputs{
bm: blindedMessages,
secrets: secrets,
rs: rs,
}, mintResponse.Signatures, keysetKeys)
if err != nil {
return nil, fmt.Errorf("error constructing proofs: %w", err), manualActionRequired
}

View File

@ -26,18 +26,27 @@ func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOpti
var chosen chosenTokens
var meltQuote string
var meltAmount uint64
var meltAmountWithoutFeeReserve uint64
feeReservePct := uint64(1)
feeReserveAbs := uint64(1)
excludeMints := make([]string, 0, 1)
for range 10 {
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
var fee uint64
chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint)
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,
@ -49,36 +58,45 @@ func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOpti
// if amount in proofs is not sufficient to pay for the melt request,
// increase the amount and get proofs again (because of lighting fees)
meltQuote = meltResp.Quote
meltAmount = meltResp.Amount + meltResp.FeeReserve + fee
if meltAmount > chosen.proofs.Amount() {
if meltResp.Amount+meltResp.FeeReserve+fee > chosen.proofs.Amount() {
feeReserveAbs++
} else {
meltQuote = meltResp.Quote
meltAmountWithoutFeeReserve = invoiceAmount + fee
goto meltworked
}
}
return "", fmt.Errorf("stop trying to do the melt because the invoice is too expensive")
return "", fmt.Errorf("stopped trying to do the melt because all the mints are charging way too much")
meltworked:
// swap our proofs so we get the exact amount for paying the invoice
principal, change, err := w.SwapProofs(ctx, chosen.mint, chosen.proofs, meltAmount)
activeKeyset, err := client.GetActiveKeyset(ctx, chosen.mint)
if err != nil {
return "", fmt.Errorf("failed to swap at %s into the exact melt amount: %w", chosen.mint, err)
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)
}
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, change, chosen.tokenIndexes); err != nil {
return "", 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: principal,
Quote: meltQuote,
Inputs: chosen.proofs,
Outputs: preChange.bm,
})
inspectmeltstatusresponse:
if err != nil || meltStatus.State == nut05.Unpaid {
@ -94,5 +112,15 @@ inspectmeltstatusresponse:
}
}
// the invoice has been paid, now we save the change we got
change, err := constructProofs(preChange, meltStatus.Change, ksKeys)
if err != nil {
return "", fmt.Errorf("failed to construct principal proofs: %w", err)
}
if err := w.saveChangeAndDeleteUsedTokens(ctx, chosen.mint, change, chosen.tokenIndexes); err != nil {
return "", err
}
return meltStatus.Preimage, nil
}

View File

@ -59,7 +59,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
w.tokensMu.Lock()
defer w.tokensMu.Unlock()
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint)
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint, nil)
if err != nil {
return "", err
}
@ -138,7 +138,7 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
deleteEvent := nostr.Event{
CreatedAt: nostr.Now(),
Kind: 5,
Tags: nostr.Tags{{"e", token.event.ID}, {"k", "7375"}},
Tags: nostr.Tags{{"e", token.event.ID}, {"k", "7375"}, {"alt", "deleting"}},
}
w.wl.kr.SignEvent(ctx, &deleteEvent)
w.wl.Changes <- deleteEvent
@ -163,12 +163,16 @@ func (w *Wallet) getProofsForSending(
ctx context.Context,
amount uint64,
specificMint string,
excludeMints []string,
) (chosenTokens, uint64, error) {
byMint := make(map[string]chosenTokens)
for t, token := range w.Tokens {
if specificMint != "" && token.Mint != specificMint {
continue
}
if slices.Contains(excludeMints, token.Mint) {
continue
}
part, ok := byMint[token.Mint]
if !ok {

View File

@ -3,8 +3,11 @@ package nip60
import (
"context"
"fmt"
"slices"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut02"
"github.com/elnosh/gonuts/cashu/nuts/nut03"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
"github.com/nbd-wtf/go-nostr/nip60/client"
@ -41,7 +44,6 @@ func (w *Wallet) SwapProofs(
opt(&ss)
}
// fetch all this keyset drama first
keysets, err := client.GetAllKeysets(ctx, mint)
if err != nil {
return nil, nil, fmt.Errorf("failed to get all keysets for %s: %w", mint, err)
@ -55,6 +57,63 @@ func (w *Wallet) SwapProofs(
return nil, nil, fmt.Errorf("failed to parse keys for %s: %w", mint, err)
}
prePrincipal, preChange, err := splitIntoPrincipalAndChange(
keysets,
proofs,
targetAmount,
activeKeyset.Id,
ss.spendingCondition,
)
if err != nil {
return nil, nil, err
}
if ss.mustSignOutputs {
for i, output := range prePrincipal.bm {
prePrincipal.bm[i].Witness, err = signOutput(w.PrivateKey, output)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign output message %d: %w", i, err)
}
}
}
req := nut03.PostSwapRequest{
Inputs: proofs,
Outputs: slices.Concat(prePrincipal.bm, preChange.bm),
}
res, err := client.PostSwap(ctx, mint, req)
if err != nil {
return nil, nil, fmt.Errorf("failed to swap tokens at %s: %w", mint, err)
}
// build the proofs locally from mint's response
principal, err = constructProofs(prePrincipal, res.Signatures[0:len(prePrincipal.bm)], ksKeys)
if err != nil {
return nil, nil, fmt.Errorf("failed to construct principal proofs: %w", err)
}
change, err = constructProofs(preChange, res.Signatures[len(prePrincipal.bm):], ksKeys)
if err != nil {
return nil, nil, fmt.Errorf("failed to construct principal proofs: %w", err)
}
return principal, change, nil
}
type preparedOutputs struct {
bm cashu.BlindedMessages
rs []*btcec.PrivateKey
secrets []string
}
func splitIntoPrincipalAndChange(
keysets []nut02.Keyset,
proofs cashu.Proofs,
targetAmount uint64,
activeKeysetId string,
spendingCondition *nut10.SpendingCondition,
) (principal preparedOutputs, change preparedOutputs, err error) {
// decide the shape of the proofs we'll swap for
proofsAmount := proofs.Amount()
var (
@ -71,8 +130,8 @@ func (w *Wallet) SwapProofs(
principalAmount = targetAmount - fee
changeAmount = 0
} else {
return nil, nil, fmt.Errorf("can't swap for more than we are sending: %d > %d",
targetAmount, proofsAmount)
err = fmt.Errorf("can't swap for more than we are sending: %d > %d", targetAmount, proofsAmount)
return
}
splits := make([]uint64, 0, len(proofs)*2)
splits = append(splits, cashu.AmountSplit(principalAmount)...)
@ -80,35 +139,19 @@ func (w *Wallet) SwapProofs(
splits = append(splits, cashu.AmountSplit(changeAmount)...)
// prepare message to send to mint
outputs, secrets, rs, err := createBlindedMessages(splits, activeKeyset.Id, ss.spendingCondition)
bm, secrets, rs, err := createBlindedMessages(splits, activeKeysetId, spendingCondition)
if err != nil {
return nil, nil, fmt.Errorf("failed to create blinded message: %w", err)
err = fmt.Errorf("failed to create blinded message: %w", err)
return
}
if ss.mustSignOutputs {
for i, output := range outputs {
outputs[i].Witness, err = signOutput(w.PrivateKey, output)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign output message %d: %w", i, err)
}
}
}
req := nut03.PostSwapRequest{
Inputs: proofs,
Outputs: outputs,
}
res, err := client.PostSwap(ctx, mint, req)
if err != nil {
return nil, nil, fmt.Errorf("failed to swap tokens at %s: %w", mint, err)
}
// build the proofs locally from mint's response
newProofs, err := constructProofs(res.Signatures, req.Outputs, secrets, rs, ksKeys)
if err != nil {
return nil, nil, fmt.Errorf("failed to construct proofs: %w", err)
}
return newProofs[0:changeStartIndex], newProofs[changeStartIndex:], nil
return preparedOutputs{
bm: bm[0:changeStartIndex],
rs: rs[0:changeStartIndex],
secrets: secrets[0:changeStartIndex],
}, preparedOutputs{
bm: bm[changeStartIndex:],
rs: rs[changeStartIndex:],
secrets: secrets[changeStartIndex:],
}, nil
}