Skip to content

feat(p2pk): implement end-to-end Cashu escrow lifecycle#512

Open
kaihere14 wants to merge 18 commits into
shopstr-eng:mainfrom
kaihere14:feat/p2pk-cashu-escrow
Open

feat(p2pk): implement end-to-end Cashu escrow lifecycle#512
kaihere14 wants to merge 18 commits into
shopstr-eng:mainfrom
kaihere14:feat/p2pk-cashu-escrow

Conversation

@kaihere14

Copy link
Copy Markdown
Contributor

Description

This PR implements end-to-end P2PK escrow support using persistent Cashu wallet identities and completes the full escrow lifecycle from escrow creation through seller claim/redeem and buyer refund.

Wallet Identity & Configuration

  • Added WalletConfig V1 support for storing Cashu wallet identities alongside wallet mint configuration.
  • Added parsing and migration support for both legacy wallet configs and V1 configs.
  • Implemented one-time Cashu wallet identity generation for users without an existing wallet identity.
  • Persisted wallet identities through encrypted kind 17375 wallet events.
  • Loaded wallet identities into CashuWalletContext for application-wide access.
  • Preserved backward compatibility with legacy wallet configuration events.

Escrow Ownership Migration

  • Migrated escrow ownership from Nostr public keys to Cashu wallet public keys.
  • Updated reclaim/refund ownership paths to use Cashu wallet identities.
  • Defaulted seller P2PK redeem keys to the user's Cashu wallet public key.
  • Removed fallback behavior that could incorrectly use Nostr identity keys for escrow ownership.

Seller Claim & Redeem Flows

  • Implemented P2PK claim flow using Cashu wallet identities.
  • Added validation to prevent P2PK claims when wallet identities are unavailable.
  • Updated claim flow to unlock P2PK proofs through wallet.receive(..., { privkey }).
  • Ensured fresh spendable proofs are stored and published after successful claims.
  • Updated redeem flow to pass the seller's Cashu private key into swap operations, allowing redemption of locked proofs through existing Lightning payout flows.
  • Removed legacy claim-signing TODO placeholders.

Buyer Refund Flow

  • Added refund support for expired P2PK escrow proofs.

  • Added refund eligibility checks using:

    • P2PK proof detection
    • Locktime expiration status
    • Authorized refund key verification
  • Added refund UI state and success handling.

  • Reused the same Cashu proof-unlocking path used for seller claims.

  • Ensured refunded proofs are converted into fresh spendable proofs before wallet storage.

Testing

Added and expanded coverage for:

  • WalletConfig V1 parsing and migration
  • Cashu wallet identity loading
  • Escrow ownership migration
  • Buyer reclaim key generation
  • Seller claim flow
  • Seller redeem flow
  • Buyer refund flow
  • Missing private-key handling
  • Legacy wallet compatibility
  • Non-P2PK regression protection

Current test status:

PASS  components/utility-components/__tests__/claim-button.test.tsx
PASS  utils/nostr/__tests__/fetch-service.test.ts
PASS  utils/cashu/__tests__/p2pk-checkout.test.ts

Test Suites: 3 passed, 3 total
Tests:       52 passed, 52 total

Notes

  • P2PK ownership now relies on Cashu wallet identities rather than Nostr identities.
  • Existing functionality for non-P2PK proofs remains unchanged.
  • Live mint validation for seller claim, seller redeem, and buyer refund flows is still requested.

Resolved or fixed issue

none

Screenshots (if applicable)

N/A (primarily protocol, wallet, and escrow flow changes)

Affirmation

  • My code follows the CONTRIBUTING.md guidelines

@GautamBytes GautamBytes force-pushed the feat/p2pk-cashu-escrow branch 2 times, most recently from 974d9d6 to df6549f Compare June 5, 2026 07:10
@GautamBytes

Copy link
Copy Markdown
Contributor

@calvadev please check it once whenever feasible!

@kaihere14

