nip60: make client better, fixes to receive flow, wallet helper methods.

This commit is contained in:
fiatjaf
2025-01-27 22:11:45 -03:00
parent c6747cdf44
commit 9c4c6529c9
5 changed files with 130 additions and 231 deletions

View File

@@ -19,40 +19,17 @@ import (
) )
func GetMintInfo(ctx context.Context, mintURL string) (*nut06.MintInfo, error) { func GetMintInfo(ctx context.Context, mintURL string) (*nut06.MintInfo, error) {
resp, err := httpGet(ctx, mintURL+"/v1/info")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var mintInfo nut06.MintInfo var mintInfo nut06.MintInfo
if err := json.Unmarshal(body, &mintInfo); err != nil { if err := httpGet(ctx, mintURL+"/v1/info", &mintInfo); err != nil {
return nil, fmt.Errorf("error reading response from mint: %v", err) return nil, err
} }
return &mintInfo, nil return &mintInfo, nil
} }
func GetActiveKeyset(ctx context.Context, mintURL string) (*nut01.Keyset, error) { func GetActiveKeyset(ctx context.Context, mintURL string) (*nut01.Keyset, error) {
resp, err := httpGet(ctx, mintURL+"/v1/keys")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var keysetRes nut01.GetKeysResponse var keysetRes nut01.GetKeysResponse
if err := json.Unmarshal(body, &keysetRes); err != nil { if err := httpGet(ctx, mintURL+"/v1/keys", &keysetRes); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
for _, keyset := range keysetRes.Keysets { for _, keyset := range keysetRes.Keysets {
@@ -65,42 +42,18 @@ func GetActiveKeyset(ctx context.Context, mintURL string) (*nut01.Keyset, error)
} }
func GetAllKeysets(ctx context.Context, mintURL string) ([]nut02.Keyset, error) { func GetAllKeysets(ctx context.Context, mintURL string) ([]nut02.Keyset, error) {
resp, err := httpGet(ctx, mintURL+"/v1/keysets")
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var keysetsRes nut02.GetKeysetsResponse var keysetsRes nut02.GetKeysetsResponse
if err := json.Unmarshal(body, &keysetsRes); err != nil { if err := httpGet(ctx, mintURL+"/v1/keysets", &keysetsRes); err != nil {
return nil, fmt.Errorf("error reading response from mint: %v", err) return nil, err
} }
return keysetsRes.Keysets, nil return keysetsRes.Keysets, nil
} }
func GetKeysetById(ctx context.Context, mintURL, id string) (map[uint64]string, error) { func GetKeysetById(ctx context.Context, mintURL, id string) (map[uint64]string, error) {
resp, err := httpGet(ctx, mintURL+"/v1/keys/"+id)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var keysetRes nut01.GetKeysResponse var keysetRes nut01.GetKeysResponse
if err := json.Unmarshal(body, &keysetRes); err != nil || len(keysetRes.Keysets) != 1 { if err := httpGet(ctx, mintURL+"/v1/keys/"+id, &keysetRes); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return keysetRes.Keysets[0].Keys, nil return keysetRes.Keysets[0].Keys, nil
} }
@@ -109,42 +62,18 @@ func PostMintQuoteBolt11(
mintURL string, mintURL string,
mintQuoteRequest nut04.PostMintQuoteBolt11Request, mintQuoteRequest nut04.PostMintQuoteBolt11Request,
) (*nut04.PostMintQuoteBolt11Response, error) { ) (*nut04.PostMintQuoteBolt11Response, error) {
resp, err := httpPost(ctx, mintURL+"/v1/mint/quote/bolt11", mintQuoteRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var reqMintResponse nut04.PostMintQuoteBolt11Response var reqMintResponse nut04.PostMintQuoteBolt11Response
if err := json.Unmarshal(body, &reqMintResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/mint/quote/bolt11", mintQuoteRequest, &reqMintResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &reqMintResponse, nil return &reqMintResponse, nil
} }
func GetMintQuoteState(ctx context.Context, mintURL, quoteId string) (*nut04.PostMintQuoteBolt11Response, error) { func GetMintQuoteState(ctx context.Context, mintURL, quoteId string) (*nut04.PostMintQuoteBolt11Response, error) {
resp, err := httpGet(ctx, mintURL+"/v1/mint/quote/bolt11/"+quoteId)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var mintQuoteResponse nut04.PostMintQuoteBolt11Response var mintQuoteResponse nut04.PostMintQuoteBolt11Response
if err := json.Unmarshal(body, &mintQuoteResponse); err != nil { if err := httpGet(ctx, mintURL+"/v1/mint/quote/bolt11/"+quoteId, &mintQuoteResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &mintQuoteResponse, nil return &mintQuoteResponse, nil
} }
@@ -153,42 +82,18 @@ func PostMintBolt11(
mintURL string, mintURL string,
mintRequest nut04.PostMintBolt11Request, mintRequest nut04.PostMintBolt11Request,
) (*nut04.PostMintBolt11Response, error) { ) (*nut04.PostMintBolt11Response, error) {
resp, err := httpPost(ctx, mintURL+"/v1/mint/bolt11", mintRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var reqMintResponse nut04.PostMintBolt11Response var reqMintResponse nut04.PostMintBolt11Response
if err := json.Unmarshal(body, &reqMintResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/mint/bolt11", mintRequest, &reqMintResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &reqMintResponse, nil return &reqMintResponse, nil
} }
func PostSwap(ctx context.Context, mintURL string, swapRequest nut03.PostSwapRequest) (*nut03.PostSwapResponse, error) { func PostSwap(ctx context.Context, mintURL string, swapRequest nut03.PostSwapRequest) (*nut03.PostSwapResponse, error) {
resp, err := httpPost(ctx, mintURL+"/v1/swap", swapRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var swapResponse nut03.PostSwapResponse var swapResponse nut03.PostSwapResponse
if err := json.Unmarshal(body, &swapResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/swap", swapRequest, &swapResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &swapResponse, nil return &swapResponse, nil
} }
@@ -197,42 +102,18 @@ func PostMeltQuoteBolt11(
mintURL string, mintURL string,
meltQuoteRequest nut05.PostMeltQuoteBolt11Request, meltQuoteRequest nut05.PostMeltQuoteBolt11Request,
) (*nut05.PostMeltQuoteBolt11Response, error) { ) (*nut05.PostMeltQuoteBolt11Response, error) {
resp, err := httpPost(ctx, mintURL+"/v1/melt/quote/bolt11", meltQuoteRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var meltQuoteResponse nut05.PostMeltQuoteBolt11Response var meltQuoteResponse nut05.PostMeltQuoteBolt11Response
if err := json.Unmarshal(body, &meltQuoteResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/melt/quote/bolt11", meltQuoteRequest, &meltQuoteResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &meltQuoteResponse, nil return &meltQuoteResponse, nil
} }
func GetMeltQuoteState(ctx context.Context, mintURL, quoteId string) (*nut05.PostMeltQuoteBolt11Response, error) { func GetMeltQuoteState(ctx context.Context, mintURL, quoteId string) (*nut05.PostMeltQuoteBolt11Response, error) {
resp, err := httpGet(ctx, mintURL+"/v1/melt/quote/bolt11/"+quoteId)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var meltQuoteResponse nut05.PostMeltQuoteBolt11Response var meltQuoteResponse nut05.PostMeltQuoteBolt11Response
if err := json.Unmarshal(body, &meltQuoteResponse); err != nil { if err := httpGet(ctx, mintURL+"/v1/melt/quote/bolt11/"+quoteId, &meltQuoteResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &meltQuoteResponse, nil return &meltQuoteResponse, nil
} }
@@ -241,22 +122,10 @@ func PostMeltBolt11(
mintURL string, mintURL string,
meltRequest nut05.PostMeltBolt11Request, meltRequest nut05.PostMeltBolt11Request,
) (*nut05.PostMeltQuoteBolt11Response, error) { ) (*nut05.PostMeltQuoteBolt11Response, error) {
resp, err := httpPost(ctx, mintURL+"/v1/melt/bolt11", meltRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var meltResponse nut05.PostMeltQuoteBolt11Response var meltResponse nut05.PostMeltQuoteBolt11Response
if err := json.Unmarshal(body, &meltResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/melt/bolt11", meltRequest, &meltResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %w", err) return nil, err
} }
return &meltResponse, nil return &meltResponse, nil
} }
@@ -265,22 +134,10 @@ func PostCheckProofState(
mintURL string, mintURL string,
stateRequest nut07.PostCheckStateRequest, stateRequest nut07.PostCheckStateRequest,
) (*nut07.PostCheckStateResponse, error) { ) (*nut07.PostCheckStateResponse, error) {
resp, err := httpPost(ctx, mintURL+"/v1/checkstate", stateRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var stateResponse nut07.PostCheckStateResponse var stateResponse nut07.PostCheckStateResponse
if err := json.Unmarshal(body, &stateResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/checkstate", stateRequest, &stateResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %v", err) return nil, err
} }
return &stateResponse, nil return &stateResponse, nil
} }
@@ -289,74 +146,71 @@ func PostRestore(
mintURL string, mintURL string,
restoreRequest nut09.PostRestoreRequest, restoreRequest nut09.PostRestoreRequest,
) (*nut09.PostRestoreResponse, error) { ) (*nut09.PostRestoreResponse, error) {
resp, err := httpPost(ctx, mintURL+"/v1/restore", restoreRequest)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var restoreResponse nut09.PostRestoreResponse var restoreResponse nut09.PostRestoreResponse
if err := json.Unmarshal(body, &restoreResponse); err != nil { if err := httpPost(ctx, mintURL+"/v1/restore", restoreRequest, &restoreResponse); err != nil {
return nil, fmt.Errorf("error reading response from mint: %v", err) return nil, err
} }
return &restoreResponse, nil return &restoreResponse, nil
} }
func httpGet(ctx context.Context, url string) (*http.Response, error) { func httpGet(ctx context.Context, url string, dst any) error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return nil, err return err
} }
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return err
} }
return parse(resp) return parse(resp, dst)
} }
func httpPost(ctx context.Context, url string, data any) (*http.Response, error) { func httpPost(ctx context.Context, url string, data any, dst any) error {
r, w := io.Pipe() r, w := io.Pipe()
json.NewEncoder(w).Encode(data) go func() {
json.NewEncoder(w).Encode(data)
w.Close()
}()
req, err := http.NewRequestWithContext(ctx, "POST", url, r) req, err := http.NewRequestWithContext(ctx, "POST", url, r)
if err != nil { if err != nil {
return nil, err return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, err return err
} }
defer resp.Body.Close()
return parse(resp) return parse(resp, dst)
} }
func parse(response *http.Response) (*http.Response, error) { func parse(response *http.Response, dst any) error {
if response.StatusCode == 400 { if response.StatusCode == 400 {
var errResponse cashu.Error var errResponse cashu.Error
err := json.NewDecoder(response.Body).Decode(&errResponse) err := json.NewDecoder(response.Body).Decode(&errResponse)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not decode error response from mint: %v", err) return fmt.Errorf("could not decode error response from mint: %v", err)
} }
return nil, errResponse return errResponse
} }
if response.StatusCode != 200 { if response.StatusCode != 200 {
body, err := io.ReadAll(response.Body) body, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
return nil, err return err
} }
return nil, fmt.Errorf("%s", body) return fmt.Errorf("%s", body)
} }
return response, nil err := json.NewDecoder(response.Body).Decode(dst)
if err != nil {
return fmt.Errorf("could not decode response from mint: %w", err)
}
return nil
} }

