nip61 and nip60 improvements and fixes.

This commit is contained in:
fiatjaf 2025-02-04 13:43:18 -03:00
parent 48ce669a3d
commit 1e4848d84d
6 changed files with 180 additions and 21 deletions

View File

@ -11,17 +11,47 @@ import (
"github.com/nbd-wtf/go-nostr/nip60/client"
)
type receiveSettings struct {
intoMint []string
isNutzap bool
}
type ReceiveOption func(*receiveSettings)
func WithMintDestination(url string) ReceiveOption {
return func(rs *receiveSettings) {
rs.intoMint = append(rs.intoMint, url)
}
}
func WithNutzap() ReceiveOption {
return func(rs *receiveSettings) {
rs.isNutzap = true
}
}
func (w *Wallet) Receive(
ctx context.Context,
proofs cashu.Proofs,
mint string,
opts ...ReceiveOption,
) error {
if w.PublishUpdate == nil {
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
source := "http" + nostr.NormalizeURL(mint)[2:]
lightningSwap := slices.Contains(w.Mints, source)
rs := receiveSettings{}
for _, opt := range opts {
opt(&rs)
}
source, _ := nostr.NormalizeHTTPURL(mint)
destination := rs.intoMint
if len(destination) == 0 {
destination = w.Mints
}
lightningSwap := slices.Contains(destination, source)
swapOpts := make([]SwapOption, 0, 1)
for i, proof := range proofs {
@ -60,7 +90,7 @@ func (w *Wallet) Receive(
// if we have to swap to our own mint we do it now by getting a bolt11 invoice from our mint
// and telling the current mint to pay it
if lightningSwap {
for _, targetMint := range w.Mints {
for _, targetMint := range destination {
swappedProofs, err, status := lightningMeltMint(
ctx,
newProofs,
@ -110,7 +140,7 @@ saveproofs:
{
EventID: newToken.event.ID,
Created: true,
IsNutzap: false,
IsNutzap: rs.isNutzap,
},
},
createdAt: nostr.Now(),

View File

@ -72,7 +72,7 @@ func (t *Token) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) err
return fmt.Errorf("failed to parse token content: %w", err)
}
t.Mint = "http" + nostr.NormalizeURL(t.Mint)[2:]
t.Mint, _ = nostr.NormalizeHTTPURL(t.Mint)
return nil
}

View File

@ -24,6 +24,7 @@ type Wallet struct {
kr nostr.Keyer
// PublishUpdate must be set to a function that publishes event to the user relays
// (if all arguments are their zero values that means it is a wallet update event).
PublishUpdate func(
event nostr.Event,
deleted *Token,
@ -78,7 +79,7 @@ func loadWalletFromPool(
kinds := []int{17375, 7375}
if withHistory {
kinds = append(kinds, 7375)
kinds = append(kinds, 7376)
}
eoseChan := make(chan struct{})
@ -229,6 +230,89 @@ func (w *Wallet) Balance() uint64 {
return sum
}
func (w *Wallet) AddMint(ctx context.Context, urls ...string) error {
if w.PublishUpdate == nil {
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
for _, url := range urls {
url, err := nostr.NormalizeHTTPURL(url)
if err != nil {
return err
}
if !slices.Contains(w.Mints, url) {
w.Mints = append(w.Mints, url)
}
}
evt := nostr.Event{}
if err := w.toEvent(ctx, w.kr, &evt); err != nil {
return err
}
w.Lock()
w.PublishUpdate(evt, nil, nil, nil, false)
w.Unlock()
return nil
}
func (w *Wallet) RemoveMint(ctx context.Context, urls ...string) error {
if w.PublishUpdate == nil {
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
for _, url := range urls {
url, err := nostr.NormalizeHTTPURL(url)
if err != nil {
return err
}
if idx := slices.Index(w.Mints, url); idx != -1 {
w.Mints = slices.Delete(w.Mints, idx, idx+1)
}
}
evt := nostr.Event{}
if err := w.toEvent(ctx, w.kr, &evt); err != nil {
return err
}
w.Lock()
w.PublishUpdate(evt, nil, nil, nil, false)
w.Unlock()
return nil
}
func (w *Wallet) SetPrivateKey(ctx context.Context, privateKey string) error {
if w.PublishUpdate == nil {
return fmt.Errorf("can't do write operations: missing PublishUpdate function")
}
skb, err := hex.DecodeString(privateKey)
if err != nil {
return err
}
if len(skb) != 32 {
return fmt.Errorf("private key must be 32 bytes, got %d", len(skb))
}
w.PrivateKey, w.PublicKey = btcec.PrivKeyFromBytes(skb)
evt := nostr.Event{}
if err := w.toEvent(ctx, w.kr, &evt); err != nil {
return err
}
w.Lock()
w.PublishUpdate(evt, nil, nil, nil, false)
w.Unlock()
return nil
}
func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
evt.CreatedAt = nostr.Now()
evt.Kind = 17375
@ -239,12 +323,15 @@ func (w *Wallet) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event)
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})
encryptedTags := make(nostr.Tags, 0, 1+len(w.Mints))
if w.PrivateKey != nil {
encryptedTags = append(encryptedTags, nostr.Tag{"privkey", hex.EncodeToString(w.PrivateKey.Serialize())})
}
jtags, _ := json.Marshal(tags)
for _, mint := range w.Mints {
encryptedTags = append(encryptedTags, nostr.Tag{"mint", mint})
}
jtags, _ := json.Marshal(encryptedTags)
evt.Content, err = kr.Encrypt(
ctx,
string(jtags),
@ -301,15 +388,12 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er
}
}
if privateKey == nil {
return fmt.Errorf("missing wallet private key")
}
// finally set these things when we know nothing will fail
w.Mints = mints
fmt.Println("mints", mints)
if privateKey != nil {
w.PrivateKey = privateKey
w.PublicKey = w.PrivateKey.PubKey()
}
w.Mints = mints
return nil
}

View File

@ -14,7 +14,7 @@ type Info struct {
Relays []string
}
func (zi *Info) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
func (zi *Info) ToEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) error {
evt.CreatedAt = nostr.Now()
evt.Kind = 10019
@ -36,7 +36,7 @@ func (zi *Info) toEvent(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) e
return nil
}
func (zi *Info) parse(evt *nostr.Event) error {
func (zi *Info) ParseEvent(evt *nostr.Event) error {
zi.Mints = make([]string, 0)
for _, tag := range evt.Tags {
if len(tag) < 2 {
@ -46,7 +46,8 @@ func (zi *Info) parse(evt *nostr.Event) error {
switch tag[0] {
case "mint":
if len(tag) == 2 || slices.Contains(tag[2:], cashu.Sat.String()) {
zi.Mints = append(zi.Mints, "http"+nostr.NormalizeURL(tag[1])[2:])
url, _ := nostr.NormalizeHTTPURL(tag[1])
zi.Mints = append(zi.Mints, url)
}
case "relay":
zi.Relays = append(zi.Relays, tag[1])

View File

@ -33,7 +33,7 @@ func SendNutzap(
}
info := Info{}
if err := info.parse(ie.Event); err != nil {
if err := info.ParseEvent(ie.Event); err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package nostr
import (
"fmt"
"net/url"
"strings"
)
@ -36,6 +37,49 @@ func NormalizeURL(u string) string {
return p.String()
}
// NormalizeHTTPURL does normalization of http(s):// URLs according to rfc3986. Don't use for relay URLs.
func NormalizeHTTPURL(s string) (string, error) {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "http") {
s = "https://" + s
}
u, err := url.Parse(s)
if err != nil {
return s, err
}
if u.Scheme == "" {
u, err = url.Parse("http://" + s)
if err != nil {
return s, err
}
}
if strings.HasPrefix(s, "//") {
s = "http:" + s
}
var p int
switch u.Scheme {
case "http":
p = 80
case "https":
p = 443
}
u.Host = strings.TrimSuffix(u.Host, fmt.Sprintf(":%d", p))
v := u.Query()
u.RawQuery = v.Encode()
u.RawQuery, _ = url.QueryUnescape(u.RawQuery)
h := u.String()
h = strings.TrimSuffix(h, "/")
return h, nil
}
// NormalizeOKMessage takes a string message that is to be sent in an `OK` or `CLOSED` command
// and prefixes it with "<prefix>: " if it doesn't already have an acceptable prefix.
func NormalizeOKMessage(reason string, prefix string) string {