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.

This commit is contained in:
fiatjaf 2025-01-30 10:32:23 -03:00
parent f0054af4d8
commit b86d5d52bb
7 changed files with 65 additions and 51 deletions

View File

@ -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)

View File

@ -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
}

View File

@ -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

View File

@ -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]
}
}
}
}

View File

@ -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,

View File

@ -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
}
}

View File

@ -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)