nip60: make it work with emitting events to be published dynamically and stuff.

This commit is contained in:
fiatjaf 2025-01-28 19:11:18 -03:00
parent 07b9b3e439
commit 2244740f61
6 changed files with 304 additions and 202 deletions

View File

@ -98,11 +98,33 @@ func TestWalletRoundtrip(t *testing.T) {
}
// convert wallets to events
events1, err := wallet1.ToPublishableEvents(ctx, kr, false)
require.NoError(t, err)
events := [][]nostr.Event{
make([]nostr.Event, 0, 4),
make([]nostr.Event, 0, 4),
}
events2, err := wallet2.ToPublishableEvents(ctx, kr, false)
require.NoError(t, err)
for i, w := range []*Wallet{&wallet1, &wallet2} {
evt := nostr.Event{}
err := w.toEvent(ctx, kr, &evt)
require.NoError(t, err)
events[i] = append(events[i], evt)
for _, token := range w.Tokens {
evt = nostr.Event{}
err = token.toEvent(ctx, kr, w.Identifier, &evt)
require.NoError(t, err)
events[i] = append(events[i], evt)
}
for _, he := range w.History {
evt = nostr.Event{}
err = he.toEvent(ctx, kr, w.Identifier, &evt)
require.NoError(t, err)
events[i] = append(events[i], evt)
}
}
events1, events2 := events[0], events[1]
// combine all events
allEvents := append(events1, events2...)
@ -127,13 +149,12 @@ func TestWalletRoundtrip(t *testing.T) {
}()
// load wallets from events
errorChan := make(chan error)
walletStash := LoadStash(ctx, kr, eventChan, errorChan)
walletStash := loadStash(ctx, kr, eventChan, make(chan struct{}))
var errorChanErr error
go func() {
for {
errorChanErr = <-errorChan
errorChanErr = <-walletStash.Processed
if errorChanErr != nil {
return
}

View File

@ -89,13 +89,25 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
}
saveproofs:
w.tokensMu.Lock()
w.Tokens = append(w.Tokens, Token{
newToken := Token{
Mint: newMint,
Proofs: newProofs,
mintedAt: nostr.Now(),
})
event: &nostr.Event{},
}
if err := newToken.toEvent(ctx, w.wl.kr, w.Identifier, newToken.event); err != nil {
return fmt.Errorf("failed to make new token: %w", err)
}
w.wl.Changes <- *newToken.event
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

@ -130,18 +130,40 @@ found:
}
// delete spent tokens and save our change
newTokens := make([]Token, 0, len(w.Tokens))
for i, token := range w.Tokens {
if slices.Contains(target.tokenIndexes, i) {
continue
}
newTokens = append(newTokens, token)
}
w.Tokens = append(newTokens, Token{
updatedTokens := make([]Token, 0, len(w.Tokens))
changeToken := Token{
mintedAt: nostr.Now(),
Mint: target.mint,
Proofs: changeProofs,
})
Deleted: make([]string, 0, len(target.tokenIndexes)),
event: &nostr.Event{},
}
for i, token := range w.Tokens {
if slices.Contains(target.tokenIndexes, i) {
if token.event != nil {
token.Deleted = append(token.Deleted, token.event.ID)
deleteEvent := nostr.Event{
CreatedAt: nostr.Now(),
Kind: 5,
Tags: nostr.Tags{{"e", token.event.ID}},
}
w.wl.kr.SignEvent(ctx, &deleteEvent)
w.wl.Changes <- deleteEvent
}
continue
}
updatedTokens = append(updatedTokens, token)
}
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.Tokens = append(updatedTokens, changeToken)
// serialize token we're sending out
token, err := cashu.NewTokenV4(proofsToSend, target.mint, cashu.Sat, true)
@ -149,5 +171,9 @@ found:
return "", err
}
wevt := nostr.Event{}
w.toEvent(ctx, w.wl.kr, &wevt)
w.wl.Changes <- wevt
return token.Serialize()
}

View File

