From b86d5d52bbd5b874830f7a5c3d298a5e47ee0f56 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Thu, 30 Jan 2025 10:32:23 -0300 Subject: [PATCH] nip60: Changes channel is a PublishUpdate hook now that must be set, to prevent unpublished updates -- and also now updates have more metadata so the client can display more info about them. --- nip60/pay.go | 4 ++++ nip60/receive.go | 14 ++++++++------ nip60/send.go | 24 ++++++++++++++++-------- nip60/stash.go | 44 ++++++++++++++++++++++++++++---------------- nip60/swap.go | 2 +- nip60/wallet.go | 18 ++---------------- nip60/wallet_test.go | 10 ++++++---- 7 files changed, 65 insertions(+), 51 deletions(-) diff --git a/nip60/pay.go b/nip60/pay.go index f611638..bcdfbc4 100644 --- a/nip60/pay.go +++ b/nip60/pay.go @@ -11,6 +11,10 @@ import ( ) func (w *Wallet) PayBolt11(ctx context.Context, invoice string, opts ...SendOption) (string, error) { + if w.wl.PublishUpdate == nil { + return "", fmt.Errorf("can't do write operations: missing PublishUpdate function") + } + ss := &sendSettings{} for _, opt := range opts { opt(ss) diff --git a/nip60/receive.go b/nip60/receive.go index 4c2a59b..c56a107 100644 --- a/nip60/receive.go +++ b/nip60/receive.go @@ -12,6 +12,10 @@ import ( ) func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error { + if w.wl.PublishUpdate == nil { + return fmt.Errorf("can't do write operations: missing PublishUpdate function") + } + token, err := cashu.DecodeToken(serializedToken) if err != nil { return err @@ -48,7 +52,7 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error } // get new proofs - newProofs, _, err := w.SwapProofs(ctx, source, proofs, proofs.Amount(), swapOpts...) + newProofs, _, err := w.swapProofs(ctx, source, proofs, proofs.Amount(), swapOpts...) if err != nil { return err } @@ -102,15 +106,13 @@ saveproofs: return fmt.Errorf("failed to make new token: %w", err) } - w.wl.Changes <- *newToken.event + w.wl.Lock() + w.wl.PublishUpdate(*newToken.event, nil, &newToken, nil, false) + w.wl.Unlock() w.tokensMu.Lock() w.Tokens = append(w.Tokens, newToken) w.tokensMu.Unlock() - wevt := nostr.Event{} - w.toEvent(ctx, w.wl.kr, &wevt) - w.wl.Changes <- wevt - return nil } diff --git a/nip60/send.go b/nip60/send.go index 1dec340..8e85a8f 100644 --- a/nip60/send.go +++ b/nip60/send.go @@ -51,6 +51,10 @@ type chosenTokens struct { } func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOption) (string, error) { + if w.wl.PublishUpdate == nil { + return "", fmt.Errorf("can't do write operations: missing PublishUpdate function") + } + ss := &sendSettings{} for _, opt := range opts { opt(ss) @@ -91,7 +95,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio } // get new proofs - proofsToSend, changeProofs, err := w.SwapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...) + proofsToSend, changeProofs, err := w.swapProofs(ctx, chosen.mint, chosen.proofs, amount, swapOpts...) if err != nil { return "", err } @@ -106,10 +110,6 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio return "", err } - wevt := nostr.Event{} - w.toEvent(ctx, w.wl.kr, &wevt) - w.wl.Changes <- wevt - return token.Serialize() } @@ -138,10 +138,13 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens( deleteEvent := nostr.Event{ CreatedAt: nostr.Now(), Kind: 5, - Tags: nostr.Tags{{"e", token.event.ID}, {"k", "7375"}, {"alt", "deleting"}}, + Tags: nostr.Tags{{"e", token.event.ID}, {"k", "7375"}}, } w.wl.kr.SignEvent(ctx, &deleteEvent) - w.wl.Changes <- deleteEvent + + w.wl.Lock() + w.wl.PublishUpdate(deleteEvent, &token, nil, nil, false) + w.wl.Unlock() } continue } @@ -152,8 +155,13 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens( if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil { return fmt.Errorf("failed to make change token: %w", err) } - w.wl.Changes <- *changeToken.event + w.wl.Lock() + w.wl.PublishUpdate(*changeToken.event, nil, nil, &changeToken, false) + w.wl.Unlock() + + w.tokensMu.Lock() w.Tokens = append(updatedTokens, changeToken) + w.tokensMu.Unlock() } return nil diff --git a/nip60/stash.go b/nip60/stash.go index d709b59..ece7d93 100644 --- a/nip60/stash.go +++ b/nip60/stash.go @@ -23,8 +23,14 @@ type WalletStash struct { kr nostr.Keyer - // Changes emits a stream of events that must be published whenever something changes - Changes chan nostr.Event + // PublishUpdate must be set to a function that publishes event to the user relays + PublishUpdate func( + event nostr.Event, + deleted *Token, + received *Token, + change *Token, + isHistory bool, + ) // Processed emits an error or nil every time an event is processed Processed chan error @@ -94,7 +100,6 @@ func loadStash( pendingHistory: make(map[string][]HistoryEntry), pendingDeletions: make([]string, 0, 128), kr: kr, - Changes: make(chan nostr.Event), Processed: make(chan error), Stable: make(chan struct{}), } @@ -258,19 +263,6 @@ func loadStash( return wl } -func (wl *WalletStash) removeDeletedToken(eventId string) { - for _, w := range wl.wallets { - for t := len(w.Tokens) - 1; t >= 0; t-- { - token := w.Tokens[t] - if token.event != nil && token.event.ID == eventId { - // swap delete - w.Tokens[t] = w.Tokens[len(w.Tokens)-1] - w.Tokens = w.Tokens[0 : len(w.Tokens)-1] - } - } - } -} - func (wl *WalletStash) EnsureWallet(id string) *Wallet { wl.Lock() defer wl.Unlock() @@ -305,3 +297,23 @@ func (wl *WalletStash) Wallets() iter.Seq[*Wallet] { } } } + +// Close waits for pending operations to end +func (wl *WalletStash) Close() error { + wl.Lock() + defer wl.Unlock() + return nil +} + +func (wl *WalletStash) removeDeletedToken(eventId string) { + for _, w := range wl.wallets { + for t := len(w.Tokens) - 1; t >= 0; t-- { + token := w.Tokens[t] + if token.event != nil && token.event.ID == eventId { + // swap delete + w.Tokens[t] = w.Tokens[len(w.Tokens)-1] + w.Tokens = w.Tokens[0 : len(w.Tokens)-1] + } + } + } +} diff --git a/nip60/swap.go b/nip60/swap.go index 0e0519d..dd34415 100644 --- a/nip60/swap.go +++ b/nip60/swap.go @@ -32,7 +32,7 @@ type swapSettings struct { mustSignOutputs bool } -func (w *Wallet) SwapProofs( +func (w *Wallet) swapProofs( ctx context.Context, mint string, proofs cashu.Proofs, diff --git a/nip60/wallet.go b/nip60/wallet.go index 80c5102..ff5b037 100644 --- a/nip60/wallet.go +++ b/nip60/wallet.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "strconv" "sync" "github.com/btcsuite/btcd/btcec/v2" @@ -26,8 +25,7 @@ type Wallet struct { Tokens []Token History []HistoryEntry - temporaryBalance uint64 - tokensMu sync.Mutex + tokensMu sync.Mutex event *nostr.Event tokensPendingDeletion []string @@ -60,7 +58,7 @@ func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) evt.Content, err = kr.Encrypt( ctx, - fmt.Sprintf(`[["balance","%d","sat"],["privkey","%x"]]`, w.Balance(), w.PrivateKey.Serialize()), + fmt.Sprintf(`[["privkey","%x"]]`, w.PrivateKey.Serialize()), pk, ) if err != nil { @@ -144,18 +142,6 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er } w.PrivateKey = secp256k1.PrivKeyFromBytes(skb) w.PublicKey = w.PrivateKey.PubKey() - case "balance": - if len(tag) < 3 { - return fmt.Errorf("'balance' tag must have at least 3 items") - } - if tag[2] != "sat" { - return fmt.Errorf("only 'sat' wallets are supported") - } - v, err := strconv.ParseUint(tag[1], 10, 64) - if err != nil { - return fmt.Errorf("invalid 'balance' %s: %w", tag[1], err) - } - w.temporaryBalance = v } } diff --git a/nip60/wallet_test.go b/nip60/wallet_test.go index 2278d0c..46cb417 100644 --- a/nip60/wallet_test.go +++ b/nip60/wallet_test.go @@ -35,6 +35,9 @@ func TestWalletTransfer(t *testing.T) { if stash1 == nil { t.Fatal("failed to load stash 1") } + stash1.PublishUpdate = func(event nostr.Event, deleted, received, change *Token, isHistory bool) { + pool.PublishMany(ctx, testRelays, event) + } // setup second wallet sk2 := os.Getenv("NIP60_SECRET_KEY_2") @@ -50,15 +53,14 @@ func TestWalletTransfer(t *testing.T) { if stash2 == nil { t.Fatal("failed to load stash 2") } + stash2.PublishUpdate = func(event nostr.Event, deleted, received, change *Token, isHistory bool) { + pool.PublishMany(ctx, testRelays, event) + } // handle events from both stashes go func() { for { select { - case evt := <-stash1.Changes: - pool.PublishMany(ctx, testRelays, evt) - case evt := <-stash2.Changes: - pool.PublishMany(ctx, testRelays, evt) case err := <-stash1.Processed: if err != nil { t.Errorf("stash1 processing error: %v", err)