implement nip60 events.

This commit is contained in:
fiatjaf 2025-01-25 22:21:39 -03:00
parent 17431dee59
commit 3334f7a48b
7 changed files with 706 additions and 6 deletions

4
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/PowerDNS/lmdb-go v1.9.2
github.com/bluekeyes/go-gitdiff v0.7.1
github.com/btcsuite/btcd/btcec/v2 v2.3.4
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/coder/websocket v1.8.12
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/dgraph-io/badger/v4 v4.5.0
@ -58,7 +58,7 @@ require (
github.com/klauspost/compress v1.17.11 // indirect
github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/ncruces/julianday v1.0.0 // indirect

10
go.sum
View File

@ -23,15 +23,15 @@ github.com/bluekeyes/go-gitdiff v0.7.1 h1:graP4ElLRshr8ecu0UtqfNTCHrtSyZd3DABQm/
github.com/bluekeyes/go-gitdiff v0.7.1/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ=
github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
@ -125,6 +125,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graph-gophers/dataloader/v7 v7.1.0 h1:Wn8HGF/q7MNXcvfaBnLEPEFJttVHR8zuEqP1obys/oc=
github.com/graph-gophers/dataloader/v7 v7.1.0/go.mod h1:1bKE0Dm6OUcTB/OAuYVOZctgIz7Q3d0XrYtlIzTgg6Q=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
@ -155,8 +156,9 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-sqlite3 v0.18.3 h1:tyMa75uh7LcINcfo0WrzOvcTkfz8Hqu0TEPX+KVyes4=

153
nip60/history.go Normal file
View File

@ -0,0 +1,153 @@
package nip60
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/nbd-wtf/go-nostr"
)
type HistoryEntry struct {
In bool // in = received, out = sent
Amount uint32
tokenEventIDs []string
nutZaps []bool
createdAt nostr.Timestamp
event *nostr.Event
}
func (h HistoryEntry) toEvent(ctx context.Context, kr nostr.Keyer, walletId string, evt *nostr.Event) error {
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
dir := "in"
if !h.In {
dir = "out"
}
evt.CreatedAt = h.createdAt
evt.Kind = 7376
evt.Tags = nostr.Tags{{"a", fmt.Sprintf("37375:%s:%s", pk, walletId)}}
encryptedTags := nostr.Tags{
nostr.Tag{"direction", dir},
nostr.Tag{"amount", strconv.FormatUint(uint64(h.Amount), 10), "sat"},
}
for i, tid := range h.tokenEventIDs {
isNutZap := h.nutZaps[i]
if h.In && isNutZap {
evt.Tags = append(evt.Tags, nostr.Tag{"e", tid, "", "redeemed"})
continue
}
marker := "created"
if !h.In {
marker = "destroyed"
}
encryptedTags = append(encryptedTags, nostr.Tag{"e", tid, "", marker})
}
jsonb, _ := json.Marshal(encryptedTags)
evt.Content, err = kr.Encrypt(
ctx,
string(jsonb),
pk,
)
if err != nil {
return err
}
err = kr.SignEvent(ctx, evt)
if err != nil {
return err
}
return nil
}
func (h *HistoryEntry) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
h.event = evt
h.createdAt = evt.CreatedAt
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
// event tags and encrypted tags are mixed together
jsonb, err := kr.Decrypt(ctx, evt.Content, pk)
if err != nil {
return err
}
var tags nostr.Tags
if len(jsonb) > 0 {
tags = make(nostr.Tags, 0, 7)
if err := json.Unmarshal([]byte(jsonb), &tags); err != nil {
return err
}
tags = append(tags, evt.Tags...)
}
essential := 0
for _, tag := range tags {
if len(tag) < 2 {
continue
}
switch tag[0] {
case "direction":
essential++
if tag[1] == "in" {
h.In = true
} else if tag[1] == "out" {
h.In = false
} else {
return fmt.Errorf("unexpected 'direction' tag %s", tag[1])
}
case "amount":
essential++
if len(tag) < 3 {
return fmt.Errorf("'amount' 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, 32)
if err != nil {
return fmt.Errorf("invalid 'amount' %s: %w", tag[1], err)
}
h.Amount = uint32(v)
case "e":
essential++
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])
}
h.tokenEventIDs = append(h.tokenEventIDs, tag[1])
switch tag[3] {
case "created":
h.nutZaps = append(h.nutZaps, false)
case "destroyed":
h.nutZaps = append(h.nutZaps, false)
case "redeemed":
h.nutZaps = append(h.nutZaps, true)
}
}
}
if essential < 3 {
return fmt.Errorf("missing essential tags")
}
return nil
}

127
nip60/lib.go Normal file
View File

