mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-17 21:32:56 +01:00
nip60: make it work with emitting events to be published dynamically and stuff.
This commit is contained in:
parent
07b9b3e439
commit
2244740f61
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
332
nip60/stash.go
332
nip60/stash.go
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user