View File

@@ -3,9 +3,11 @@ package nip60
import ( import (
"context" "context"
"fmt" "fmt"
"iter"
"strings" "strings"
"sync" "sync"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr"
) )
@@ -17,6 +19,40 @@ type WalletStash struct {
pendingHistory map[string][]HistoryEntry // history entries not yet assigned to a wallet pendingHistory map[string][]HistoryEntry // history entries not yet assigned to a wallet
} }
func (wl *WalletStash) EnsureWallet(id string) *Wallet {
wl.Lock()
defer wl.Unlock()
if w, ok := wl.wallets[id]; ok {
return w
}
sk, err := btcec.NewPrivateKey()
if err != nil {
panic(err)
}
w := &Wallet{
Identifier: id,
PrivateKey: sk,
PublicKey: sk.PubKey(),
}
wl.wallets[id] = w
return w
}
func (wl *WalletStash) Wallets() iter.Seq[*Wallet] {
return func(yield func(*Wallet) bool) {
wl.Lock()
defer wl.Unlock()
for _, w := range wl.wallets {
if !yield(w) {
return
}
}
}
}
func NewStash() *WalletStash { func NewStash() *WalletStash {
return &WalletStash{ return &WalletStash{
wallets: make(map[string]*Wallet, 1), wallets: make(map[string]*Wallet, 1),
@@ -28,7 +64,7 @@ func NewStash() *WalletStash {
func LoadStash( func LoadStash(
ctx context.Context, ctx context.Context,
kr nostr.Keyer, kr nostr.Keyer,
events <-chan *nostr.Event, events <-chan nostr.RelayEvent,
errors chan<- error, errors chan<- error,
) *WalletStash { ) *WalletStash {
wl := &WalletStash{ wl := &WalletStash{
@@ -38,15 +74,15 @@ func LoadStash(
} }
go func() { go func() {
for evt := range events { for ie := range events {
wl.Lock() wl.Lock()
switch evt.Kind { switch ie.Event.Kind {
case 37375: case 37375:
wallet := &Wallet{} wallet := &Wallet{}
if err := wallet.parse(ctx, kr, evt); err != nil { if err := wallet.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, err) errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
} }
continue continue
} }
@@ -61,11 +97,11 @@ func LoadStash(
wl.wallets[wallet.Identifier] = wallet wl.wallets[wallet.Identifier] = wallet
case 7375: // token case 7375: // token
ref := evt.Tags.GetFirst([]string{"a", ""}) ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil { if ref == nil {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", evt) errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
} }
continue continue
} }
@@ -73,16 +109,16 @@ func LoadStash(
if len(spl) < 3 { if len(spl) < 3 {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", evt) errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
} }
continue continue
} }
token := Token{} token := Token{}
if err := token.parse(ctx, kr, evt); err != nil { if err := token.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, err) errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
} }
continue continue
} }
@@ -94,11 +130,11 @@ func LoadStash(
} }
case 7376: // history case 7376: // history
ref := evt.Tags.GetFirst([]string{"a", ""}) ref := ie.Event.Tags.GetFirst([]string{"a", ""})
if ref == nil { if ref == nil {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s missing 'a' tag", evt) errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event)
} }
continue continue
} }
@@ -106,16 +142,16 @@ func LoadStash(
if len(spl) < 3 { if len(spl) < 3 {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s invalid 'a' tag", evt) errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event)
} }
continue continue
} }
he := HistoryEntry{} he := HistoryEntry{}
if err := he.parse(ctx, kr, evt); err != nil { if err := he.parse(ctx, kr, ie.Event); err != nil {
wl.Unlock() wl.Unlock()
if errors != nil { if errors != nil {
errors <- fmt.Errorf("event %s failed: %w", evt, err) errors <- fmt.Errorf("event %s failed: %w", ie.Event, err)
} }
continue continue
} }

View File

@@ -116,11 +116,11 @@ func TestWalletRoundtrip(t *testing.T) {
for _, allEvents := range [][]nostr.Event{allEvents, reversedAllEvents} { for _, allEvents := range [][]nostr.Event{allEvents, reversedAllEvents} {
// create channel and feed events into it // create channel and feed events into it
eventChan := make(chan *nostr.Event) eventChan := make(chan nostr.RelayEvent)
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
for _, evt := range allEvents { for _, evt := range allEvents {
eventChan <- &evt eventChan <- nostr.RelayEvent{Event: &evt}
} }
close(eventChan) close(eventChan)
done <- struct{}{} done <- struct{}{}

View File

@@ -28,20 +28,19 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
for i, proof := range proofs { for i, proof := range proofs {
if proof.Secret != "" { if proof.Secret != "" {
nut10Secret, err := nut10.DeserializeSecret(proof.Secret) nut10Secret, err := nut10.DeserializeSecret(proof.Secret)
if err != nil { if err == nil {
return fmt.Errorf("invalid nip10 secret at %d: %w", i, err) switch nut10Secret.Kind {
} case nut10.P2PK:
switch nut10Secret.Kind { isp2pk = true
case nut10.P2PK: proofs[i].Witness, err = signInput(w.PrivateKey, w.PublicKey, proof, nut10Secret)
isp2pk = true if err != nil {
proofs[i].Witness, err = signInput(w.PrivateKey, w.PublicKey, proof, nut10Secret) return fmt.Errorf("failed to sign locked proof %d: %w", i, err)
if err != nil { }
return fmt.Errorf("failed to sign locked proof %d: %w", i, err) case nut10.HTLC:
return fmt.Errorf("HTLC token not supported yet")
case nut10.AnyoneCanSpend:
// ok
} }
case nut10.HTLC:
return fmt.Errorf("HTLC token not supported yet")
case nut10.AnyoneCanSpend:
// ok
} }
} }
} }
@@ -90,7 +89,7 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error
res, err := client.PostSwap(ctx, source, req) res, err := client.PostSwap(ctx, source, req)
if err != nil { if err != nil {
return fmt.Errorf("failed to swap %s->%s: %w", source, w.Mints[0], err) return fmt.Errorf("failed to claim received tokens at %s: %w", source, err)
} }
newProofs, err := constructProofs(res.Signatures, req.Outputs, secrets, rs, sourceActiveKeys) newProofs, err := constructProofs(res.Signatures, req.Outputs, secrets, rs, sourceActiveKeys)

View File

@@ -34,7 +34,18 @@ func (w Wallet) Balance() uint64 {
return sum return sum
} }
func (w Wallet) ToPublishableEvents(ctx context.Context, kr nostr.Keyer, skipExisting bool) ([]nostr.Event, error) { func (w Wallet) DisplayName() string {
if w.Name != "" {
return fmt.Sprintf("%s (%s)", w.Name, w.Identifier)
}
return w.Identifier
}
func (w Wallet) ToPublishableEvents(
ctx context.Context,
kr nostr.Keyer,
skipExisting bool,
) ([]nostr.Event, error) {
evt := nostr.Event{ evt := nostr.Event{
CreatedAt: nostr.Now(), CreatedAt: nostr.Now(),
Kind: 37375, Kind: 37375,
@@ -162,7 +173,6 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er
case "relay": case "relay":
w.Relays = append(w.Relays, tag[1]) w.Relays = append(w.Relays, tag[1])
case "mint": case "mint":
essential++
w.Mints = append(w.Mints, tag[1]) w.Mints = append(w.Mints, tag[1])
case "privkey": case "privkey":
essential++ essential++
@@ -187,8 +197,8 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er
} }
} }
if essential < 4 { if essential != 3 {
return fmt.Errorf("missing essential tags %s", evt) return fmt.Errorf("missing essential tags")
} }
return nil return nil