From b711548b0384ff6eaa8f0efff43f7e91dceed537 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 5 May 2025 16:36:44 -0300 Subject: [PATCH] nip60: use more reasonable options instead of the crazy WithWhatever() pattern. --- nip60/history.go | 2 +- nip60/pay.go | 18 +++++++-------- nip60/receive.go | 32 ++++++-------------------- nip60/send-external.go | 8 ++++--- nip60/send.go | 52 ++++++++++++------------------------------ nip60/wallet.go | 21 ++++++++--------- nip61/nip61.go | 21 ++++++++++++++--- 7 files changed, 63 insertions(+), 91 deletions(-) diff --git a/nip60/history.go b/nip60/history.go index 62f4ee5..98221f8 100644 --- a/nip60/history.go +++ b/nip60/history.go @@ -131,7 +131,7 @@ func (h *HistoryEntry) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Eve } id, err := nostr.IDFromHex(tag[1]) if err != nil { - return fmt.Errorf("'e' tag has invalid event id %s: %w", tag[1]) + return fmt.Errorf("'e' tag has invalid event id %s: %w", tag[1], err) } tf := TokenRef{EventID: id} diff --git a/nip60/pay.go b/nip60/pay.go index 14ba272..f4316a3 100644 --- a/nip60/pay.go +++ b/nip60/pay.go @@ -5,22 +5,22 @@ import ( "fmt" "time" - "github.com/elnosh/gonuts/cashu" - "github.com/elnosh/gonuts/cashu/nuts/nut05" "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip60/client" + "github.com/elnosh/gonuts/cashu" + "github.com/elnosh/gonuts/cashu/nuts/nut05" ) -func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOption) (string, error) { +// PayOptions contains options for paying a bolt11 invoice +type PayOptions struct { + FromMint string +} + +func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts PayOptions) (string, error) { if w.PublishUpdate == nil { return "", fmt.Errorf("can't do write operations: missing PublishUpdate function") } - ss := &sendSettings{} - for _, opt := range opts { - opt(ss) - } - invoiceAmount, err := GetSatoshisAmountFromBolt11(invoice) if err != nil { return "", err @@ -41,7 +41,7 @@ func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOpti for range 5 { amount := invoiceAmount*(100+feeReservePct)/100 + feeReserveAbs var fee uint64 - chosen, fee, err = w.getProofsForSending(ctx, amount, ss.specificMint, excludeMints) + chosen, fee, err = w.getProofsForSending(ctx, amount, opts.FromMint, excludeMints) if err != nil { return "", err } diff --git a/nip60/receive.go b/nip60/receive.go index 30dbca3..5930d6d 100644 --- a/nip60/receive.go +++ b/nip60/receive.go @@ -11,42 +11,24 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut10" ) -type receiveSettings struct { - intoMint []string - isNutzap bool -} - -type ReceiveOption func(*receiveSettings) - -func WithMintDestination(url string) ReceiveOption { - return func(rs *receiveSettings) { - rs.intoMint = append(rs.intoMint, url) - } -} - -func WithNutzap() ReceiveOption { - return func(rs *receiveSettings) { - rs.isNutzap = true - } +// ReceiveOptions contains options for receiving tokens +type ReceiveOptions struct { + IntoMint []string + IsNutzap bool } func (w *Wallet) Receive( ctx context.Context, proofs cashu.Proofs, mint string, - opts ...ReceiveOption, + opts ReceiveOptions, ) error { if w.PublishUpdate == nil { return fmt.Errorf("can't do write operations: missing PublishUpdate function") } - rs := receiveSettings{} - for _, opt := range opts { - opt(&rs) - } - source, _ := nostr.NormalizeHTTPURL(mint) - destination := rs.intoMint + destination := opts.IntoMint swapSettings := swapSettings{} @@ -138,7 +120,7 @@ saveproofs: { EventID: newToken.event.ID, Created: true, - IsNutzap: rs.isNutzap, + IsNutzap: opts.IsNutzap, }, }, createdAt: nostr.Now(), diff --git a/nip60/send-external.go b/nip60/send-external.go index 9d24a01..23ba125 100644 --- a/nip60/send-external.go +++ b/nip60/send-external.go @@ -4,16 +4,16 @@ import ( "context" "fmt" + "fiatjaf.com/nostr/nip60/client" "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/cashu/nuts/nut04" - "fiatjaf.com/nostr/nip60/client" ) func (w *Wallet) SendExternal( ctx context.Context, mint string, targetAmount uint64, - opts ...SendOption, + opts SendOptions, ) (cashu.Proofs, error) { if w.PublishUpdate == nil { return nil, fmt.Errorf("can't do write operations: missing PublishUpdate function") @@ -28,7 +28,9 @@ func (w *Wallet) SendExternal( return nil, fmt.Errorf("failed to generate mint quote: %w", err) } - if _, err := w.PayBolt11(ctx, mintResp.Request, opts...); err != nil { + if _, err := w.PayBolt11(ctx, mintResp.Request, PayOptions{ + FromMint: opts.SpecificSourceMint, + }); err != nil { return nil, err } diff --git a/nip60/send.go b/nip60/send.go index 0bbe1f4..d722b88 100644 --- a/nip60/send.go +++ b/nip60/send.go @@ -15,30 +15,11 @@ import ( "github.com/elnosh/gonuts/cashu/nuts/nut11" ) -type SendOption func(opts *sendSettings) - -type sendSettings struct { - specificMint string - p2pk *btcec.PublicKey - refundtimelock int64 -} - -func WithP2PK(pubkey nostr.PubKey) SendOption { - return func(opts *sendSettings) { - opts.p2pk, _ = btcec.ParsePubKey(append([]byte{2}, pubkey[:]...)) - } -} - -func WithRefundable(timelock nostr.Timestamp) SendOption { - return func(opts *sendSettings) { - opts.refundtimelock = int64(timelock) - } -} - -func WithMint(url string) SendOption { - return func(opts *sendSettings) { - opts.specificMint = url - } +// SendOptions contains options for sending tokens +type SendOptions struct { + SpecificSourceMint string + P2PK *btcec.PublicKey + RefundTimelock nostr.Timestamp } type chosenTokens struct { @@ -49,26 +30,21 @@ type chosenTokens struct { keysets []nut02.Keyset } -func (w *Wallet) Send(ctx context.Context, amount uint64, opts ...SendOption) (cashu.Proofs, string, error) { +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") } - ss := &sendSettings{} - for _, opt := range opts { - opt(ss) - } - w.tokensMu.Lock() defer w.tokensMu.Unlock() - chosen, _, err := w.getProofsForSending(ctx, amount, ss.specificMint, nil) + chosen, _, err := w.getProofsForSending(ctx, amount, opts.SpecificSourceMint, nil) if err != nil { return nil, "", err } swapSettings := swapSettings{} - if ss.p2pk != nil { + 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) } @@ -76,16 +52,16 @@ func (w *Wallet) Send(ctx context.Context, amount uint64, opts ...SendOption) (c tags := nut11.P2PKTags{ NSigs: 1, Locktime: 0, - Pubkeys: []*btcec.PublicKey{ss.p2pk}, + Pubkeys: []*btcec.PublicKey{opts.P2PK}, } - if ss.refundtimelock != 0 { + if opts.RefundTimelock != 0 { tags.Refund = []*btcec.PublicKey{w.PublicKey} - tags.Locktime = ss.refundtimelock + tags.Locktime = int64(opts.RefundTimelock) } swapSettings.spendingCondition = &nut10.SpendingCondition{ Kind: nut10.P2PK, - Data: hex.EncodeToString(ss.p2pk.SerializeCompressed()), + Data: hex.EncodeToString(opts.P2PK.SerializeCompressed()), Tags: nut11.SerializeP2PKTags(tags), } } @@ -188,12 +164,12 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens( func (w *Wallet) getProofsForSending( ctx context.Context, amount uint64, - specificMint string, + fromMint string, excludeMints []string, ) (chosenTokens, uint64, error) { byMint := make(map[string]chosenTokens) for t, token := range w.Tokens { - if specificMint != "" && token.Mint != specificMint { + if fromMint != "" && token.Mint != fromMint { continue } if slices.Contains(excludeMints, token.Mint) { diff --git a/nip60/wallet.go b/nip60/wallet.go index 20759f1..af2f501 100644 --- a/nip60/wallet.go +++ b/nip60/wallet.go @@ -14,6 +14,11 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" ) +// WalletOptions contains options for loading a wallet +type WalletOptions struct { + WithHistory bool +} + type Wallet struct { sync.Mutex tokensMu sync.Mutex @@ -52,17 +57,9 @@ func LoadWallet( kr nostr.Keyer, pool *nostr.Pool, relays []string, + opts WalletOptions, ) *Wallet { - return loadWalletFromPool(ctx, kr, pool, relays, false) -} - -func LoadWalletWithHistory( - ctx context.Context, - kr nostr.Keyer, - pool *nostr.Pool, - relays []string, -) *Wallet { - return loadWalletFromPool(ctx, kr, pool, relays, true) + return loadWalletFromPool(ctx, kr, pool, relays, opts) } func loadWalletFromPool( @@ -70,7 +67,7 @@ func loadWalletFromPool( kr nostr.Keyer, pool *nostr.Pool, relays []string, - withHistory bool, + opts WalletOptions, ) *Wallet { pk, err := kr.GetPublicKey(ctx) if err != nil { @@ -78,7 +75,7 @@ func loadWalletFromPool( } kinds := []nostr.Kind{17375, 7375} - if withHistory { + if opts.WithHistory { kinds = append(kinds, 7376) } diff --git a/nip61/nip61.go b/nip61/nip61.go index c8d41a5..c7c931f 100644 --- a/nip61/nip61.go +++ b/nip61/nip61.go @@ -11,6 +11,7 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/nip60" + "github.com/btcsuite/btcd/btcec/v2" ) var NutzapsNotAccepted = errors.New("user doesn't accept nutzaps") @@ -27,7 +28,11 @@ func SendNutzap( amount uint64, message string, ) (chan nostr.PublishResult, error) { - ie := pool.QuerySingle(ctx, relays, nostr.Filter{Kinds: []nostr.Kind{10019}, Authors: []nostr.PubKey{targetUserPublickey}}, nostr.SubscriptionOptions{}) + ie := pool.QuerySingle(ctx, relays, nostr.Filter{ + Kinds: []nostr.Kind{10019}, + Authors: []nostr.PubKey{targetUserPublickey}, + }, + nostr.SubscriptionOptions{Label: "pre-nutzap"}) if ie == nil { return nil, NutzapsNotAccepted } @@ -60,9 +65,17 @@ func SendNutzap( nutzap.Tags = append(nutzap.Tags, nostr.Tag{"e", eventId.Hex()}) } + p2pk, err := btcec.ParsePubKey(append([]byte{2}, info.PublicKey[:]...)) + if err != nil { + return nil, fmt.Errorf("invalid p2pk target '%s': %w", info.PublicKey.Hex(), err) + } + // check if we have enough tokens in any of these mints for mint := range getEligibleTokensWeHave(info.Mints, w.Tokens, amount) { - proofs, _, err := w.Send(ctx, amount, nip60.WithP2PK(info.PublicKey), nip60.WithMint(mint)) + proofs, _, err := w.Send(ctx, amount, nip60.SendOptions{ + P2PK: p2pk, + SpecificSourceMint: mint, + }) if err != nil { continue } @@ -83,7 +96,9 @@ func SendNutzap( // we don't have tokens at the desired target mint, so we first have to create some for _, mint := range info.Mints { - proofs, err := w.SendExternal(ctx, mint, amount) + proofs, err := w.SendExternal(ctx, mint, amount, nip60.SendOptions{ + P2PK: p2pk, + }) if err != nil { if strings.Contains(err.Error(), "generate mint quote") { continue