@ -0,0 +1,127 @@
package nip60
import (
"context"
"fmt"
"strings"
"sync"
"github.com/nbd-wtf/go-nostr"
)
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
}
func LoadWallets(
ctx context.Context,
kr nostr.Keyer,
events <-chan *nostr.Event,
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 evt := range events {
wl.Lock()
switch evt.Kind {
case 37375:
wallet := &Wallet{}
if err := wallet.parse(ctx, kr, evt); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, err)
}
continue
}
for _, he := range wl.pendingHistory[wallet.Identifier] {
wallet.History = append(wallet.History, he)
}
for _, token := range wl.pendingTokens[wallet.Identifier] {
wallet.Tokens = append(wallet.Tokens, token)
}
wl.wallets[wallet.Identifier] = wallet
case 7375: // token
ref := evt.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", evt)
}
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", evt)
}
continue
}
token := Token{}
if err := token.parse(ctx, kr, evt); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, err)
}
continue
}
if wallet, ok := wl.wallets[spl[2]]; ok {
wallet.Tokens = append(wallet.Tokens, token)
} else {
wl.pendingTokens[spl[2]] = append(wl.pendingTokens[spl[2]], token)
}
case 7376: // history
ref := evt.Tags.GetFirst([]string{"a", ""})
if ref == nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", evt)
}
continue
}
spl := strings.SplitN((*ref)[1], ":", 3)
if len(spl) < 3 {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", evt)
}
continue
}
he := HistoryEntry{}
if err := he.parse(ctx, kr, evt); err != nil {
wl.Unlock()
if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, 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
}

161
nip60/nip60_test.go Normal file
View File

@ -0,0 +1,161 @@
package nip60
import (
"context"
"fmt"
"testing"
"time"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/keyer"
"github.com/stretchr/testify/require"
)
func TestWalletRoundtrip(t *testing.T) {
ctx := context.Background()
kr, err := keyer.NewPlainKeySigner("94b46586f475bbc92746cb8f14d59b083047ac3ab747774b066d17673c1cc527")
require.NoError(t, err)
// create initial wallets with arbitrary data
wallet1 := Wallet{
Identifier: "wallet1",
Name: "My First Wallet",
Description: "Test wallet number one",
PrivateKey: "secret123",
Relays: []string{"wss://relay1.example.com", "wss://relay2.example.com"},
Mints: []string{"https://mint1.example.com"},
Tokens: []Token{
{
Mint: "https://mint1.example.com",
Proofs: []Proof{
{ID: "proof1", Amount: 100, Secret: "secret1", C: "c1"},
{ID: "proof2", Amount: 200, Secret: "secret2", C: "c2"},
},
mintedAt: nostr.Now(),
},
{
Mint: "https://mint2.example.com",
Proofs: []Proof{
{ID: "proof3", Amount: 500, Secret: "secret3", C: "c3"},
},
mintedAt: nostr.Now(),
},
},
History: []HistoryEntry{
{
In: true,
Amount: 300,
tokenEventIDs: []string{
"559cecf5aba6ab825347bedfd56ff603a2c6aa7c8d88790ca1e232759699bbc7",
"8f2c40b064e3e601d070362f53ace6fe124992da8a7322357c0868f22f6c2350",
},
nutZaps: []bool{false, false},
createdAt: nostr.Now(),
},
},
}
wallet2 := Wallet{
Identifier: "wallet2",
Name: "Second Wallet",
Description: "Test wallet number two",
PrivateKey: "secret456",
Relays: []string{"wss://relay3.example.com"},
Mints: []string{"https://mint2.example.com"},
Tokens: []Token{
{
Mint: "https://mint2.example.com",
Proofs: []Proof{
{ID: "proof3", Amount: 500, Secret: "secret3", C: "c3"},
},
mintedAt: nostr.Now(),
},
},
History: []HistoryEntry{
{
In: false,
Amount: 200,
tokenEventIDs: []string{
"cc9dd6298ae7e1ae0866448f11fed1c3a818b7db837caf8d5c48e496200477fe",
},
nutZaps: []bool{false},
createdAt: nostr.Now(),
},
{
In: true,
Amount: 300,
tokenEventIDs: []string{
"63e8ff4ca4f16d6edc0c93dd1659cc8029178560aef2c9a00ca323738ed680e3",
"3898e1c01fd6043dd46b819ce6a940867ccc116bc7c733124d2c0658fb1d569e",
},
nutZaps: []bool{false, false},
createdAt: nostr.Now(),
},
},
}
// convert wallets to events
events1, err := wallet1.ToPublishableEvents(ctx, kr, false)
require.NoError(t, err)
events2, err := wallet2.ToPublishableEvents(ctx, kr, false)
require.NoError(t, err)
// combine all events
allEvents := append(events1, events2...)
require.Len(t, allEvents, 8)
// make a derived shuffled version
reversedAllEvents := make([]nostr.Event, len(allEvents))
for i, evt := range allEvents {
reversedAllEvents[len(allEvents)-1-i] = evt
}
for _, allEvents := range [][]nostr.Event{allEvents, reversedAllEvents} {
// create channel and feed events into it
eventChan := make(chan *nostr.Event)
done := make(chan struct{})
go func() {
for _, evt := range allEvents {
eventChan <- &evt
}
close(eventChan)
done <- struct{}{}
}()
// load wallets from events
errorChan := make(chan error)
walletStash := LoadWallets(ctx, kr, eventChan, errorChan)
var errorChanErr error
go func() {
for {
errorChanErr = <-errorChan
fmt.Println(errorChanErr)
}
}()
<-done
time.Sleep(time.Millisecond * 200)
require.NoError(t, errorChanErr, "errorChan shouldn't have received any errors: %w", errorChanErr)
// compare loaded wallets with original ones
loadedWallet1 := walletStash.wallets[wallet1.Identifier]
require.Equal(t, wallet1.Name, loadedWallet1.Name)
require.Equal(t, wallet1.Description, loadedWallet1.Description)
require.Equal(t, wallet1.PrivateKey, loadedWallet1.PrivateKey)
require.Len(t, loadedWallet1.Tokens, len(wallet1.Tokens))
require.Len(t, loadedWallet1.History, len(wallet1.History))
loadedWallet2 := walletStash.wallets[wallet2.Identifier]
require.Equal(t, wallet2.Name, loadedWallet2.Name)
require.Equal(t, wallet2.Description, loadedWallet2.Description)
require.Equal(t, wallet2.PrivateKey, loadedWallet2.PrivateKey)
require.Len(t, loadedWallet2.Tokens, len(wallet2.Tokens))
require.Len(t, loadedWallet2.History, len(wallet2.History))
// check token amounts
require.Equal(t, wallet1.Balance(), loadedWallet1.Balance())
require.Equal(t, wallet2.Balance(), loadedWallet2.Balance())
}
}

