From d3154da81c179cca3171d723cf59f5a5b38c34e0 Mon Sep 17 00:00:00 2001 From: naliyi <154817482+naliyi@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:48:44 +0000 Subject: [PATCH 1/4] fix(nipcash): stop assuming cash_consolidate delivery is keyed like cash_transfer's CashConsolidateParams.ParseResult unconditionally decrypted new_wallet_token as if it were keyed to the caller, like cash_transfer. cash_consolidate actually keys it to the target: plaintext for a bearer/connection_key target, encrypted to that pubkey for a third-party target. Both cases failed client-side with a decrypt error even though the merge itself already succeeded on the Hub. ParseResult now only decrypts when the target is the caller's own pubkey (added Credential.ownIdentityPubkey to check this without building a new proof), and otherwise passes the token through as received. --- CHANGELOG.md | 13 ++++ nipcash/cash_consolidate.go | 46 ++++++++---- nipcash/cash_consolidate_test.go | 120 ++++++++++++++++++++++++++++++- nipcash/identity.go | 10 +++ nipcash/proof.go | 24 +++++++ 5 files changed, 199 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36c242c..216a39f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [0.3.1] + +### Fixed + +- `nipcash.CashConsolidateParams.ParseResult` always decrypted + `new_wallet_token` as if it were keyed to the caller, like + `cash_transfer`. `cash_consolidate` actually keys it to the target: + plaintext for a bearer/connection_key target, encrypted to that pubkey + for a third-party target. Both cases failed with a decrypt error even + though the merge itself succeeded. `ParseResult` now only decrypts when + the target is the caller's own pubkey, and otherwise passes the token + through as received. + ## [0.3.0] ### Added diff --git a/nipcash/cash_consolidate.go b/nipcash/cash_consolidate.go index 355e3b1..effa541 100644 --- a/nipcash/cash_consolidate.go +++ b/nipcash/cash_consolidate.go @@ -109,13 +109,22 @@ type CashConsolidateResult struct { ExpiresAt *int64 } -// ParseResult parses cash_consolidate's wire response, decrypting -// NewWalletToken with the first source's own Credential — any source's -// decryptDelivery derives the identical key, since the inner delivery layer -// is keyed to the caller's own real identity privkey and the new wallet's -// pubkey, not to any one specific source. Exported for nipcash/client's -// use; a caller using nipcash/client's CashConsolidate method never calls -// this directly. +// ParseResult parses cash_consolidate's wire response. Unlike cash_transfer, +// cash_consolidate keys new_wallet_token's delivery to the TARGET (p.To), +// not the caller: a bearer/connection_key target has no real pubkey yet, so +// the Hub sends the token in the clear; a pubkey target gets it encrypted to +// THAT pubkey. A source's Credential can only ever derive the caller's own +// delivery key, so decrypting only makes sense when the target IS the +// caller's own pubkey (the ordinary "merge my own slices" case) — anything +// else is handled without attempting decryption: +// - bearer/connection_key target: NewWalletToken is already plaintext. +// - third-party pubkey target: the caller structurally cannot decrypt this +// (it's encrypted to the recipient, not them) — the raw ciphertext is +// preserved as-is rather than erroring, since the merge itself already +// succeeded; only the real target's own client can read it. +// +// Exported for nipcash/client's use; a caller using nipcash/client's +// CashConsolidate method never calls this directly. func (p CashConsolidateParams) ParseResult(data []byte) (*CashConsolidateResult, error) { var wire cashConsolidateResponseWire if err := json.Unmarshal(data, &wire); err != nil { @@ -126,12 +135,23 @@ func (p CashConsolidateParams) ParseResult(data []byte) (*CashConsolidateResult, NewWalletPubkey: wire.NewWalletPubkey, ExpiresAt: wire.ExpiresAt, } - if wire.NewWalletToken != "" && len(p.Sources) > 0 { - token, err := p.Sources[0].Credential.decryptDelivery(wire.NewWalletPubkey, wire.NewWalletToken) - if err != nil { - return nil, err - } - result.NewWalletToken = token + if wire.NewWalletToken == "" || len(p.Sources) == 0 { + return result, nil + } + result.NewWalletToken = wire.NewWalletToken + + if !IsPubkeyTarget(p.To) { + return result, nil + } + targetPubkey := p.To.(targetFields).identityValue() + ownPubkey, ok := p.Sources[0].Credential.ownIdentityPubkey() + if !ok || ownPubkey != targetPubkey { + return result, nil + } + token, err := p.Sources[0].Credential.decryptDelivery(wire.NewWalletPubkey, wire.NewWalletToken) + if err != nil { + return nil, err } + result.NewWalletToken = token return result, nil } diff --git a/nipcash/cash_consolidate_test.go b/nipcash/cash_consolidate_test.go index 36a6b7b..5e9c13a 100644 --- a/nipcash/cash_consolidate_test.go +++ b/nipcash/cash_consolidate_test.go @@ -1,6 +1,9 @@ package nipcash -import "testing" +import ( + "encoding/json" + "testing" +) func TestCashConsolidateParams_Request_TooFewSources(t *testing.T) { privKeyHex, _ := generateTestKeypair(t) @@ -146,3 +149,118 @@ func TestByProof_MalformedJSON(t *testing.T) { t.Fatal("expected an error for malformed captured proof JSON") } } + +// cash_consolidate keys new_wallet_token's delivery to the target, not the +// caller — ParseResult must only decrypt when the target is the caller's +// own pubkey, and otherwise return the wire value as-is (plaintext for +// bearer/connection_key targets, still-opaque ciphertext for a third +// party's pubkey) without erroring. + +func TestCashConsolidateParams_ParseResult_SelfTarget_Decrypts(t *testing.T) { + callerPrivHex, callerPubHex := generateTestKeypair(t) + newWalletPrivHex, newWalletPubHex := generateTestKeypair(t) + + // Self-consolidate: new_identity == the caller's own pubkey, so the Hub + // encrypts to the same pubkey the caller's own credential can decrypt + // with. + ciphertext := encryptForTest(t, newWalletPrivHex, callerPubHex, "lokicash1thetoken") + + p := CashConsolidateParams{ + Sources: []Source{From(randomKeyHex(t), 1000, BySigning(callerPrivHex))}, + To: Pubkey(callerPubHex), + } + raw, err := json.Marshal(cashConsolidateResponseWire{ + AmountMillis: 1000, + NewWalletPubkey: newWalletPubHex, + NewWalletToken: ciphertext, + }) + if err != nil { + t.Fatal(err) + } + result, err := p.ParseResult(raw) + if err != nil { + t.Fatalf("ParseResult: %v", err) + } + if result.NewWalletToken != "lokicash1thetoken" { + t.Fatalf("NewWalletToken: got %q, want decrypted plaintext", result.NewWalletToken) + } +} + +func TestCashConsolidateParams_ParseResult_ThirdPartyPubkeyTarget_NoDecryptAttempt(t *testing.T) { + callerPrivHex, _ := generateTestKeypair(t) + _, carolPubHex := generateTestKeypair(t) + newWalletPrivHex, newWalletPubHex := generateTestKeypair(t) + + // Encrypted to Carol (the target), not the caller — the caller's own + // credential cannot derive this key, so ParseResult must not even try. + ciphertext := encryptForTest(t, newWalletPrivHex, carolPubHex, "lokicash1forcarol") + + p := CashConsolidateParams{ + Sources: []Source{From(randomKeyHex(t), 1000, BySigning(callerPrivHex))}, + To: Pubkey(carolPubHex), + } + raw, err := json.Marshal(cashConsolidateResponseWire{ + AmountMillis: 1000, + NewWalletPubkey: newWalletPubHex, + NewWalletToken: ciphertext, + }) + if err != nil { + t.Fatal(err) + } + result, err := p.ParseResult(raw) + if err != nil { + t.Fatalf("ParseResult must not error on an undecryptable third-party delivery: %v", err) + } + if result.NewWalletToken != ciphertext { + t.Fatalf("NewWalletToken: got %q, want the raw ciphertext preserved as-is", result.NewWalletToken) + } +} + +func TestCashConsolidateParams_ParseResult_BearerTarget_PassesThroughPlaintext(t *testing.T) { + callerPrivHex, _ := generateTestKeypair(t) + bt := NewBearerTarget() + + p := CashConsolidateParams{ + Sources: []Source{From(randomKeyHex(t), 1000, BySigning(callerPrivHex))}, + To: bt, + } + raw, err := json.Marshal(cashConsolidateResponseWire{ + AmountMillis: 1000, + NewWalletPubkey: randomKeyHex(t), + NewWalletToken: "lokicash1plaintext", + }) + if err != nil { + t.Fatal(err) + } + result, err := p.ParseResult(raw) + if err != nil { + t.Fatalf("ParseResult: %v", err) + } + if result.NewWalletToken != "lokicash1plaintext" { + t.Fatalf("NewWalletToken: got %q, want the plaintext value passed through unchanged", result.NewWalletToken) + } +} + +func TestCashConsolidateParams_ParseResult_ConnectionKeyTarget_PassesThroughPlaintext(t *testing.T) { + callerPrivHex, _ := generateTestKeypair(t) + + p := CashConsolidateParams{ + Sources: []Source{From(randomKeyHex(t), 1000, BySigning(callerPrivHex))}, + To: ConnectionKey("discord", "someone", "iapub"), + } + raw, err := json.Marshal(cashConsolidateResponseWire{ + AmountMillis: 1000, + NewWalletPubkey: randomKeyHex(t), + NewWalletToken: "lokicash1plaintext", + }) + if err != nil { + t.Fatal(err) + } + result, err := p.ParseResult(raw) + if err != nil { + t.Fatalf("ParseResult: %v", err) + } + if result.NewWalletToken != "lokicash1plaintext" { + t.Fatalf("NewWalletToken: got %q, want the plaintext value passed through unchanged", result.NewWalletToken) + } +} diff --git a/nipcash/identity.go b/nipcash/identity.go index e5b3ebf..0b52dd0 100644 --- a/nipcash/identity.go +++ b/nipcash/identity.go @@ -212,4 +212,14 @@ type Credential interface { // requires this case deliver in the clear instead — see NIP-CASH // §Spinning a Slice Off's own "bearer-current caller" paragraph). decryptDelivery(newWalletPubkey, ciphertext string) (string, error) + + // ownIdentityPubkey reports this credential's own real Nostr pubkey, if + // it has one to disclose without building a new proof — true only for + // BySigning/BySigningConnectionKey (derived from their held privkey). + // BySecret has no pubkey at all, and ByProof holds a captured proof but + // never the private key behind it. Used only by CashConsolidateParams. + // ParseResult, to tell a self-targeted pubkey consolidate (decryptable) + // apart from a third-party one (structurally not — see that method's + // own doc comment). + ownIdentityPubkey() (pubkey string, ok bool) } diff --git a/nipcash/proof.go b/nipcash/proof.go index cb5134c..1ddec39 100644 --- a/nipcash/proof.go +++ b/nipcash/proof.go @@ -126,6 +126,10 @@ func (secretCredential) decryptDelivery(_, ciphertext string) (string, error) { return ciphertext, nil } +// ownIdentityPubkey has no real pubkey — a bearer-current caller's proof is +// their raw secret, not a signing key. +func (secretCredential) ownIdentityPubkey() (string, bool) { return "", false } + // --- BySigning: pubkey credential --- type signingCredential struct{ privKeyHex string } @@ -155,6 +159,14 @@ func (c signingCredential) decryptDelivery(newWalletPubkey, ciphertext string) ( return decryptFromPubkey(c.privKeyHex, newWalletPubkey, ciphertext) } +func (c signingCredential) ownIdentityPubkey() (string, bool) { + pubkey, err := utils.GetPublicKey(c.privKeyHex) + if err != nil { + return "", false + } + return pubkey, true +} + // --- BySigningConnectionKey: connection_key credential --- type connectionKeyCredential struct { @@ -204,6 +216,14 @@ func (c connectionKeyCredential) decryptDelivery(newWalletPubkey, ciphertext str return decryptFromPubkey(c.privKeyHex, newWalletPubkey, ciphertext) } +func (c connectionKeyCredential) ownIdentityPubkey() (string, bool) { + pubkey, err := utils.GetPublicKey(c.privKeyHex) + if err != nil { + return "", false + } + return pubkey, true +} + // --- ByProof: a proof captured earlier, not built from a live signing key --- type proofCredential struct { @@ -240,3 +260,7 @@ func (c proofCredential) buildProof(proofBinding) (identityType, identityValue s func (proofCredential) decryptDelivery(string, string) (string, error) { return "", errors.New("nipcash: a ByProof credential has no private key and cannot decrypt a delivery") } + +// ownIdentityPubkey has no real pubkey either — ByProof never holds the +// private key behind the proof it captured, only the proof itself. +func (proofCredential) ownIdentityPubkey() (string, bool) { return "", false } From abb370131b00f73d446754e288adfdef7ac0b4f6 Mon Sep 17 00:00:00 2001 From: naliyi <154817482+naliyi@users.noreply.github.com> Date: Mon, 21 Sep 2026 12:49:08 +0000 Subject: [PATCH 2/4] feat(nip57,utils): export LUD-06 chain parsing and add a zap invoice helper Closes the private-vs-exported gap from issue #27: relay/verification.go's profile worker already parsed a LUD-06 pay response's chain/* metadata entries, but only for its own internal score, with no way for a client to reuse that logic. utils.ParsePayMetadataChains and utils.FetchLud16PayResponse export it directly; verification.go now calls the exported helper instead of keeping its own copy. Also adds nip57.RequestZapInvoice, a high-level zap helper: resolve a recipient's LUD-16 address, build and sign the zap request, and fetch back an invoice, in one call. ZapRequestParams gained a Content field so a zap can carry a comment. --- CHANGELOG.md | 15 ++++ nip57/nip57.go | 3 +- nip57/zap_invoice.go | 130 +++++++++++++++++++++++++++++++++ nip57/zap_invoice_test.go | 147 ++++++++++++++++++++++++++++++++++++++ relay/verification.go | 40 +---------- utils/lnurl.go | 14 ++++ utils/lud06.go | 101 ++++++++++++++++++++++++++ utils/lud06_test.go | 114 +++++++++++++++++++++++++++++ 8 files changed, 525 insertions(+), 39 deletions(-) create mode 100644 nip57/zap_invoice.go create mode 100644 nip57/zap_invoice_test.go create mode 100644 utils/lud06.go create mode 100644 utils/lud06_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 216a39f..c1ae92c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ ## [0.3.1] +### Added + +- `utils.ParsePayMetadataChains`/`utils.FetchLud16PayResponse`: parse a + LUD-06 pay response and list the Lightning-routable chains it + advertises. Previously only available privately inside the relay's + own profile-verification worker. +- `nip57.RequestZapInvoice`: a high-level zap helper — resolves a + recipient's LUD-16 address, builds and signs the zap request, and + fetches an invoice back in one call. + +### Changed + +- `nip57.ZapRequestParams` gained a `Content` field, so a zap request + can carry an optional public comment. + ### Fixed - `nipcash.CashConsolidateParams.ParseResult` always decrypted diff --git a/nip57/nip57.go b/nip57/nip57.go index ae1c264..4bc68df 100644 --- a/nip57/nip57.go +++ b/nip57/nip57.go @@ -296,6 +296,7 @@ type ZapRequestParams struct { EventID *string // optional "e" tag — zapped event ID ATag string // optional "a" tag — zapped addressable event coordinate KTag string // optional "k" tag — zapped event's kind + Content string // optional public note attached to the zap } // NewZapRequest creates a new NIP-57 zap request event (kind 9734). @@ -328,7 +329,7 @@ func NewZapRequest(p ZapRequestParams) *nip01.Event { CreatedAt: uint64(time.Now().Unix()), Kind: KindZapRequest, Tags: tags, - Content: "", + Content: p.Content, } } diff --git a/nip57/zap_invoice.go b/nip57/zap_invoice.go new file mode 100644 index 0000000..d88ffcf --- /dev/null +++ b/nip57/zap_invoice.go @@ -0,0 +1,130 @@ +package nip57 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/ohstr/nmilat/utils" +) + +// Failure modes specific to RequestZapInvoice. +var ( + ErrRecipientDoesNotAcceptZaps = errors.New("nip57: recipient's LNURL provider does not support zaps") + ErrAmountOutOfRange = errors.New("nip57: amount outside the provider's min/maxSendable range") + ErrCallbackFailed = errors.New("nip57: callback did not return an invoice") +) + +// RequestZapInvoiceParams describes a zap payment: who to zap, how much, and +// what the resulting invoice should carry. +type RequestZapInvoiceParams struct { + SenderPrivateKey string // nsec hex, signs the zap request + RecipientPubkey string // recipient's Nostr pubkey — "p" tag + RecipientLud16 string // recipient's LUD-16 identifier, resolved for the LNURL provider + AmountMsat int64 // required + Relays []string // relays the zap receipt should be published to + Comment string // optional public note attached to the zap + EventID *string // optional "e" tag — zapped event ID +} + +// RequestZapInvoiceResult is a zap invoice request's outcome. +type RequestZapInvoiceResult struct { + Bolt11 string + PayResponse *utils.PayResponse // the recipient's LNURL provider info, e.g. to inspect Chains +} + +// RequestZapInvoice runs the client side of a zap payment up to "pay this +// invoice" (NIP-57 Appendix A): resolve the recipient's LUD-16 identifier, +// build and sign a zap request naming that provider (kind 9734), and call +// back for an invoice. It does not pay the invoice or wait for a receipt — +// pay Bolt11 with your own Lightning client, then verify the resulting kind +// 9735 receipt with ValidateZapReceipt. +func RequestZapInvoice(ctx context.Context, client *http.Client, p RequestZapInvoiceParams) (*RequestZapInvoiceResult, error) { + if p.AmountMsat <= 0 { + return nil, ErrInvalidAmountValue + } + + payResp, err := utils.FetchLud16PayResponse(ctx, client, p.RecipientLud16) + if err != nil { + return nil, err + } + if !payResp.AllowsNostr || payResp.NostrPubkey == "" { + return nil, ErrRecipientDoesNotAcceptZaps + } + if payResp.MinSendable > 0 && p.AmountMsat < payResp.MinSendable { + return nil, fmt.Errorf("%w: %d msat below minimum %d", ErrAmountOutOfRange, p.AmountMsat, payResp.MinSendable) + } + if payResp.MaxSendable > 0 && p.AmountMsat > payResp.MaxSendable { + return nil, fmt.Errorf("%w: %d msat above maximum %d", ErrAmountOutOfRange, p.AmountMsat, payResp.MaxSendable) + } + + bech32LNURL, err := utils.EncodeLNURL(utils.GetLud16URL(p.RecipientLud16)) + if err != nil { + return nil, fmt.Errorf("encode lnurl: %w", err) + } + + event := NewZapRequest(ZapRequestParams{ + Recipient: p.RecipientPubkey, + Relays: p.Relays, + AmountMsat: p.AmountMsat, + Lnurl: bech32LNURL, + EventID: p.EventID, + Content: p.Comment, + }) + if err := event.Sign(p.SenderPrivateKey); err != nil { + return nil, fmt.Errorf("sign zap request: %w", err) + } + eventJSON, err := json.Marshal(event) + if err != nil { + return nil, fmt.Errorf("marshal zap request: %w", err) + } + + bolt11, err := requestInvoice(ctx, client, payResp.Callback, p.AmountMsat, string(eventJSON), bech32LNURL) + if err != nil { + return nil, err + } + return &RequestZapInvoiceResult{Bolt11: bolt11, PayResponse: payResp}, nil +} + +// requestInvoice calls an LNURL-pay callback with the zap request attached, +// per NIP-57 Appendix A / LUD-06. +func requestInvoice(ctx context.Context, client *http.Client, callback string, amountMsat int64, zapRequestJSON, lnurl string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, callback, nil) + if err != nil { + return "", err + } + q := req.URL.Query() + q.Set("amount", fmt.Sprintf("%d", amountMsat)) + q.Set("nostr", zapRequestJSON) + q.Set("lnurl", lnurl) + req.URL.RawQuery = q.Encode() + + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("%w: status %d", ErrCallbackFailed, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + var wire struct { + PR string `json:"pr"` + Status string `json:"status"` + Reason string `json:"reason"` + } + if err := json.Unmarshal(body, &wire); err != nil { + return "", fmt.Errorf("%w: parse response: %w", ErrCallbackFailed, err) + } + if wire.Status == "ERROR" || wire.PR == "" { + return "", fmt.Errorf("%w: %s", ErrCallbackFailed, wire.Reason) + } + return wire.PR, nil +} diff --git a/nip57/zap_invoice_test.go b/nip57/zap_invoice_test.go new file mode 100644 index 0000000..1d40b0c --- /dev/null +++ b/nip57/zap_invoice_test.go @@ -0,0 +1,147 @@ +package nip57 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +// redirectToServerTransport rewrites every request's scheme/host to server's, +// so code that builds a fixed https:///... URL (GetLud16URL) can +// still be exercised against an httptest.Server. +type redirectToServerTransport struct{ server *httptest.Server } + +func (rt redirectToServerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + serverURL, _ := req.URL.Parse(rt.server.URL) + clone := req.Clone(req.Context()) + clone.URL.Scheme = serverURL.Scheme + clone.URL.Host = serverURL.Host + clone.Host = "" + return http.DefaultTransport.RoundTrip(clone) +} + +func TestRequestZapInvoice_HappyPath(t *testing.T) { + recipientPubkey := "0000000000000000000000000000000000000000000000000000000000000001" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/lnurlp/alice": + _, _ = w.Write([]byte(fmt.Sprintf(`{ + "callback": "%s/callback", + "minSendable": 1000, + "maxSendable": 100000000, + "metadata": "[[\"chain/flokicoin\",\"\"]]", + "allowsNostr": true, + "nostrPubkey": "%s" + }`, "http://"+r.Host, recipientPubkey))) + case "/callback": + nostrEvent := r.URL.Query().Get("nostr") + var event struct { + Kind int `json:"kind"` + } + if err := json.Unmarshal([]byte(nostrEvent), &event); err != nil || event.Kind != KindZapRequest { + w.WriteHeader(http.StatusBadRequest) + return + } + _, _ = w.Write([]byte(`{"pr":"lnbc1invoice"}`)) + default: + w.WriteHeader(http.StatusNotFound) + } + })) + defer server.Close() + + client := &http.Client{Transport: redirectToServerTransport{server: server}} + result, err := RequestZapInvoice(context.Background(), client, RequestZapInvoiceParams{ + SenderPrivateKey: zapsTestPrivKey, + RecipientPubkey: recipientPubkey, + RecipientLud16: "alice@example.com", + AmountMsat: 5000, + Relays: []string{"wss://relay.example.com"}, + }) + if err != nil { + t.Fatalf("RequestZapInvoice: %v", err) + } + if result.Bolt11 != "lnbc1invoice" { + t.Fatalf("Bolt11: got %q", result.Bolt11) + } + if len(result.PayResponse.Chains) != 1 || result.PayResponse.Chains[0] != "flokicoin" { + t.Fatalf("PayResponse.Chains: got %v, want [flokicoin]", result.PayResponse.Chains) + } +} + +func TestRequestZapInvoice_RecipientDoesNotAcceptZaps(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"callback":"https://example.com/callback","allowsNostr":false}`)) + })) + defer server.Close() + + client := &http.Client{Transport: redirectToServerTransport{server: server}} + _, err := RequestZapInvoice(context.Background(), client, RequestZapInvoiceParams{ + SenderPrivateKey: zapsTestPrivKey, + RecipientPubkey: "0000000000000000000000000000000000000000000000000000000000000001", + RecipientLud16: "alice@example.com", + AmountMsat: 5000, + Relays: []string{"wss://relay.example.com"}, + }) + if err == nil { + t.Fatal("expected an error when the provider doesn't support zaps") + } +} + +func TestRequestZapInvoice_AmountOutOfRange(t *testing.T) { + recipientPubkey := "0000000000000000000000000000000000000000000000000000000000000001" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(fmt.Sprintf(`{ + "callback": "https://example.com/callback", + "minSendable": 10000, + "maxSendable": 100000, + "allowsNostr": true, + "nostrPubkey": "%s" + }`, recipientPubkey))) + })) + defer server.Close() + + client := &http.Client{Transport: redirectToServerTransport{server: server}} + _, err := RequestZapInvoice(context.Background(), client, RequestZapInvoiceParams{ + SenderPrivateKey: zapsTestPrivKey, + RecipientPubkey: recipientPubkey, + RecipientLud16: "alice@example.com", + AmountMsat: 5000, // below minSendable + Relays: []string{"wss://relay.example.com"}, + }) + if err == nil { + t.Fatal("expected an error for an amount below minSendable") + } +} + +func TestRequestZapInvoice_CallbackError(t *testing.T) { + recipientPubkey := "0000000000000000000000000000000000000000000000000000000000000001" + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/lnurlp/alice": + _, _ = w.Write([]byte(fmt.Sprintf(`{ + "callback": "%s/callback", + "allowsNostr": true, + "nostrPubkey": "%s" + }`, "http://"+r.Host, recipientPubkey))) + case "/callback": + _, _ = w.Write([]byte(`{"status":"ERROR","reason":"amount too small"}`)) + } + })) + defer server.Close() + + client := &http.Client{Transport: redirectToServerTransport{server: server}} + _, err := RequestZapInvoice(context.Background(), client, RequestZapInvoiceParams{ + SenderPrivateKey: zapsTestPrivKey, + RecipientPubkey: recipientPubkey, + RecipientLud16: "alice@example.com", + AmountMsat: 5000, + Relays: []string{"wss://relay.example.com"}, + }) + if err == nil { + t.Fatal("expected an error when the callback reports ERROR") + } +} diff --git a/relay/verification.go b/relay/verification.go index 541b21d..ed40891 100644 --- a/relay/verification.go +++ b/relay/verification.go @@ -3,9 +3,7 @@ package relay import ( "context" "fmt" - "io" "net/http" - "strings" "sync" "sync/atomic" "time" @@ -151,42 +149,8 @@ func (w *ProfileVerificationWorker) processJob(job VerificationJob) { wg.Add(1) go func() { defer wg.Done() - url := utils.GetLud16URL(job.Lud16) - if url != "" { - req, _ := http.NewRequestWithContext(w.ctx, "GET", url, nil) - if resp, err := w.httpClient.Do(req); err == nil { - defer func() { _ = resp.Body.Close() }() - if resp.StatusCode == http.StatusOK { - body, err := io.ReadAll(resp.Body) - if err == nil { - var payResponse struct { - Metadata string `json:"metadata"` - } - if err := utils.UnmarshalJSON(body, &payResponse); err != nil { - // Alternative: some services return the metadata directly as an array or object - // but LUD-06 says it's inside a metadata field as a string. - } else { - var metadata [][]interface{} - if err := utils.UnmarshalJSON([]byte(payResponse.Metadata), &metadata); err == nil { - chainCount := 0 - for _, item := range metadata { - if len(item) > 0 { - if tag, ok := item[0].(string); ok && strings.HasPrefix(tag, "chain/") { - chainCount++ - } - } - } - if chainCount == 0 { - // Bitcoin is assumed if no chain tags provided - ludChains = 1 - } else { - ludChains = chainCount - } - } - } - } - } - } + if payResp, err := utils.FetchLud16PayResponse(w.ctx, w.httpClient, job.Lud16); err == nil { + ludChains = len(payResp.Chains) } }() } diff --git a/utils/lnurl.go b/utils/lnurl.go index 177493d..e69ed36 100644 --- a/utils/lnurl.go +++ b/utils/lnurl.go @@ -45,3 +45,17 @@ func ValidateLNURL(lnurlStr string) error { return nil } + +// EncodeLNURL bech32-encodes rawURL as an "lnurl1..." string — the inverse +// of ValidateLNURL's decode step. +func EncodeLNURL(rawURL string) (string, error) { + bits5, err := bech32.ConvertBits([]byte(rawURL), 8, 5, true) + if err != nil { + return "", fmt.Errorf("failed to convert bits: %w", err) + } + encoded, err := bech32.Encode("lnurl", bits5) + if err != nil { + return "", fmt.Errorf("failed to encode bech32: %w", err) + } + return encoded, nil +} diff --git a/utils/lud06.go b/utils/lud06.go new file mode 100644 index 0000000..64453ac --- /dev/null +++ b/utils/lud06.go @@ -0,0 +1,101 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +// PayResponse is a LUD-06 payRequest document (a LUD-16 identifier's +// well-known endpoint), plus NIP-57's allowsNostr/nostrPubkey fields and the +// Zap Protocol's chain/* metadata entries already parsed out. +type PayResponse struct { + Callback string + MinSendable int64 + MaxSendable int64 + AllowsNostr bool + NostrPubkey string + Chains []string // never empty; see ParsePayMetadataChains +} + +// ParsePayMetadataChains extracts the Lightning-routable chains a LUD-06 +// payRequest's "metadata" field advertises (the Zap Protocol's "chain/" +// entries, e.g. "chain/flokicoin"). metadata is the raw metadata string +// exactly as the payRequest response carries it — a JSON-encoded array, not +// a nested array itself. Returns ["bitcoin"] when no chain entry is present, +// or when metadata is malformed. +func ParsePayMetadataChains(metadata string) []string { + var entries [][]any + if err := json.Unmarshal([]byte(metadata), &entries); err != nil { + return []string{"bitcoin"} + } + var chains []string + for _, entry := range entries { + if len(entry) == 0 { + continue + } + tag, ok := entry[0].(string) + if !ok || !strings.HasPrefix(tag, "chain/") { + continue + } + chains = append(chains, strings.TrimPrefix(tag, "chain/")) + } + if len(chains) == 0 { + return []string{"bitcoin"} + } + return chains +} + +// FetchLud16PayResponse fetches and parses a LUD-16 identifier's LNURL-pay +// document. Returns an error only for a transport failure, a non-200 +// response, or a document missing its callback URL; a malformed metadata +// field degrades to Chains = ["bitcoin"] instead of failing the whole fetch. +func FetchLud16PayResponse(ctx context.Context, client *http.Client, lud16 string) (*PayResponse, error) { + endpoint := GetLud16URL(lud16) + if endpoint == "" { + return nil, fmt.Errorf("invalid lud16 identifier %q", lud16) + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("lud16 %q: unexpected status %d", lud16, resp.StatusCode) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var wire struct { + Callback string `json:"callback"` + MinSendable int64 `json:"minSendable"` + MaxSendable int64 `json:"maxSendable"` + Metadata string `json:"metadata"` + AllowsNostr bool `json:"allowsNostr"` + NostrPubkey string `json:"nostrPubkey"` + } + if err := json.Unmarshal(body, &wire); err != nil { + return nil, fmt.Errorf("lud16 %q: parse pay response: %w", lud16, err) + } + if wire.Callback == "" { + return nil, fmt.Errorf("lud16 %q: pay response has no callback", lud16) + } + + return &PayResponse{ + Callback: wire.Callback, + MinSendable: wire.MinSendable, + MaxSendable: wire.MaxSendable, + AllowsNostr: wire.AllowsNostr, + NostrPubkey: wire.NostrPubkey, + Chains: ParsePayMetadataChains(wire.Metadata), + }, nil +} diff --git a/utils/lud06_test.go b/utils/lud06_test.go new file mode 100644 index 0000000..acf5125 --- /dev/null +++ b/utils/lud06_test.go @@ -0,0 +1,114 @@ +package utils + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +func TestParsePayMetadataChains_DefaultsToBitcoin(t *testing.T) { + got := ParsePayMetadataChains(`[["text/plain","a description"]]`) + if len(got) != 1 || got[0] != "bitcoin" { + t.Fatalf("got %v, want [bitcoin]", got) + } +} + +func TestParsePayMetadataChains_MalformedDefaultsToBitcoin(t *testing.T) { + got := ParsePayMetadataChains("not json") + if len(got) != 1 || got[0] != "bitcoin" { + t.Fatalf("got %v, want [bitcoin]", got) + } +} + +func TestParsePayMetadataChains_ExtractsChainEntries(t *testing.T) { + got := ParsePayMetadataChains(`[["text/plain","a description"],["chain/flokicoin",""],["chain/litecoin",""]]`) + want := []string{"flokicoin", "litecoin"} + if len(got) != len(want) { + t.Fatalf("got %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("got %v, want %v", got, want) + } + } +} + +func TestFetchLud16PayResponse_HappyPath(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/.well-known/lnurlp/alice" { + t.Fatalf("unexpected path: %s", r.URL.Path) + } + _, _ = w.Write([]byte(`{ + "callback": "https://example.com/callback", + "minSendable": 1000, + "maxSendable": 100000000, + "metadata": "[[\"chain/flokicoin\",\"\"]]", + "allowsNostr": true, + "nostrPubkey": "abc123" + }`)) + })) + defer server.Close() + + resp, err := fetchTestPayResponse(t, server, "alice") + if err != nil { + t.Fatalf("FetchLud16PayResponse: %v", err) + } + if resp.Callback != "https://example.com/callback" { + t.Fatalf("Callback: got %q", resp.Callback) + } + if !resp.AllowsNostr || resp.NostrPubkey != "abc123" { + t.Fatalf("AllowsNostr/NostrPubkey: got %v/%q", resp.AllowsNostr, resp.NostrPubkey) + } + if len(resp.Chains) != 1 || resp.Chains[0] != "flokicoin" { + t.Fatalf("Chains: got %v, want [flokicoin]", resp.Chains) + } +} + +func TestFetchLud16PayResponse_MissingCallbackErrors(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"minSendable": 1000}`)) + })) + defer server.Close() + + if _, err := fetchTestPayResponse(t, server, "alice"); err == nil { + t.Fatal("expected an error for a pay response with no callback") + } +} + +func TestFetchLud16PayResponse_NonOKStatusErrors(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + if _, err := fetchTestPayResponse(t, server, "alice"); err == nil { + t.Fatal("expected an error for a non-200 response") + } +} + +// fetchTestPayResponse calls FetchLud16PayResponse against server by +// rewriting GetLud16URL's fixed https:///... shape onto server's own +// address — GetLud16URL always builds an https URL, so tests exercise the +// request-building and response-parsing directly against server's handler +// via a client whose transport redirects to it. +func fetchTestPayResponse(t *testing.T, server *httptest.Server, name string) (*PayResponse, error) { + t.Helper() + client := &http.Client{Transport: redirectToServerTransport{server: server}} + return FetchLud16PayResponse(context.Background(), client, name+"@example.com") +} + +type redirectToServerTransport struct { + server *httptest.Server +} + +func (rt redirectToServerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + target := *req.URL + serverURL, _ := req.URL.Parse(rt.server.URL) + target.Scheme = serverURL.Scheme + target.Host = serverURL.Host + clone := req.Clone(req.Context()) + clone.URL = &target + clone.Host = "" + return http.DefaultTransport.RoundTrip(clone) +} From 7d3c898ac4522308844b3b65c347263ebe4cf563 Mon Sep 17 00:00:00 2001 From: naliyi <154817482+naliyi@users.noreply.github.com> Date: Mon, 21 Sep 2026 18:01:57 +0000 Subject: [PATCH 3/4] test(nip57): use fmt.Fprintf instead of Write(Sprintf) in zap_invoice tests Fixes the three staticcheck QF1012 findings golangci-lint v2.13 reports on the RequestZapInvoice test handlers. --- nip57/zap_invoice_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nip57/zap_invoice_test.go b/nip57/zap_invoice_test.go index 1d40b0c..198656b 100644 --- a/nip57/zap_invoice_test.go +++ b/nip57/zap_invoice_test.go @@ -29,14 +29,14 @@ func TestRequestZapInvoice_HappyPath(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/.well-known/lnurlp/alice": - _, _ = w.Write([]byte(fmt.Sprintf(`{ + _, _ = fmt.Fprintf(w, `{ "callback": "%s/callback", "minSendable": 1000, "maxSendable": 100000000, "metadata": "[[\"chain/flokicoin\",\"\"]]", "allowsNostr": true, "nostrPubkey": "%s" - }`, "http://"+r.Host, recipientPubkey))) + }`, "http://"+r.Host, recipientPubkey) case "/callback": nostrEvent := r.URL.Query().Get("nostr") var event struct { @@ -94,13 +94,13 @@ func TestRequestZapInvoice_RecipientDoesNotAcceptZaps(t *testing.T) { func TestRequestZapInvoice_AmountOutOfRange(t *testing.T) { recipientPubkey := "0000000000000000000000000000000000000000000000000000000000000001" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(fmt.Sprintf(`{ + _, _ = fmt.Fprintf(w, `{ "callback": "https://example.com/callback", "minSendable": 10000, "maxSendable": 100000, "allowsNostr": true, "nostrPubkey": "%s" - }`, recipientPubkey))) + }`, recipientPubkey) })) defer server.Close() @@ -122,11 +122,11 @@ func TestRequestZapInvoice_CallbackError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/.well-known/lnurlp/alice": - _, _ = w.Write([]byte(fmt.Sprintf(`{ + _, _ = fmt.Fprintf(w, `{ "callback": "%s/callback", "allowsNostr": true, "nostrPubkey": "%s" - }`, "http://"+r.Host, recipientPubkey))) + }`, "http://"+r.Host, recipientPubkey) case "/callback": _, _ = w.Write([]byte(`{"status":"ERROR","reason":"amount too small"}`)) } From 6657f073746d9d6f103d2faaea895435494fa69a Mon Sep 17 00:00:00 2001 From: naliyi <154817482+naliyi@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:01:27 +0000 Subject: [PATCH 4/4] fix(nipcash): decrypt cash_consolidate delivery when possible, else keep the raw value Replaces the ownIdentityPubkey gating from the previous nipcash fix with a simpler rule that doesn't depend on which party the Hub keys a pubkey target's delivery to. ParseResult passes a bearer/connection_key target's token through unchanged (it arrives in the clear), and for a pubkey target always attempts to decrypt with the first source's credential. If decryption fails it preserves the raw value instead of returning an error, since the merge itself has already succeeded on the Hub. Drops Credential.ownIdentityPubkey and its implementations, adds a test for a third-party pubkey target whose delivery is encrypted to the caller, and rewords the 0.3.1 changelog entry to match. --- CHANGELOG.md | 14 +++++------ nipcash/cash_consolidate.go | 39 ++++++++++--------------------- nipcash/cash_consolidate_test.go | 40 ++++++++++++++++++++++++++++---- nipcash/identity.go | 10 -------- nipcash/proof.go | 24 ------------------- 5 files changed, 55 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1ae92c..0babae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,13 +20,13 @@ ### Fixed - `nipcash.CashConsolidateParams.ParseResult` always decrypted - `new_wallet_token` as if it were keyed to the caller, like - `cash_transfer`. `cash_consolidate` actually keys it to the target: - plaintext for a bearer/connection_key target, encrypted to that pubkey - for a third-party target. Both cases failed with a decrypt error even - though the merge itself succeeded. `ParseResult` now only decrypts when - the target is the caller's own pubkey, and otherwise passes the token - through as received. + `new_wallet_token` and returned an error if that failed, even though + the merge itself had already succeeded. A bearer/connection_key target + has no real pubkey yet, so the Hub delivers the token in the clear and + decryption always failed for it. `ParseResult` now passes that token + through unchanged; for a pubkey target it still decrypts with the first + source's credential, but a decryption failure now preserves the raw + value instead of returning an error. ## [0.3.0] diff --git a/nipcash/cash_consolidate.go b/nipcash/cash_consolidate.go index effa541..bdd570d 100644 --- a/nipcash/cash_consolidate.go +++ b/nipcash/cash_consolidate.go @@ -109,19 +109,15 @@ type CashConsolidateResult struct { ExpiresAt *int64 } -// ParseResult parses cash_consolidate's wire response. Unlike cash_transfer, -// cash_consolidate keys new_wallet_token's delivery to the TARGET (p.To), -// not the caller: a bearer/connection_key target has no real pubkey yet, so -// the Hub sends the token in the clear; a pubkey target gets it encrypted to -// THAT pubkey. A source's Credential can only ever derive the caller's own -// delivery key, so decrypting only makes sense when the target IS the -// caller's own pubkey (the ordinary "merge my own slices" case) — anything -// else is handled without attempting decryption: -// - bearer/connection_key target: NewWalletToken is already plaintext. -// - third-party pubkey target: the caller structurally cannot decrypt this -// (it's encrypted to the recipient, not them) — the raw ciphertext is -// preserved as-is rather than erroring, since the merge itself already -// succeeded; only the real target's own client can read it. +// ParseResult parses cash_consolidate's wire response, decrypting +// NewWalletToken with the first source's own Credential when the target is +// a pubkey — the Hub encrypts it to the caller, same as cash_transfer's own +// delivery. A bearer/connection_key target has no real pubkey yet, so the +// Hub sends the token in the clear instead; ParseResult passes it through +// unchanged rather than attempting decryption. If decryption ever fails +// (e.g. an older Hub still keying pubkey-target delivery some other way), +// the raw value is preserved as-is instead of erroring — the merge itself +// already succeeded. // // Exported for nipcash/client's use; a caller using nipcash/client's // CashConsolidate method never calls this directly. @@ -135,23 +131,12 @@ func (p CashConsolidateParams) ParseResult(data []byte) (*CashConsolidateResult, NewWalletPubkey: wire.NewWalletPubkey, ExpiresAt: wire.ExpiresAt, } - if wire.NewWalletToken == "" || len(p.Sources) == 0 { - return result, nil - } result.NewWalletToken = wire.NewWalletToken - - if !IsPubkeyTarget(p.To) { + if wire.NewWalletToken == "" || len(p.Sources) == 0 || !IsPubkeyTarget(p.To) { return result, nil } - targetPubkey := p.To.(targetFields).identityValue() - ownPubkey, ok := p.Sources[0].Credential.ownIdentityPubkey() - if !ok || ownPubkey != targetPubkey { - return result, nil - } - token, err := p.Sources[0].Credential.decryptDelivery(wire.NewWalletPubkey, wire.NewWalletToken) - if err != nil { - return nil, err + if token, err := p.Sources[0].Credential.decryptDelivery(wire.NewWalletPubkey, wire.NewWalletToken); err == nil { + result.NewWalletToken = token } - result.NewWalletToken = token return result, nil } diff --git a/nipcash/cash_consolidate_test.go b/nipcash/cash_consolidate_test.go index 5e9c13a..9276cad 100644 --- a/nipcash/cash_consolidate_test.go +++ b/nipcash/cash_consolidate_test.go @@ -186,13 +186,45 @@ func TestCashConsolidateParams_ParseResult_SelfTarget_Decrypts(t *testing.T) { } } -func TestCashConsolidateParams_ParseResult_ThirdPartyPubkeyTarget_NoDecryptAttempt(t *testing.T) { +func TestCashConsolidateParams_ParseResult_ThirdPartyPubkeyTarget_Decrypts(t *testing.T) { + callerPrivHex, callerPubHex := generateTestKeypair(t) + _, carolPubHex := generateTestKeypair(t) + newWalletPrivHex, newWalletPubHex := generateTestKeypair(t) + + // The Hub encrypts to the CALLER even though the target is Carol (same + // convention as cash_transfer) — the caller decrypts it themselves and + // hands Carol a plain token out of band. + ciphertext := encryptForTest(t, newWalletPrivHex, callerPubHex, "lokicash1forcarol") + + p := CashConsolidateParams{ + Sources: []Source{From(randomKeyHex(t), 1000, BySigning(callerPrivHex))}, + To: Pubkey(carolPubHex), + } + raw, err := json.Marshal(cashConsolidateResponseWire{ + AmountMillis: 1000, + NewWalletPubkey: newWalletPubHex, + NewWalletToken: ciphertext, + }) + if err != nil { + t.Fatal(err) + } + result, err := p.ParseResult(raw) + if err != nil { + t.Fatalf("ParseResult: %v", err) + } + if result.NewWalletToken != "lokicash1forcarol" { + t.Fatalf("NewWalletToken: got %q, want decrypted plaintext", result.NewWalletToken) + } +} + +func TestCashConsolidateParams_ParseResult_UndecryptableDelivery_FallsBackToRawValue(t *testing.T) { callerPrivHex, _ := generateTestKeypair(t) _, carolPubHex := generateTestKeypair(t) newWalletPrivHex, newWalletPubHex := generateTestKeypair(t) - // Encrypted to Carol (the target), not the caller — the caller's own - // credential cannot derive this key, so ParseResult must not even try. + // Encrypted to a key the caller doesn't hold — e.g. a Hub still keying + // pubkey-target delivery some other way. Decryption fails; ParseResult + // must not error, just preserve the raw value instead of losing it. ciphertext := encryptForTest(t, newWalletPrivHex, carolPubHex, "lokicash1forcarol") p := CashConsolidateParams{ @@ -209,7 +241,7 @@ func TestCashConsolidateParams_ParseResult_ThirdPartyPubkeyTarget_NoDecryptAttem } result, err := p.ParseResult(raw) if err != nil { - t.Fatalf("ParseResult must not error on an undecryptable third-party delivery: %v", err) + t.Fatalf("ParseResult must not error on an undecryptable delivery: %v", err) } if result.NewWalletToken != ciphertext { t.Fatalf("NewWalletToken: got %q, want the raw ciphertext preserved as-is", result.NewWalletToken) diff --git a/nipcash/identity.go b/nipcash/identity.go index 0b52dd0..e5b3ebf 100644 --- a/nipcash/identity.go +++ b/nipcash/identity.go @@ -212,14 +212,4 @@ type Credential interface { // requires this case deliver in the clear instead — see NIP-CASH // §Spinning a Slice Off's own "bearer-current caller" paragraph). decryptDelivery(newWalletPubkey, ciphertext string) (string, error) - - // ownIdentityPubkey reports this credential's own real Nostr pubkey, if - // it has one to disclose without building a new proof — true only for - // BySigning/BySigningConnectionKey (derived from their held privkey). - // BySecret has no pubkey at all, and ByProof holds a captured proof but - // never the private key behind it. Used only by CashConsolidateParams. - // ParseResult, to tell a self-targeted pubkey consolidate (decryptable) - // apart from a third-party one (structurally not — see that method's - // own doc comment). - ownIdentityPubkey() (pubkey string, ok bool) } diff --git a/nipcash/proof.go b/nipcash/proof.go index 1ddec39..cb5134c 100644 --- a/nipcash/proof.go +++ b/nipcash/proof.go @@ -126,10 +126,6 @@ func (secretCredential) decryptDelivery(_, ciphertext string) (string, error) { return ciphertext, nil } -// ownIdentityPubkey has no real pubkey — a bearer-current caller's proof is -// their raw secret, not a signing key. -func (secretCredential) ownIdentityPubkey() (string, bool) { return "", false } - // --- BySigning: pubkey credential --- type signingCredential struct{ privKeyHex string } @@ -159,14 +155,6 @@ func (c signingCredential) decryptDelivery(newWalletPubkey, ciphertext string) ( return decryptFromPubkey(c.privKeyHex, newWalletPubkey, ciphertext) } -func (c signingCredential) ownIdentityPubkey() (string, bool) { - pubkey, err := utils.GetPublicKey(c.privKeyHex) - if err != nil { - return "", false - } - return pubkey, true -} - // --- BySigningConnectionKey: connection_key credential --- type connectionKeyCredential struct { @@ -216,14 +204,6 @@ func (c connectionKeyCredential) decryptDelivery(newWalletPubkey, ciphertext str return decryptFromPubkey(c.privKeyHex, newWalletPubkey, ciphertext) } -func (c connectionKeyCredential) ownIdentityPubkey() (string, bool) { - pubkey, err := utils.GetPublicKey(c.privKeyHex) - if err != nil { - return "", false - } - return pubkey, true -} - // --- ByProof: a proof captured earlier, not built from a live signing key --- type proofCredential struct { @@ -260,7 +240,3 @@ func (c proofCredential) buildProof(proofBinding) (identityType, identityValue s func (proofCredential) decryptDelivery(string, string) (string, error) { return "", errors.New("nipcash: a ByProof credential has no private key and cannot decrypt a delivery") } - -// ownIdentityPubkey has no real pubkey either — ByProof never holds the -// private key behind the proof it captured, only the proof itself. -func (proofCredential) ownIdentityPubkey() (string, bool) { return "", false }