Copy link
Copy Markdown
Contributor Author

@calvadev can you please review this pr

@GautamBytes

Copy link
Copy Markdown
Contributor

@calvadev can you please review this pr

@kaihere14 meanwhile please resolve the conflict!

@kaihere14

Copy link
Copy Markdown
Contributor Author

@calvadev can you please review this pr

@kaihere14 meanwhile please resolve the conflict!

okay

@calvadev

Copy link
Copy Markdown
Collaborator

A few things before this hits real buyers:

Blocking:

  1. Reclaim record (kind 30406) is write-only. The dashboard only reads the reclaim token from local storage; the encrypted relay copy is never read back. So reclaim breaks on a new device or after clearing storage. Either wire up the 30406 read/decrypt path or document reclaim as local-only.
  2. Badges/seller toggle aren't flag-gated, but checkout is. With the flag off, sellers can enable P2PK and buyers see escrow badges, then checkout hard-fails. Gate the toggle + badges behind the same flag.

Follow-up:

  1. fetchCashuWallet publishes a new 17375 event for any user without a Cashu privkey. Likely a write for every user on first load post-deploy. Confirm that's intended.
  2. Tests fully mock the cashu Wallet, so the lock -> unlock round-trip is never exercised. Please attach one live-mint integration test (or a documented runbook pass) before we trust redeem/reclaim.
  3. Reclaim eligibility compares against the current cashuPubkey, so a rotated keypair silently hides the reclaim button. Worth a guard.

Nits: cashuPrivkey now lives in app-wide context; the pubkeysEqual test mock is looser than the real fn; a couple of unrelated typing fixes are bundled in.

kaihere14 and others added 13 commits June 15, 2026 10:26
* skip LN melt flow when P2PK escrow is active
* ensure sellers receive locked ecash tokens instead of instant LN payout
* preserve escrow behavior across product and cart checkout flows
- add wallet-config parser utilities
- support legacy and v1 wallet config schemas
- merge mints across wallet config events
- load keypair from newest v1 wallet config
- extend fetchCashuWallet return type with optional keys
- add parser and fetch-service tests
- preserve existing wallet publishing and UI behavior
- add cashuPubkey and cashuPrivkey to CashuWalletContext
- extend editCashuWalletContext to accept wallet keys
- propagate v1 wallet keys from fetchCashuWallet into context
- preserve legacy wallet behavior with undefined keys
- add fetch-service tests for context key propagation
- keep wallet creation and publishing logic unchanged
- add Cashu wallet keypair generation helpers
- publish WalletConfig v1 wallet events
- persist wallet identities in encrypted kind 17375 events
- load existing wallet identities on startup
- expose generated keys through CashuWalletContext
- preserve backward compatibility with legacy wallet configs
- add generation and persistence test coverage
- replace buyer Nostr pubkeys with Cashu wallet pubkeys in reclaim paths
- ensure buyer Cashu pubkey is always included in refund keys
- remove fallback to Nostr identity keys during escrow creation
- default seller P2PK redeem key to Cashu wallet identity
- fail safely when Cashu wallet identity is unavailable
- update escrow documentation and tests
- preserve compatibility with existing seller profile overrides
- add Cashu wallet identity guard for P2PK claims
- unlock P2PK proofs through wallet.receive()
- store and publish fresh spendable proofs after claim
- pass privkey to safeSwap for P2PK redeem flows
- remove legacy claim-signing TODO stub
- add coverage for claim, redeem, guard, and regression paths
- introduce refund eligibility check based on locktime and refund keys
- implement refund button in ClaimButton component
- display success modal upon successful refund
- enhance tests to cover refund scenarios and edge cases
- ensure proper state management for refund process
@kaihere14 kaihere14 force-pushed the feat/p2pk-cashu-escrow branch from 366f128 to d696b7f Compare June 15, 2026 04:58
@kaihere14

Copy link
Copy Markdown
Contributor Author

A few things before this hits real buyers:

