- Migrates Quartz from Android to CommonMain

- Fully converts OpenTimestamp Java codebase to Kotlin, migrating the sync and async HTTP call interfaces to OkHttp and coroutines
- Redesigns parsing of relay commands, messages and filters for performance in Jackson.
- Starts the use of KotlinX Serialization when speed is not a requirement
- Migrates all Jackson field annotations to Kotlin Serialization
- Migrates Regex use in Quarts to Kotlin's Regex class
- Migrates Base64 from Android to Kotlin
- Migrates UUID from Android/Java to Kotlin
- Migrates LRUCache usage from Android/Java to Kotlin collections
- Migrates all String to bytearray conversions to Kotlin methods
- Migrates all System.arraycopy calls to kotlin native ones.
- Separates parsing code from the data classes Companion objects
- Exposes Rfc3986 normalizations to each platform.
- Exposes URI parsing classes to each platform.
- Exposes URL Encoders to each platform.
- Exposes BigDecimal to each platform.
- Exposes the Url Detector to each platform.
- Exposes MacInstances to each platform
- Exposes Diggest instances to each platform.
- Exposes a BitSet to each platform.
- Exposes GZip to each platform.
- Exposes Secp256k1 to each platform.
- Exposes SecureRandom to each platform.
- Exposes Time in seconds to each platform.
- Exposes the LargeCache to each platform.
- Exposes AES CBC and AES GCM encryption/decryption to each platform
- Migrate test assertions to Kotlin Tests
- Exposes Address class to each platform because of the Parceleable
- Creates our own ByteArrayOutputStream.
- Removes Lock features inside the Bloomfilters because we don't need data consistency/
- Migrates UserMetadata parser from Jackson to Kotlin serialization
- Removes the need for Static methods in each tag.
- Adds an event template serializer
- Adds KotlinX Datetime to migrate some of the date-based logs
- Adds support for LibSodium in the JVM platform
- Creates a shared test build for iOS targets
- Fixes several usages of Reflection when serializing classes
- Fixes a bug on loading RelayDB for the HintBloom filter test
- Increases the Bloom filter space to better use hints in the app.
- Removes support for iOS in x86
- Creates a Jackson mapper just for NIP-55, which stays in the Android build only.
- Keeps the event store in the android build as well.
- Removes @Syncronized tags in favor of Mutexes.
- Improved sendAndWaitForResponse NostrClient method to properly account for returns from each relay.
- Removes the need for GlobalScope and async calls in the downloadFirstEvent method.
- Restructures the parser and serialization of the relay messages and commands for performance with Jackson
- Removes the dependency on Jackson's error classes across the codebase.
- Moves the hint to quote tag extension methods to their own packages.
- Speeds up the generation of Bech32 addresses
- Migrates NIP-6 and Blossom uploads to use Kotlin Serialization
This commit is contained in:
Vitor Pamplona
2025-09-22 19:44:51 -04:00
parent 7949fc66b2
commit 7a280a3cde
1195 changed files with 12671 additions and 5183 deletions
@@ -0,0 +1,67 @@
/**
* 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.nip01Core.core
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
actual data class Address actual constructor(
actual val kind: Kind,
actual val pubKeyHex: HexKey,
actual val dTag: String,
) : Comparable<Address> {
actual fun toValue() = assemble(kind, pubKeyHex, dTag)
actual fun countMemory(): Int =
3 * pointerSizeInBytes +
8 + // kind
pubKeyHex.bytesUsedInMemory() +
dTag.bytesUsedInMemory()
actual override fun compareTo(other: Address): Int {
val result = kind.compareTo(other.kind)
return if (result == 0) {
val result2 = pubKeyHex.compareTo(other.pubKeyHex)
if (result2 == 0) {
dTag.compareTo(other.dTag)
} else {
result2
}
} else {
result
}
}
actual companion object {
actual fun assemble(
kind: Int,
pubKeyHex: HexKey,
dTag: String,
) = AddressSerializer.assemble(kind, pubKeyHex, dTag)
actual fun parse(addressId: String) = AddressSerializer.parse(addressId)
actual fun isOfKind(
addressId: String,
kind: String,
) = addressId.startsWith(kind) && addressId[kind.length] == ':'
}
}
@@ -0,0 +1,104 @@
/**
* 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.utils
import com.goterl.lazysodium.LazySodiumJava
import com.goterl.lazysodium.SodiumJava
actual object LibSodiumInstance {
private val libSodium = SodiumJava()
private val lazySodium = LazySodiumJava(libSodium)
actual fun cryptoAeadXChaCha20Poly1305IetfDecrypt(
message: ByteArray,
nSec: ByteArray,
ciphertext: ByteArray,
ad: ByteArray,
nPub: ByteArray,
k: ByteArray,
): Boolean =
lazySodium.cryptoAeadXChaCha20Poly1305IetfDecrypt(
message,
longArrayOf(message.size.toLong()),
nSec,
ciphertext,
ciphertext.size.toLong(),
ad,
ad.size.toLong(),
nPub,
k,
)
actual fun cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext: ByteArray,
message: ByteArray,
ad: ByteArray,
nSec: ByteArray,
nPub: ByteArray,
k: ByteArray,
): Boolean =
lazySodium.cryptoAeadXChaCha20Poly1305IetfEncrypt(
ciphertext,
longArrayOf(ciphertext.size.toLong()),
message,
message.size.toLong(),
ad,
ad.size.toLong(),
nSec,
nPub,
k,
)
actual fun cryptoStreamChaCha20IetfXor(
message: ByteArray,
nonce: ByteArray?,
key: ByteArray?,
): ByteArray {
val ciphertext = ByteArray(message.size)
lazySodium.cryptoStreamChaCha20IetfXor(ciphertext, message, message.size.toLong(), nonce, key)
return ciphertext
}
actual fun cryptoStreamXChaCha20Xor(
messageBytes: ByteArray,
nonce: ByteArray,
key: ByteArray,
): ByteArray {
val cipher = ByteArray(messageBytes.size)
val k2 = ByteArray(32)
val nonceChaCha = nonce.drop(16).toByteArray()
assert(nonceChaCha.size == 8)
libSodium.crypto_core_hchacha20(k2, nonce, key, null)
val resultCode =
libSodium.crypto_stream_chacha20_xor_ic(
cipher,
messageBytes,
messageBytes.size.toLong(),
nonceChaCha,
0,
k2,
)
return if (resultCode == 0) cipher else throw IllegalStateException("Could not decrypt message")
}
}
@@ -0,0 +1,61 @@
/**
* 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.utils
actual object Log {
actual fun w(
tag: String,
message: String,
throwable: Throwable?,
) {
if (throwable != null) {
println("WARN: [$tag] $message. Throwable: ${throwable.message}")
} else {
println("WARN: [$tag] $message")
}
}
actual fun e(
tag: String,
message: String,
throwable: Throwable?,
) {
if (throwable != null) {
println("ERROR: [$tag] $message. Throwable: ${throwable.message}")
} else {
println("ERROR: [$tag] $message")
}
}
actual fun d(
tag: String,
message: String,
) {
println("DEBUG: [$tag] $message")
}
actual fun i(
tag: String,
message: String,
) {
println("INFO: [$tag] $message")
}
}
@@ -18,6 +18,8 @@
* 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
package com.vitorpamplona.quartz.utils
actual fun platform() = "JVM"
actual fun currentTimeSeconds() = System.currentTimeMillis() / 1000
@@ -0,0 +1,59 @@
/**
* 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.utils
import fr.acinq.secp256k1.Secp256k1
actual object Secp256k1Instance {
private val h02 = Hex.decode("02")
private val secp256k1 = Secp256k1.get()
actual fun compressedPubKeyFor(privKey: ByteArray) = secp256k1.pubKeyCompress(secp256k1.pubkeyCreate(privKey))
actual fun isPrivateKeyValid(il: ByteArray): Boolean = secp256k1.secKeyVerify(il)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
nonce: ByteArray?,
): ByteArray = secp256k1.signSchnorr(data, privKey, nonce)
actual fun signSchnorr(
data: ByteArray,
privKey: ByteArray,
): ByteArray = secp256k1.signSchnorr(data, privKey, null)
actual fun verifySchnorr(
signature: ByteArray,
hash: ByteArray,
pubKey: ByteArray,
): Boolean = secp256k1.verifySchnorr(signature, hash, pubKey)
actual fun privateKeyAdd(
first: ByteArray,
second: ByteArray,
): ByteArray = secp256k1.privKeyTweakAdd(first, second)
actual fun pubKeyTweakMulCompact(
pubKey: ByteArray,
privateKey: ByteArray,
): ByteArray = secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
}
@@ -0,0 +1,73 @@
/**
* 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.utils
import java.net.URI
import java.net.URLDecoder
actual class UriParser actual constructor(
uri: String,
) {
val myUri = URI.create(uri)
val queryParameters: Map<String, String> by lazy {
myUri.query?.ifBlank { null }?.let { query ->
query.split('&').associate { paramValue ->
val parts = paramValue.split("=", limit = 2)
if (parts.size == 2) {
parts[0] to parts[1]
} else {
parts[0] to "" // Handle parameters without a value, e.g., "param&other=value"
}
}
} ?: emptyMap()
}
val fragments: Map<String, String> by lazy {
myUri.rawFragment?.ifBlank { null }?.let { keyValuePair ->
keyValuePair.split('&').associate { paramValue ->
val parts = paramValue.split("=", limit = 2)
if (parts.size == 2) {
parts[0] to URLDecoder.decode(parts[1], "UTF-8")
} else {
parts[0] to "" // Handle parameters without a value, e.g., "param&other=value"
}
}
} ?: emptyMap()
}
actual fun scheme(): String? = myUri.scheme
actual fun host(): String? = myUri.host
actual fun port(): Int? {
// java.net.URI.getPort() returns -1 if the port is not set, so we handle that case.
val port = myUri.port
return if (port == -1) null else port
}
actual fun path(): String? = myUri.path
actual fun queryParameterNames(): Set<String> = queryParameters.keys
actual fun getQueryParameter(param: String): String? = queryParameters[param]
actual fun fragments() = fragments
}