diff --git a/nip60/lightning-swap.go b/nip60/lightning-swap.go index 5528d55..9707030 100644 --- a/nip60/lightning-swap.go +++ b/nip60/lightning-swap.go @@ -5,11 +5,12 @@ import ( "fmt" "time" + "fiatjaf.com/nostr/nip60/client" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut02" "github.com/elnosh/gonuts/cashu/nuts/nut04" "github.com/elnosh/gonuts/cashu/nuts/nut05" - "fiatjaf.com/nostr/nip60/client" + "github.com/elnosh/gonuts/cashu/nuts/nut10" ) 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 { return nil, 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 } +// redeemMinted just downloads proofs from a lightning invoice paid at some mint. func redeemMinted( ctx context.Context, mint string, mintQuote string, mintAmount uint64, + spendingCondition *nut10.SpendingCondition, ) (cashu.Proofs, error) { // source mint says it has paid the invoice, now check it against the target mint // 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) } 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 { return nil, fmt.Errorf("error creating blinded messages: %w", err) } diff --git a/nip60/send-external.go b/nip60/send-external.go index 23ba125..ab8f307 100644 --- a/nip60/send-external.go +++ b/nip60/send-external.go @@ -34,5 +34,5 @@ func (w *Wallet) SendExternal( return nil, err } - return redeemMinted(ctx, mint, mintResp.Quote, targetAmount) + return redeemMinted(ctx, mint, mintResp.Quote, targetAmount, opts.asSpendingCondition(w.PublicKey)) } diff --git a/nip60/send-internal.go b/nip60/send-internal.go new file mode 100644 index 0000000..2d3d3f5 --- /dev/null +++ b/nip60/send-internal.go @@ -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 +} diff --git a/nip60/send.go b/nip60/send.go index d722b88..adb38d0 100644 --- a/nip60/send.go +++ b/nip60/send.go @@ -20,6 +20,54 @@ type SendOptions struct { SpecificSourceMint string P2PK *btcec.PublicKey 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 { @@ -30,69 +78,6 @@ type chosenTokens struct { 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( ctx context.Context, mintURL string, diff --git a/nip60/swap.go b/nip60/swap.go index ded3809..3831e1e 100644 --- a/nip60/swap.go +++ b/nip60/swap.go @@ -5,12 +5,12 @@ import ( "fmt" "slices" + "fiatjaf.com/nostr/nip60/client" "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" - "fiatjaf.com/nostr/nip60/client" ) type swapSettings struct { diff --git a/nip61/nip61.go b/nip61/nip61.go index 8d52392..92fd235 100644 --- a/nip61/nip61.go +++ b/nip61/nip61.go @@ -86,7 +86,7 @@ func SendNutzap( continue } - proofs, _, err := w.Send(ctx, amount, nip60.SendOptions{ + proofs, _, err := w.SendInternal(ctx, amount, nip60.SendOptions{ P2PK: p2pk, SpecificSourceMint: mint, })