81
nip60/token.go Normal file
View File

@ -0,0 +1,81 @@
package nip60
import (
"context"
"encoding/json"
"fmt"
"github.com/nbd-wtf/go-nostr"
)
type Token struct {
Mint string `json:"mint"`
Proofs []Proof `json:"proofs"`
mintedAt nostr.Timestamp
event *nostr.Event
}
type Proof struct {
ID string `json:"id"`
Amount uint32 `json:"amount"`
Secret string `json:"secret"`
C string `json:"C"`
}
func (t Token) Amount() uint32 {
var sum uint32
for _, p := range t.Proofs {
sum += p.Amount
}
return sum
}
func (t Token) toEvent(ctx context.Context, kr nostr.Keyer, walletId string, evt *nostr.Event) error {
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
evt.CreatedAt = t.mintedAt
evt.Kind = 7375
evt.Tags = nostr.Tags{{"a", fmt.Sprintf("37375:%s:%s", pk, walletId)}}
content, _ := json.Marshal(t)
evt.Content, err = kr.Encrypt(
ctx,
string(content),
pk,
)
if err != nil {
return err
}
err = kr.SignEvent(ctx, evt)
if err != nil {
return err
}
return nil
}
func (t *Token) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
t.event = evt
t.mintedAt = evt.CreatedAt
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
content, err := kr.Decrypt(ctx, evt.Content, pk)
if err != nil {
return err
}
if err := json.Unmarshal([]byte(content), t); err != nil {
return fmt.Errorf("failed to parse token content: %w", err)
}
return nil
}

176
nip60/wallet.go Normal file
View File

@ -0,0 +1,176 @@
package nip60
import (
"context"
"encoding/json"
"fmt"
"strconv"
"github.com/nbd-wtf/go-nostr"
)
type Wallet struct {
Identifier string
Description string
Name string
PrivateKey string
Relays []string
Mints []string
Tokens []Token
History []HistoryEntry
temporaryBalance uint32
}
func (w Wallet) Balance() uint32 {
var sum uint32
for _, token := range w.Tokens {
sum += token.Amount()
}
return sum
}
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),
}
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return nil, err
}
evt.Content, err = kr.Encrypt(
ctx,
fmt.Sprintf(`[["balance","%d","sat"],["privkey","%s"]]`, w.Balance(), w.PrivateKey),
pk,
)
if err != nil {
return nil, 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})
}
err = kr.SignEvent(ctx, &evt)
if err != nil {
return nil, err
}
events := make([]nostr.Event, 0, 1+len(w.Tokens))
events = append(events, evt)
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)
}
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
}
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)
pk, err := kr.GetPublicKey(ctx)
if err != nil {
return err
}
jsonb, err := kr.Decrypt(ctx, evt.Content, pk)
if err != nil {
return err
}
var tags nostr.Tags
if len(jsonb) > 0 {
tags = make(nostr.Tags, 0, 7)
if err := json.Unmarshal([]byte(jsonb), &tags); err != nil {
return err
}
tags = append(tags, evt.Tags...)
}
essential := 0
for _, tag := range tags {
if len(tag) < 2 {
continue
}
switch tag[0] {
case "d":
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])
case "privkey":
w.PrivateKey = tag[1]
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, 32)
if err != nil {
return fmt.Errorf("invalid 'balance' %s: %w", tag[1], err)
}
w.temporaryBalance = uint32(v)
}
}
return nil
}