nip60/nip61: update to latest nip changes.
(a single default wallet, always default to sats, no names etc)
This commit is contained in:
@@ -2,114 +2,208 @@ package nip60
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/elnosh/gonuts/cashu"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/keyer"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/exp/rand"
|
||||
)
|
||||
|
||||
var testRelays = []string{
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band",
|
||||
}
|
||||
|
||||
func TestWalletTransfer(t *testing.T) {
|
||||
func TestWallet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// setup first wallet
|
||||
sk1 := os.Getenv("NIP60_SECRET_KEY_1")
|
||||
if sk1 == "" {
|
||||
t.Skip("NIP60_SECRET_KEY_1 not set")
|
||||
}
|
||||
kr1, err := keyer.NewPlainKeySigner(sk1)
|
||||
kr, err := keyer.NewPlainKeySigner("040cbf11f24b080ad9d8669d7514d9f3b7b1f58e5a6dcb75549352b041656537")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pool := nostr.NewSimplePool(ctx)
|
||||
stash1 := LoadStash(ctx, kr1, pool, testRelays)
|
||||
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)
|
||||
privateKey, _ := btcec.NewPrivateKey()
|
||||
|
||||
w := &Wallet{
|
||||
kr: kr,
|
||||
PrivateKey: privateKey,
|
||||
PublicKey: privateKey.PubKey(),
|
||||
Mints: []string{"https://mint1.com", "https://mint2.com"},
|
||||
Tokens: []Token{
|
||||
{
|
||||
Mint: "https://mint1.com",
|
||||
Proofs: cashu.Proofs{{Amount: 100}},
|
||||
mintedAt: nostr.Timestamp(time.Now().Add(-3 * time.Hour).Unix()),
|
||||
},
|
||||
{
|
||||
Mint: "https://mint2.com",
|
||||
Proofs: cashu.Proofs{{Amount: 200}},
|
||||
mintedAt: nostr.Timestamp(time.Now().Add(-2 * time.Hour).Unix()),
|
||||
},
|
||||
{
|
||||
Mint: "https://mint1.com",
|
||||
Proofs: cashu.Proofs{{Amount: 300}},
|
||||
mintedAt: nostr.Timestamp(time.Now().Add(-1 * time.Hour).Unix()),
|
||||
},
|
||||
},
|
||||
History: []HistoryEntry{
|
||||
{
|
||||
In: true,
|
||||
Amount: 100,
|
||||
createdAt: nostr.Timestamp(time.Now().Add(-3 * time.Hour).Unix()),
|
||||
TokenReferences: []TokenRef{
|
||||
{Created: true, EventID: "645babb9051f46ddc97d960e68f82934e627f136dde7b860bf87c9213d937b58"},
|
||||
},
|
||||
},
|
||||
{
|
||||
In: true,
|
||||
Amount: 200,
|
||||
createdAt: nostr.Timestamp(time.Now().Add(-2 * time.Hour).Unix()),
|
||||
TokenReferences: []TokenRef{
|
||||
{Created: false, EventID: "add072ae7d7a027748e03024267a1c073f3fbc26cca468ba8630d039a7f5df72"},
|
||||
{Created: true, EventID: "b8460b5589b68a0d9a017ac3784d17a0729046206aa631f7f4b763b738e36cf8"},
|
||||
},
|
||||
},
|
||||
{
|
||||
In: true,
|
||||
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},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// setup second wallet
|
||||
sk2 := os.Getenv("NIP60_SECRET_KEY_2")
|
||||
if sk2 == "" {
|
||||
t.Skip("NIP60_SECRET_KEY_2 not set")
|
||||
}
|
||||
kr2, err := keyer.NewPlainKeySigner(sk2)
|
||||
if err != nil {
|
||||
// turn everything into events
|
||||
events := make([]*nostr.Event, 0, 7)
|
||||
|
||||
// wallet metadata event
|
||||
metaEvent := &nostr.Event{}
|
||||
if err := w.toEvent(ctx, kr, metaEvent); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
events = append(events, metaEvent)
|
||||
|
||||
stash2 := LoadStash(ctx, kr2, pool, testRelays)
|
||||
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)
|
||||
// token events
|
||||
for i := range w.Tokens {
|
||||
evt := &nostr.Event{}
|
||||
evt.Tags = nostr.Tags{}
|
||||
if err := w.Tokens[i].toEvent(ctx, kr, evt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Tokens[i].event = evt
|
||||
events = append(events, evt)
|
||||
}
|
||||
|
||||
// wait for initial load
|
||||
select {
|
||||
case <-stash1.Stable:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timeout waiting for stash 1 to load")
|
||||
// history events
|
||||
for i := range w.History {
|
||||
evt := &nostr.Event{}
|
||||
evt.Tags = nostr.Tags{}
|
||||
if err := w.History[i].toEvent(ctx, kr, evt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.History[i].event = evt
|
||||
events = append(events, evt)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-stash2.Stable:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("timeout waiting for stash 2 to load")
|
||||
// test different orderings
|
||||
testCases := []struct {
|
||||
name string
|
||||
sort func([]*nostr.Event)
|
||||
}{
|
||||
{
|
||||
name: "random order",
|
||||
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]
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "most recent first",
|
||||
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 {
|
||||
return int(a.CreatedAt - b.CreatedAt)
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ensure wallets exist and have tokens
|
||||
w1 := stash1.EnsureWallet(ctx, "test")
|
||||
require.Greater(t, w1.Balance(), uint64(0), "wallet 1 has no balance")
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// make a copy and sort it
|
||||
eventsCopy := make([]*nostr.Event, len(events))
|
||||
copy(eventsCopy, events)
|
||||
tc.sort(eventsCopy)
|
||||
|
||||
w2 := stash2.EnsureWallet(ctx, "test")
|
||||
initialBalance1 := w1.Balance()
|
||||
initialBalance2 := w2.Balance()
|
||||
// create relay event channel
|
||||
evtChan := make(chan nostr.RelayEvent)
|
||||
eoseChan := make(chan struct{})
|
||||
|
||||
t.Logf("initial balances: w1=%d w2=%d", initialBalance1, initialBalance2)
|
||||
// send events in a goroutine
|
||||
go func() {
|
||||
for _, evt := range eventsCopy {
|
||||
evtChan <- nostr.RelayEvent{Event: evt}
|
||||
}
|
||||
close(eoseChan)
|
||||
close(evtChan)
|
||||
}()
|
||||
|
||||
// send half of wallet 1's balance to wallet 2
|
||||
pk2, err := kr2.GetPublicKey(ctx)
|
||||
require.NoError(t, err)
|
||||
// load wallet from events
|
||||
loaded := loadWallet(ctx, kr, evtChan, eoseChan)
|
||||
loaded.Processed = func(evt *nostr.Event, err error) {
|
||||
fmt.Println("processed", evt, err)
|
||||
}
|
||||
|
||||
halfBalance := initialBalance1 / 2
|
||||
proofs, mint, err := w1.Send(ctx, halfBalance, WithP2PK(pk2))
|
||||
require.NoError(t, err)
|
||||
<-loaded.Stable
|
||||
|
||||
// receive token in wallet 2
|
||||
err = w2.Receive(ctx, proofs, mint)
|
||||
require.NoError(t, err)
|
||||
// check if loaded wallet matches original
|
||||
if len(loaded.Tokens) != len(w.Tokens) {
|
||||
t.Errorf("token count mismatch: %d != %d", len(loaded.Tokens), len(w.Tokens))
|
||||
}
|
||||
if len(loaded.History) != len(w.History) {
|
||||
t.Errorf("history count mismatch: %d != %d", len(loaded.History), len(w.History))
|
||||
}
|
||||
|
||||
// verify balances
|
||||
require.Equal(t, initialBalance1-halfBalance, w1.Balance(), "wallet 1 balance wrong after send")
|
||||
require.Equal(t, initialBalance2+halfBalance, w2.Balance(), "wallet 2 balance wrong after receive")
|
||||
// check tokens are equal regardless of order
|
||||
for _, ta := range loaded.Tokens {
|
||||
found := false
|
||||
for _, tb := range w.Tokens {
|
||||
if ta.Mint == tb.Mint && ta.Proofs[0].Amount == tb.Proofs[0].Amount {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("token not found in loaded wallet: %v", ta)
|
||||
}
|
||||
}
|
||||
|
||||
// now send it back
|
||||
pk1, err := kr1.GetPublicKey(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
proofs, mint, err = w2.Send(ctx, halfBalance, WithP2PK(pk1))
|
||||
require.NoError(t, err)
|
||||
|
||||
// receive token back in wallet 1
|
||||
err = w1.Receive(ctx, proofs, mint)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify final balances match initial
|
||||
require.Equal(t, initialBalance1, w1.Balance(), "wallet 1 final balance wrong")
|
||||
require.Equal(t, initialBalance2, w2.Balance(), "wallet 2 final balance wrong")
|
||||
|
||||
t.Logf("final balances: w1=%d w2=%d", w1.Balance(), w2.Balance())
|
||||
// check history entries are equal regardless of order
|
||||
for _, ha := range loaded.History {
|
||||
found := false
|
||||
for _, hb := range w.History {
|
||||
if ha.In == hb.In && ha.Amount == hb.Amount {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("history entry not found in loaded wallet: %v", ha)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user