mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
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:
+10
-1
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip60Cashu.mintApi.MeltQuoteBolt11ResponseDto
|
||||
import com.vitorpamplona.quartz.nip60Cashu.mintApi.MintHttpException
|
||||
import com.vitorpamplona.quartz.nip60Cashu.p2pk.anyP2pkLocked
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser
|
||||
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -909,7 +910,15 @@ class CashuWalletViewModel : ViewModel() {
|
||||
// Keys that can unlock a P2PK-locked token: our wallet key, and
|
||||
// — for a local nsec login only — the identity key (some senders,
|
||||
// e.g. Bey Wallet, P2PK-lock ecash straight to the recipient npub).
|
||||
val (walletKey, identityKey) = state.redeemSigningKeys()
|
||||
// Only gathered when a proof is actually locked: redeemSigningKeys()
|
||||
// decrypts the kind:17375 privkey, which is a signer round-trip on
|
||||
// a bunker/external signer we shouldn't pay for a plain token.
|
||||
val (walletKey, identityKey) =
|
||||
if (parsedTokens.any { it.proofs.anyP2pkLocked() }) {
|
||||
state.redeemSigningKeys()
|
||||
} else {
|
||||
null to null
|
||||
}
|
||||
val total =
|
||||
parsedTokens.sumOf {
|
||||
ops
|
||||
|
||||
+7
-3
@@ -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
|
||||
|
||||
@@ -92,8 +92,13 @@ object P2PK {
|
||||
* Parse a Cashu P2PK secret string. Returns null if the string is not a
|
||||
* NUT-11 P2PK secret.
|
||||
*/
|
||||
fun parseSecret(secret: String): ParsedP2pk? =
|
||||
try {
|
||||
fun parseSecret(secret: String): ParsedP2pk? {
|
||||
// Fast reject before the (throwing) JSON parse: NUT-10 well-known
|
||||
// secrets are a JSON array (`["P2PK", …]`), while a plain Cashu secret
|
||||
// is opaque hex/base64. Redeeming a token calls this once per proof, so
|
||||
// skipping the parse+exception for the common plain case matters.
|
||||
if (!secret.looksLikeJsonArray()) return null
|
||||
return try {
|
||||
val arr = json.parseToJsonElement(secret) as? JsonArray ?: return null
|
||||
if (arr.size < 2) return null
|
||||
val kind = (arr[0] as? JsonPrimitive)?.content
|
||||
@@ -105,6 +110,16 @@ object P2PK {
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** True when the first non-whitespace char is `[` — i.e. it may be a JSON array. */
|
||||
private fun String.looksLikeJsonArray(): Boolean {
|
||||
for (c in this) {
|
||||
if (c.isWhitespace()) continue
|
||||
return c == '['
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* BIP-340 Schnorr signature over `sha256(secret_bytes)` used as the unlock
|
||||
|
||||
Reference in New Issue
Block a user