- 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,45 @@
/**
* 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.nip01Core.jackson.JacksonMapper
actual object OptimizedJsonMapper {
fun <T> runCatching(parsingAction: () -> T): T =
try {
parsingAction()
// wraps jackson errors
} catch (e: com.fasterxml.jackson.core.JsonParseException) {
throw IllegalArgumentException(e.message, e)
}
actual fun fromJson(json: String): Event = runCatching { JacksonMapper.fromJson(json) }
actual fun toJson(event: Event) = JacksonMapper.toJson(event)
actual fun fromJsonToTagArray(json: String): Array<Array<String>> = runCatching { JacksonMapper.fromJsonToTagArray(json) }
actual fun toJson(tags: Array<Array<String>>): String = JacksonMapper.toJson(tags)
actual inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = runCatching { JacksonMapper.fromJsonTo<T>(json) }
actual fun toJson(value: OptimizedSerializable): String = JacksonMapper.toJson(value)
}
@@ -0,0 +1,114 @@
/**
* 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.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.jackson.JacksonMapper
import java.io.IOException
actual object EventHasherSerializer {
fun makeJsonObjectForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): ArrayNode {
val factory = JsonNodeFactory.instance
return factory.arrayNode(6).apply {
add(0)
add(pubKey)
add(createdAt)
add(kind)
add(
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
add(content)
}
}
actual fun makeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): String = JacksonMapper.toJson(makeJsonObjectForId(pubKey, createdAt, kind, tags, content))
actual fun fastMakeJsonForId(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): ByteArray {
val br: BufferRecycler = JacksonMapper.mapper.factory._getBufferRecycler()
try {
ByteArrayBuilder(br).use { bb ->
val generator = JacksonMapper.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()
}
}
}
@@ -0,0 +1,33 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.core.Event
class EventDeserializer : StdDeserializer<Event>(Event::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Event = EventManualDeserializer.fromJson(jp.codec.readTree(jp))
}
@@ -0,0 +1,43 @@
/**
* 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.jackson
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.utils.EventFactory
class EventManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): Event =
EventFactory.create(
id = jsonObject.get("id").asText().intern(),
pubKey = jsonObject.get("pubkey").asText().intern(),
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags =
jsonObject.get("tags").toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
},
content = jsonObject.get("content").asText(),
sig = jsonObject.get("sig").asText(),
)
}
}
@@ -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.nip01Core.jackson
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.nip01Core.core.HexKey
class EventManualSerializer {
companion object {
fun assemble(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: String,
): ObjectNode {
val factory = JsonNodeFactory.instance
return factory.objectNode().apply {
put("id", id)
put("pubkey", pubKey)
put("created_at", createdAt)
put("kind", kind)
replace(
"tags",
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
put("content", content)
put("sig", sig)
}
}
fun toJson(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: String,
): String {
val obj = assemble(id, pubKey, createdAt, kind, tags, content, sig)
return JacksonMapper.mapper.writeValueAsString(obj)
}
}
}
@@ -0,0 +1,52 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip01Core.core.Event
class EventSerializer : StdSerializer<Event>(Event::class.java) {
override fun serialize(
event: Event,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
gen.writeStringField("id", event.id)
gen.writeStringField("pubkey", event.pubKey)
gen.writeNumberField("created_at", event.createdAt)
gen.writeNumberField("kind", event.kind)
gen.writeArrayFieldStart("tags")
for (i in event.tags.indices) {
gen.writeStartArray()
for (j in event.tags[i].indices) {
gen.writeString(event.tags[i][j])
}
gen.writeEndArray()
}
gen.writeEndArray()
gen.writeStringField("content", event.content)
gen.writeStringField("sig", event.sig)
gen.writeEndObject()
}
}
@@ -0,0 +1,81 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.core.util.Separators
class InliningTagArrayPrettyPrinter : DefaultPrettyPrinter {
companion object {
val MY_SEPARATORS =
DEFAULT_SEPARATORS
.withObjectFieldValueSpacing(Separators.Spacing.AFTER)
}
init {
indentArraysWith(DefaultIndenter(" ", "\n"))
}
constructor(separators: Separators? = MY_SEPARATORS) : super(separators)
constructor(base: InliningTagArrayPrettyPrinter) : super(base)
override fun createInstance(): DefaultPrettyPrinter = InliningTagArrayPrettyPrinter(this)
override fun writeStartArray(g: JsonGenerator) {
if (!_arrayIndenter.isInline) {
++_nesting
}
g.writeRaw('[')
}
override fun beforeArrayValues(g: JsonGenerator) {
if (_nesting < 3) {
_arrayIndenter.writeIndentation(g, _nesting)
}
}
override fun writeArrayValueSeparator(g: JsonGenerator) {
g.writeRaw(_arrayValueSeparator)
if (_nesting < 3) {
_arrayIndenter.writeIndentation(g, _nesting)
} else {
g.writeRaw(' ')
}
}
override fun writeEndArray(
g: JsonGenerator,
nrOfValues: Int,
) {
if (!_arrayIndenter.isInline) {
--_nesting
}
if (nrOfValues > 0) {
if (_nesting < 2) {
_arrayIndenter.writeIndentation(g, _nesting)
}
}
g.writeRaw(']')
}
}
@@ -0,0 +1,25 @@
/**
* 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.jackson
import com.fasterxml.jackson.databind.JsonNode
inline fun <reified R> JsonNode.toTypedArray(transform: (JsonNode) -> R): Array<R> = Array(size()) { transform(get(it)) }
@@ -0,0 +1,122 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.OptimizedSerializable
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.Message
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MessageDeserializer
import com.vitorpamplona.quartz.nip01Core.relay.commands.toClient.MessageSerializer
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.Command
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CommandDeserializer
import com.vitorpamplona.quartz.nip01Core.relay.commands.toRelay.CommandSerializer
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterDeserializer
import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplateDeserializer
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplateSerializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerMessageDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerRequestSerializer
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.jackson.BunkerResponseSerializer
import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.RequestDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.jackson.ResponseDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.jackson.RumorSerializer
import java.io.InputStream
import kotlin.jvm.java
class JacksonMapper {
companion object {
val defaultPrettyPrinter = InliningTagArrayPrettyPrinter()
val mapper =
jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
.setDefaultPrettyPrinter(defaultPrettyPrinter)
.registerModule(
SimpleModule()
// nip 01
.addSerializer(Event::class.java, EventSerializer())
.addDeserializer(Event::class.java, EventDeserializer())
.addSerializer(Filter::class.java, FilterSerializer())
.addDeserializer(Filter::class.java, FilterDeserializer())
.addSerializer(Message::class.java, MessageSerializer())
.addDeserializer(Message::class.java, MessageDeserializer())
.addSerializer(Command::class.java, CommandSerializer())
.addDeserializer(Command::class.java, CommandDeserializer())
.addDeserializer(TagArray::class.java, TagArrayDeserializer())
.addSerializer(TagArray::class.java, TagArraySerializer())
.addDeserializer(EventTemplate::class.java, EventTemplateDeserializer())
.addSerializer(EventTemplate::class.java, EventTemplateSerializer())
// nip 59
.addSerializer(Rumor::class.java, RumorSerializer())
.addDeserializer(Rumor::class.java, RumorDeserializer())
// nip 47
.addDeserializer(Response::class.java, ResponseDeserializer())
.addDeserializer(Request::class.java, RequestDeserializer())
// nip 46
.addDeserializer(BunkerMessage::class.java, BunkerMessageDeserializer())
.addSerializer(BunkerRequest::class.java, BunkerRequestSerializer())
.addDeserializer(BunkerRequest::class.java, BunkerRequestDeserializer())
.addSerializer(BunkerResponse::class.java, BunkerResponseSerializer())
.addDeserializer(BunkerResponse::class.java, BunkerResponseDeserializer()),
)
fun fromJson(json: String): Event = mapper.readValue<Event>(json)
fun fromJson(json: JsonNode): Event = EventManualDeserializer.fromJson(json)
fun fromJsonToTagArray(json: String): Array<Array<String>> = mapper.readValue<Array<Array<String>>>(json)
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: String): T = mapper.readValue<T>(json)
inline fun <reified T : OptimizedSerializable> fromJsonTo(json: InputStream): T = mapper.readValue<T>(json)
fun toJson(event: Event): String = EventManualSerializer.toJson(event.id, event.pubKey, event.createdAt, event.kind, event.tags, event.content, event.sig)
fun toJson(event: ArrayNode): String = mapper.writeValueAsString(event)
fun toJson(event: ObjectNode?): String = mapper.writeValueAsString(event)
fun toJson(value: OptimizedSerializable): String = mapper.writeValueAsString(value)
fun toJson(tags: Array<Array<String>>): String = mapper.writeValueAsString(tags)
}
}
@@ -0,0 +1,32 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
class TagArrayDeserializer : StdDeserializer<Array<Array<String>>>(Array<Array<String>>::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Array<Array<String>> = TagArrayManualDeserializer.fromJson(jp.codec.readTree(jp))
}
@@ -0,0 +1,32 @@
/**
* 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.jackson
import com.fasterxml.jackson.databind.JsonNode
class TagArrayManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): Array<Array<String>> =
jsonObject.toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
}
}
}
@@ -0,0 +1,43 @@
/**
* 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.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
class TagArraySerializer : StdSerializer<Array<Array<String>>>(Array<Array<String>>::class.java) {
override fun serialize(
tags: Array<Array<String>>,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartArray()
for (i in tags.indices) {
gen.writeStartArray()
for (j in tags[i].indices) {
gen.writeString(tags[i][j])
}
gen.writeEndArray()
}
gen.writeEndArray()
}
}
@@ -0,0 +1,94 @@
/**
* 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.relay.commands.toClient
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualDeserializer
class MessageDeserializer : StdDeserializer<Message>(Message::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Message? {
// Expect to start with a JSON array token
if (jp.currentToken != JsonToken.START_ARRAY) {
ctxt.reportWrongTokenException(this, JsonToken.START_ARRAY, "Expected START_ARRAY token")
}
val type = jp.nextTextValue()
val message =
when (type) {
EventMessage.LABEL -> {
val subId = jp.nextTextValue()
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
EventMessage(
subId = subId,
event = EventManualDeserializer.fromJson(event),
)
}
EoseMessage.LABEL ->
EoseMessage(
subId = jp.nextTextValue(),
)
NoticeMessage.LABEL ->
NoticeMessage(
message = jp.nextTextValue(),
)
OkMessage.LABEL ->
OkMessage(
eventId = jp.nextTextValue(),
success = jp.nextBooleanValue(),
message = jp.nextTextValue() ?: "",
)
AuthMessage.LABEL ->
AuthMessage(
challenge = jp.nextTextValue(),
)
NotifyMessage.LABEL ->
NotifyMessage(
message = jp.nextTextValue(),
)
ClosedMessage.LABEL ->
ClosedMessage(
subId = jp.nextTextValue(),
message = jp.nextTextValue(),
)
else -> {
throw IllegalArgumentException("Message $type is not supported")
}
}
if (jp.currentToken != JsonToken.END_ARRAY) {
// cleaning out other array elements
while (jp.nextToken() != JsonToken.END_ARRAY) {
// clears out
}
}
return message
}
}
@@ -0,0 +1,75 @@
/**
* 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.relay.commands.toClient
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventSerializer
class MessageSerializer : StdSerializer<Message>(Message::class.java) {
val eventSerializer = EventSerializer()
override fun serialize(
msg: Message,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartArray()
gen.writeString(msg.label())
when (msg) {
is EventMessage -> {
gen.writeString(msg.subId)
eventSerializer.serialize(msg.event, gen, provider)
}
is NoticeMessage -> {
gen.writeString(msg.message)
}
is OkMessage -> {
gen.writeString(msg.eventId)
gen.writeString(msg.success.toString())
if (msg.message.isNotBlank()) {
gen.writeString(msg.message)
}
}
is AuthMessage -> {
gen.writeString(msg.challenge)
}
is NotifyMessage -> {
gen.writeString(msg.message)
}
is ClosedMessage -> {
gen.writeString(msg.subId)
gen.writeString(msg.message)
}
else -> null
}
gen.writeEndArray()
}
}
@@ -0,0 +1,115 @@
/**
* 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.relay.commands.toRelay
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualDeserializer
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.filters.ManualFilterDeserializer
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
class CommandDeserializer : StdDeserializer<Command>(Command::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Command? {
// Expect to start with a JSON array token
if (jp.currentToken != JsonToken.START_ARRAY) {
ctxt.reportWrongTokenException(this, JsonToken.START_ARRAY, "Expected START_ARRAY token")
}
val type = jp.nextTextValue()
val message =
when (type) {
ReqCmd.LABEL -> {
val subId = jp.nextTextValue()
val filters = mutableListOf<Filter>()
while (jp.nextToken() != JsonToken.END_ARRAY) {
val filterObj: ObjectNode = jp.codec.readTree(jp)
val filter = ManualFilterDeserializer.fromJson(filterObj)
filters.add(filter)
}
ReqCmd(
subId = jp.nextTextValue(),
filters = filters,
)
}
CountCmd.LABEL -> {
val subId = jp.nextTextValue()
val filters = mutableListOf<Filter>()
while (jp.nextToken() != JsonToken.END_ARRAY) {
val filterObj: ObjectNode = jp.codec.readTree(jp)
val filter = ManualFilterDeserializer.fromJson(filterObj)
filters.add(filter)
}
CountCmd(
subId = subId,
filters = filters,
)
}
EventCmd.LABEL -> {
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
EventCmd(
event = EventManualDeserializer.fromJson(event),
)
}
CloseCmd.LABEL ->
CloseCmd(
subId = jp.nextTextValue(),
)
AuthCmd.LABEL -> {
jp.nextToken()
val event: JsonNode = jp.codec.readTree(jp)
AuthCmd(
event = EventManualDeserializer.fromJson(event) as RelayAuthEvent,
)
}
else -> {
throw IllegalArgumentException("Message $type is not supported")
}
}
if (jp.currentToken != JsonToken.END_ARRAY) {
// cleaning out other array elements
while (jp.nextToken() != JsonToken.END_ARRAY) {
// clears out
}
}
return message
}
}
@@ -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.nip01Core.relay.commands.toRelay
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventSerializer
import com.vitorpamplona.quartz.nip01Core.relay.filters.FilterSerializer
class CommandSerializer : StdSerializer<Command>(Command::class.java) {
val eventSerializer = EventSerializer()
val filterSerializer = FilterSerializer()
override fun serialize(
cmd: Command,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartArray()
gen.writeString(cmd.label())
when (cmd) {
is ReqCmd -> {
gen.writeString(cmd.subId)
cmd.filters.forEach {
filterSerializer.serialize(it, gen, provider)
}
}
is EventCmd -> {
eventSerializer.serialize(cmd.event, gen, provider)
}
is CloseCmd -> {
gen.writeString(cmd.subId)
}
is AuthCmd -> {
eventSerializer.serialize(cmd.event, gen, provider)
}
is CountCmd -> {
gen.writeString(cmd.subId)
cmd.filters.forEach {
filterSerializer.serialize(it, gen, provider)
}
}
else -> null
}
gen.writeEndArray()
}
}
@@ -0,0 +1,57 @@
/**
* 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.relay.filters
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node.ObjectNode
class FilterDeserializer : StdDeserializer<Filter>(Filter::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Filter = ManualFilterDeserializer.fromJson(jp.codec.readTree(jp))
}
class ManualFilterDeserializer {
companion object {
fun fromJson(jsonObject: ObjectNode): Filter {
val tags = mutableListOf<String>()
jsonObject.fieldNames().forEach {
if (it.startsWith("#")) {
tags.add(it.substring(1))
}
}
return Filter(
ids = jsonObject.get("ids").map { it.asText() },
authors = jsonObject.get("authors").map { it.asText() },
kinds = jsonObject.get("kinds").map { it.asInt() },
tags = tags.associateWith { jsonObject.get(it).map { it.asText() } },
since = jsonObject.get("since").asLong(),
until = jsonObject.get("until").asLong(),
limit = jsonObject.get("limit").asInt(),
search = jsonObject.get("search").asText(),
)
}
}
}
@@ -0,0 +1,76 @@
/**
* 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.relay.filters
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
class FilterSerializer : StdSerializer<Filter>(Filter::class.java) {
override fun serialize(
filter: Filter,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
filter.ids?.run {
gen.writeArrayFieldStart("ids")
for (i in indices) {
gen.writeString(this[i])
}
gen.writeEndArray()
}
filter.authors?.run {
gen.writeArrayFieldStart("authors")
for (i in indices) {
gen.writeString(this[i])
}
gen.writeEndArray()
}
filter.kinds?.run {
gen.writeArrayFieldStart("kinds")
for (i in indices) {
gen.writeNumber(this[i])
}
gen.writeEndArray()
}
filter.tags?.run {
entries.forEach { kv ->
gen.writeArrayFieldStart("#${kv.key}")
for (i in kv.value.indices) {
gen.writeString(kv.value[i])
}
gen.writeEndArray()
}
}
filter.since?.run { gen.writeNumberField("since", this) }
filter.until?.run { gen.writeNumberField("until", this) }
filter.limit?.run { gen.writeNumberField("limit", this) }
filter.search?.run { gen.writeStringField("search", this) }
gen.writeEndObject()
}
}
@@ -0,0 +1,98 @@
/**
* 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.relay.sockets.okhttp
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket as OkHttpWebSocket
import okhttp3.WebSocketListener as OkHttpWebSocketListener
class BasicOkHttpWebSocket(
val url: NormalizedRelayUrl,
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
val out: WebSocketListener,
) : WebSocket {
private var socket: OkHttpWebSocket? = null
override fun needsReconnect() = socket == null
override fun connect() {
val request = Request.Builder().url(url.url).build()
val listener =
object : OkHttpWebSocketListener() {
override fun onOpen(
webSocket: OkHttpWebSocket,
response: Response,
) = out.onOpen(
(response.receivedResponseAtMillis - response.sentRequestAtMillis).toInt(),
response.headers["Sec-WebSocket-Extensions"]?.contains("permessage-deflate") ?: false,
)
override fun onMessage(
webSocket: OkHttpWebSocket,
text: String,
) = out.onMessage(text)
override fun onClosing(
webSocket: OkHttpWebSocket,
code: Int,
reason: String,
) = out.onClosing(code, reason)
override fun onClosed(
webSocket: OkHttpWebSocket,
code: Int,
reason: String,
) = out.onClosed(code, reason)
override fun onFailure(
webSocket: OkHttpWebSocket,
t: Throwable,
response: Response?,
) = out.onFailure(t, response?.code, response?.message)
}
socket = httpClient(url).newWebSocket(request, listener)
}
override fun disconnect() {
socket?.cancel()
socket = null
}
override fun send(msg: String): Boolean = socket?.send(msg) ?: false
class Builder(
val httpClient: (NormalizedRelayUrl) -> OkHttpClient,
) : WebsocketBuilder {
// Called when connecting.
override fun build(
url: NormalizedRelayUrl,
out: WebSocketListener,
) = BasicOkHttpWebSocket(url, httpClient, out)
}
}
@@ -0,0 +1,33 @@
/**
* 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.signers
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.core.Event
class EventTemplateDeserializer : StdDeserializer<EventTemplate<Event>>(EventTemplate::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): EventTemplate<Event> = EventTemplateManualDeserializer.fromJson(jp.codec.readTree(jp))
}
@@ -0,0 +1,40 @@
/**
* 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.signers
import com.fasterxml.jackson.databind.JsonNode
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.jackson.toTypedArray
class EventTemplateManualDeserializer {
companion object {
fun fromJson(jsonObject: JsonNode): EventTemplate<Event> =
EventTemplate(
createdAt = jsonObject.get("created_at").asLong(),
kind = jsonObject.get("kind").asInt(),
tags =
jsonObject.get("tags").toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
},
content = jsonObject.get("content").asText(),
)
}
}
@@ -0,0 +1,48 @@
/**
* 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.signers
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
class EventTemplateSerializer : StdSerializer<EventTemplate<*>>(EventTemplate::class.java) {
override fun serialize(
event: EventTemplate<*>,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
gen.writeNumberField("created_at", event.createdAt)
gen.writeNumberField("kind", event.kind)
gen.writeArrayFieldStart("tags")
for (i in event.tags.indices) {
gen.writeStartArray()
for (j in event.tags[i].indices) {
gen.writeString(event.tags[i][j])
}
gen.writeEndArray()
}
gen.writeEndArray()
gen.writeStringField("content", event.content)
gen.writeEndObject()
}
}
@@ -0,0 +1,124 @@
/**
* 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.nip03Timestamp.okhttp
import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper
import com.vitorpamplona.quartz.nip03Timestamp.ots.BitcoinExplorer
import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
import com.vitorpamplona.quartz.utils.Log
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.coroutines.executeAsync
class OkHttpBitcoinExplorer(
val baseUrl: () -> String,
val client: OkHttpClient,
val cache: OtsBlockHeightCache,
) : BitcoinExplorer {
/**
* Retrieve the block information from the block hash.
*
* @param hash Hash of the block.
* @return the blockheader of the hash
* @throws Exception desc
*/
override suspend fun block(hash: String): BlockHeader {
cache.getHeader(hash)?.let {
return it
}
val baseAPI = baseUrl()
val url = "$baseAPI/block/$hash"
val request =
Request
.Builder()
.header("Accept", "application/json")
.url(url)
.get()
.build()
return client.newCall(request).execute().use {
if (it.isSuccessful) {
Log.d("OkHttpBlockstreamExplorer", "$baseAPI/block/$hash")
val jsonObject = JacksonMapper.mapper.readTree(it.body.string())
val merkleRoot = jsonObject["merkle_root"].asText()
val time = jsonObject["timestamp"].asInt().toString()
val blockHeader = BlockHeader(merkleRoot, hash, time)
cache.putHeader(hash, blockHeader)
blockHeader
} else {
throw UrlException(
"Couldn't open $url: " + it.message + " " + it.code,
)
}
}
}
/**
* Retrieve the block hash from the block height.
*
* @param height Height of the block.
* @return the hash of the block at height height
* @throws Exception desc
*/
@Throws(Exception::class)
override suspend fun blockHash(height: Int): String {
cache.getHeight(height)?.let {
return it
}
val baseAPI = baseUrl()
val url = "$baseAPI/block-height/$height"
val request =
Request
.Builder()
.url(url)
.get()
.build()
return client.newCall(request).executeAsync().use {
if (it.isSuccessful) {
val blockHash = it.body.string()
Log.d("OkHttpBlockstreamExplorer", "$url $blockHash")
cache.putHeight(height, blockHash)
blockHash
} else {
throw UrlException("Couldn't open $url: " + it.message + " " + it.code)
}
}
}
companion object {
// doesn't accept Tor
const val BLOCKSTREAM_API_URL = "https://blockstream.info/api"
// accepts Tor
const val MEMPOOL_API_URL = "https://mempool.space/api/"
}
}
@@ -0,0 +1,115 @@
/**
* 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.nip03Timestamp.okhttp
import com.vitorpamplona.quartz.nip03Timestamp.ots.RemoteCalendar
import com.vitorpamplona.quartz.nip03Timestamp.ots.StreamDeserializationContext
import com.vitorpamplona.quartz.nip03Timestamp.ots.Timestamp
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.CommitmentNotFoundException
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.DeserializationException
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.ExceededSizeException
import com.vitorpamplona.quartz.nip03Timestamp.ots.exceptions.UrlException
import com.vitorpamplona.quartz.utils.Hex
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.coroutines.executeAsync
/**
* Class representing remote calendar server interface.
*/
class OkHttpCalendar(
val okHttpClient: (url: String) -> OkHttpClient,
) : RemoteCalendar {
override suspend fun submit(
url: String,
digest: ByteArray,
): Timestamp {
val url = "$url/digest"
val mediaType = "application/x-www-form-urlencoded; charset=utf-8".toMediaType()
val requestBody = digest.toRequestBody(mediaType)
val request =
Request
.Builder()
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.post(requestBody)
.build()
val client = okHttpClient(url)
return client.newCall(request).executeAsync().use {
if (it.isSuccessful) {
val ctx =
StreamDeserializationContext(
it.body.bytes(),
)
Timestamp.deserialize(ctx, digest)
} else {
throw UrlException(
"Failed to open $url",
)
}
}
}
override suspend fun getTimestamp(
url: String,
commitment: ByteArray,
): Timestamp =
try {
val url = url + "/timestamp/" + Hex.encode(commitment)
val request =
Request
.Builder()
.header("Accept", "application/vnd.opentimestamps.v1")
.header("Content-Type", "application/x-www-form-urlencoded")
.url(url)
.get()
.build()
val client = okHttpClient(url)
client.newCall(request).executeAsync().use { response ->
withContext(Dispatchers.IO) {
if (response.isSuccessful) {
val ctx = StreamDeserializationContext(response.body.bytes())
Timestamp.deserialize(ctx, commitment)
} else {
throw CommitmentNotFoundException("Calendar response a status code != 200: " + response.code)
}
}
}
} catch (e: DeserializationException) {
throw e
} catch (e: ExceededSizeException) {
throw e
} catch (e: CommitmentNotFoundException) {
throw e
} catch (e: Exception) {
throw UrlException(e.message)
}
}
@@ -0,0 +1,44 @@
/**
* 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.nip03Timestamp.okhttp
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolver
import com.vitorpamplona.quartz.nip03Timestamp.OtsResolverBuilder
import com.vitorpamplona.quartz.nip03Timestamp.ots.OtsBlockHeightCache
import okhttp3.OkHttpClient
class OkHttpOtsResolverBuilder(
val cache: OtsBlockHeightCache = OtsBlockHeightCache(),
val okHttpClient: (url: String) -> OkHttpClient,
) : OtsResolverBuilder {
override fun build(): OtsResolver =
OtsResolver(
explorer =
OkHttpBitcoinExplorer(
baseUrl = {
OkHttpBitcoinExplorer.BLOCKSTREAM_API_URL
},
client = okHttpClient(OkHttpBitcoinExplorer.MEMPOOL_API_URL),
cache = cache,
),
calendar = OkHttpCalendar(okHttpClient),
)
}
@@ -0,0 +1,46 @@
/**
* 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.nip46RemoteSigner.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerMessage
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import kotlin.jvm.java
class BunkerMessageDeserializer : StdDeserializer<BunkerMessage>(BunkerMessage::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): BunkerMessage {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val isRequest = jsonObject.has("method")
if (isRequest) {
return jp.codec.treeToValue(jsonObject, BunkerRequest::class.java)
} else {
return jp.codec.treeToValue(jsonObject, BunkerResponse::class.java)
}
}
}
@@ -0,0 +1,64 @@
/**
* 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.nip46RemoteSigner.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.toTypedArray
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetRelays
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Decrypt
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip04Encrypt
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Decrypt
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign
class BunkerRequestDeserializer : StdDeserializer<BunkerRequest>(BunkerRequest::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): BunkerRequest {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val id = jsonObject.get("id").asText().intern()
val method = jsonObject.get("method").asText().intern()
val params = jsonObject.get("params")?.toTypedArray { it.asText().intern() } ?: emptyArray()
return when (method) {
BunkerRequestConnect.METHOD_NAME -> BunkerRequestConnect.parse(id, params)
BunkerRequestGetPublicKey.METHOD_NAME -> BunkerRequestGetPublicKey.parse(id, params)
BunkerRequestGetRelays.METHOD_NAME -> BunkerRequestGetRelays.parse(id, params)
BunkerRequestNip04Decrypt.METHOD_NAME -> BunkerRequestNip04Decrypt.parse(id, params)
BunkerRequestNip04Encrypt.METHOD_NAME -> BunkerRequestNip04Encrypt.parse(id, params)
BunkerRequestNip44Decrypt.METHOD_NAME -> BunkerRequestNip44Decrypt.parse(id, params)
BunkerRequestNip44Encrypt.METHOD_NAME -> BunkerRequestNip44Encrypt.parse(id, params)
BunkerRequestPing.METHOD_NAME -> BunkerRequestPing.parse(id, params)
BunkerRequestSign.METHOD_NAME -> BunkerRequestSign.parse(id, params)
else -> BunkerRequest(id, method, params)
}
}
}
@@ -0,0 +1,45 @@
/**
* 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.nip46RemoteSigner.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
import kotlin.collections.forEach
class BunkerRequestSerializer : StdSerializer<BunkerRequest>(BunkerRequest::class.java) {
override fun serialize(
value: BunkerRequest,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
gen.writeStringField("id", value.id)
gen.writeStringField("method", value.method)
gen.writeArrayFieldStart("params")
value.params.forEach {
gen.writeString(it)
}
gen.writeEndArray()
gen.writeEndObject()
}
}
@@ -0,0 +1,79 @@
/**
* 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.nip46RemoteSigner.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseAck
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
import com.vitorpamplona.quartz.utils.Hex
class BunkerResponseDeserializer : StdDeserializer<BunkerResponse>(BunkerResponse::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): BunkerResponse {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val id = jsonObject.get("id").asText().intern()
val result = jsonObject.get("result")?.asText()
val error = jsonObject.get("error")?.asText()
if (error != null) {
return com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
.parse(id, result, error)
}
if (result != null) {
when (result) {
BunkerResponseAck.RESULT -> return BunkerResponseAck.parse(id, result, error)
BunkerResponsePong.RESULT -> return BunkerResponsePong.parse(id, result, error)
else -> {
if (result.length == 64 && Hex.isHex(result)) {
return BunkerResponsePublicKey.parse(id, result)
}
if (result.get(0) == '{') {
try {
return com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent
.parse(id, result)
} catch (_: Exception) {
}
try {
return BunkerResponseGetRelays.parse(id, result)
} catch (_: Exception) {
}
}
return BunkerResponse(id, result, error)
}
}
}
return BunkerResponse(id, result, error)
}
}
@@ -0,0 +1,40 @@
/**
* 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.nip46RemoteSigner.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
class BunkerResponseSerializer : StdSerializer<BunkerResponse>(BunkerResponse::class.java) {
override fun serialize(
value: BunkerResponse,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
gen.writeStringField("id", value.id)
value.result?.let { gen.writeStringField("result", value.result) }
value.error?.let { gen.writeStringField("error", it) }
gen.writeEndObject()
}
}
@@ -0,0 +1,43 @@
/**
* 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.nip47WalletConnect.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
import com.vitorpamplona.quartz.nip47WalletConnect.Request
class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Request? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val method = jsonObject.get("method")?.asText()
if (method == "pay_invoice") {
return jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java)
}
return null
}
}
@@ -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.nip47WalletConnect.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.nip47WalletConnect.Response
class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Response? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val resultType = jsonObject.get("result_type")?.asText()
if (resultType == "pay_invoice") {
val result = jsonObject.get("result")
val error = jsonObject.get("error")
if (result != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (error != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java)
}
} else {
// tries to guess
if (jsonObject.get("result")?.get("preimage") != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceSuccessResponse::class.java)
}
if (jsonObject.get("error")?.get("code") != null) {
return jp.codec.treeToValue(jsonObject, PayInvoiceErrorResponse::class.java)
}
}
return null
}
}
@@ -0,0 +1,48 @@
/**
* 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.nip59Giftwrap.rumors.jackson
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.toTypedArray
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
class RumorDeserializer : StdDeserializer<Rumor>(Rumor::class.java) {
override fun deserialize(
jp: JsonParser,
ctxt: DeserializationContext,
): Rumor {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return Rumor(
id = jsonObject.get("id")?.asText()?.intern(),
pubKey = jsonObject.get("pubkey")?.asText()?.intern(),
createdAt = jsonObject.get("created_at")?.asLong(),
kind = jsonObject.get("kind")?.asInt(),
tags =
jsonObject.get("tags").toTypedArray {
it.toTypedArray { s -> if (s.isNull) "" else s.asText().intern() }
},
content = jsonObject.get("content")?.asText(),
)
}
}
@@ -0,0 +1,47 @@
/**
* 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.nip59Giftwrap.rumors.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
class RumorSerializer : StdSerializer<Rumor>(Rumor::class.java) {
override fun serialize(
event: Rumor,
gen: JsonGenerator,
provider: SerializerProvider,
) {
gen.writeStartObject()
event.id?.let { gen.writeStringField("id", it) }
event.pubKey?.let { gen.writeStringField("pubkey", it) }
event.createdAt?.let { gen.writeNumberField("created_at", it) }
event.kind?.let { gen.writeNumberField("kind", it) }
event.tags?.let {
gen.writeArrayFieldStart("tags")
event.tags.forEach { tag -> gen.writeArray(tag, 0, tag.size) }
gen.writeEndArray()
}
event.content?.let { gen.writeStringField("content", it) }
gen.writeEndObject()
}
}
@@ -0,0 +1,39 @@
/**
* 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.nip96FileStorage.info
import java.net.URI
import java.net.URL
actual fun makeAbsoluteIfRelativeUrl(
baseUrl: String,
potentiallyRelativeUrl: String,
): String =
try {
val apiUrl = URI(potentiallyRelativeUrl)
if (apiUrl.isAbsolute) {
potentiallyRelativeUrl
} else {
URL(URL(baseUrl), potentiallyRelativeUrl).toString()
}
} catch (e: Exception) {
potentiallyRelativeUrl
}
@@ -0,0 +1,25 @@
/**
* 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.math.BigDecimal as JBigDecimal
actual typealias BigDecimal = JBigDecimal
@@ -0,0 +1,27 @@
/**
* 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.util.BitSet as JvmBitSet
actual typealias BitSet = JvmBitSet
actual fun bitSetValueOf(bytes: ByteArray): BitSet = JvmBitSet.valueOf(bytes)
@@ -0,0 +1,35 @@
/**
* 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.io.ByteArrayOutputStream
import java.util.zip.GZIPInputStream
import java.util.zip.GZIPOutputStream
actual object GZip {
actual fun compress(content: String): ByteArray {
val bos = ByteArrayOutputStream()
GZIPOutputStream(bos).bufferedWriter(Charsets.UTF_8).use { it.write(content) }
return bos.toByteArray()
}
actual fun decompress(content: ByteArray): String = GZIPInputStream(content.inputStream()).bufferedReader(Charsets.UTF_8).use { it.readText() }
}
@@ -0,0 +1,65 @@
/**
* 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 org.czeal.rfc3986.URIReference
actual object Rfc3986 {
actual fun normalize(uri: String) =
URIReference
.parse(uri)
.normalize()
.toString()
actual fun isValidUrl(url: String): Boolean =
runCatching {
URIReference.parse(url)
}.isSuccess
actual fun normalizeAndRemoveFragment(url: String): String =
URIReference
.parse(url)
.normalize()
.toStringNoFragment()
.intern()
actual fun host(url: String): String = URIReference.parse(url).host.value
}
fun URIReference.toStringSchemeHost(): String {
val sb = StringBuilder()
if (scheme != null) sb.append(scheme).append(":")
if (authority != null) sb.append("//").append(authority.toString())
return sb.toString()
}
fun URIReference.toStringNoFragment(): String {
val sb = StringBuilder()
if (scheme != null) sb.append(scheme).append(":")
if (authority != null) sb.append("//").append(authority.toString())
if (path != null) sb.append(path)
if (query != null) sb.append("?").append(query)
return sb.toString()
}
@@ -0,0 +1,25 @@
/**
* 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.security.SecureRandom as JvmSecureRandom
actual typealias SecureRandom = JvmSecureRandom
@@ -0,0 +1,23 @@
/**
* 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 fun String.internIfPossible() = this.intern()
@@ -0,0 +1,27 @@
/**
* 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.text.Normalizer
actual class UnicodeNormalizer {
actual fun normalizeNFKC(input: String): String = Normalizer.normalize(input, Normalizer.Form.NFKC)
}
@@ -0,0 +1,30 @@
/**
* 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.URLDecoder
import java.net.URLEncoder
actual object UrlEncoder {
actual fun encode(value: String): String = URLEncoder.encode(value, "utf-8")
actual fun decode(value: String): String = URLDecoder.decode(value, "utf-8")
}
@@ -0,0 +1,26 @@
/**
* 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.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
actual fun fastFindURLs(text: String): List<String> = UrlDetector(text, UrlDetectorOptions.Default).detect().map { it.originalUrl }
@@ -0,0 +1,323 @@
/**
* 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.cache
import com.vitorpamplona.quartz.utils.cache.CacheCollectors.BiFilter
import com.vitorpamplona.quartz.utils.cache.CacheCollectors.BiMapper
import com.vitorpamplona.quartz.utils.cache.CacheCollectors.BiMapperNotNull
import com.vitorpamplona.quartz.utils.cache.CacheCollectors.BiNotNullMapper
import com.vitorpamplona.quartz.utils.cache.CacheCollectors.BiSumOfLong
import java.util.function.BiConsumer
class BiFilterCollector<K, V>(
private val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
val results: ArrayList<V> = ArrayList()
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
results.add(v)
}
}
}
class BiFilterUniqueCollector<K, V>(
private val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
val results: HashSet<V> = HashSet()
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
results.add(v)
}
}
}
class BiMapCollector<K, V, R>(
private val mapper: BiMapper<K, V, R?>,
) : BiConsumer<K, V> {
val results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.add(result)
}
}
}
class BiMapUniqueCollector<K, V, R>(
private val mapper: BiMapper<K, V, R?>,
) : BiConsumer<K, V> {
val results: HashSet<R> = HashSet()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.add(result)
}
}
}
class BiMapFlattenCollector<K, V, R>(
private val mapper: BiMapper<K, V, Collection<R>?>,
) : BiConsumer<K, V> {
val results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.addAll(result)
}
}
}
class BiMapFlattenUniqueCollector<K, V, R>(
private val mapper: BiMapper<K, V, Collection<R>?>,
) : BiConsumer<K, V> {
val results: HashSet<R> = HashSet()
override fun accept(
k: K,
v: V,
) {
val result = mapper.map(k, v)
if (result != null) {
results.addAll(result)
}
}
}
class BiNotNullMapCollector<K, V, R>(
private val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
val results: ArrayList<R> = ArrayList()
override fun accept(
k: K,
v: V,
) {
results.add(mapper.map(k, v))
}
}
class BiMaxOfCollector<K, V>(
private val filter: BiFilter<K, V>,
private val comparator: Comparator<V>,
) : BiConsumer<K, V> {
private var _maxK: K? = null
private var _maxV: V? = null
val maxK: K? get() = _maxK
val maxV: V? get() = _maxV
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) {
if (_maxK == null || comparator.compare(v, _maxV) > 0) {
_maxK = k
_maxV = v
}
}
}
}
class BiSumOfCollector<K, V>(
private val mapper: CacheCollectors.BiSumOf<K, V>,
) : BiConsumer<K, V> {
private var _sum = 0
val sum: Int get() = _sum
override fun accept(
k: K,
v: V,
) {
_sum += mapper.map(k, v)
}
}
class BiSumOfLongCollector<K, V>(
private val mapper: BiSumOfLong<K, V>,
) : BiConsumer<K, V> {
private var _sum = 0L
val sum: Long get() = _sum
override fun accept(
k: K,
v: V,
) {
_sum += mapper.map(k, v)
}
}
class BiGroupByCollector<K, V, R>(
private val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
val results = HashMap<R, ArrayList<V>>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val list = results[group]
if (list == null) {
val answer = ArrayList<V>()
answer.add(v)
results[group] = answer
} else {
list.add(v)
}
}
}
class BiCountByGroupCollector<K, V, R>(
private val mapper: BiNotNullMapper<K, V, R>,
) : BiConsumer<K, V> {
val results = HashMap<R, Int>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val count = results[group]
if (count == null) {
results[group] = 1
} else {
results[group] = count + 1
}
}
}
class BiSumByGroupCollector<K, V, R>(
private val mapper: BiNotNullMapper<K, V, R>,
private val sumOf: BiNotNullMapper<K, V, Long>,
) : BiConsumer<K, V> {
val results = HashMap<R, Long>()
override fun accept(
k: K,
v: V,
) {
val group = mapper.map(k, v)
val sum = results[group]
if (sum == null) {
results[group] = sumOf.map(k, v)
} else {
results[group] = sum + sumOf.map(k, v)
}
}
}
class BiCountIfCollector<K, V>(
private val filter: BiFilter<K, V>,
) : BiConsumer<K, V> {
private var _count = 0
val count: Int get() = _count
override fun accept(
k: K,
v: V,
) {
if (filter.filter(k, v)) _count++
}
}
class BiAssociateCollector<K, V, T, U>(
size: Int,
private val mapper: BiMapperNotNull<K, V, Pair<T, U>>,
) : BiConsumer<K, V> {
val results: LinkedHashMap<T, U> = LinkedHashMap(size)
override fun accept(
k: K,
v: V,
) {
val pair = mapper.map(k, v)
results[pair.first] = pair.second
}
}
class BiAssociateNotNullCollector<K, V, T, U>(
size: Int,
private val mapper: BiMapper<K, V, Pair<T, U>?>,
) : BiConsumer<K, V> {
val results: LinkedHashMap<T, U> = LinkedHashMap(size)
override fun accept(
k: K,
v: V,
) {
val pair = mapper.map(k, v)
if (pair != null) {
results[pair.first] = pair.second
}
}
}
class BiAssociateWithCollector<K, V, U>(
size: Int,
private val mapper: BiMapper<K, V, U?>,
) : BiConsumer<K, V> {
val results: LinkedHashMap<K, U?> = LinkedHashMap(size)
override fun accept(
k: K,
v: V,
) {
results[k] = mapper.map(k, v)
}
}
class BiAssociateNotNullWithCollector<K, V, U>(
size: Int,
private val mapper: BiMapper<K, V, U>,
) : BiConsumer<K, V> {
val results: LinkedHashMap<K, U> = LinkedHashMap(size)
override fun accept(
k: K,
v: V,
) {
val newValue = mapper.map(k, v)
if (newValue != null) {
results[k] = newValue
}
}
}
@@ -0,0 +1,176 @@
/**
* 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.cache
import java.util.function.BiConsumer
class MyBiConsumer<K, V>(
private val consumer: ICacheBiConsumer<K, V>,
) : BiConsumer<K, V> {
override fun accept(
k: K,
v: V,
) {
consumer.accept(k, v)
}
}
interface CacheOperations<K, V> : ICacheOperations<K, V> {
fun forEach(consumer: BiConsumer<K, V>)
override fun size(): Int
override fun forEach(consumer: ICacheBiConsumer<K, V>) {
forEach(MyBiConsumer(consumer))
}
override fun filter(consumer: CacheCollectors.BiFilter<K, V>): List<V> {
val runner = BiFilterCollector(consumer)
forEach(runner)
return runner.results
}
override fun filterIntoSet(consumer: CacheCollectors.BiFilter<K, V>): Set<V> {
val runner = BiFilterUniqueCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> map(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): List<R> {
val runner = BiNotNullMapCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> mapNotNull(consumer: CacheCollectors.BiMapper<K, V, R?>): List<R> {
val runner = BiMapCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> mapNotNullIntoSet(consumer: CacheCollectors.BiMapper<K, V, R?>): Set<R> {
val runner = BiMapUniqueCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> mapFlatten(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): List<R> {
val runner = BiMapFlattenCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> mapFlattenIntoSet(consumer: CacheCollectors.BiMapper<K, V, Collection<R>?>): Set<R> {
val runner = BiMapFlattenUniqueCollector(consumer)
forEach(runner)
return runner.results
}
override fun maxOrNullOf(
filter: CacheCollectors.BiFilter<K, V>,
comparator: Comparator<V>,
): V? {
val runner = BiMaxOfCollector(filter, comparator)
forEach(runner)
return runner.maxV
}
override fun sumOf(consumer: CacheCollectors.BiSumOf<K, V>): Int {
val runner = BiSumOfCollector(consumer)
forEach(runner)
return runner.sum
}
override fun sumOfLong(consumer: CacheCollectors.BiSumOfLong<K, V>): Long {
val runner = BiSumOfLongCollector(consumer)
forEach(runner)
return runner.sum
}
override fun <R> groupBy(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, List<V>> {
val runner = BiGroupByCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> countByGroup(consumer: CacheCollectors.BiNotNullMapper<K, V, R>): Map<R, Int> {
val runner = BiCountByGroupCollector(consumer)
forEach(runner)
return runner.results
}
override fun <R> sumByGroup(
groupMap: CacheCollectors.BiNotNullMapper<K, V, R>,
sumOf: CacheCollectors.BiNotNullMapper<K, V, Long>,
): Map<R, Long> {
val runner = BiSumByGroupCollector(groupMap, sumOf)
forEach(runner)
return runner.results
}
override fun count(consumer: CacheCollectors.BiFilter<K, V>): Int {
val runner = BiCountIfCollector(consumer)
forEach(runner)
return runner.count
}
override fun <T, U> associate(transform: (K, V) -> Pair<T, U>): Map<T, U> {
val runner = BiAssociateCollector(size(), transform)
forEach(runner)
return runner.results
}
override fun <U> associateWith(transform: (K, V) -> U?): Map<K, U?> {
val runner = BiAssociateWithCollector(size(), transform)
forEach(runner)
return runner.results
}
override fun joinToString(
separator: CharSequence,
prefix: CharSequence,
postfix: CharSequence,
limit: Int,
truncated: CharSequence,
transform: ((K, V) -> CharSequence)?,
): String {
val buffer = StringBuilder()
buffer.append(prefix)
var count = 0
forEach { key, value ->
val str = if (transform != null) transform(key, value) else ""
if (str.isNotEmpty()) {
if (++count > 1) buffer.append(separator)
if (limit < 0 || count <= limit) {
when {
transform != null -> buffer.append(str)
else -> buffer.append("$key $value")
}
} else {
return@forEach
}
}
}
if (limit >= 0 && count > limit) buffer.append(truncated)
buffer.append(postfix)
return buffer.toString()
}
}
@@ -0,0 +1,82 @@
/**
* 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.cache
import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
actual class LargeCache<K, V> : CacheOperations<K, V> {
private val cache = ConcurrentSkipListMap<K, V>()
actual fun keys(): Set<K> = cache.keys
actual fun values(): Iterable<V> = cache.values
actual fun get(key: K) = cache.get(key)
actual fun remove(key: K) = cache.remove(key)
actual override fun size() = cache.size
actual fun isEmpty() = cache.isEmpty()
actual fun clear() = cache.clear()
actual fun containsKey(key: K) = cache.containsKey(key)
actual fun put(
key: K,
value: V,
) {
cache.put(key, value)
}
actual fun getOrCreate(
key: K,
builder: (key: K) -> V,
): V {
val value = cache.get(key)
return if (value != null) {
value
} else {
val newObject = builder(key)
cache.putIfAbsent(key, newObject) ?: newObject
}
}
actual fun createIfAbsent(
key: K,
builder: (key: K) -> V,
): Boolean {
val value = cache.get(key)
return if (value != null) {
false
} else {
val newObject = builder(key)
cache.putIfAbsent(key, newObject) == null
}
}
override fun forEach(consumer: BiConsumer<K, V>) {
cache.forEach(consumer)
}
}
@@ -0,0 +1,64 @@
/**
* 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.ciphers
import com.vitorpamplona.quartz.utils.Log
import java.security.GeneralSecurityException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
actual class AESCBC actual constructor(
actual val keyBytes: ByteArray,
actual val iv: ByteArray,
) : NostrCipher {
private fun newCipher() = Cipher.getInstance("AES/CBC/PKCS5Padding")
private fun keySpec() = SecretKeySpec(keyBytes, "AES")
private fun param() = IvParameterSpec(iv)
actual override fun name() = NAME
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.ENCRYPT_MODE, keySpec(), param())
doFinal(bytesToEncrypt)
}
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.DECRYPT_MODE, keySpec(), param())
doFinal(bytesToDecrypt)
}
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
try {
decrypt(bytesToDecrypt)
} catch (e: GeneralSecurityException) {
Log.w("AESCBC", "Failed to decrypt", e)
null
}
companion object {
const val NAME = "aes-cbc"
}
}
@@ -0,0 +1,71 @@
/**
* 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.ciphers
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.Log
import java.security.GeneralSecurityException
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
actual class AESGCM actual constructor(
actual val keyBytes: ByteArray,
actual val nonce: ByteArray,
) : NostrCipher {
private fun newCipher() = Cipher.getInstance("AES/GCM/NoPadding")
private fun keySpec() = SecretKeySpec(keyBytes, "AES")
private fun param() = GCMParameterSpec(128, nonce)
actual override fun name() = NAME
fun copyUsingUTF8Nonce(): AESGCM =
AESGCM(
keyBytes,
nonce.toHexKey().toByteArray(Charsets.UTF_8),
)
actual override fun encrypt(bytesToEncrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.ENCRYPT_MODE, keySpec(), param())
doFinal(bytesToEncrypt)
}
actual override fun decrypt(bytesToDecrypt: ByteArray): ByteArray =
with(newCipher()) {
init(Cipher.DECRYPT_MODE, keySpec(), param())
doFinal(bytesToDecrypt)
}
actual override fun decryptOrNull(bytesToDecrypt: ByteArray): ByteArray? =
try {
decrypt(bytesToDecrypt)
} catch (e: GeneralSecurityException) {
Log.w("AESGCM", "Failed to decrypt", e)
null
}
companion object {
const val NAME = "aes-gcm"
}
}
@@ -0,0 +1,37 @@
/**
* 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.diggest
import java.security.MessageDigest
actual class DigestInstance actual constructor(
algorithm: String,
) {
val digest: MessageDigest = MessageDigest.getInstance(algorithm)
actual fun update(array: ByteArray) = digest.update(array)
actual fun update(byte: Byte) = digest.update(byte)
actual fun digest(): ByteArray = digest.digest()
actual fun digest(input: ByteArray): ByteArray = digest.digest(input)
}
@@ -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.utils.mac
import javax.crypto.SecretKey
/**
* Simple key spec that doesn't clone the key bytearray
*/
class FixedKey(
val key: ByteArray,
val algo: String,
) : SecretKey {
override fun getAlgorithm() = algo
override fun getEncoded() = key
override fun getFormat() = "RAW"
override fun hashCode() = key.contentHashCode() xor algo.hashCode()
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is FixedKey) return false
if (!key.contentEquals(other.key)) return false
// Old algorithm names
val thatAlg = other.algorithm
if (!(thatAlg.equals(this.algorithm, ignoreCase = true))) {
if ((
!(thatAlg.equals("DESede", ignoreCase = true)) ||
!(this.algorithm.equals("TripleDES", ignoreCase = true))
) &&
(
!(thatAlg.equals("TripleDES", ignoreCase = true)) ||
!(this.algorithm.equals("DESede", ignoreCase = true))
)
) {
return false
}
}
return true
}
override fun destroy() = key.fill(0)
override fun isDestroyed() = key.all { it.toInt() == 0 }
}
@@ -0,0 +1,51 @@
/**
* 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.mac
import javax.crypto.Mac
actual class MacInstance actual constructor(
algorithm: String,
key: ByteArray,
) {
val mac: Mac =
Mac.getInstance(algorithm).apply {
init(FixedKey(key, algorithm))
}
actual fun init(
key: ByteArray,
algorithm: String,
) = mac.init(FixedKey(key, algorithm))
actual fun getMacLength() = mac.macLength
actual fun update(array: ByteArray) = mac.update(array)
actual fun update(byte: Byte) = mac.update(byte)
actual fun doFinal() = mac.doFinal()
actual fun doFinal(
output: ByteArray,
offset: Int,
) = mac.doFinal(output, offset)
}
@@ -0,0 +1,25 @@
/**
* 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.sha256
val pool = Sha256Pool(5) // max parallel operations
actual fun sha256(data: ByteArray) = pool.hash(data)
@@ -0,0 +1,33 @@
/**
* 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.sha256
import java.security.MessageDigest
class Sha256Hasher {
val digest = MessageDigest.getInstance("SHA-256")
fun hash(byteArray: ByteArray) = digest.digest(byteArray).also { digest.reset() }
fun digest(byteArray: ByteArray) = digest.digest(byteArray)
fun reset() = digest.reset()
}
@@ -0,0 +1,57 @@
/**
* 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.sha256
import com.vitorpamplona.quartz.utils.Log
import java.util.concurrent.ArrayBlockingQueue
class Sha256Pool(
size: Int,
) {
private val pool = ArrayBlockingQueue<Sha256Hasher>(size)
init {
repeat(size) {
pool.add(Sha256Hasher())
}
}
private fun acquire(): Sha256Hasher {
if (pool.isEmpty()) {
Log.w("SHA256Pool", "Pool running low in available digests")
}
return pool.take()
}
private fun release(digest: Sha256Hasher) {
digest.reset()
pool.put(digest)
}
fun hash(byteArray: ByteArray): ByteArray {
val hasher = acquire()
try {
return hasher.digest(byteArray)
} finally {
release(hasher)
}
}
}