@ -15,8 +15,213 @@ type WalletStash struct {
sync.Mutex
wallets map[string]*Wallet
pendingTokens map[string][]Token // tokens not yet assigned to a wallet
pendingHistory map[string][]HistoryEntry // history entries not yet assigned to a wallet
pendingHistory map[string][]HistoryEntry // history entries not yet assigned to a wallet
pendingTokens map[string][]Token // tokens not yet assigned to a wallet
pendingDeletions []string // token events that should be deleted
kr nostr.Keyer
// Changes emits a stream of events that must be published whenever something changes
Changes chan nostr.Event
// Processed emits an error or nil every time an event is processed
Processed chan error
// Stable is closed when we have gotten an EOSE from all relays
Stable chan struct{}
}
func LoadStash(
ctx context.Context,
kr nostr.Keyer,
pool *nostr.SimplePool,
relays []string,
) *WalletStash {
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return nil
}
eoseChan := make(chan struct{})
events := pool.SubManyNotifyEOSE(
ctx,
relays,
nostr.Filters{{Kinds: []int{5, 37375, 7375, 7376}, Authors: []string{pk}}},
eoseChan,
)
return loadStash(ctx, kr, events, eoseChan)
}
func loadStash(
ctx context.Context,
kr nostr.Keyer,
events chan nostr.RelayEvent,
eoseChan chan struct{},
) *WalletStash {
wl := &WalletStash{
wallets: make(map[string]*Wallet, 1),
pendingTokens: make(map[string][]Token),
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{}),
}
eosed := false
go func() {
<-eoseChan
eosed = true
// check all pending deletions and delete stuff locally
for _, id := range wl.pendingDeletions {
wl.removeDeletedToken(id)
}
wl.pendingDeletions = nil
close(wl.Stable)
}()
go func() {
for ie := range events {
wl.Lock()
switch ie.Event.Kind {
case 5:
if !eosed {
for _, tag := range ie.Event.Tags.All([]string{"e", ""}) {
wl.pendingDeletions = append(wl.pendingDeletions, tag[1])
}
} else {
for _, tag := range ie.Event.Tags.All([]string{"e", ""}) {
wl.removeDeletedToken(tag[1])
}
}
case 37375:
wallet := &Wallet{
wl: wl,
}
if err := wallet.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s failed: %w", ie.Event, err)
continue
}
// if we already have a wallet with this identifier then we must be careful
if curr, ok := wl.wallets[wallet.Identifier]; ok {
// if the metadata we have is newer ignore this event
if curr.event.CreatedAt > ie.Event.CreatedAt {
wl.Unlock()
continue
}
// otherwise transfer history events and tokens to the new wallet object
wallet.Tokens = curr.Tokens
wallet.History = curr.History
}
// get all pending stuff and assign them to this, then delete the pending stuff
for _, he := range wl.pendingHistory[wallet.Identifier] {
wallet.History = append(wallet.History, he)
}
delete(wl.pendingHistory, wallet.Identifier)
wallet.tokensMu.Lock()
for _, token := range wl.pendingTokens[wallet.Identifier] {
wallet.Tokens = append(wallet.Tokens, token)
}
delete(wl.pendingTokens, wallet.Identifier)
wallet.tokensMu.Unlock()
// finally save the new wallet object
wl.wallets[wallet.Identifier] = wallet
case 7375: // token
ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
continue
}
token := Token{}
if err := token.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s failed: %w", ie.Event, err)
continue
}
if wallet, ok := wl.wallets[spl[2]]; ok {
wallet.tokensMu.Lock()
wallet.Tokens = append(wallet.Tokens, token)
wallet.tokensMu.Unlock()
} else {
wl.pendingTokens[spl[2]] = append(wl.pendingTokens[spl[2]], token)
}
// keep track tokens that were deleted by this, if they exist
if !eosed {
for _, del := range token.Deleted {
wl.pendingDeletions = append(wl.pendingDeletions, del)
}
} else {
for _, del := range token.Deleted {
wl.removeDeletedToken(del)
}
}
case 7376: // history
ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
continue
}
he := HistoryEntry{}
if err := he.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
wl.Processed <- fmt.Errorf("event %s failed: %w", ie.Event, err)
continue
}
if wallet, ok := wl.wallets[spl[2]]; ok {
wallet.History = append(wallet.History, he)
} else {
wl.pendingHistory[spl[2]] = append(wl.pendingHistory[spl[2]], he)
}
}
wl.Unlock()
}
}()
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 {
@ -35,6 +240,7 @@ func (wl *WalletStash) EnsureWallet(id string) *Wallet {
Identifier: id,
PrivateKey: sk,
PublicKey: sk.PubKey(),
wl: wl,
}
wl.wallets[id] = w
return w
@ -52,125 +258,3 @@ func (wl *WalletStash) Wallets() iter.Seq[*Wallet] {
}
}
}
func NewStash() *WalletStash {
return &WalletStash{
wallets: make(map[string]*Wallet, 1),
pendingTokens: make(map[string][]Token),
pendingHistory: make(map[string][]HistoryEntry),
}
}
func LoadStash(
ctx context.Context,
kr nostr.Keyer,
events <-chan nostr.RelayEvent,
errors chan<- error,
) *WalletStash {
wl := &WalletStash{
wallets: make(map[string]*Wallet, 1),
pendingTokens: make(map[string][]Token),
pendingHistory: make(map[string][]HistoryEntry),
}
go func() {
for ie := range events {
wl.Lock()
switch ie.Event.Kind {
case 37375:
wallet := &Wallet{}
if err := wallet.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
}
continue
}
for _, he := range wl.pendingHistory[wallet.Identifier] {
wallet.History = append(wallet.History, he)
}
wallet.tokensMu.Lock()
for _, token := range wl.pendingTokens[wallet.Identifier] {
wallet.Tokens = append(wallet.Tokens, token)
}
wallet.tokensMu.Unlock()
wl.wallets[wallet.Identifier] = wallet
case 7375: // token
ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
}
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
}
continue
}
token := Token{}
if err := token.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
}
continue
}
if wallet, ok := wl.wallets[spl[2]]; ok {
wallet.tokensMu.Lock()
wallet.Tokens = append(wallet.Tokens, token)
wallet.tokensMu.Unlock()
} else {
wl.pendingTokens[spl[2]] = append(wl.pendingTokens[spl[2]], token)
}
case 7376: // history
ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
}
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
}
continue
}
he := HistoryEntry{}
if err := he.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
}
continue
}
if wallet, ok := wl.wallets[spl[2]]; ok {
wallet.History = append(wallet.History, he)
} else {
wl.pendingHistory[spl[2]] = append(wl.pendingHistory[spl[2]], he)
}
}
wl.Unlock()
}
}()
return wl
}

