Merge upstream changes and adjust.
This commit is contained in:
+114
@@ -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.relay.client
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface INostrClient {
|
||||
fun relayStatusFlow(): StateFlow<RelayPool.RelayPoolStatus>
|
||||
|
||||
fun connect()
|
||||
|
||||
fun disconnect()
|
||||
|
||||
fun reconnect(onlyIfChanged: Boolean = false)
|
||||
|
||||
fun isActive(): Boolean
|
||||
|
||||
fun openReqSubscription(
|
||||
subId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
)
|
||||
|
||||
fun openCountSubscription(
|
||||
subId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
)
|
||||
|
||||
fun close(subscriptionId: String)
|
||||
|
||||
fun sendIfExists(
|
||||
event: Event,
|
||||
connectedRelay: NormalizedRelayUrl,
|
||||
)
|
||||
|
||||
fun send(
|
||||
event: Event,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
)
|
||||
|
||||
fun subscribe(listener: IRelayClientListener)
|
||||
|
||||
fun unsubscribe(listener: IRelayClientListener)
|
||||
|
||||
fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>?
|
||||
|
||||
fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>?
|
||||
}
|
||||
|
||||
object EmptyNostrClient : INostrClient {
|
||||
override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus())
|
||||
|
||||
override fun connect() { }
|
||||
|
||||
override fun disconnect() { }
|
||||
|
||||
override fun reconnect(onlyIfChanged: Boolean) { }
|
||||
|
||||
override fun isActive() = false
|
||||
|
||||
override fun openReqSubscription(
|
||||
subId: String,
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) { }
|
||||
|
||||
override fun openCountSubscription(
|
||||
subId: String,
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) { }
|
||||
|
||||
override fun close(subscriptionId: String) { }
|
||||
|
||||
override fun sendIfExists(
|
||||
event: Event,
|
||||
connectedRelay: NormalizedRelayUrl,
|
||||
) { }
|
||||
|
||||
override fun send(
|
||||
event: Event,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
) { }
|
||||
|
||||
override fun subscribe(listener: IRelayClientListener) {}
|
||||
|
||||
override fun unsubscribe(listener: IRelayClientListener) {}
|
||||
|
||||
override fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = null
|
||||
|
||||
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = null
|
||||
}
|
||||
+19
-17
@@ -29,7 +29,6 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.pool.PoolSubscriptionRepo
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayPool
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.basic.BasicRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.stats.RelayStats
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
@@ -73,7 +72,8 @@ import kotlinx.coroutines.flow.stateIn
|
||||
class NostrClient(
|
||||
private val websocketBuilder: WebsocketBuilder,
|
||||
private val scope: CoroutineScope,
|
||||
) : IRelayClientListener {
|
||||
) : INostrClient,
|
||||
IRelayClientListener {
|
||||
private val relayPool: RelayPool = RelayPool(this, ::buildRelay)
|
||||
private val activeRequests: PoolSubscriptionRepository = PoolSubscriptionRepository()
|
||||
private val activeCounts: PoolSubscriptionRepository = PoolSubscriptionRepository()
|
||||
@@ -124,20 +124,20 @@ class NostrClient(
|
||||
}
|
||||
|
||||
// Reconnects all relays that may have disconnected
|
||||
fun connect() {
|
||||
override fun connect() {
|
||||
isActive = true
|
||||
relayPool.connect()
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
override fun disconnect() {
|
||||
isActive = false
|
||||
relayPool.disconnect()
|
||||
}
|
||||
|
||||
fun isActive() = isActive
|
||||
override fun isActive() = isActive
|
||||
|
||||
@Synchronized
|
||||
fun reconnect(onlyIfChanged: Boolean = false) {
|
||||
override fun reconnect(onlyIfChanged: Boolean) {
|
||||
if (onlyIfChanged) {
|
||||
relayPool.reconnectIfNeedsToORIfItIsTime()
|
||||
} else {
|
||||
@@ -204,8 +204,8 @@ class NostrClient(
|
||||
return false
|
||||
}
|
||||
|
||||
fun openReqSubscription(
|
||||
subId: String = newSubId(),
|
||||
override fun openReqSubscription(
|
||||
subId: String,
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) {
|
||||
val oldFilters = activeRequests.getSubscriptionFiltersOrNull(subId) ?: emptyMap()
|
||||
@@ -240,8 +240,8 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun openCountSubscription(
|
||||
subId: String = newSubId(),
|
||||
override fun openCountSubscription(
|
||||
subId: String,
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
) {
|
||||
val oldFilters = activeCounts.getSubscriptionFiltersOrNull(subId) ?: emptyMap()
|
||||
@@ -276,7 +276,7 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun sendIfExists(
|
||||
override fun sendIfExists(
|
||||
event: Event,
|
||||
connectedRelay: NormalizedRelayUrl,
|
||||
) {
|
||||
@@ -288,7 +288,7 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun send(
|
||||
override fun send(
|
||||
event: Event,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
@@ -301,7 +301,7 @@ class NostrClient(
|
||||
}
|
||||
}
|
||||
|
||||
fun close(subscriptionId: String) {
|
||||
override fun close(subscriptionId: String) {
|
||||
activeRequests.remove(subscriptionId)
|
||||
activeCounts.remove(subscriptionId)
|
||||
relayPool.close(subscriptionId)
|
||||
@@ -386,13 +386,13 @@ class NostrClient(
|
||||
listeners.forEach { it.onError(relay, subId, error) }
|
||||
}
|
||||
|
||||
fun subscribe(listener: IRelayClientListener) {
|
||||
override fun subscribe(listener: IRelayClientListener) {
|
||||
listeners = listeners.plus(listener)
|
||||
}
|
||||
|
||||
fun isSubscribed(listener: IRelayClientListener): Boolean = listeners.contains(listener)
|
||||
|
||||
fun unsubscribe(listener: IRelayClientListener) {
|
||||
override fun unsubscribe(listener: IRelayClientListener) {
|
||||
listeners = listeners.minus(listener)
|
||||
}
|
||||
|
||||
@@ -402,7 +402,9 @@ class NostrClient(
|
||||
|
||||
fun activeOutboxCache(url: NormalizedRelayUrl): Set<HexKey> = eventOutbox.activeOutboxCacheFor(url)
|
||||
|
||||
fun getSubscriptionFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeRequests.getSubscriptionFiltersOrNull(subId)
|
||||
override fun getReqFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeRequests.getSubscriptionFiltersOrNull(subId)
|
||||
|
||||
fun relayStatusFlow() = relayPool.statusFlow
|
||||
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId)
|
||||
|
||||
override fun relayStatusFlow() = relayPool.statusFlow
|
||||
}
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
class NostrClientSubscription(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val filter: () -> Map<NormalizedRelayUrl, List<Filter>> = { emptyMap() },
|
||||
val onEvent: (event: Event) -> Unit = {},
|
||||
) : IRelayClientListener {
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
* Listens to NostrClient's onEvent messages for caching purposes.
|
||||
*/
|
||||
class EventCollector(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val onEvent: (event: Event, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
private val clientListener =
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.RelayState
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
@@ -32,7 +32,7 @@ import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
suspend fun NostrClient.sendAndWaitForResponse(
|
||||
suspend fun INostrClient.sendAndWaitForResponse(
|
||||
event: Event,
|
||||
relayList: Set<NormalizedRelayUrl>,
|
||||
timeoutInSeconds: Long = 15,
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
@@ -32,7 +32,7 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun NostrClient.downloadFirstEvent(
|
||||
fun INostrClient.downloadFirstEvent(
|
||||
subscriptionId: String = newSubId(),
|
||||
filters: Map<NormalizedRelayUrl, List<Filter>>,
|
||||
onResponse: (Event) -> Unit,
|
||||
|
||||
+2
-2
@@ -21,14 +21,14 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RelayAuthenticator(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val scope: CoroutineScope,
|
||||
val authenticate: suspend (challenge: String, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
* Listens to NostrClient's onEvent messages for caching purposes.
|
||||
*/
|
||||
class RelayInsertConfirmationCollector(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val onRelayReceived: (eventId: HexKey, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
private val clientListener =
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
|
||||
@@ -30,7 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
* Listens to NostrClient's onNotify messages from the relay
|
||||
*/
|
||||
class RelayLogger(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val notify: (message: String, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
private val clientListener =
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.accessories
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
|
||||
@@ -29,7 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
* Listens to NostrClient's onNotify messages from the relay
|
||||
*/
|
||||
class RelayNotifier(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
val notify: (message: String, relay: IRelayClient) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
|
||||
+10
-3
@@ -94,24 +94,31 @@ class RelayPool(
|
||||
relay.connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connect() =
|
||||
fun connect() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected() =
|
||||
fun connectIfDisconnected() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected()
|
||||
|
||||
fun disconnect() =
|
||||
fun disconnect() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.disconnect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun sendRequest(
|
||||
relay: NormalizedRelayUrl,
|
||||
|
||||
+3
-3
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
@@ -47,7 +47,7 @@ import com.vitorpamplona.quartz.utils.LargeCache
|
||||
* - Dismiss subscriptions with [dismissSubscription].
|
||||
*/
|
||||
class SubscriptionController(
|
||||
val client: NostrClient,
|
||||
val client: INostrClient,
|
||||
) {
|
||||
private val subscriptions = LargeCache<String, Subscription>()
|
||||
private val stats = SubscriptionStats()
|
||||
@@ -108,7 +108,7 @@ class SubscriptionController(
|
||||
fun updateRelays() {
|
||||
val currentFilters =
|
||||
subscriptions.associateWith { id, sub ->
|
||||
client.getSubscriptionFiltersOrNull(id)
|
||||
client.getReqFiltersOrNull(id)
|
||||
}
|
||||
|
||||
subscriptions.forEach { id, sub ->
|
||||
|
||||
@@ -61,17 +61,30 @@ class OtsEvent(
|
||||
const val KIND = 1040
|
||||
const val ALT = "Opentimestamps Attestation"
|
||||
|
||||
/**
|
||||
* Stamp is used to save OTS requests locally while we want for the bitcoin
|
||||
* blockchain to include the block with the proof.
|
||||
*/
|
||||
fun stamp(
|
||||
eventId: HexKey,
|
||||
resolver: OtsResolver,
|
||||
) = resolver.stamp(eventId.hexToByteArray())
|
||||
|
||||
/**
|
||||
* Converts a stamp into an attestation as soon as the bitcoin chain confirms
|
||||
* the block with it. If the return is not null, the attestation can be sent
|
||||
* to Nostr.
|
||||
*/
|
||||
fun upgrade(
|
||||
otsState: ByteArray,
|
||||
eventId: HexKey,
|
||||
resolver: OtsResolver,
|
||||
) = resolver.upgrade(otsState, eventId.hexToByteArray())
|
||||
|
||||
/**
|
||||
* Verifies if the attestation contained in a Nostr Event is valid by checking
|
||||
* with the blockchain.
|
||||
*/
|
||||
fun verify(
|
||||
otsState: ByteArray,
|
||||
eventId: HexKey,
|
||||
|
||||
@@ -33,14 +33,35 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.http.CalendarPureJavaBuilder
|
||||
import com.vitorpamplona.quartz.nip03Timestamp.ots.op.OpSHA256
|
||||
import kotlinx.coroutines.CancellationException
|
||||
|
||||
/**
|
||||
* Resolver class for OpenTimestamps operations.
|
||||
*
|
||||
* This class provides functionality to stamp data with OpenTimestamps,
|
||||
* upgrade existing timestamps, and verify timestamp proofs against data.
|
||||
*
|
||||
* @property explorer The Bitcoin explorer used for verification operations.
|
||||
* @property calendarBuilder The calendar builder used for stamping operations.
|
||||
*/
|
||||
class OtsResolver(
|
||||
explorer: BitcoinExplorer = BlockstreamExplorer(),
|
||||
calendarBuilder: CalendarBuilder = CalendarPureJavaBuilder(),
|
||||
) {
|
||||
val ots: OpenTimestamps = OpenTimestamps(explorer, calendarBuilder)
|
||||
|
||||
/**
|
||||
* Returns a human-readable information string about the given OTS file.
|
||||
*
|
||||
* @param otsState The serialized OpenTimestamps file data.
|
||||
* @return A string containing information about the timestamp.
|
||||
*/
|
||||
fun info(otsState: ByteArray): String = ots.info(DetachedTimestampFile.deserialize(otsState))
|
||||
|
||||
/**
|
||||
* Creates a local timestamp proof for the given data.
|
||||
*
|
||||
* @param data The data to be timestamped.
|
||||
* @return A serialized DetachedTimestampFile containing the timestamp proof.
|
||||
*/
|
||||
fun stamp(data: ByteArray): ByteArray {
|
||||
val hash = Hash(data, OpSHA256.TAG)
|
||||
val file = DetachedTimestampFile.from(hash)
|
||||
@@ -49,6 +70,13 @@ class OtsResolver(
|
||||
return detachedToSerialize.serialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to upgrade an existing timestamp to a verified proof.
|
||||
*
|
||||
* @param otsState The serialized OpenTimestamps file data to upgrade.
|
||||
* @param data The original data that was timestamped.
|
||||
* @return A serialized DetachedTimestampFile with upgraded proof if successful, null otherwise.
|
||||
*/
|
||||
fun upgrade(
|
||||
otsState: ByteArray,
|
||||
data: ByteArray,
|
||||
@@ -67,11 +95,25 @@ class OtsResolver(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an OpenTimestamps proof against the original data.
|
||||
*
|
||||
* @param otsFile The serialized OpenTimestamps file data.
|
||||
* @param data The original data to verify against.
|
||||
* @return VerificationState indicating the result of the verification.
|
||||
*/
|
||||
fun verify(
|
||||
otsFile: ByteArray,
|
||||
data: ByteArray,
|
||||
): VerificationState = verify(DetachedTimestampFile.deserialize(otsFile), data)
|
||||
|
||||
/**
|
||||
* Verifies an OpenTimestamps proof against the original data.
|
||||
*
|
||||
* @param detachedOts The DetachedTimestampFile containing the proof.
|
||||
* @param data The original data to verify against.
|
||||
* @return VerificationState indicating the result of the verification.
|
||||
*/
|
||||
fun verify(
|
||||
detachedOts: DetachedTimestampFile,
|
||||
data: ByteArray,
|
||||
|
||||
+4
@@ -23,3 +23,7 @@ package com.vitorpamplona.quartz.nip03Timestamp
|
||||
interface OtsResolverBuilder {
|
||||
fun build(): OtsResolver
|
||||
}
|
||||
|
||||
class DefaultOtsResolverBuilder : OtsResolverBuilder {
|
||||
override fun build(): OtsResolver = OtsResolver()
|
||||
}
|
||||
|
||||
+9
-13
@@ -24,35 +24,31 @@ import android.util.LruCache
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
class VerificationStateCache {
|
||||
class VerificationStateCache(
|
||||
val otsResolverBuilder: OtsResolverBuilder,
|
||||
) {
|
||||
private val cache = LruCache<HexKey, VerificationState>(200)
|
||||
|
||||
fun verify(
|
||||
event: OtsEvent,
|
||||
resolverBuilder: OtsResolverBuilder,
|
||||
): VerificationState {
|
||||
fun verify(event: OtsEvent): VerificationState {
|
||||
cache.put(event.id, VerificationState.Verifying)
|
||||
return event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) }
|
||||
return event.verifyState(otsResolverBuilder.build()).also { cache.put(event.id, it) }
|
||||
}
|
||||
|
||||
fun cacheVerify(
|
||||
event: OtsEvent,
|
||||
resolverBuilder: OtsResolverBuilder,
|
||||
): VerificationState =
|
||||
fun cacheVerify(event: OtsEvent): VerificationState =
|
||||
when (val verif = cache[event.id]) {
|
||||
is VerificationState.Verifying -> verif
|
||||
is VerificationState.Verified -> verif
|
||||
is VerificationState.NetworkError -> {
|
||||
// try again in 5 mins
|
||||
if (verif.time < TimeUtils.fiveMinutesAgo()) {
|
||||
event.verifyState(resolverBuilder.build()).also { cache.put(event.id, it) }
|
||||
event.verifyState(otsResolverBuilder.build()).also { cache.put(event.id, it) }
|
||||
} else {
|
||||
verify(event, resolverBuilder)
|
||||
verify(event)
|
||||
}
|
||||
}
|
||||
is VerificationState.Error -> verif
|
||||
else -> {
|
||||
verify(event, resolverBuilder)
|
||||
verify(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-20
@@ -26,26 +26,6 @@ import com.vitorpamplona.quartz.nip03Timestamp.ots.BlockHeader
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class BlockstreamExplorer : BitcoinExplorer {
|
||||
/**
|
||||
* Retrieve the block information from the block hash.
|
||||
|
||||
+9
-9
@@ -50,8 +50,8 @@ class Request(
|
||||
|
||||
try {
|
||||
val httpURLConnection = url.openConnection() as HttpURLConnection
|
||||
httpURLConnection.setReadTimeout(10000)
|
||||
httpURLConnection.setConnectTimeout(10000)
|
||||
httpURLConnection.readTimeout = 10000
|
||||
httpURLConnection.connectTimeout = 10000
|
||||
httpURLConnection.setRequestProperty("User-Agent", "OpenTimestamps Java")
|
||||
httpURLConnection.setRequestProperty("Accept", "application/json")
|
||||
httpURLConnection.setRequestProperty("Accept-Encoding", "gzip")
|
||||
@@ -62,17 +62,17 @@ class Request(
|
||||
|
||||
if (data != null) {
|
||||
httpURLConnection.setDoOutput(true)
|
||||
httpURLConnection.setRequestMethod("POST")
|
||||
httpURLConnection.requestMethod = "POST"
|
||||
httpURLConnection.setRequestProperty(
|
||||
"Content-Length",
|
||||
"" + this.data!!.size.toString(),
|
||||
)
|
||||
val wr = DataOutputStream(httpURLConnection.getOutputStream())
|
||||
wr.write(this.data, 0, this.data!!.size)
|
||||
wr.flush()
|
||||
wr.close()
|
||||
DataOutputStream(httpURLConnection.getOutputStream()).use { wr ->
|
||||
wr.write(this.data, 0, this.data!!.size)
|
||||
wr.flush()
|
||||
}
|
||||
} else {
|
||||
httpURLConnection.setRequestMethod("GET")
|
||||
httpURLConnection.requestMethod = "GET"
|
||||
}
|
||||
|
||||
httpURLConnection.connect()
|
||||
@@ -84,7 +84,7 @@ class Request(
|
||||
response.status = responseCode
|
||||
response.fromUrl = url.toString()
|
||||
var `is` = httpURLConnection.getInputStream()
|
||||
if ("gzip" == httpURLConnection.getContentEncoding()) {
|
||||
if ("gzip" == httpURLConnection.contentEncoding) {
|
||||
`is` = GZIPInputStream(`is`)
|
||||
}
|
||||
response.setStream(`is`)
|
||||
|
||||
+8
-2
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip03Timestamp.ots.http
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
@@ -30,7 +31,7 @@ import java.nio.charset.StandardCharsets
|
||||
/**
|
||||
* Holds the response from an HTTP request.
|
||||
*/
|
||||
class Response {
|
||||
class Response : Closeable {
|
||||
private var stream: InputStream? = null
|
||||
|
||||
var fromUrl: String? = null
|
||||
@@ -53,7 +54,7 @@ class Response {
|
||||
|
||||
@get:Throws(IOException::class)
|
||||
val string: String
|
||||
get() = kotlin.text.String(this.bytes, StandardCharsets.UTF_8)
|
||||
get() = String(this.bytes, StandardCharsets.UTF_8)
|
||||
|
||||
@get:Throws(IOException::class)
|
||||
val bytes: ByteArray
|
||||
@@ -79,4 +80,9 @@ class Response {
|
||||
JsonMapper.builder().build()
|
||||
return builder.readTree(jsonString)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
stream?.close()
|
||||
stream = null
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -62,7 +62,7 @@ abstract class OpCrypto internal constructor() : OpUnary() {
|
||||
val digest = MessageDigest.getInstance(this.hashLibName())
|
||||
var chunk = ctx.read(1048576)
|
||||
|
||||
while (chunk != null && chunk.size > 0) {
|
||||
while (chunk.isNotEmpty()) {
|
||||
digest.update(chunk)
|
||||
chunk = ctx.read(1048576)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ abstract class OpCrypto internal constructor() : OpUnary() {
|
||||
}
|
||||
|
||||
@Throws(IOException::class, NoSuchAlgorithmException::class)
|
||||
fun hashFd(file: File?): ByteArray = hashFd(FileInputStream(file))
|
||||
fun hashFd(file: File?): ByteArray = FileInputStream(file).use { inputStream -> hashFd(inputStream) }
|
||||
|
||||
@Throws(IOException::class, NoSuchAlgorithmException::class)
|
||||
fun hashFd(bytes: ByteArray): ByteArray {
|
||||
@@ -93,6 +93,7 @@ abstract class OpCrypto internal constructor() : OpUnary() {
|
||||
count = inputStream.read(chunk, 0, 1048576)
|
||||
}
|
||||
|
||||
// TODO: Is this needed? Closing of stream should be callers responsibility?
|
||||
inputStream.close()
|
||||
val hash = digest.digest()
|
||||
|
||||
|
||||
+3
@@ -34,6 +34,9 @@ class DecryptZapResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<ZapEventDecryptionResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
val eventJson = intent.result
|
||||
return if (!eventJson.isNullOrBlank()) {
|
||||
if (eventJson.startsWith("{")) {
|
||||
|
||||
+3
@@ -33,6 +33,9 @@ class DeriveKeyResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DerivationResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
val newPrivateKey = intent.result
|
||||
return if (newPrivateKey != null) {
|
||||
SignerResult.RequestAddressed.Successful(DerivationResult(newPrivateKey))
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ class Nip04DecryptResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DecryptionResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
val plaintext = intent.result
|
||||
return if (plaintext != null) {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext))
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ class Nip04EncryptResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<EncryptionResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
|
||||
val ciphertext = intent.result
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext))
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ class Nip44DecryptResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DecryptionResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
val plaintext = intent.result
|
||||
return if (plaintext != null) {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(plaintext))
|
||||
|
||||
+3
@@ -32,6 +32,9 @@ class Nip44EncryptResponse {
|
||||
)
|
||||
|
||||
fun parse(intent: IntentResult): SignerResult.RequestAddressed<EncryptionResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
val ciphertext = intent.result
|
||||
return if (ciphertext != null) {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(ciphertext))
|
||||
|
||||
+4
@@ -39,6 +39,10 @@ class SignResponse {
|
||||
intent: IntentResult,
|
||||
unsignedEvent: Event,
|
||||
): SignerResult.RequestAddressed<SignResult> {
|
||||
if (intent.rejected) {
|
||||
return SignerResult.RequestAddressed.ManuallyRejected()
|
||||
}
|
||||
|
||||
val eventJson = intent.event
|
||||
return if (eventJson != null) {
|
||||
if (eventJson.startsWith("{")) {
|
||||
|
||||
+2
@@ -29,6 +29,7 @@ data class IntentResult(
|
||||
val result: String? = null,
|
||||
val event: String? = null,
|
||||
val id: String? = null,
|
||||
val rejected: Boolean = false,
|
||||
) {
|
||||
fun toJson(): String = JsonMapper.mapper.writeValueAsString(this)
|
||||
|
||||
@@ -48,6 +49,7 @@ data class IntentResult(
|
||||
result = data.getStringExtra("result"),
|
||||
event = data.getStringExtra("event"),
|
||||
`package` = data.getStringExtra("package"),
|
||||
rejected = data.extras?.containsKey("rejected") == true,
|
||||
)
|
||||
|
||||
fun fromJson(json: String): IntentResult = JsonMapper.mapper.readValue<IntentResult>(json)
|
||||
|
||||
+1
@@ -36,6 +36,7 @@ class IntentResultJsonDeserializer : StdDeserializer<IntentResult>(IntentResult:
|
||||
result = jsonObject.get("result")?.asText()?.intern(),
|
||||
event = jsonObject.get("event")?.asText()?.intern(),
|
||||
id = jsonObject.get("id")?.asText()?.intern(),
|
||||
rejected = jsonObject.get("rejected")?.asBoolean() ?: false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ class IntentResultJsonSerializer : StdSerializer<IntentResult>(IntentResult::cla
|
||||
result.result?.let { gen.writeStringField("result", it) }
|
||||
result.event?.let { gen.writeStringField("event", it) }
|
||||
result.id?.let { gen.writeStringField("id", it) }
|
||||
result.rejected.let { gen.writeBooleanField("rejected", it) }
|
||||
gen.writeEndObject()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,6 @@ object TimeUtils {
|
||||
fun randomWithTwoDays() = now() - RandomInstance.int(twoDays())
|
||||
|
||||
fun ninetyDaysFromNow() = now() + NINETY_DAYS
|
||||
|
||||
fun oneYearAgo() = now() - ONE_YEAR
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user