From b8db7ae6b7d8bd187d901d9136d4f8f33c4ac85b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 17:26:48 +0000 Subject: [PATCH] perf(cashu): dispatch token prefixes via DualCase startsWith MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace startsWith(..., ignoreCase = true) — which case-folds on every call — with the precomputed DualCase prefixes and the new String.startsWith(DualCase) helper, so the cashuA/cashuB dispatch only compares against already-cased strings. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013UMNKix4qEfiAPP9s2a4gB --- .../ui/screen/loggedIn/wallet/CashuWalletViewModel.kt | 3 ++- .../quartz/nip60Cashu/token/CashuTokenB64Parser.kt | 10 ++++++++-- .../com/vitorpamplona/quartz/utils/StringUtils.kt | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt index 8b1b677264..0d0cd0ce79 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt @@ -34,6 +34,7 @@ import com.vitorpamplona.quartz.nip60Cashu.mintApi.MintHttpException import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent import com.vitorpamplona.quartz.utils.Log +import com.vitorpamplona.quartz.utils.startsWith import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -838,7 +839,7 @@ class CashuWalletViewModel : ViewModel() { return } - if (!trimmed.startsWith("cashuA", ignoreCase = true) && !trimmed.startsWith("cashuB", ignoreCase = true)) { + if (!trimmed.startsWith(CashuTokenB64Parser.CashuAPrefix) && !trimmed.startsWith(CashuTokenB64Parser.CashuBPrefix)) { _redeemState.value = CashuRedeemFlowState.Error("Not a Cashu token (must start with cashuA or cashuB)") return diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/token/CashuTokenB64Parser.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/token/CashuTokenB64Parser.kt index 2889b4aaeb..eefe13a01f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/token/CashuTokenB64Parser.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip60Cashu/token/CashuTokenB64Parser.kt @@ -21,6 +21,8 @@ package com.vitorpamplona.quartz.nip60Cashu.token import com.vitorpamplona.quartz.nip01Core.core.toHexKey +import com.vitorpamplona.quartz.utils.DualCase +import com.vitorpamplona.quartz.utils.startsWith import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromByteArray @@ -45,6 +47,10 @@ object CashuTokenB64Parser { /** Both "cashuA" and "cashuB" prefixes are 6 chars. */ private const val PREFIX_LENGTH = 6 + /** Precomputed dual-case prefixes so dispatch avoids per-call case folding. */ + val CashuAPrefix = DualCase("cashuA") + val CashuBPrefix = DualCase("cashuB") + private val json = Json { ignoreUnknownKeys = true @@ -53,8 +59,8 @@ object CashuTokenB64Parser { fun parse(token: String): List? = when { - token.startsWith("cashuA", ignoreCase = true) -> parseCashuA(token) - token.startsWith("cashuB", ignoreCase = true) -> parseCashuB(token) + token.startsWith(CashuAPrefix) -> parseCashuA(token) + token.startsWith(CashuBPrefix) -> parseCashuB(token) else -> null } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt index 7a22427f21..e02e2227d6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/utils/StringUtils.kt @@ -109,6 +109,8 @@ fun String.startsWithAny(terms: List): Boolean { return terms.any { startsWithIgnoreCase(it.lowercase, it.uppercase) } } +fun String.startsWith(prefix: DualCase): Boolean = startsWithIgnoreCase(prefix.lowercase, prefix.uppercase) + class DualCase( val lowercase: String, val uppercase: String,