Merge branch 'vitorpamplona:main' into add-feed-indicator-to-live-bubbles

This commit is contained in:
David Kaspar
2025-09-18 15:55:20 +01:00
committed by GitHub
25 changed files with 317 additions and 47 deletions
@@ -27,13 +27,17 @@ import com.vitorpamplona.amethyst.model.AccountSettings
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.location.LocationState import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.okhttp.OkHttpWebSocket 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.amethyst.ui.screen.loggedIn.threadview.dal.ThreadFeedFilter
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper 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.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.verify 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
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -67,6 +71,25 @@ class ThreadDualAxisChartAssemblerTest {
}, },
CoroutineScope(Dispatchers.IO + SupervisorJob()), 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 = val db =
@@ -27,7 +27,6 @@ import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey 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.TorSettings
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
import com.vitorpamplona.amethyst.ui.tor.TorType import com.vitorpamplona.amethyst.ui.tor.TorType
@@ -34,7 +34,6 @@ import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformLatest
import kotlin.collections.ifEmpty
class AccountsTorStateConnector( class AccountsTorStateConnector(
accountsCache: AccountCacheState, accountsCache: AccountCacheState,
@@ -172,7 +172,7 @@ class LightningAddressResolver {
val messageNode = tree.get("message") val messageNode = tree.get("message")
val statusNode = tree.get("status") 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()) { if (errorNode.asBoolean()) {
return messageNode.asText() return messageNode.asText()
} }
@@ -29,7 +29,6 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCache.chatroomList
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
@@ -20,6 +20,8 @@
*/ */
package com.vitorpamplona.amethyst.service.okhttp package com.vitorpamplona.amethyst.service.okhttp
import android.os.Build
import android.util.Log
import okhttp3.Dispatcher import okhttp3.Dispatcher
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.net.InetSocketAddress import java.net.InetSocketAddress
@@ -35,6 +37,22 @@ class OkHttpClientFactory(
const val DEFAULT_IS_MOBILE: Boolean = false const val DEFAULT_IS_MOBILE: Boolean = false
const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10 const val DEFAULT_TIMEOUT_ON_WIFI_SECS: Int = 10
const val DEFAULT_TIMEOUT_ON_MOBILE_SECS: Int = 30 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() val logging = LoggingInterceptor()
@@ -42,22 +60,12 @@ class OkHttpClientFactory(
val myDispatcher = val myDispatcher =
Dispatcher().apply { Dispatcher().apply {
maxRequests = 512 if (!isEmulator()) {
} maxRequests = 512
} else {
/* Log.i("OkHttpClientFactory", "Emulator detected, using default maxRequests: 64.")
DEBUG OK HTTP connections here.
init {
if (isDebug) {
GlobalScope.launch(Dispatchers.IO) {
while (true) {
Log.d("OkHttpClientFactory", "Active threads ${myDispatcher.runningCallsCount()}")
delay(5000)
}
} }
} }
}
*/
private val rootClient = private val rootClient =
OkHttpClient OkHttpClient
@@ -26,7 +26,6 @@ import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.ChannelState import com.vitorpamplona.amethyst.model.ChannelState
import com.vitorpamplona.amethyst.model.LocalCache.notes
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel import com.vitorpamplona.amethyst.model.nip28PublicChats.PublicChatChannel
import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.nip53LiveActivities.LiveActivitiesChannel
@@ -28,7 +28,6 @@ import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.debugState
@@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R 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.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar import com.vitorpamplona.amethyst.ui.navigation.topbars.SavingTopBar
import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody
@@ -41,12 +41,10 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.collections.map
@Stable @Stable
class RelayFeedViewModel : class RelayFeedViewModel :
@@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -32,7 +32,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R 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.components.appendLink
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog import com.vitorpamplona.amethyst.ui.tor.ConnectTorDialog
@@ -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)
}
}
}
@@ -43,6 +43,13 @@ class HexBenchmark {
fr.acinq.secp256k1.Hex fr.acinq.secp256k1.Hex
.decode(hex) .decode(hex)
@Test
fun hexIsEqual() {
r.measureRepeated {
assert(Hex.isEqual(hex, bytes))
}
}
@Test @Test
fun hexDecodeOurs() { fun hexDecodeOurs() {
r.measureRepeated { r.measureRepeated {
+9 -9
View File
@@ -1,6 +1,6 @@
[versions] [versions]
accompanistAdaptive = "0.37.3" accompanistAdaptive = "0.37.3"
activityCompose = "1.10.1" activityCompose = "1.11.0"
agp = "8.13.0" agp = "8.13.0"
android-compileSdk = "36" android-compileSdk = "36"
android-minSdk = "26" android-minSdk = "26"
@@ -9,25 +9,25 @@ androidKotlinGeohash = "b481c6a64e"
androidxJunit = "1.3.0" androidxJunit = "1.3.0"
appcompat = "1.7.1" appcompat = "1.7.1"
audiowaveform = "1.1.2" audiowaveform = "1.1.2"
benchmark = "1.4.0" benchmark = "1.4.1"
benchmarkJunit4 = "1.4.0" benchmarkJunit4 = "1.4.1"
biometricKtx = "1.2.0-alpha05" biometricKtx = "1.2.0-alpha05"
coil = "3.3.0" coil = "3.3.0"
composeBom = "2025.08.01" composeBom = "2025.09.00"
coreKtx = "1.17.0" coreKtx = "1.17.0"
datastore = "1.1.7" datastore = "1.1.7"
espressoCore = "3.7.0" espressoCore = "3.7.0"
firebaseBom = "34.2.0" firebaseBom = "34.2.0"
fragmentKtx = "1.8.9" fragmentKtx = "1.8.9"
gms = "4.4.3" gms = "4.4.3"
jacksonModuleKotlin = "2.19.2" jacksonModuleKotlin = "2.20.0"
jna = "5.17.0" jna = "5.17.0"
jtorctl = "0.4.5.7" jtorctl = "0.4.5.7"
junit = "4.13.2" junit = "4.13.2"
kotlin = "2.2.10" kotlin = "2.2.20"
kotlinxCollectionsImmutable = "0.4.0" kotlinxCollectionsImmutable = "0.4.0"
kotlinxSerialization = "1.9.0" kotlinxSerialization = "1.9.0"
kotlinxSerializationPlugin = "2.2.10" kotlinxSerializationPlugin = "2.2.20"
languageId = "17.0.6" languageId = "17.0.6"
lazysodiumAndroid = "5.2.0" lazysodiumAndroid = "5.2.0"
lifecycleRuntimeKtx = "2.9.3" lifecycleRuntimeKtx = "2.9.3"
@@ -36,7 +36,7 @@ markdown = "e1151c8"
media3 = "1.8.0" media3 = "1.8.0"
mockk = "1.14.5" mockk = "1.14.5"
kotlinx-coroutines-test = "1.10.2" kotlinx-coroutines-test = "1.10.2"
navigationCompose = "2.9.3" navigationCompose = "2.9.4"
okhttp = "5.1.0" okhttp = "5.1.0"
runner = "1.7.0" runner = "1.7.0"
rfc3986 = "0.1.2" rfc3986 = "0.1.2"
@@ -53,7 +53,7 @@ zoomable = "2.8.1"
zxing = "3.5.3" zxing = "3.5.3"
zxingAndroidEmbedded = "4.3.0" zxingAndroidEmbedded = "4.3.0"
windowCoreAndroid = "1.4.0" windowCoreAndroid = "1.4.0"
androidxCamera = "1.4.2" androidxCamera = "1.5.0"
kotlinStdlib = "2.2.10" kotlinStdlib = "2.2.10"
kotlinTest = "2.2.10" kotlinTest = "2.2.10"
core = "1.7.0" core = "1.7.0"
@@ -21,7 +21,14 @@
package com.vitorpamplona.quartz.nip01Core package com.vitorpamplona.quartz.nip01Core
import androidx.test.ext.junit.runners.AndroidJUnit4 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.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.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -58,4 +65,98 @@ class EventSigCheck {
// Should pass // Should pass
event.checkSignature() event.checkSignature()
} }
@Test
fun checkSerializationWithEmojis() {
val event =
EventFactory.create<ReactionEvent>(
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<ClassifiedsEvent>(
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<LnZapEvent>(
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())
}
} }
@@ -30,7 +30,7 @@ fun Event.generateId(): String = EventHasher.hashId(pubKey, createdAt, kind, tag
fun Event.verifyId(): Boolean { fun Event.verifyId(): Boolean {
if (id.isEmpty()) return false if (id.isEmpty()) return false
return id == generateId() return EventHasher.hashIdEquals(id, pubKey, createdAt, kind, tags, content)
} }
fun Event.verifySignature(): Boolean { fun Event.verifySignature(): Boolean {
@@ -20,12 +20,20 @@
*/ */
package com.vitorpamplona.quartz.nip01Core.crypto 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.ArrayNode
import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper import com.vitorpamplona.quartz.nip01Core.jackson.JsonMapper
import com.vitorpamplona.quartz.utils.Hex
import com.vitorpamplona.quartz.utils.sha256.sha256 import com.vitorpamplona.quartz.utils.sha256.sha256
import java.io.IOException
class EventHasher { class EventHasher {
companion object { companion object {
@@ -63,13 +71,58 @@ class EventHasher {
content: String, content: String,
): String = JsonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content)) ): String = JsonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content))
fun fastMakeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
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( fun hashIdBytes(
pubKey: HexKey, pubKey: HexKey,
createdAt: Long, createdAt: Long,
kind: Int, kind: Int,
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, 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() fun hashId(serializedJsonAsBytes: ByteArray): String = sha256(serializedJsonAsBytes).toHexKey()
@@ -80,5 +133,17 @@ class EventHasher {
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
): String = hashIdBytes(pubKey, createdAt, kind, tags, content).toHexKey() ): String = hashIdBytes(pubKey, createdAt, kind, tags, content).toHexKey()
fun hashIdEquals(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): Boolean {
val outId = hashIdBytes(pubKey, createdAt, kind, tags, content)
return Hex.isEqual(id, outId)
}
} }
} }
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
object RelayStats { object RelayStats {
private val innerCache = private val innerCache =
object : LruCache<NormalizedRelayUrl, RelayStat>(100) { object : LruCache<NormalizedRelayUrl, RelayStat>(1000) {
override fun create(key: NormalizedRelayUrl?) = RelayStat() override fun create(key: NormalizedRelayUrl?) = RelayStat()
} }
@@ -79,13 +79,13 @@ class PoWMiner(
val bytes = val bytes =
EventHasher EventHasher
.makeJsonForId( .fastMakeJsonForId(
pubKey, pubKey = pubKey,
template.createdAt, createdAt = template.createdAt,
template.kind, kind = template.kind,
template.tags + PoWTag.assemble(initialNonce, desiredPoW), tags = template.tags + PoWTag.assemble(initialNonce, desiredPoW),
template.content, content = template.content,
).toByteArray() )
val startIndex = bytes.indexOf(initialNonce.toByteArray()) val startIndex = bytes.indexOf(initialNonce.toByteArray())
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip28PublicChat.admin
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.core.JsonParseException 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.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -68,6 +69,9 @@ class ChannelCreateEvent(
} catch (e: JsonParseException) { } catch (e: JsonParseException) {
Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e) Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e)
ChannelDataNorm() ChannelDataNorm()
} catch (e: MismatchedInputException) {
Log.w("ChannelCreateEvent", "Failure to parse ${this.toJson()}", e)
ChannelDataNorm()
} }
cache = newInfo cache = newInfo
@@ -47,7 +47,6 @@ import com.vitorpamplona.quartz.nip19Bech32.eventIds
import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints import com.vitorpamplona.quartz.nip19Bech32.pubKeyHints
import com.vitorpamplona.quartz.nip19Bech32.pubKeys import com.vitorpamplona.quartz.nip19Bech32.pubKeys
import com.vitorpamplona.quartz.nip23LongContent.tags.PublishedAtTag 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.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip50Search.SearchableEvent import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
@@ -27,7 +27,6 @@ import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag import com.vitorpamplona.quartz.nip01Core.tags.dTags.dTag
import com.vitorpamplona.quartz.nip22Comments.RootScope import com.vitorpamplona.quartz.nip22Comments.RootScope
import com.vitorpamplona.quartz.nip31Alts.alt import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip71Video.videoIMetas
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID import java.util.UUID
@@ -74,4 +74,21 @@ object Hex {
} }
return String(out) 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
}
} }