mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-07-15 14:02:20 +02:00
nip60: just rely on nut08 overpaid fees to get change when melting, don't try to swap beforehand.
This commit is contained in:
@ -115,16 +115,12 @@ func signOutput(
|
|||||||
|
|
||||||
// constructProofs unblinds the blindedSignatures and returns the proofs
|
// constructProofs unblinds the blindedSignatures and returns the proofs
|
||||||
func constructProofs(
|
func constructProofs(
|
||||||
|
prep preparedOutputs,
|
||||||
blindedSignatures cashu.BlindedSignatures,
|
blindedSignatures cashu.BlindedSignatures,
|
||||||
blindedMessages cashu.BlindedMessages,
|
|
||||||
secrets []string,
|
|
||||||
rs []*secp256k1.PrivateKey,
|
|
||||||
keys map[uint64]*btcec.PublicKey,
|
keys map[uint64]*btcec.PublicKey,
|
||||||
) (cashu.Proofs, error) {
|
) (cashu.Proofs, error) {
|
||||||
sigsLenght := len(blindedSignatures)
|
// blinded sigs might be less than slices in prep, but that is fine, we just ignore the last
|
||||||
if sigsLenght != len(secrets) || sigsLenght != len(rs) {
|
// items in prep. it happens when we are building proofs from change sent by a mint after melt.
|
||||||
return nil, errors.New("lengths do not match")
|
|
||||||
}
|
|
||||||
|
|
||||||
proofs := make(cashu.Proofs, len(blindedSignatures))
|
proofs := make(cashu.Proofs, len(blindedSignatures))
|
||||||
for i, blindedSignature := range blindedSignatures {
|
for i, blindedSignature := range blindedSignatures {
|
||||||
@ -139,7 +135,7 @@ func constructProofs(
|
|||||||
if !nut12.VerifyBlindSignatureDLEQ(
|
if !nut12.VerifyBlindSignatureDLEQ(
|
||||||
*blindedSignature.DLEQ,
|
*blindedSignature.DLEQ,
|
||||||
pubkey,
|
pubkey,
|
||||||
blindedMessages[i].B_,
|
prep.bm[i].B_,
|
||||||
blindedSignature.C_,
|
blindedSignature.C_,
|
||||||
) {
|
) {
|
||||||
return nil, errors.New("got blinded signature with invalid DLEQ proof")
|
return nil, errors.New("got blinded signature with invalid DLEQ proof")
|
||||||
@ -147,19 +143,19 @@ func constructProofs(
|
|||||||
dleq = &cashu.DLEQProof{
|
dleq = &cashu.DLEQProof{
|
||||||
E: blindedSignature.DLEQ.E,
|
E: blindedSignature.DLEQ.E,
|
||||||
S: blindedSignature.DLEQ.S,
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
proof := cashu.Proof{
|
proof := cashu.Proof{
|
||||||
Amount: blindedSignature.Amount,
|
Amount: blindedSignature.Amount,
|
||||||
Secret: secrets[i],
|
Secret: prep.secrets[i],
|
||||||
C: C,
|
C: C,
|
||||||
Id: blindedSignature.Id,
|
Id: blindedSignature.Id,
|
||||||
DLEQ: dleq,
|
DLEQ: dleq,
|
||||||
|
@ -139,7 +139,11 @@ inspectmeltstatusresponse:
|
|||||||
return nil, fmt.Errorf("mint request to %s failed (%s): %w", to, mintQuote, err), manualActionRequired
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error constructing proofs: %w", err), manualActionRequired
|
return nil, fmt.Errorf("error constructing proofs: %w", err), manualActionRequired
|
||||||
}
|
}
|
||||||
|
58
nip60/pay.go
58
nip60/pay.go
@ -26,18 +26,27 @@ func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOpti
|
|||||||
|
|
||||||
var chosen chosenTokens
|
var chosen chosenTokens
|
||||||
var meltQuote string
|
var meltQuote string
|
||||||
var meltAmount uint64
|
var meltAmountWithoutFeeReserve uint64
|
||||||
|
|
||||||
feeReservePct := uint64(1)
|
feeReservePct := uint64(1)
|
||||||
feeReserveAbs := uint64(1)
|
feeReserveAbs := uint64(1)
|
||||||
|
|
||||||
|
excludeMints := make([]string, 0, 1)
|
||||||
|
|
||||||
for range 10 {
|
for range 10 {
|
||||||
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
|
amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs
|
||||||
var fee uint64
|
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 {
|
if err != nil {
|
||||||
return "", err
|
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)
|
// 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{
|
meltResp, err := client.PostMeltQuoteBolt11(ctx, chosen.mint, nut05.PostMeltQuoteBolt11Request{
|
||||||
Request: invoice,
|
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,
|
// if amount in proofs is not sufficient to pay for the melt request,
|
||||||
// increase the amount and get proofs again (because of lighting fees)
|
// increase the amount and get proofs again (because of lighting fees)
|
||||||
meltQuote = meltResp.Quote
|
if meltResp.Amount+meltResp.FeeReserve+fee > chosen.proofs.Amount() {
|
||||||
meltAmount = meltResp.Amount + meltResp.FeeReserve + fee
|
|
||||||
|
|
||||||
if meltAmount > chosen.proofs.Amount() {
|
|
||||||
feeReserveAbs++
|
feeReserveAbs++
|
||||||
} else {
|
} else {
|
||||||
|
meltQuote = meltResp.Quote
|
||||||
|
meltAmountWithoutFeeReserve = invoiceAmount + fee
|
||||||
goto meltworked
|
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:
|
meltworked:
|
||||||
// swap our proofs so we get the exact amount for paying the invoice
|
activeKeyset, err := client.GetActiveKeyset(ctx, chosen.mint)
|
||||||
principal, change, err := w.SwapProofs(ctx, chosen.mint, chosen.proofs, meltAmount)
|
|
||||||
if err != nil {
|
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 {
|
// since we rely on nut08 we will send all the proofs we've gathered and expect a change
|
||||||
return "", err
|
// 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
|
// request from mint to _melt_ into paying the invoice
|
||||||
delay := 200 * time.Millisecond
|
delay := 200 * time.Millisecond
|
||||||
// this request will block until the invoice is paid or it fails
|
// 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)
|
// (but the API also says it can return "pending" so we handle both)
|
||||||
meltStatus, err := client.PostMeltBolt11(ctx, chosen.mint, nut05.PostMeltBolt11Request{
|
meltStatus, err := client.PostMeltBolt11(ctx, chosen.mint, nut05.PostMeltBolt11Request{
|
||||||
Quote: meltQuote,
|
Quote: meltQuote,
|
||||||
Inputs: principal,
|
Inputs: chosen.proofs,
|
||||||
|
Outputs: preChange.bm,
|
||||||
})
|
})
|
||||||
inspectmeltstatusresponse:
|
inspectmeltstatusresponse:
|
||||||
if err != nil || meltStatus.State == nut05.Unpaid {
|
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
|
return meltStatus.Preimage, nil
|
||||||
}
|
}
|
||||||
|
@ -59,7 +59,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
|
|||||||
w.tokensMu.Lock()
|
w.tokensMu.Lock()
|
||||||
defer w.tokensMu.Unlock()
|
defer w.tokensMu.Unlock()
|
||||||
|
|
||||||
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint)
|
chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -138,7 +138,7 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
|
|||||||
deleteEvent := nostr.Event{
|
deleteEvent := nostr.Event{
|
||||||
CreatedAt: nostr.Now(),
|
CreatedAt: nostr.Now(),
|
||||||
Kind: 5,
|
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.kr.SignEvent(ctx, &deleteEvent)
|
||||||
w.wl.Changes <- deleteEvent
|
w.wl.Changes <- deleteEvent
|
||||||
@ -163,12 +163,16 @@ func (w *Wallet) getProofsForSending(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
amount uint64,
|
amount uint64,
|
||||||
specificMint string,
|
specificMint string,
|
||||||
|
excludeMints []string,
|
||||||
) (chosenTokens, uint64, error) {
|
) (chosenTokens, uint64, error) {
|
||||||
byMint := make(map[string]chosenTokens)
|
byMint := make(map[string]chosenTokens)
|
||||||
for t, token := range w.Tokens {
|
for t, token := range w.Tokens {
|
||||||
if specificMint != "" && token.Mint != specificMint {
|
if specificMint != "" && token.Mint != specificMint {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if slices.Contains(excludeMints, token.Mint) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
part, ok := byMint[token.Mint]
|
part, ok := byMint[token.Mint]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
105
nip60/swap.go
105
nip60/swap.go
@ -3,8 +3,11 @@ package nip60
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/elnosh/gonuts/cashu"
|
"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/nut03"
|
||||||
"github.com/elnosh/gonuts/cashu/nuts/nut10"
|
"github.com/elnosh/gonuts/cashu/nuts/nut10"
|
||||||
"github.com/nbd-wtf/go-nostr/nip60/client"
|
"github.com/nbd-wtf/go-nostr/nip60/client"
|
||||||
@ -41,7 +44,6 @@ func (w *Wallet) SwapProofs(
|
|||||||
opt(&ss)
|
opt(&ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch all this keyset drama first
|
|
||||||
keysets, err := client.GetAllKeysets(ctx, mint)
|
keysets, err := client.GetAllKeysets(ctx, mint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("failed to get all keysets for %s: %w", mint, err)
|
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)
|
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
|
// decide the shape of the proofs we'll swap for
|
||||||
proofsAmount := proofs.Amount()
|
proofsAmount := proofs.Amount()
|
||||||
var (
|
var (
|
||||||
@ -71,8 +130,8 @@ func (w *Wallet) SwapProofs(
|
|||||||
principalAmount = targetAmount - fee
|
principalAmount = targetAmount - fee
|
||||||
changeAmount = 0
|
changeAmount = 0
|
||||||
} else {
|
} else {
|
||||||
return nil, nil, fmt.Errorf("can't swap for more than we are sending: %d > %d",
|
err = fmt.Errorf("can't swap for more than we are sending: %d > %d", targetAmount, proofsAmount)
|
||||||
targetAmount, proofsAmount)
|
return
|
||||||
}
|
}
|
||||||
splits := make([]uint64, 0, len(proofs)*2)
|
splits := make([]uint64, 0, len(proofs)*2)
|
||||||
splits = append(splits, cashu.AmountSplit(principalAmount)...)
|
splits = append(splits, cashu.AmountSplit(principalAmount)...)
|
||||||
@ -80,35 +139,19 @@ func (w *Wallet) SwapProofs(
|
|||||||
splits = append(splits, cashu.AmountSplit(changeAmount)...)
|
splits = append(splits, cashu.AmountSplit(changeAmount)...)
|
||||||
|
|
||||||
// prepare message to send to mint
|
// 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 {
|
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 {
|
return preparedOutputs{
|
||||||
for i, output := range outputs {
|
bm: bm[0:changeStartIndex],
|
||||||
outputs[i].Witness, err = signOutput(w.PrivateKey, output)
|
rs: rs[0:changeStartIndex],
|
||||||
if err != nil {
|
secrets: secrets[0:changeStartIndex],
|
||||||
return nil, nil, fmt.Errorf("failed to sign output message %d: %w", i, err)
|
}, preparedOutputs{
|
||||||
}
|
bm: bm[changeStartIndex:],
|
||||||
}
|
rs: rs[changeStartIndex:],
|
||||||
}
|
secrets: secrets[changeStartIndex:],
|
||||||
|
}, nil
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user