Blocking:

  1. Reclaim record (kind 30406) is write-only. The dashboard only reads the reclaim token from local storage; the encrypted relay copy is never read back. So reclaim breaks on a new device or after clearing storage. Either wire up the 30406 read/decrypt path or document reclaim as local-only.
  2. Badges/seller toggle aren't flag-gated, but checkout is. With the flag off, sellers can enable P2PK and buyers see escrow badges, then checkout hard-fails. Gate the toggle + badges behind the same flag.

Follow-up:

  1. fetchCashuWallet publishes a new 17375 event for any user without a Cashu privkey. Likely a write for every user on first load post-deploy. Confirm that's intended.
  2. Tests fully mock the cashu Wallet, so the lock -> unlock round-trip is never exercised. Please attach one live-mint integration test (or a documented runbook pass) before we trust redeem/reclaim.
  3. Reclaim eligibility compares against the current cashuPubkey, so a rotated keypair silently hides the reclaim button. Worth a guard.

Nits: cashuPrivkey now lives in app-wide context; the pubkeysEqual test mock is looser than the real fn; a couple of unrelated typing fixes are bundled in.

Addressed the 30406 recovery issue.

I added a dedicated escrow-record restore flow during app startup (separate from wallet loading). It restores buyer escrow records from both the DB cache and relays, decrypts them, and rehydrates local escrow state before the orders dashboard loads.

This allows reclaim data to survive:

  • new devices
  • cleared browser storage
  • fresh installs

Deduplication is handled through the existing orderId-based upsert path, and kind 30406 already uses a replaceable event keyed by the order d-tag.

Added coverage for the restore path as well:

  • p2pk-escrow-records tests
  • fetch-escrow API tests
  • fetch-service restore tests

Current validation:

129 tests passing.

@kaihere14

Copy link
Copy Markdown
Contributor Author

@calvadev Addressed the feature-flag gating issue.

The existing checkout flag is now used consistently across all P2PK UI surfaces:

  • Seller P2PK shop toggle is hidden when the feature flag is disabled
  • Product escrow badges are hidden when the feature flag is disabled
  • Checkout escrow badges are hidden when the feature flag is disabled

This prevents users from enabling or seeing escrow features when checkout is blocked by policy.

Added coverage for all three surfaces as well:

  • product-card.test.tsx
  • checkout-card-p2pk-badge.test.tsx
  • user-profile-p2pk-section.test.tsx

Validation:

  • 3 test suites passing
  • 20 tests passing

@kaihere14

kaihere14 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@calvadev Confirmed this is intentional.

The migration behavior is:

  • Existing users with a WalletConfig V1 (cashuPubkey + cashuPrivkey) do not publish anything new.
  • Users without a Cashu wallet identity generate a keypair once, publish a V1 wallet config (kind 17375), and then reuse that identity going forward.

So there is a one-time write for users being migrated to WalletConfig V1, but it is not a recurring write on subsequent loads. Once the wallet identity exists, fetchCashuWallet() loads and reuses it rather than generating or publishing again.

@kaihere14

Copy link
Copy Markdown
Contributor Author

@calvadev
I investigated this and found the underlying issue was identity regeneration rather than reclaim eligibility itself.

Previously, if wallet config loading failed, fetchCashuWallet() could generate a new Cashu identity and publish a replacement WalletConfig V1. That could cause existing escrow records to reference an older identity while reclaim eligibility was evaluated against the newly generated one.

I've changed the generation logic so a new identity is only created when wallet-config fetching succeeds and no existing wallet identity is found.

If wallet-config loading fails, no replacement identity is generated or published. Instead, the wallet is marked unavailable and the user can retry once relay access is restored.

Added coverage for:

  • successful fetch + no wallet identity → generate/publish
  • existing wallet identity → no generation
  • relay failure → no generation, no publish, walletIdentityUnavailable surfaced

Current validation:
127 test suites passing
1353 tests passing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants