nip60: fix spending conditions over SendExternal() and SendInternal()
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
64
nip60/send-internal.go
Normal 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
|
||||||
|
}
|
||||||
111
nip60/send.go
111
nip60/send.go
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user