it never ends.

This commit is contained in:
fiatjaf
2025-04-16 02:59:47 -03:00
parent cb0dd45a32
commit 5b8954461f
53 changed files with 396 additions and 673 deletions

View File

@@ -20,7 +20,7 @@ type HistoryEntry struct {
}
type TokenRef struct {
EventID string
EventID nostr.ID
Created bool
IsNutzap bool
}
@@ -47,7 +47,7 @@ func (h HistoryEntry) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Ev
for _, tf := range h.TokenReferences {
if tf.IsNutzap {
evt.Tags = append(evt.Tags, nostr.Tag{"e", tf.EventID, "", "redeemed"})
evt.Tags = append(evt.Tags, nostr.Tag{"e", tf.EventID.Hex(), "", "redeemed"})
continue
}
@@ -56,7 +56,7 @@ func (h HistoryEntry) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Ev
marker = "created"
}
encryptedTags = append(encryptedTags, nostr.Tag{"e", tf.EventID, "", marker})
encryptedTags = append(encryptedTags, nostr.Tag{"e", tf.EventID.Hex(), "", marker})
}
jsonb, _ := json.Marshal(encryptedTags)
@@ -129,11 +129,12 @@ func (h *HistoryEntry) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Eve
if len(tag) < 4 {
return fmt.Errorf("'e' tag must have at least 4 items")
}
if !nostr.IsValid32ByteHex(tag[1]) {
return fmt.Errorf("'e' tag has invalid event id %s", tag[1])
id, err := nostr.IDFromHex(tag[1])
if err != nil {
return fmt.Errorf("'e' tag has invalid event id %s: %w", tag[1])
}
tf := TokenRef{EventID: tag[1]}
tf := TokenRef{EventID: id}
switch tag[3] {
case "created":
tf.Created = true

View File

@@ -5,10 +5,10 @@ import (
"fmt"
"slices"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip60/client"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
)
type receiveSettings struct {

View File

@@ -6,13 +6,13 @@ import (
"fmt"
"slices"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip60/client"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/elnosh/gonuts/cashu"
"github.com/elnosh/gonuts/cashu/nuts/nut02"
"github.com/elnosh/gonuts/cashu/nuts/nut10"
"github.com/elnosh/gonuts/cashu/nuts/nut11"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/nip60/client"
)
type SendOption func(opts *sendSettings)
@@ -23,10 +23,9 @@ type sendSettings struct {
refundtimelock int64
}
func WithP2PK(pubkey string) SendOption {
func WithP2PK(pubkey nostr.PubKey) SendOption {
return func(opts *sendSettings) {
pkb, _ := hex.DecodeString(pubkey)
opts.p2pk, _ = btcec.ParsePubKey(pkb)
opts.p2pk, _ = btcec.ParsePubKey(append([]byte{2}, pubkey[:]...))
}
}
@@ -132,7 +131,7 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
mintedAt: nostr.Now(),
Mint: mintURL,
Proofs: changeProofs,
Deleted: make([]string, 0, len(usedTokenIndexes)),
Deleted: make([]nostr.ID, 0, len(usedTokenIndexes)),
event: &nostr.Event{},
}
@@ -144,7 +143,7 @@ func (w *Wallet) saveChangeAndDeleteUsedTokens(
deleteEvent := nostr.Event{
CreatedAt: nostr.Now(),
Kind: 5,
Tags: nostr.Tags{{"e", token.event.ID}, {"k", "7375"}},
Tags: nostr.Tags{{"e", token.event.ID.Hex()}, {"k", "7375"}},
}
w.kr.SignEvent(ctx, &deleteEvent)

View File

@@ -5,14 +5,14 @@ import (
"encoding/json"
"fmt"
"github.com/elnosh/gonuts/cashu"
"fiatjaf.com/nostr"
"github.com/elnosh/gonuts/cashu"
)
type Token struct {
Mint string `json:"mint"`
Proofs cashu.Proofs `json:"proofs"`
Deleted []string `json:"del,omitempty"`
Deleted []nostr.ID `json:"del,omitempty"`
mintedAt nostr.Timestamp
event *nostr.Event
@@ -20,7 +20,7 @@ type Token struct {
func (t Token) ID() string {
if t.event != nil {
return t.event.ID
return t.event.ID.Hex()
}
return "<not-published>"

View File

@@ -9,9 +9,9 @@ import (
"sync"
"time"
"fiatjaf.com/nostr"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"fiatjaf.com/nostr"
)
type Wallet struct {
@@ -19,7 +19,7 @@ type Wallet struct {
tokensMu sync.Mutex
event *nostr.Event
pendingDeletions []string // token events that should be deleted
pendingDeletions []nostr.ID // token events that should be deleted
kr nostr.Keyer
@@ -34,7 +34,7 @@ type Wallet struct {
)
// Processed, if not nil, is called every time a received event is processed
Processed func(*nostr.Event, error)
Processed func(nostr.Event, error)
// Stable is closed when we have gotten an EOSE from all relays
Stable chan struct{}
@@ -77,7 +77,7 @@ func loadWalletFromPool(
return nil
}
kinds := []int{17375, 7375}
kinds := []uint16{17375, 7375}
if withHistory {
kinds = append(kinds, 7376)
}
@@ -86,16 +86,18 @@ func loadWalletFromPool(
events := pool.SubscribeManyNotifyEOSE(
ctx,
relays,
nostr.Filter{Kinds: kinds, Authors: []string{pk}},
nostr.Filter{Kinds: kinds, Authors: []nostr.PubKey{pk}},
eoseChanE,
nostr.SubscriptionOptions{},
)
eoseChanD := make(chan struct{})
deletions := pool.SubscribeManyNotifyEOSE(
ctx,
relays,
nostr.Filter{Kinds: []int{5}, Tags: nostr.TagMap{"k": []string{"7375"}}, Authors: []string{pk}},
nostr.Filter{Kinds: []uint16{5}, Tags: nostr.TagMap{"k": []string{"7375"}}, Authors: []nostr.PubKey{pk}},
eoseChanD,
nostr.SubscriptionOptions{},
)
eoseChan := make(chan struct{})
@@ -116,7 +118,7 @@ func loadWallet(
eoseChan chan struct{},
) *Wallet {
w := &Wallet{
pendingDeletions: make([]string, 0, 128),
pendingDeletions: make([]nostr.ID, 0, 128),
kr: kr,
Stable: make(chan struct{}),
Tokens: make([]Token, 0, 128),
@@ -143,11 +145,15 @@ func loadWallet(
w.Lock()
if !eosed {
for tag := range ie.Event.Tags.FindAll("e") {
w.pendingDeletions = append(w.pendingDeletions, tag[1])
if id, err := nostr.IDFromHex(tag[1]); err == nil {
w.pendingDeletions = append(w.pendingDeletions, id)
}
}
} else {
for tag := range ie.Event.Tags.FindAll("e") {
w.removeDeletedToken(tag[1])
if id, err := nostr.IDFromHex(tag[1]); err == nil {
w.removeDeletedToken(id)
}
}
}
w.Unlock()
@@ -159,7 +165,7 @@ func loadWallet(
w.Lock()
switch ie.Event.Kind {
case 17375:
if err := w.parse(ctx, kr, ie.Event); err != nil {
if err := w.parse(ctx, kr, &ie.Event); err != nil {
if w.Processed != nil {
w.Processed(ie.Event, err)
}
@@ -169,11 +175,11 @@ func loadWallet(
// if this metadata is newer than what we had, update
if w.event == nil || ie.Event.CreatedAt > w.event.CreatedAt {
w.parse(ctx, kr, ie.Event) // this will either fail or set the new metadata
w.parse(ctx, kr, &ie.Event) // this will either fail or set the new metadata
}
case 7375: // token
token := Token{}
if err := token.parse(ctx, kr, ie.Event); err != nil {
if err := token.parse(ctx, kr, &ie.Event); err != nil {
if w.Processed != nil {
w.Processed(ie.Event, err)
}
@@ -200,7 +206,7 @@ func loadWallet(
case 7376: // history
he := HistoryEntry{}
if err := he.parse(ctx, kr, ie.Event); err != nil {
if err := he.parse(ctx, kr, &ie.Event); err != nil {
if w.Processed != nil {
w.Processed(ie.Event, err)
}
@@ -230,7 +236,7 @@ func (w *Wallet) Close() error {
return nil
}
func (w *Wallet) removeDeletedToken(eventId string) {
func (w *Wallet) removeDeletedToken(eventId nostr.ID) {
for t := len(w.Tokens) - 1; t >= 0; t-- {
token := w.Tokens[t]
if token.event != nil && token.event.ID == eventId {

View File

@@ -1,6 +1,7 @@
package nip60
import (
"bytes"
"cmp"
"context"
"fmt"
@@ -8,17 +9,17 @@ import (
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/elnosh/gonuts/cashu"
"fiatjaf.com/nostr"
"fiatjaf.com/nostr/keyer"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/elnosh/gonuts/cashu"
"github.com/stretchr/testify/require"
"golang.org/x/exp/rand"
)
func TestWallet(t *testing.T) {
ctx := context.Background()
kr, err := keyer.NewPlainKeySigner("040cbf11f24b080ad9d8669d7514d9f3b7b1f58e5a6dcb75549352b041656537")
kr, err := keyer.NewPlainKeySigner(nostr.MustSecretKeyFromHex("040cbf11f24b080ad9d8669d7514d9f3b7b1f58e5a6dcb75549352b041656537"))
if err != nil {
t.Fatal(err)
}
@@ -53,7 +54,7 @@ func TestWallet(t *testing.T) {
Amount: 100,
createdAt: nostr.Timestamp(time.Now().Add(-3 * time.Hour).Unix()),
TokenReferences: []TokenRef{
{Created: true, EventID: "645babb9051f46ddc97d960e68f82934e627f136dde7b860bf87c9213d937b58"},
{Created: true, EventID: nostr.MustIDFromHex("645babb9051f46ddc97d960e68f82934e627f136dde7b860bf87c9213d937b58")},
},
},
{
@@ -61,8 +62,8 @@ func TestWallet(t *testing.T) {
Amount: 200,
createdAt: nostr.Timestamp(time.Now().Add(-2 * time.Hour).Unix()),
TokenReferences: []TokenRef{
{Created: false, EventID: "add072ae7d7a027748e03024267a1c073f3fbc26cca468ba8630d039a7f5df72"},
{Created: true, EventID: "b8460b5589b68a0d9a017ac3784d17a0729046206aa631f7f4b763b738e36cf8"},
{Created: false, EventID: nostr.MustIDFromHex("add072ae7d7a027748e03024267a1c073f3fbc26cca468ba8630d039a7f5df72")},
{Created: true, EventID: nostr.MustIDFromHex("b8460b5589b68a0d9a017ac3784d17a0729046206aa631f7f4b763b738e36cf8")},
},
},
{
@@ -70,52 +71,52 @@ func TestWallet(t *testing.T) {
Amount: 300,
createdAt: nostr.Timestamp(time.Now().Add(-1 * time.Hour).Unix()),
TokenReferences: []TokenRef{
{Created: false, EventID: "61f86031d0ab95e9134a3ab955e96104cb1f4d610172838d28aa7ae9dc1cc924"},
{Created: true, EventID: "588b78e4af06e960434239e7367a0bedf84747d4c52ff943f5e8b7daa3e1b601", IsNutzap: true},
{Created: false, EventID: "8f14c0a4ff1bf85ccc26bf0125b9a289552f9b59bbb310b163d6a88a7bbd4ebc"},
{Created: true, EventID: "41a6f442b7c3c9e2f1e8c4835c00f17c56b3e3be4c9f7cf7bc4cdd705b1b61db", IsNutzap: true},
{Created: false, EventID: nostr.MustIDFromHex("61f86031d0ab95e9134a3ab955e96104cb1f4d610172838d28aa7ae9dc1cc924")},
{Created: true, EventID: nostr.MustIDFromHex("588b78e4af06e960434239e7367a0bedf84747d4c52ff943f5e8b7daa3e1b601"), IsNutzap: true},
{Created: false, EventID: nostr.MustIDFromHex("8f14c0a4ff1bf85ccc26bf0125b9a289552f9b59bbb310b163d6a88a7bbd4ebc")},
{Created: true, EventID: nostr.MustIDFromHex("41a6f442b7c3c9e2f1e8c4835c00f17c56b3e3be4c9f7cf7bc4cdd705b1b61db"), IsNutzap: true},
},
},
},
}
// turn everything into events
events := make([]*nostr.Event, 0, 7)
events := make([]nostr.Event, 0, 7)
// wallet metadata event
metaEvent := &nostr.Event{}
err = w.toEvent(ctx, kr, metaEvent)
metaEvent := nostr.Event{}
err = w.toEvent(ctx, kr, &metaEvent)
require.NoError(t, err)
events = append(events, metaEvent)
// token events
for i := range w.Tokens {
evt := &nostr.Event{}
evt := nostr.Event{}
evt.Tags = nostr.Tags{}
err := w.Tokens[i].toEvent(ctx, kr, evt)
err := w.Tokens[i].toEvent(ctx, kr, &evt)
require.NoError(t, err)
w.Tokens[i].event = evt
w.Tokens[i].event = &evt
events = append(events, evt)
}
// history events
for i := range w.History {
evt := &nostr.Event{}
evt := nostr.Event{}
evt.Tags = nostr.Tags{}
err := w.History[i].toEvent(ctx, kr, evt)
err := w.History[i].toEvent(ctx, kr, &evt)
require.NoError(t, err)
w.History[i].event = evt
w.History[i].event = &evt
events = append(events, evt)
}
// test different orderings
testCases := []struct {
name string
sort func([]*nostr.Event)
sort func([]nostr.Event)
}{
{
name: "random order",
sort: func(evts []*nostr.Event) {
sort: func(evts []nostr.Event) {
r := rand.New(rand.NewSource(42)) // deterministic
r.Shuffle(len(evts), func(i, j int) {
evts[i], evts[j] = evts[j], evts[i]
@@ -124,16 +125,16 @@ func TestWallet(t *testing.T) {
},
{
name: "most recent first",
sort: func(evts []*nostr.Event) {
slices.SortFunc(evts, func(a, b *nostr.Event) int {
sort: func(evts []nostr.Event) {
slices.SortFunc(evts, func(a, b nostr.Event) int {
return int(b.CreatedAt - a.CreatedAt)
})
},
},
{
name: "least recent first",
sort: func(evts []*nostr.Event) {
slices.SortFunc(evts, func(a, b *nostr.Event) int {
sort: func(evts []nostr.Event) {
slices.SortFunc(evts, func(a, b nostr.Event) int {
return int(a.CreatedAt - b.CreatedAt)
})
},
@@ -143,7 +144,7 @@ func TestWallet(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// make a copy and sort it
eventsCopy := make([]*nostr.Event, len(events))
eventsCopy := make([]nostr.Event, len(events))
copy(eventsCopy, events)
tc.sort(eventsCopy)
@@ -162,7 +163,7 @@ func TestWallet(t *testing.T) {
// load wallet from events
loaded := loadWallet(ctx, kr, evtChan, make(chan nostr.RelayEvent), eoseChan)
loaded.Processed = func(evt *nostr.Event, err error) {
loaded.Processed = func(evt nostr.Event, err error) {
fmt.Println("processed", evt.Kind, err)
}
@@ -174,8 +175,8 @@ func TestWallet(t *testing.T) {
slices.SortFunc(loaded.History, func(a, b HistoryEntry) int { return cmp.Compare(a.createdAt, b.createdAt) })
slices.SortFunc(w.History, func(a, b HistoryEntry) int { return cmp.Compare(a.createdAt, b.createdAt) })
for i := range w.History {
slices.SortFunc(loaded.History[i].TokenReferences, func(a, b TokenRef) int { return cmp.Compare(a.EventID, b.EventID) })
slices.SortFunc(w.History[i].TokenReferences, func(a, b TokenRef) int { return cmp.Compare(a.EventID, b.EventID) })
slices.SortFunc(loaded.History[i].TokenReferences, func(a, b TokenRef) int { return bytes.Compare(a.EventID[:], b.EventID[:]) })
slices.SortFunc(w.History[i].TokenReferences, func(a, b TokenRef) int { return bytes.Compare(a.EventID[:], b.EventID[:]) })
require.Equal(t, loaded.History[i], w.History[i])
}
require.ElementsMatch(t, loaded.Mints, w.Mints)