View File

@ -10,8 +10,9 @@ import (
)
type Token struct {
Mint string `json:"mint"`
Proofs cashu.Proofs `json:"proofs"`
Mint string `json:"mint"`
Proofs cashu.Proofs `json:"proofs"`
Deleted []string `json:"del,omitempty"`
mintedAt nostr.Timestamp
event *nostr.Event

View File

@ -14,6 +14,8 @@ import (
)
type Wallet struct {
wl *WalletStash
Identifier string
Description string
Name string
@ -26,6 +28,9 @@ type Wallet struct {
temporaryBalance uint64
tokensMu sync.Mutex
event *nostr.Event
tokensPendingDeletion []string
}
func (w *Wallet) Balance() uint64 {
@ -43,20 +48,14 @@ func (w *Wallet) DisplayName() string {
return w.Identifier
}
func (w *Wallet) ToPublishableEvents(
ctx context.Context,
kr nostr.Keyer,
skipExisting bool,
) ([]nostr.Event, error) {
evt := nostr.Event{
CreatedAt: nostr.Now(),
Kind: 37375,
Tags: make(nostr.Tags, 0, 7),
}
func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
evt.CreatedAt = nostr.Now()
evt.Kind = 37375
evt.Tags = make(nostr.Tags, 0, 7)
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return nil, err
return err
}
evt.Content, err = kr.Encrypt(
@ -65,7 +64,7 @@ func (w *Wallet) ToPublishableEvents(
pk,
)
if err != nil {
return nil, err
return err
}
evt.Tags = append(evt.Tags,
@ -85,64 +84,23 @@ func (w *Wallet) ToPublishableEvents(
evt.Tags = append(evt.Tags, nostr.Tag{"mint", mint})
}
err = kr.SignEvent(ctx, &evt)
err = kr.SignEvent(ctx, evt)
if err != nil {
return nil, err
return err
}
events := make([]nostr.Event, 0, 1+len(w.Tokens))
events = append(events, evt)
w.tokensMu.Lock()
for _, t := range w.Tokens {
var evt nostr.Event
if t.event != nil {
if skipExisting {
continue
}
evt = *t.event
} else {
err := t.toEvent(ctx, kr, w.Identifier, &evt)
if err != nil {
return nil, err
}
}
events = append(events, evt)
}
w.tokensMu.Unlock()
for _, h := range w.History {
var evt nostr.Event
if h.event != nil {
if skipExisting {
continue
}
evt = *h.event
} else {
err := h.toEvent(ctx, kr, w.Identifier, &evt)
if err != nil {
return nil, err
}
}
events = append(events, evt)
}
return events, nil
return nil
}
func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
w.Tokens = make([]Token, 0, 128)
w.History = make([]HistoryEntry, 0, 128)
w.event = evt
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
jsonb, err := kr.Decrypt(ctx, evt.Content, pk)
if err != nil {
return err