nip60: fix spending conditions over SendExternal() and SendInternal()

This commit is contained in:
fiatjaf
2025-05-06 16:24:51 -03:00
parent a58015b3e9
commit 93745cafff
6 changed files with 121 additions and 69 deletions

View File

@@ -5,11 +5,12 @@ import (
"fmt" "fmt"
"time" "time"
"fiatjaf.com/nostr/nip60/client"
"github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut02" "github.com/elnosh/gonuts/cashu/nuts/nut02"
"github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut04"
"github.com/elnosh/gonuts/cashu/nuts/nut05" "github.com/elnosh/gonuts/cashu/nuts/nut05"
"fiatjaf.com/nostr/nip60/client" "github.com/elnosh/gonuts/cashu/nuts/nut10"
) )
type lightningSwapStatus int type lightningSwapStatus int
@@ -95,7 +96,7 @@ inspectmeltstatusresponse:
} }
} }
proofs, err = redeemMinted(ctx, to, mintQuote, amount) proofs, err = redeemMinted(ctx, to, mintQuote, amount, nil)
if err != nil { if err != nil {
return nil, return nil,
fmt.Errorf("failed to redeem minted proofs at %s (after successfully melting at %s): %w", to, from, err), fmt.Errorf("failed to redeem minted proofs at %s (after successfully melting at %s): %w", to, from, err),
@@ -105,11 +106,13 @@ inspectmeltstatusresponse:
return proofs, nil, nothingCanBeDone return proofs, nil, nothingCanBeDone
} }
// redeemMinted just downloads proofs from a lightning invoice paid at some mint.
func redeemMinted( func redeemMinted(
ctx context.Context, ctx context.Context,
mint string, mint string,
mintQuote string, mintQuote string,
mintAmount uint64, mintAmount uint64,
spendingCondition *nut10.SpendingCondition,
) (cashu.Proofs, error) { ) (cashu.Proofs, error) {
// source mint says it has paid the invoice, now check it against the target mint // source mint says it has paid the invoice, now check it against the target mint
// check if the _mint_ invoice was paid // check if the _mint_ invoice was paid
@@ -139,7 +142,7 @@ func redeemMinted(
return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", mint, err) return nil, fmt.Errorf("target mint %s sent us an invalid keyset: %w", mint, err)
} }
split := cashu.AmountSplit(mintAmount) split := cashu.AmountSplit(mintAmount)
blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, nil) blindedMessages, secrets, rs, err := createBlindedMessages(split, keyset.Id, spendingCondition)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating blinded messages: %w", err) return nil, fmt.Errorf("error creating blinded messages: %w", err)
} }

View File

@@ -34,5 +34,5 @@ func (w *Wallet) SendExternal(
return nil, err return nil, err
} }
return redeemMinted(ctx, mint, mintResp.Quote, targetAmount) return redeemMinted(ctx, mint, mintResp.Quote, targetAmount, opts.asSpendingCondition(w.PublicKey))
} }

64
nip60/send-internal.go Normal file
View File

