diff --git a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt index 99db5b628..1292e6b5f 100644 --- a/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt +++ b/amethyst/src/androidTest/java/com/vitorpamplona/amethyst/ThreadDualAxisChartAssemblerTest.kt @@ -27,13 +27,17 @@ import com.vitorpamplona.amethyst.model.AccountSettings import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket +import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.verify +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OkHttpOtsResolverBuilder +import com.vitorpamplona.quartz.nip03Timestamp.ots.okhttp.OtsBlockHeightCache import junit.framework.TestCase import junit.framework.TestCase.assertEquals import kotlinx.coroutines.CoroutineScope @@ -67,6 +71,25 @@ class ThreadDualAxisChartAssemblerTest { }, CoroutineScope(Dispatchers.IO + SupervisorJob()), ), + nwcFilterAssembler = + NWCPaymentFilterAssembler( + NostrClient( + OkHttpWebSocket.Builder { + OkHttpClient + .Builder() + .followRedirects(true) + .followSslRedirects(true) + .build() + }, + CoroutineScope(Dispatchers.IO + SupervisorJob()), + ), + ), + otsResolverBuilder = + OkHttpOtsResolverBuilder( + okHttpClient = { OkHttpClient.Builder().build() }, + isTorActive = { false }, + cache = OtsBlockHeightCache(), + ), ) val db = diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt index af7fdaa96..89a19d8c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/preferences/TorSharedPreferences.kt @@ -27,7 +27,6 @@ import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey -import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk.save import com.vitorpamplona.amethyst.ui.tor.TorSettings import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorType diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt index cea459d20..538ac3c50 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/torState/AccountsTorStateConnector.kt @@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest -import kotlin.collections.ifEmpty class AccountsTorStateConnector( accountsCache: AccountCacheState, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt index 42cc045fa..c814e07ea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/lnurl/LightningAddressResolver.kt @@ -172,7 +172,7 @@ class LightningAddressResolver { val messageNode = tree.get("message") val statusNode = tree.get("status") - if (tree.get("error").isBoolean && messageNode != null) { + if (tree.get("error") != null && tree.get("error").isBoolean && messageNode != null) { if (errorNode.asBoolean()) { return messageNode.asText() } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 3b0727d02..f5e625416 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.LocalCache.chatroomList import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt index 9489f8a06..ec055e448 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/okhttp/OkHttpClientFactory.kt @@ -20,6 +20,8 @@ */ package com.vitorpamplona.amethyst.service.okhttp +import android.os.Build +import android.util.Log import okhttp3.Dispatcher import okhttp3.OkHttpClient import java.net.InetSocketAddress @@ -35,6 +37,22 @@ class OkHttpClientFactory( const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 + + private fun isEmulator(): Boolean = + Build.FINGERPRINT.startsWith("generic") || + Build.FINGERPRINT.lowercase().contains("emulator") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.lowercase().contains("droid4x") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.MANUFACTURER.contains("Genymotion") || + (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || + "google_sdk" == Build.PRODUCT || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.HARDWARE.contains("vbox86") || + Build.HARDWARE.contains("nox") || + Build.HARDWARE.contains("cuttlefish") } val logging = LoggingInterceptor() @@ -42,22 +60,12 @@ class OkHttpClientFactory( val myDispatcher = Dispatcher().apply { - maxRequests = 512 - } - - /* - DEBUG OK HTTP connections here. - init { - if (isDebug) { - GlobalScope.launch(Dispatchers.IO) { - while (true) { - Log.d("OkHttpClientFactory", "Active threads ${myDispatcher.runningCallsCount()}") - delay(5000) - } + if (!isEmulator()) { + maxRequests = 512 + } else { + Log.i("OkHttpClientFactory", "Emulator detected, using default maxRequests: 64.") } } - } - */ private val rootClient = OkHttpClient diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt index 4400ed68a..0e60c26c7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/relayClient/reqCommand/channel/ChannelObservers.kt @@ -26,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.ChannelState -import com.vitorpamplona.amethyst.model.LocalCache.notes import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 2b8a9e1f4..9cb4de2e4 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -28,7 +28,6 @@ import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.debugState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt index 2ea3f5fc3..887a7b177 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/privacy/PrivacyOptionsScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 2ebbcd3fe..078ed2623 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -41,12 +41,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch -import kotlin.collections.map @Stable class RelayFeedViewModel : diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt index 58ade6e62..4c211d588 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt index 6e036b25b..575c6a1d7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/relays/common/BasicRelaySetupInfoDialog.kt @@ -21,7 +21,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt index c991b54df..d23445c1f 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/TorSettingsSetup.kt @@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.model.preferences.AccountPreferenceStores.Companion.torSettings import com.vitorpamplona.amethyst.ui.components.appendLink import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt new file mode 100644 index 000000000..6fa96f183 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/EventVerifySerializer.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Benchmark, which will execute on an Android device. + * + * The body of [BenchmarkRule.measureRepeated] is measured in a loop, and Studio will output the + * result. Modify your code to see how it affects performance. + */ +@RunWith(AndroidJUnit4::class) +class EventVerifySerializer { + @get:Rule + val benchmarkRule = BenchmarkRule() + + @Test + fun serializeToCheckId() { + val event = Event.fromJson(largeKind1Event) + benchmarkRule.measureRepeated { + EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + } + } + + @Test + fun fastSerializeToCheckId() { + val event = Event.fromJson(largeKind1Event) + benchmarkRule.measureRepeated { + EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + } + } +} diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt index bb1272d4e..e4553ade8 100644 --- a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/HexBenchmark.kt @@ -43,6 +43,13 @@ class HexBenchmark { fr.acinq.secp256k1.Hex .decode(hex) + @Test + fun hexIsEqual() { + r.measureRepeated { + assert(Hex.isEqual(hex, bytes)) + } + } + @Test fun hexDecodeOurs() { r.measureRepeated { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 535ed3a99..01ae64a44 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] accompanistAdaptive = "0.37.3" -activityCompose = "1.10.1" +activityCompose = "1.11.0" agp = "8.13.0" android-compileSdk = "36" android-minSdk = "26" @@ -9,25 +9,25 @@ androidKotlinGeohash = "b481c6a64e" androidxJunit = "1.3.0" appcompat = "1.7.1" audiowaveform = "1.1.2" -benchmark = "1.4.0" -benchmarkJunit4 = "1.4.0" +benchmark = "1.4.1" +benchmarkJunit4 = "1.4.1" biometricKtx = "1.2.0-alpha05" coil = "3.3.0" -composeBom = "2025.08.01" +composeBom = "2025.09.00" coreKtx = "1.17.0" datastore = "1.1.7" espressoCore = "3.7.0" firebaseBom = "34.2.0" fragmentKtx = "1.8.9" gms = "4.4.3" -jacksonModuleKotlin = "2.19.2" +jacksonModuleKotlin = "2.20.0" jna = "5.17.0" jtorctl = "0.4.5.7" junit = "4.13.2" -kotlin = "2.2.10" +kotlin = "2.2.20" kotlinxCollectionsImmutable = "0.4.0" kotlinxSerialization = "1.9.0" -kotlinxSerializationPlugin = "2.2.10" +kotlinxSerializationPlugin = "2.2.20" languageId = "17.0.6" lazysodiumAndroid = "5.2.0" lifecycleRuntimeKtx = "2.9.3" @@ -36,7 +36,7 @@ markdown = "e1151c8" media3 = "1.8.0" mockk = "1.14.5" kotlinx-coroutines-test = "1.10.2" -navigationCompose = "2.9.3" +navigationCompose = "2.9.4" okhttp = "5.1.0" runner = "1.7.0" rfc3986 = "0.1.2" @@ -53,7 +53,7 @@ zoomable = "2.8.1" zxing = "3.5.3" zxingAndroidEmbedded = "4.3.0" windowCoreAndroid = "1.4.0" -androidxCamera = "1.4.2" +androidxCamera = "1.5.0" kotlinStdlib = "2.2.10" kotlinTest = "2.2.10" core = "1.7.0" diff --git a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt index 23a80683b..cb46c720d 100644 --- a/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt +++ b/quartz/src/androidInstrumentedTest/kotlin/com/vitorpamplona/quartz/nip01Core/EventSigCheck.kt @@ -21,7 +21,14 @@ package com.vitorpamplona.quartz.nip01Core import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.EventFactory +import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertTrue import org.junit.Test import org.junit.runner.RunWith @@ -58,4 +65,98 @@ class EventSigCheck { // Should pass event.checkSignature() } + + @Test + fun checkSerializationWithEmojis() { + val event = + EventFactory.create( + id = "ac2fb0c9b72a6fefe60262fbce6eb8740380b7f964200cb8efdd2e72fcb1ddb0", + pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", + createdAt = 1690288395, + kind = 7, + tags = + arrayOf( + arrayOf("e", "2cc5b6e012c5a64fcf580fc1b53bed25ed0d7f785fd896744524b1a114dcc86e"), + arrayOf("p", "c80b5248fbe8f392bc3ba45091fb4e6e2b5872387601bf90f53992366b30d720"), + ), + content = "🚀", + sig = "6df6aeef98c21a0508a788cbe6a2a6825e5cc69e57dd843dc6e6d4ce2c28dd042947723fda3d8b24489305d86b1e81f9f66b390d6f7dabf9742cd3775e17e53f", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + assertEquals(old.toByteArray().joinToString(), new.joinToString()) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } + + @Test + fun checkSerializationWithUnicode() { + val event = + EventFactory.create( + id = "e395a1a4cad5b9cf930533c48c2dd175a40331a1d33af8f11b776a0970bc1446", + pubKey = "3fa2504f693c7f0fe71bded634339ae5383a8b14b4164d7c4935830b048dce12", + createdAt = 1707335824, + kind = 30402, + tags = + arrayOf( + arrayOf("d", "b5dd96da-eb92-463a-9cfd-70f1c5ca7e64"), + arrayOf("title", "Google Pixel 7A (NUOVO)\n8GB RAM/128GB MEMORIA"), + arrayOf("summary", "📱Google Pixel 7A (NUOVO)\n👉 8GB RAM/128GB MEMORIA \n\nRete : 5G\nColore : Grigio antracite\n\n👤Sistema operativo : GrapheneOS \n\n🔥Riacquista la tua privacy🔥\n\nℹ️Spacchettato solo per modifica ROM.\n\nNessuna app google e servizi google\n\nSolo app opensource \n\nSet-up privacy oriented incluso dedicato in base a vostre esigenze con Profili dedicati personale/lavoro, Vpn e Tor\n\n🔸️Android 14\n🔸️GrapheneOS  ROM !!!!!!!\n🔸️Adattatore incluso Usb c / Usb per passaggio dati\n🔸️Ricarica con connettore USB Type-C \n🔸Dual SIM (nano SIM singola ed eSIM)\n🔸️Schermo da 6.1 pollici\n🔸️Fotocamera da 64 megapixel\n🔸️8 gb memoria ram\n🔸️128 gb memoria interna\n🔸Dimensioni: 155 x 152 x 72.9 mm\n🔸Sblocco con l'impronta tramite sensore di impronte digitali integrato nel display\n🔸️Batteria da 4385 mAh (Ricarica veloce e Ricarica wireless)\n🔸Materiali: Rivestimento in vetro Corning Gorilla Glass 3 resistente ai graffi\n🔸Resistenza all'acqua e alla polvere di grado IP67 \n\n✅️Cover in slicone in omaggio🔥\n\n💶PREZZO💶 \n\n💰380 € ( SOLO IN BITCOIN) \n\n🚚spedizione privacy oriented da Punto di ritiro a Punto di ritiro (SOLO ITALIA) inclusa, solo scrivendo allo Zio il ref: \"Ziophone21\" \n\n🚚Spedizione Full Privacy ( PREMIUM) \nNessun dato da parte utente, per chi necessitasse di questa sped, la differenza si paga a parte.\n\nAccettati pagamenti:\nBTC 🔗\nBTC ⚡️\nBTC💧\nhttps://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("price", "1000", "SATS"), + arrayOf("t", "Electronics"), + arrayOf("location", "Brescia Italia"), + arrayOf("publishedAt", "1707335824"), + arrayOf("condition", "like new"), + arrayOf("image", "https://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("r", "https://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6"), + arrayOf("alt", "Classifieds listing"), + ), + content = "📱Google Pixel 7A (NUOVO)\n👉 8GB RAM/128GB MEMORIA \n\nRete : 5G\nColore : Grigio antracite\n\n👤Sistema operativo : GrapheneOS \n\n🔥Riacquista la tua privacy🔥\n\nℹ️Spacchettato solo per modifica ROM.\n\nNessuna app google e servizi google\n\nSolo app opensource \n\nSet-up privacy oriented incluso dedicato in base a vostre esigenze con Profili dedicati personale/lavoro, Vpn e Tor\n\n🔸️Android 14\n🔸️GrapheneOS  ROM !!!!!!!\n🔸️Adattatore incluso Usb c / Usb per passaggio dati\n🔸️Ricarica con connettore USB Type-C \n🔸Dual SIM (nano SIM singola ed eSIM)\n🔸️Schermo da 6.1 pollici\n🔸️Fotocamera da 64 megapixel\n🔸️8 gb memoria ram\n🔸️128 gb memoria interna\n🔸Dimensioni: 155 x 152 x 72.9 mm\n🔸Sblocco con l'impronta tramite sensore di impronte digitali integrato nel display\n🔸️Batteria da 4385 mAh (Ricarica veloce e Ricarica wireless)\n🔸Materiali: Rivestimento in vetro Corning Gorilla Glass 3 resistente ai graffi\n🔸Resistenza all'acqua e alla polvere di grado IP67 \n\n✅️Cover in slicone in omaggio🔥\n\n💶PREZZO💶 \n\n💰380 € ( SOLO IN BITCOIN) \n\n🚚spedizione privacy oriented da Punto di ritiro a Punto di ritiro (SOLO ITALIA) inclusa, solo scrivendo allo Zio il ref: \"Ziophone21\" \n\n🚚Spedizione Full Privacy ( PREMIUM) \nNessun dato da parte utente, per chi necessitasse di questa sped, la differenza si paga a parte.\n\nAccettati pagamenti:\nBTC 🔗\nBTC ⚡️\nBTC💧\nhttps://image.nostr.build/87c79c56f270ca04607bc6d72b21786837f81344a960a3787820dc0c482c6660.jpg#m=image%2Fjpeg&dim=1254x1280&blurhash=%7CWHB--yGi%5E4mR3IVV%40RhIT%25LIpf5t6RjofafofWBMcMxbH%25MbJofofogt7j%5Bt7azWBoeWVj%5BayayD%24RikCt8t8off7j%5DogWBoej%5BWCofayazayoeROV%40j%5DogfloebHa%7DkCj%3Fayj%5Bj%5Bj%5Bayj%5BWVj%40e-fPa%7Dj%5BWVj%40j%5Ba%23j%5B&x=17b7ff98875a6e6d6e6bddee30e0a1c18ccf4281db940ee8b5bcc0736d2137c6", + sig = "e03c81ec2543e777583bfcf93c0d82d3055a47a541b1a961db010ed2390e56ddc59e4f891505672757b7ac0d52e891854fdd61f1a4e13b4e6aa563e94cb9368e", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + println(old) + println(String(new)) + + assertEquals(old, String(new)) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } + + @Test + fun checkSerializationLargeZap() { + val event = + EventFactory.create( + id = "e7f09fddf39fc6cb604708b6af7b4d4adbb07b412847ebb004064040fe8c4b1e", + pubKey = "79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432", + createdAt = 1728921077, + kind = 9735, + tags = + arrayOf( + arrayOf("p", "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"), + arrayOf("e", "58f22b06a219f92fb535c71f7098e076751cf2798208acabbbdefbcb950585f5"), + arrayOf("P", "21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad"), + arrayOf("bolt11", "lnbc420n1pns600jdqjfah8wctjvss0p8at5ynp4qtfc238rdkzsj26waa3l8zgag9damzltzsqcrlscj9gvpc7ch2qs2pp5ks03qwm8laa78hnh0xy0p78l6wmyuj3zfqas3gzc2a52lj2zrmtqsp539528j3tvvfzpk8n5v966zccvj3pq2l3etxqxh5qsp8emh29yw3q9qyysgqcqpcxqyz5vqrzjqvdnqyc82a9maxu6c7mee0shqr33u4z9z04wpdwhf96gxzpln8jcrapyqqqqqqp2rcqqqqlgqqqqqzsq2qrzjqw9fu4j39mycmg440ztkraa03u5qhtuc5zfgydsv6ml38qd4azymlapyqqqqqqqp9sqqqqlgqqqq86qqjqrzjq26922n6s5n5undqrf78rjjhgpcczafws45tx8237y7pzx3fg8wwxrgayyqq2mgqqqqqqqqqqqqqqqqq2quc90y7tgfxuauh0vjfvhxjgektaycfesne76jcuk4u9mt6a9l39pddzk3muwy03sjvfk0w8390xxnpzu2656jf2l73ya59ye2yx9aaqpdgxmaq"), + arrayOf("preimage", "0718bf6ba9f503917a2568dac7fe7a5b9361eeb708cd5f57b700060a173ed652"), + arrayOf("description", "{\"id\":\"1110d1216fff3200ed562439556be508a42e4202f66b55f09c9abeed06307d57\",\"pubkey\":\"21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad\",\"created_at\":1728921073,\"kind\":9734,\"tags\":[[\"p\",\"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c\"],[\"e\",\"58f22b06a219f92fb535c71f7098e076751cf2798208acabbbdefbcb950585f5\"],[\"amount\",\"42000\"],[\"relays\",\"wss://christpill.nostr1.com\",\"wss://relay.nostr.bg\",\"wss://relay.noderunners.network\",\"wss://relay.nostrplebs.com\",\"wss://relay.utxo.one\",\"wss://filter.nostr.wine\",\"wss://pyramid.fiatjaf.com\",\"wss://wot.utxo.one\",\"wss://relay.mostr.pub\",\"wss://relay.nostr.band\",\"wss://relay.primal.net\",\"wss://relay.snort.social\",\"wss://purplepag.es\",\"wss://wot.nostr.party\",\"wss://catstrr.swarmstr.com\",\"wss://nostr-relay.derekross.me\",\"wss://relay.damus.io\",\"wss://nos.lol\",\"wss://nostr.wine\",\"wss://relay.bitcoinpark.com\",\"wss://sendit.nosflare.com\",\"wss://nostrelites.org\",\"wss://relay.momostr.pink\",\"ws://localhost:4869\"]],\"content\":\"Onward 🫡\",\"sig\":\"82333a70103b6541f125ac64379a32ad0e9a51eb2769cf899c88df8406bdfa339693d530e640d1c701f4d9d7d847806e1ee2118655b7ba3f9795ce56747eff1b\"}"), + ), + content = "Onward \uD83E\uDEE1", + sig = "81d08765524bd8b774585a57c76d25b1d2c09eaa23efcb075226ba8b64f42e3d9d9403e5edf888e0e58702a24abc084259e21b97e24a275021c5e36186c65f0c", + ) + + val old = EventHasher.makeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + val new = EventHasher.fastMakeJsonForId(event.pubKey, event.createdAt, event.kind, event.tags, event.content) + + println(old) + println(String(new)) + + assertEquals(old.toByteArray().joinToString(), new.joinToString()) + assertTrue(event.verifySignature()) + assertTrue(event.verifyId()) + } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt index 81e828a65..a5cd423e0 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/EventExt.kt @@ -30,7 +30,7 @@ fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tag fun Event.verifyId(): Boolean { if (id.isEmpty()) return false - return id == generateId() + return EventHasher.hashIdEquals(id, pubKey, createdAt, kind, tags, content) } fun Event.verifySignature(): Boolean { diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt index 5fcfd9e8c..46af39a31 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/crypto/EventHasher.kt @@ -20,12 +20,20 @@ */ package com.vitorpamplona.quartz.nip01Core.crypto +import com.fasterxml.jackson.core.JsonEncoding +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.core.util.BufferRecycler +import com.fasterxml.jackson.core.util.ByteArrayBuilder +import com.fasterxml.jackson.databind.JsonMappingException import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper +import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.sha256.sha256 +import java.io.IOException class EventHasher { companion object { @@ -63,13 +71,58 @@ class EventHasher { content: String, ): String = JsonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) + fun fastMakeJsonForId( + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): ByteArray { + val br: BufferRecycler = JsonMapper.mapper.factory._getBufferRecycler() + try { + ByteArrayBuilder(br).use { bb -> + val generator = JsonMapper.mapper.createGenerator(bb, JsonEncoding.UTF8) + generator.enable(JsonGenerator.Feature.COMBINE_UNICODE_SURROGATES_IN_UTF8) + generator.use { + it.writeStartArray() + it.writeNumber(0) + it.writeString(pubKey) + it.writeNumber(createdAt) + it.writeNumber(kind) + it.writeStartArray() + tags.forEach { tag -> + it.writeStartArray() + tag.forEach { value -> + it.writeString(value) + } + it.writeEndArray() + } + it.writeEndArray() + it.writeString(content) + it.writeEndArray() + } + + val result = bb.toByteArray() + bb.release() + return result + } + } catch (e: JsonProcessingException) { + throw e + } catch (e: IOException) { + // shouldn't really happen, but is declared as possibility so: + throw JsonMappingException.fromUnexpectedIOE(e) + } finally { + br.releaseToPool() + } + } + fun hashIdBytes( pubKey: HexKey, createdAt: Long, kind: Int, tags: Array>, content: String, - ): ByteArray = sha256(makeJsonForId(pubKey, createdAt, kind, tags, content).toByteArray()) + ): ByteArray = sha256(fastMakeJsonForId(pubKey, createdAt, kind, tags, content)) fun hashId(serializedJsonAsBytes: ByteArray): String = sha256(serializedJsonAsBytes).toHexKey() @@ -80,5 +133,17 @@ class EventHasher { tags: Array>, content: String, ): String = hashIdBytes(pubKey, createdAt, kind, tags, content).toHexKey() + + fun hashIdEquals( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): Boolean { + val outId = hashIdBytes(pubKey, createdAt, kind, tags, content) + return Hex.isEqual(id, outId) + } } } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt index a282c9e49..4f1ee25e7 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip01Core/relay/client/stats/RelayStats.kt @@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl object RelayStats { private val innerCache = - object : LruCache(100) { + object : LruCache(1000) { override fun create(key: NormalizedRelayUrl?) = RelayStat() } diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt index 7dd033dcd..22b4ca190 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip13Pow/miner/PoWMiner.kt @@ -79,13 +79,13 @@ class PoWMiner( val bytes = EventHasher - .makeJsonForId( - pubKey, - template.createdAt, - template.kind, - template.tags + PoWTag.assemble(initialNonce, desiredPoW), - template.content, - ).toByteArray() + .fastMakeJsonForId( + pubKey = pubKey, + createdAt = template.createdAt, + kind = template.kind, + tags = template.tags + PoWTag.assemble(initialNonce, desiredPoW), + content = template.content, + ) val startIndex = bytes.indexOf(initialNonce.toByteArray()) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt index b43d0e8ab..e5ad9f340 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip28PublicChat/admin/ChannelCreateEvent.kt @@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin import android.util.Log import androidx.compose.runtime.Immutable import com.fasterxml.jackson.core.JsonParseException +import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder @@ -68,6 +69,9 @@ class ChannelCreateEvent( } catch (e: JsonParseException) { Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) ChannelDataNorm() + } catch (e: MismatchedInputException) { + Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) + ChannelDataNorm() } cache = newInfo diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt index a33a6c2ad..085c2cfb5 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip54Wiki/WikiNoteEvent.kt @@ -47,7 +47,6 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag -import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag.Companion.parse import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.utils.TimeUtils diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt index 39738bb91..4701b216a 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nip71Video/VideoHorizontalEvent.kt @@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip31Alts.alt -import com.vitorpamplona.quartz.nip71Video.videoIMetas import com.vitorpamplona.quartz.utils.TimeUtils import java.util.UUID diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt index 270d66a5e..0ff6eec22 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/utils/Hex.kt @@ -74,4 +74,21 @@ object Hex { } return String(out) } + + fun isEqual( + id: String, + ourId: ByteArray, + ): Boolean { + var charIndex = 0 + for (i in 0 until ourId.size) { + val chars = byteToHex[ourId[i].toInt() and 0xFF] + if ( + id[charIndex++] != (chars shr 8).toChar() || + id[charIndex++] != (chars and 0xFF).toChar() + ) { + return false + } + } + return true + } }