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) { 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{} ss := &sendSettings{}
for _, opt := range opts { for _, opt := range opts {
opt(ss) opt(ss)

View File

@ -12,6 +12,10 @@ import (
) )
func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error { 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) token, err := cashu.DecodeToken(serializedToken)
if err != nil { if err != nil {
return err return err
@ -48,7 +52,7 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
} }
// get new proofs // 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 { if err != nil {
return err return err
} }
@ -102,15 +106,13 @@ saveproofs:
return fmt.Errorf("failed to make new token: %w", err) 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.tokensMu.Lock()
w.Tokens = append(w.Tokens, newToken) w.Tokens = append(w.Tokens, newToken)
w.tokensMu.Unlock() w.tokensMu.Unlock()
wevt := nostr.Event{}
w.toEvent(ctx, w.wl.kr, &wevt)
w.wl.Changes <- wevt
return nil return nil
} }

View File

@ -51,6 +51,10 @@ type chosenTokens struct {
} }
func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOption) (string, error) { 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{} ss := &sendSettings{}
for _, opt := range opts { for _, opt := range opts {
opt(ss) opt(ss)
@ -91,7 +95,7 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
} }
// get new proofs // 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 { if err != nil {
return "", err return "", err
} }
@ -106,10 +110,6 @@ func (w *Wallet) SendToken(ctx context.Context, amount uint64, opts ...SendOptio
return "", err return "", err
} }
wevt := nostr.Event{}
w.toEvent(ctx, w.wl.kr, &wevt)
w.wl.Changes <- wevt
return token.Serialize() return token.Serialize()
} }
@ -138,10 +138,13 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
deleteEvent := nostr.Event{ deleteEvent := nostr.Event{
CreatedAt: nostr.Now(), CreatedAt: nostr.Now(),
Kind: 5, 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.kr.SignEvent(ctx, &deleteEvent)
w.wl.Changes <- deleteEvent
w.wl.Lock()
w.wl.PublishUpdate(deleteEvent, &token, nil, nil, false)
w.wl.Unlock()
} }
continue continue
} }
@ -152,8 +155,13 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil { if err := changeToken.toEvent(ctx, w.wl.kr, w.Identifier, changeToken.event); err != nil {
return fmt.Errorf("failed to make change token: %w", err) 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.Tokens = append(updatedTokens, changeToken)
w.tokensMu.Unlock()
} }
return nil return nil

View File

@ -23,8 +23,14 @@ type WalletStash struct {
kr nostr.Keyer kr nostr.Keyer
// Changes emits a stream of events that must be published whenever something changes // PublishUpdate must be set to a function that publishes event to the user relays
Changes chan nostr.Event 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 emits an error or nil every time an event is processed
Processed chan error Processed chan error
@ -94,7 +100,6 @@ func loadStash(
pendingHistory: make(map[string][]HistoryEntry), pendingHistory: make(map[string][]HistoryEntry),
pendingDeletions: make([]string, 0, 128), pendingDeletions: make([]string, 0, 128),
kr: kr, kr: kr,
Changes: make(chan nostr.Event),
Processed: make(chan error), Processed: make(chan error),
Stable: make(chan struct{}), Stable: make(chan struct{}),
} }
@ -258,19 +263,6 @@ func loadStash(
return wl 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 { func (wl *WalletStash) EnsureWallet(id string) *Wallet {
wl.Lock() wl.Lock()
defer wl.Unlock() 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 mustSignOutputs bool
} }
func (w *Wallet) SwapProofs( func (w *Wallet) swapProofs(
ctx context.Context, ctx context.Context,
mint string, mint string,
proofs cashu.Proofs, proofs cashu.Proofs,

View File

@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"sync" "sync"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
@ -26,7 +25,6 @@ type Wallet struct {
Tokens []Token Tokens []Token
History []HistoryEntry History []HistoryEntry
temporaryBalance uint64
tokensMu sync.Mutex tokensMu sync.Mutex
event *nostr.Event event *nostr.Event
@ -60,7 +58,7 @@ func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event)
evt.Content, err = kr.Encrypt( evt.Content, err = kr.Encrypt(
ctx, ctx,
fmt.Sprintf(`[["balance","%d","sat"],["privkey","%x"]]`, w.Balance(), w.PrivateKey.Serialize()), fmt.Sprintf(`[["privkey","%x"]]`, w.PrivateKey.Serialize()),
pk, pk,
) )
if err != nil { 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.PrivateKey = secp256k1.PrivKeyFromBytes(skb)
w.PublicKey = w.PrivateKey.PubKey() 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 { if stash1 == nil {
t.Fatal("failed to load stash 1") 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 // setup second wallet
sk2 := os.Getenv("NIP60_SECRET_KEY_2") sk2 := os.Getenv("NIP60_SECRET_KEY_2")
@ -50,15 +53,14 @@ func TestWalletTransfer(t *testing.T) {
if stash2 == nil { if stash2 == nil {
t.Fatal("failed to load stash 2") 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 // handle events from both stashes
go func() { go func() {
for { for {
select { select {
case evt := <-stash1.Changes:
pool.PublishMany(ctx, testRelays, evt)
case evt := <-stash2.Changes:
pool.PublishMany(ctx, testRelays, evt)
case err := <-stash1.Processed: case err := <-stash1.Processed:
if err != nil { if err != nil {
t.Errorf("stash1 processing error: %v", err) t.Errorf("stash1 processing error: %v", err)