perf(cashu): avoid needless wallet-key decrypt and secret parse on redeem

Follow-up to the P2PK redeem support:

- Gate the redeem signing-key gathering behind an actual P2PK lock. The
  wallet P2PK key is decrypted from kind:17375 via the signer — a network
  round-trip on a NIP-46 bunker (and a possible approval prompt). The common
  case (a plain, unlocked token) needs none of it, so only fetch keys when
  `anyP2pkLocked()` is true. Applies to both the wallet ViewModel and the amy
  CLI token-redeem path.
- Fast-reject in `P2PK.parseSecret`: NUT-10 well-known secrets are JSON
  arrays, so bail before the throwing JSON parse when the string isn't one.
  Redeem parses every proof's secret once, so this drops a thrown+caught
  exception per plain proof (also benefits the nutzap redeem path).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QKeRaX749TYnJ7oR8UpqA4
This commit is contained in:
Claude
2026-07-27 01:21:29 +00:00
parent 63ff055b53
commit ee5efba88d
3 changed files with 34 additions and 6 deletions
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip60Cashu.p2pk.P2PKUnredeemableException
import com.vitorpamplona.quartz.nip60Cashu.p2pk.anyP2pkLocked
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser
/**
@@ -162,9 +163,12 @@ object CashuReceiveCommands {
// Keys that can unlock a P2PK-locked token: the wallet's kind:17375
// key, plus — for a local key account — the identity key (some
// senders, e.g. Bey Wallet, P2PK-lock ecash to the recipient npub).
val snap = ctx.cashuSnapshot()
val walletKey = snap.walletEvent?.let { runCatching { it.privkey(ctx.signer) }.getOrNull() }
val identityKey = (ctx.signer as? NostrSignerInternal)?.keyPair?.privKey?.toHexKey()
// Only decrypt the wallet key when a proof is actually locked (a
// signer round-trip on bunker accounts, wasted on a plain token).
val locked = parsed.any { it.proofs.anyP2pkLocked() }
val walletKey =
if (locked) ctx.cashuSnapshot().walletEvent?.let { runCatching { it.privkey(ctx.signer) }.getOrNull() } else null
val identityKey = if (locked) (ctx.signer as? NostrSignerInternal)?.keyPair?.privKey?.toHexKey() else null
var total = 0L
var lastTokenEventId: String? = null
var lastHistoryEventId: String? = null