From 9c4c6529c94c62e7650c55b89fccfe1dade06d41 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Mon, 27 Jan 2025 22:11:45 -0300 Subject: [PATCH] nip60: make client better, fixes to receive flow, wallet helper methods. --- nip60/client/client.go | 246 +++++++++-------------------------------- nip60/lib.go | 66 ++++++++--- nip60/nip60_test.go | 4 +- nip60/receive.go | 27 +++-- nip60/wallet.go | 18 ++- 5 files changed, 130 insertions(+), 231 deletions(-) diff --git a/nip60/client/client.go b/nip60/client/client.go index 7d0436c..cc78244 100644 --- a/nip60/client/client.go +++ b/nip60/client/client.go @@ -19,40 +19,17 @@ import ( ) 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 - if err := json.Unmarshal(body, &mintInfo); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v", err) + if err := httpGet(ctx, mintURL+"/v1/info", &mintInfo); err != nil { + return nil, err } - return &mintInfo, nil } 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 - if err := json.Unmarshal(body, &keysetRes); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpGet(ctx, mintURL+"/v1/keys", &keysetRes); err != nil { + return nil, err } 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) { - 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 - if err := json.Unmarshal(body, &keysetsRes); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v", err) + if err := httpGet(ctx, mintURL+"/v1/keysets", &keysetsRes); err != nil { + return nil, err } - return keysetsRes.Keysets, nil } 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 - if err := json.Unmarshal(body, &keysetRes); err != nil || len(keysetRes.Keysets) != 1 { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpGet(ctx, mintURL+"/v1/keys/"+id, &keysetRes); err != nil { + return nil, err } - return keysetRes.Keysets[0].Keys, nil } @@ -109,42 +62,18 @@ func PostMintQuoteBolt11( mintURL string, mintQuoteRequest nut04.PostMintQuoteBolt11Request, ) (*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 - if err := json.Unmarshal(body, &reqMintResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpPost(ctx, mintURL+"/v1/mint/quote/bolt11", mintQuoteRequest, &reqMintResponse); err != nil { + return nil, err } - return &reqMintResponse, nil } 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 - if err := json.Unmarshal(body, &mintQuoteResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpGet(ctx, mintURL+"/v1/mint/quote/bolt11/"+quoteId, &mintQuoteResponse); err != nil { + return nil, err } - return &mintQuoteResponse, nil } @@ -153,42 +82,18 @@ func PostMintBolt11( mintURL string, mintRequest nut04.PostMintBolt11Request, ) (*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 - if err := json.Unmarshal(body, &reqMintResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpPost(ctx, mintURL+"/v1/mint/bolt11", mintRequest, &reqMintResponse); err != nil { + return nil, err } - return &reqMintResponse, nil } 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 - if err := json.Unmarshal(body, &swapResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpPost(ctx, mintURL+"/v1/swap", swapRequest, &swapResponse); err != nil { + return nil, err } - return &swapResponse, nil } @@ -197,42 +102,18 @@ func PostMeltQuoteBolt11( mintURL string, meltQuoteRequest nut05.PostMeltQuoteBolt11Request, ) (*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 - if err := json.Unmarshal(body, &meltQuoteResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpPost(ctx, mintURL+"/v1/melt/quote/bolt11", meltQuoteRequest, &meltQuoteResponse); err != nil { + return nil, err } - return &meltQuoteResponse, nil } 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 - if err := json.Unmarshal(body, &meltQuoteResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpGet(ctx, mintURL+"/v1/melt/quote/bolt11/"+quoteId, &meltQuoteResponse); err != nil { + return nil, err } - return &meltQuoteResponse, nil } @@ -241,22 +122,10 @@ func PostMeltBolt11( mintURL string, meltRequest nut05.PostMeltBolt11Request, ) (*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 - if err := json.Unmarshal(body, &meltResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %w", err) + if err := httpPost(ctx, mintURL+"/v1/melt/bolt11", meltRequest, &meltResponse); err != nil { + return nil, err } - return &meltResponse, nil } @@ -265,22 +134,10 @@ func PostCheckProofState( mintURL string, stateRequest nut07.PostCheckStateRequest, ) (*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 - if err := json.Unmarshal(body, &stateResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v", err) + if err := httpPost(ctx, mintURL+"/v1/checkstate", stateRequest, &stateResponse); err != nil { + return nil, err } - return &stateResponse, nil } @@ -289,74 +146,71 @@ func PostRestore( mintURL string, restoreRequest nut09.PostRestoreRequest, ) (*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 - if err := json.Unmarshal(body, &restoreResponse); err != nil { - return nil, fmt.Errorf("error reading response from mint: %v", err) + if err := httpPost(ctx, mintURL+"/v1/restore", restoreRequest, &restoreResponse); err != nil { + return nil, err } - 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) if err != nil { - return nil, err + return err } resp, err := http.DefaultClient.Do(req) 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() - json.NewEncoder(w).Encode(data) + go func() { + json.NewEncoder(w).Encode(data) + w.Close() + }() req, err := http.NewRequestWithContext(ctx, "POST", url, r) if err != nil { - return nil, err + return err } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) 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 { var errResponse cashu.Error err := json.NewDecoder(response.Body).Decode(&errResponse) 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 { body, err := io.ReadAll(response.Body) 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 } diff --git a/nip60/lib.go b/nip60/lib.go index 32056cf..754ba1c 100644 --- a/nip60/lib.go +++ b/nip60/lib.go @@ -3,9 +3,11 @@ package nip60 import ( "context" "fmt" + "iter" "strings" "sync" + "github.com/btcsuite/btcd/btcec/v2" "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 } +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 { return &WalletStash{ wallets: make(map[string]*Wallet, 1), @@ -28,7 +64,7 @@ func NewStash() *WalletStash { func LoadStash( ctx context.Context, kr nostr.Keyer, - events <-chan *nostr.Event, + events <-chan nostr.RelayEvent, errors chan<- error, ) *WalletStash { wl := &WalletStash{ @@ -38,15 +74,15 @@ func LoadStash( } go func() { - for evt := range events { + for ie := range events { wl.Lock() - switch evt.Kind { + switch ie.Event.Kind { case 37375: wallet := &Wallet{} - if err := wallet.parse(ctx, kr, evt); err != nil { + if err := wallet.parse(ctx, kr, ie.Event); err != nil { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s failed: %w", evt, err) + errors <- fmt.Errorf("event %s failed: %w", ie.Event, err) } continue } @@ -61,11 +97,11 @@ func LoadStash( wl.wallets[wallet.Identifier] = wallet case 7375: // token - ref := evt.Tags.GetFirst([]string{"a", ""}) + ref := ie.Event.Tags.GetFirst([]string{"a", ""}) if ref == nil { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s missing 'a' tag", evt) + errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event) } continue } @@ -73,16 +109,16 @@ func LoadStash( if len(spl) < 3 { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s invalid 'a' tag", evt) + errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event) } continue } token := Token{} - if err := token.parse(ctx, kr, evt); err != nil { + if err := token.parse(ctx, kr, ie.Event); err != nil { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s failed: %w", evt, err) + errors <- fmt.Errorf("event %s failed: %w", ie.Event, err) } continue } @@ -94,11 +130,11 @@ func LoadStash( } case 7376: // history - ref := evt.Tags.GetFirst([]string{"a", ""}) + ref := ie.Event.Tags.GetFirst([]string{"a", ""}) if ref == nil { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s missing 'a' tag", evt) + errors <- fmt.Errorf("event %s missing 'a' tag", ie.Event) } continue } @@ -106,16 +142,16 @@ func LoadStash( if len(spl) < 3 { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s invalid 'a' tag", evt) + errors <- fmt.Errorf("event %s invalid 'a' tag", ie.Event) } continue } he := HistoryEntry{} - if err := he.parse(ctx, kr, evt); err != nil { + if err := he.parse(ctx, kr, ie.Event); err != nil { wl.Unlock() if errors != nil { - errors <- fmt.Errorf("event %s failed: %w", evt, err) + errors <- fmt.Errorf("event %s failed: %w", ie.Event, err) } continue } diff --git a/nip60/nip60_test.go b/nip60/nip60_test.go index 97cc36e..c672a9d 100644 --- a/nip60/nip60_test.go +++ b/nip60/nip60_test.go @@ -116,11 +116,11 @@ func TestWalletRoundtrip(t *testing.T) { for _, allEvents := range [][]nostr.Event{allEvents, reversedAllEvents} { // create channel and feed events into it - eventChan := make(chan *nostr.Event) + eventChan := make(chan nostr.RelayEvent) done := make(chan struct{}) go func() { for _, evt := range allEvents { - eventChan <- &evt + eventChan <- nostr.RelayEvent{Event: &evt} } close(eventChan) done <- struct{}{} diff --git a/nip60/receive.go b/nip60/receive.go index 4792df9..7e012ed 100644 --- a/nip60/receive.go +++ b/nip60/receive.go @@ -28,20 +28,19 @@ func (w *Wallet) ReceiveToken(ctx context.Context, serializedToken string) error for i, proof := range proofs { if proof.Secret != "" { nut10Secret, err := nut10.DeserializeSecret(proof.Secret) - if err != nil { - return fmt.Errorf("invalid nip10 secret at %d: %w", i, err) - } - switch nut10Secret.Kind { - case nut10.P2PK: - isp2pk = true - proofs[i].Witness, err = signInput(w.PrivateKey, w.PublicKey, proof, nut10Secret) - if err != nil { - return fmt.Errorf("failed to sign locked proof %d: %w", i, err) + if err == nil { + switch nut10Secret.Kind { + case nut10.P2PK: + isp2pk = true + proofs[i].Witness, err = signInput(w.PrivateKey, w.PublicKey, proof, nut10Secret) + 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) 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) diff --git a/nip60/wallet.go b/nip60/wallet.go index d923073..4121115 100644 --- a/nip60/wallet.go +++ b/nip60/wallet.go @@ -34,7 +34,18 @@ func (w Wallet) Balance() uint64 { 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{ CreatedAt: nostr.Now(), Kind: 37375, @@ -162,7 +173,6 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er case "relay": w.Relays = append(w.Relays, tag[1]) case "mint": - essential++ w.Mints = append(w.Mints, tag[1]) case "privkey": essential++ @@ -187,8 +197,8 @@ func (w *Wallet) parse(ctx context.Context, kr nostr.Keyer, evt *nostr.Event) er } } - if essential < 4 { - return fmt.Errorf("missing essential tags %s", evt) + if essential != 3 { + return fmt.Errorf("missing essential tags") } return nil