@@ -0,0 +1,64 @@
package nip60
import (
"context"
"fmt"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip60/client"
"github.com/elnosh/gonuts/cashu"
)
func (w *Wallet) SendInternal(ctx context.Context, amount uint64, opts SendOptions) (cashu.Proofs, string, error) {
if w.PublishUpdate == nil {
return nil, "", fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
w.tokensMu.Lock()
defer w.tokensMu.Unlock()
chosen, _, err := w.getProofsForSending(ctx, amount, opts.SpecificSourceMint, nil)
if err != nil {
return nil, "", err
}
if opts.Hashlock != [32]byte{} {
if info, err := client.GetMintInfo(ctx, chosen.mint); err != nil || !info.Nuts.Nut14.Supported {
return nil, chosen.mint, fmt.Errorf("mint doesn't support htlc: %w", err)
}
} else if opts.P2PK != nil {
if info, err := client.GetMintInfo(ctx, chosen.mint); err != nil || !info.Nuts.Nut11.Supported {
return nil, chosen.mint, fmt.Errorf("mint doesn't support p2pk: %w", err)
}
}
swapSettings := swapSettings{
spendingCondition: opts.asSpendingCondition(w.PublicKey),
}
// get new proofs
proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapSettings)
if err != nil {
return nil, chosen.mint, 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 nil, chosen.mint, 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 proofsToSend, chosen.mint, nil
}

View File

@@ -20,6 +20,54 @@ type SendOptions struct {
SpecificSourceMint string SpecificSourceMint string
P2PK *btcec.PublicKey P2PK *btcec.PublicKey
RefundTimelock nostr.Timestamp RefundTimelock nostr.Timestamp
Hashlock [32]byte
}
func (opts SendOptions) asSpendingCondition(refund *btcec.PublicKey) *nut10.SpendingCondition {
if opts.Hashlock != [32]byte{} {
// when we have an HTLC condition:
// (it can also include a P2PK and a timelock)
tags := nut11.P2PKTags{
NSigs: 1,
Locktime: 0,
Sigflag: nut11.SIGINPUTS,
}
if opts.P2PK != nil {
tags.Pubkeys = []*btcec.PublicKey{opts.P2PK}
}
if opts.RefundTimelock != 0 {
tags.Refund = []*btcec.PublicKey{refund}
tags.Locktime = int64(opts.RefundTimelock)
}
return &nut10.SpendingCondition{
Kind: nut10.HTLC,
Data: hex.EncodeToString(opts.Hashlock[:]),
Tags: nut11.SerializeP2PKTags(tags),
}
} else if opts.P2PK != nil {
// otherwise when it is just a P2PK condition with no hashlock
// (may also have a timelock)
tags := nut11.P2PKTags{
NSigs: 1,
Locktime: 0,
Pubkeys: []*btcec.PublicKey{opts.P2PK},
Sigflag: nut11.SIGINPUTS,
}
if opts.RefundTimelock != 0 {
tags.Refund = []*btcec.PublicKey{refund}
tags.Locktime = int64(opts.RefundTimelock)
}
return &nut10.SpendingCondition{
Kind: nut10.P2PK,
Data: hex.EncodeToString(opts.P2PK.SerializeCompressed()),
Tags: nut11.SerializeP2PKTags(tags),
}
} else {
return nil
}
} }
type chosenTokens struct { type chosenTokens struct {
@@ -30,69 +78,6 @@ type chosenTokens struct {
keysets []nut02.Keyset keysets []nut02.Keyset
} }
func (w *Wallet) Send(ctx context.Context, amount uint64, opts SendOptions) (cashu.Proofs, string, error) {
if w.PublishUpdate == nil {
return nil, "", fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
w.tokensMu.Lock()
defer w.tokensMu.Unlock()
chosen, _, err := w.getProofsForSending(ctx, amount, opts.SpecificSourceMint, nil)
if err != nil {
return nil, "", err
}
swapSettings := swapSettings{}
if opts.P2PK != nil {
if info, err := client.GetMintInfo(ctx, chosen.mint); err != nil || !info.Nuts.Nut11.Supported {
return nil, chosen.mint, fmt.Errorf("mint doesn't support p2pk: %w", err)
}
tags := nut11.P2PKTags{
NSigs: 1,
Locktime: 0,
Pubkeys: []*btcec.PublicKey{opts.P2PK},
}
if opts.RefundTimelock != 0 {
tags.Refund = []*btcec.PublicKey{w.PublicKey}
tags.Locktime = int64(opts.RefundTimelock)
}
swapSettings.spendingCondition = &nut10.SpendingCondition{
Kind: nut10.P2PK,
Data: hex.EncodeToString(opts.P2PK.SerializeCompressed()),
Tags: nut11.SerializeP2PKTags(tags),
}
}
// get new proofs
proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapSettings)
if err != nil {
return nil, chosen.mint, 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 nil, chosen.mint, 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 proofsToSend, chosen.mint, nil
}
func (w *Wallet) saveChangeAndDeleteUsedTokens( func (w *Wallet) saveChangeAndDeleteUsedTokens(
ctx context.Context, ctx context.Context,
mintURL string, mintURL string,

View File

@@ -5,12 +5,12 @@ import (
"fmt" "fmt"
"slices" "slices"
"fiatjaf.com/nostr/nip60/client"
"github.com/btcsuite/btcd/btcec/v2" "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/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"
"fiatjaf.com/nostr/nip60/client"
) )
type swapSettings struct { type swapSettings struct {

View File

@@ -86,7 +86,7 @@ func SendNutzap(
continue continue
} }
proofs, _, err := w.Send(ctx, amount, nip60.SendOptions{ proofs, _, err := w.SendInternal(ctx, amount, nip60.SendOptions{
P2PK: p2pk, P2PK: p2pk,
SpecificSourceMint: mint, SpecificSourceMint: mint,
}) })