mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-11-18 18:16:50 +01:00
nip60/nip61: update to latest nip changes.
(a single default wallet, always default to sats, no names etc)
This commit is contained in:
287
nip60/wallet.go
287
nip60/wallet.go
@@ -5,7 +5,9 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
@@ -13,22 +15,210 @@ import (
|
||||
)
|
||||
|
||||
type Wallet struct {
|
||||
wl *WalletStash
|
||||
|
||||
Identifier string
|
||||
Description string
|
||||
Name string
|
||||
PrivateKey *btcec.PrivateKey
|
||||
PublicKey *btcec.PublicKey
|
||||
Relays []string
|
||||
Mints []string
|
||||
Tokens []Token
|
||||
History []HistoryEntry
|
||||
|
||||
sync.Mutex
|
||||
tokensMu sync.Mutex
|
||||
event *nostr.Event
|
||||
|
||||
event *nostr.Event
|
||||
tokensPendingDeletion []string
|
||||
pendingDeletions []string // token events that should be deleted
|
||||
|
||||
kr nostr.Keyer
|
||||
|
||||
// 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, if not nil, is called every time a received event is processed
|
||||
Processed func(*nostr.Event, error)
|
||||
|
||||
// Stable is closed when we have gotten an EOSE from all relays
|
||||
Stable chan struct{}
|
||||
|
||||
// properties that come in events
|
||||
PrivateKey *btcec.PrivateKey
|
||||
PublicKey *btcec.PublicKey
|
||||
Mints []string
|
||||
Tokens []Token
|
||||
History []HistoryEntry
|
||||
}
|
||||
|
||||
func LoadWallet(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
) *Wallet {
|
||||
return loadWalletFromPool(ctx, kr, pool, relays, false)
|
||||
}
|
||||
|
||||
func LoadWalletWithHistory(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
) *Wallet {
|
||||
return loadWalletFromPool(ctx, kr, pool, relays, true)
|
||||
}
|
||||
|
||||
func loadWalletFromPool(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
pool *nostr.SimplePool,
|
||||
relays []string,
|
||||
withHistory bool,
|
||||
) *Wallet {
|
||||
pk, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
kinds := []int{37375, 7375}
|
||||
if withHistory {
|
||||
kinds = append(kinds, 7375)
|
||||
}
|
||||
|
||||
eoseChan := make(chan struct{})
|
||||
events := pool.SubManyNotifyEOSE(
|
||||
ctx,
|
||||
relays,
|
||||
nostr.Filters{
|
||||
{Kinds: kinds, Authors: []string{pk}},
|
||||
{Kinds: []int{5}, Tags: nostr.TagMap{"k": []string{"7375"}}, Authors: []string{pk}},
|
||||
},
|
||||
eoseChan,
|
||||
)
|
||||
|
||||
return loadWallet(ctx, kr, events, eoseChan)
|
||||
}
|
||||
|
||||
func loadWallet(
|
||||
ctx context.Context,
|
||||
kr nostr.Keyer,
|
||||
events chan nostr.RelayEvent,
|
||||
eoseChan chan struct{},
|
||||
) *Wallet {
|
||||
w := &Wallet{
|
||||
pendingDeletions: make([]string, 0, 128),
|
||||
kr: kr,
|
||||
Stable: make(chan struct{}),
|
||||
Tokens: make([]Token, 0, 128),
|
||||
History: make([]HistoryEntry, 0, 128),
|
||||
}
|
||||
|
||||
eosed := false
|
||||
go func() {
|
||||
<-eoseChan
|
||||
eosed = true
|
||||
|
||||
// check all pending deletions and delete stuff locally
|
||||
for _, id := range w.pendingDeletions {
|
||||
w.removeDeletedToken(id)
|
||||
}
|
||||
w.pendingDeletions = nil
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // race condition hack
|
||||
close(w.Stable)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for ie := range events {
|
||||
w.Lock()
|
||||
switch ie.Event.Kind {
|
||||
case 5:
|
||||
if !eosed {
|
||||
for _, tag := range ie.Event.Tags.All([]string{"e", ""}) {
|
||||
w.pendingDeletions = append(w.pendingDeletions, tag[1])
|
||||
}
|
||||
} else {
|
||||
for _, tag := range ie.Event.Tags.All([]string{"e", ""}) {
|
||||
w.removeDeletedToken(tag[1])
|
||||
}
|
||||
}
|
||||
case 17375:
|
||||
if err := w.parse(ctx, kr, ie.Event); err != nil {
|
||||
if w.Processed != nil {
|
||||
w.Processed(ie.Event, err)
|
||||
}
|
||||
w.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
case 7375: // token
|
||||
token := Token{}
|
||||
if err := token.parse(ctx, kr, ie.Event); err != nil {
|
||||
if w.Processed != nil {
|
||||
w.Processed(ie.Event, err)
|
||||
}
|
||||
w.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
w.tokensMu.Lock()
|
||||
if !slices.ContainsFunc(w.Tokens, func(c Token) bool { return c.event.ID == token.event.ID }) {
|
||||
w.Tokens = append(w.Tokens, token)
|
||||
}
|
||||
w.tokensMu.Unlock()
|
||||
|
||||
// keep track tokens that were deleted by this, if they exist
|
||||
if !eosed {
|
||||
for _, del := range token.Deleted {
|
||||
w.pendingDeletions = append(w.pendingDeletions, del)
|
||||
}
|
||||
} else {
|
||||
for _, del := range token.Deleted {
|
||||
w.removeDeletedToken(del)
|
||||
}
|
||||
}
|
||||
|
||||
case 7376: // history
|
||||
he := HistoryEntry{}
|
||||
if err := he.parse(ctx, kr, ie.Event); err != nil {
|
||||
if w.Processed != nil {
|
||||
w.Processed(ie.Event, err)
|
||||
}
|
||||
w.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if !slices.ContainsFunc(w.History, func(c HistoryEntry) bool { return c.event.ID == he.event.ID }) {
|
||||
w.History = append(w.History, he)
|
||||
}
|
||||
}
|
||||
|
||||
if w.Processed != nil {
|
||||
w.Processed(ie.Event, nil)
|
||||
}
|
||||
w.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Close waits for pending operations to end
|
||||
func (w *Wallet) Close() error {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Wallet) removeDeletedToken(eventId string) {
|
||||
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 (w *Wallet) Balance() uint64 {
|
||||
@@ -39,49 +229,31 @@ func (w *Wallet) Balance() uint64 {
|
||||
return sum
|
||||
}
|
||||
|
||||
func (w *Wallet) DisplayName() string {
|
||||
if w.Name != "" {
|
||||
return fmt.Sprintf("%s (%s)", w.Name, w.Identifier)
|
||||
}
|
||||
return w.Identifier
|
||||
}
|
||||
|
||||
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)
|
||||
evt.Tags = nostr.Tags{}
|
||||
|
||||
pk, err := kr.GetPublicKey(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tags := make(nostr.Tags, 0, 1+len(w.Mints))
|
||||
tags = append(tags, nostr.Tag{"privkey", hex.EncodeToString(w.PrivateKey.Serialize())})
|
||||
for _, mint := range w.Mints {
|
||||
tags = append(tags, nostr.Tag{"mint", mint})
|
||||
}
|
||||
jtags, _ := json.Marshal(tags)
|
||||
evt.Content, err = kr.Encrypt(
|
||||
ctx,
|
||||
fmt.Sprintf(`[["privkey","%x"]]`, w.PrivateKey.Serialize()),
|
||||
string(jtags),
|
||||
pk,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
evt.Tags = append(evt.Tags,
|
||||
nostr.Tag{"d", w.Identifier},
|
||||
nostr.Tag{"unit", "sat"},
|
||||
)
|
||||
if w.Name != "" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"name", w.Name})
|
||||
}
|
||||
if w.Description != "" {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"description", w.Description})
|
||||
}
|
||||
for _, relay := range w.Relays {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"relay", relay})
|
||||
}
|
||||
for _, mint := range w.Mints {
|
||||
evt.Tags = append(evt.Tags, nostr.Tag{"mint", mint})
|
||||
}
|
||||
|
||||
err = kr.SignEvent(ctx, evt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -91,8 +263,6 @@ func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -112,42 +282,33 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er
|
||||
tags = append(tags, evt.Tags...)
|
||||
}
|
||||
|
||||
essential := 0
|
||||
var mints []string
|
||||
var privateKey *btcec.PrivateKey
|
||||
|
||||
for _, tag := range tags {
|
||||
if len(tag) < 2 {
|
||||
continue
|
||||
}
|
||||
switch tag[0] {
|
||||
case "d":
|
||||
essential++
|
||||
w.Identifier = tag[1]
|
||||
case "name":
|
||||
w.Name = tag[1]
|
||||
case "description":
|
||||
w.Description = tag[1]
|
||||
case "unit":
|
||||
essential++
|
||||
if tag[1] != "sat" {
|
||||
return fmt.Errorf("only 'sat' wallets are supported")
|
||||
}
|
||||
case "relay":
|
||||
w.Relays = append(w.Relays, tag[1])
|
||||
case "mint":
|
||||
w.Mints = append(w.Mints, tag[1])
|
||||
mints = append(mints, tag[1])
|
||||
case "privkey":
|
||||
essential++
|
||||
skb, err := hex.DecodeString(tag[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
w.PrivateKey = secp256k1.PrivKeyFromBytes(skb)
|
||||
w.PublicKey = w.PrivateKey.PubKey()
|
||||
privateKey = secp256k1.PrivKeyFromBytes(skb)
|
||||
}
|
||||
}
|
||||
|
||||
if essential != 3 {
|
||||
return fmt.Errorf("missing essential tags")
|
||||
if privateKey == nil {
|
||||
return fmt.Errorf("missing wallet private key")
|
||||
}
|
||||
|
||||
// finally set these things when we know nothing will fail
|
||||
w.Mints = mints
|
||||
w.PrivateKey = privateKey
|
||||
w.PublicKey = w.PrivateKey.PubKey()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user