Fix some conflicts
This commit is contained in:
@@ -255,7 +255,7 @@ mavenPublishing {
|
||||
coordinates(
|
||||
groupId = "com.vitorpamplona.quartz",
|
||||
artifactId = "quartz",
|
||||
version = "1.03.0"
|
||||
version = "1.04.2"
|
||||
)
|
||||
|
||||
// Configure publishing to Maven Central
|
||||
|
||||
+6
-3
@@ -22,7 +22,6 @@ 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.reqs.IRequestListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId
|
||||
@@ -32,7 +31,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface INostrClient {
|
||||
fun relayStatusFlow(): StateFlow<RelayPool.RelayPoolStatus>
|
||||
fun connectedRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun availableRelaysFlow(): StateFlow<Set<NormalizedRelayUrl>>
|
||||
|
||||
fun connect()
|
||||
|
||||
@@ -80,7 +81,9 @@ interface INostrClient {
|
||||
}
|
||||
|
||||
object EmptyNostrClient : INostrClient {
|
||||
override fun relayStatusFlow() = MutableStateFlow(RelayPool.RelayPoolStatus())
|
||||
override fun connectedRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
|
||||
|
||||
override fun availableRelaysFlow() = MutableStateFlow(emptySet<NormalizedRelayUrl>())
|
||||
|
||||
override fun connect() { }
|
||||
|
||||
|
||||
+3
-1
@@ -320,5 +320,7 @@ class NostrClient(
|
||||
|
||||
override fun getCountFiltersOrNull(subId: String): Map<NormalizedRelayUrl, List<Filter>>? = activeCounts.getSubscriptionFiltersOrNull(subId)
|
||||
|
||||
override fun relayStatusFlow() = relayPool.statusFlow
|
||||
override fun connectedRelaysFlow() = relayPool.connectedRelays
|
||||
|
||||
override fun availableRelaysFlow() = relayPool.availableRelays
|
||||
}
|
||||
|
||||
+28
-52
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.nip01Core.relay.client.pool
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.EmptyClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.listeners.IRelayClientListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.single.IRelayClient
|
||||
@@ -31,8 +30,8 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
/**
|
||||
* RelayPool manages a collection of Nostr relays, abstracting individual connections and providing
|
||||
@@ -61,9 +60,11 @@ class RelayPool(
|
||||
) : IRelayClientListener {
|
||||
private val relays = LargeCache<NormalizedRelayUrl, IRelayClient>()
|
||||
|
||||
// Backing property to avoid flow emissions from other classes
|
||||
private val _statusFlow = MutableStateFlow<RelayPoolStatus>(RelayPoolStatus())
|
||||
val statusFlow: StateFlow<RelayPoolStatus> = _statusFlow.asStateFlow()
|
||||
private val _connectedRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||
val connectedRelays = _connectedRelays.asStateFlow()
|
||||
|
||||
private val _availableRelays = MutableStateFlow<Set<NormalizedRelayUrl>>(emptySet())
|
||||
val availableRelays = _availableRelays.asStateFlow()
|
||||
|
||||
fun getRelay(url: NormalizedRelayUrl): IRelayClient? = relays.get(url)
|
||||
|
||||
@@ -87,21 +88,18 @@ class RelayPool(
|
||||
relay.connectAndSyncFiltersIfDisconnected(ignoreRetryDelays)
|
||||
}
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connect() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected() {
|
||||
relays.forEach { url, relay ->
|
||||
relay.connectAndSyncFiltersIfDisconnected()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun connectIfDisconnected(relay: NormalizedRelayUrl) = relays.get(relay)?.connectAndSyncFiltersIfDisconnected()
|
||||
@@ -110,7 +108,6 @@ class RelayPool(
|
||||
relays.forEach { url, relay ->
|
||||
relay.disconnect()
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
fun sendOrConnectAndSync(
|
||||
@@ -144,7 +141,13 @@ class RelayPool(
|
||||
// --------------------
|
||||
// Pool Maintenance
|
||||
// --------------------
|
||||
fun getOrCreateRelay(relay: NormalizedRelayUrl) = relays.getOrCreate(relay, ::createNewRelay)
|
||||
fun getOrCreateRelay(relay: NormalizedRelayUrl): IRelayClient {
|
||||
val r = relays.getOrCreate(relay, ::createNewRelay)
|
||||
if (_availableRelays.value.size != relays.size()) {
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
fun createRelayIfAbsent(relay: NormalizedRelayUrl): Boolean = relays.createIfAbsent(relay, ::createNewRelay)
|
||||
|
||||
@@ -155,39 +158,40 @@ class RelayPool(
|
||||
val toRemove = relays.keys() - newRelays
|
||||
var atLeastOne = false
|
||||
|
||||
newRelays.forEach {
|
||||
if (createRelayIfAbsent(it)) {
|
||||
newRelays.forEach { relay ->
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
toRemove.forEach {
|
||||
if (removeRelayInner(it)) {
|
||||
toRemove.forEach { relay ->
|
||||
if (removeRelayInner(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
fun addRelay(relay: NormalizedRelayUrl): IRelayClient {
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
return getOrCreateRelay(relay)
|
||||
}
|
||||
|
||||
fun addAllRelays(relayList: List<NormalizedRelayUrl>) {
|
||||
var atLeastOne = false
|
||||
relayList.forEach {
|
||||
if (createRelayIfAbsent(it)) {
|
||||
relayList.forEach { relay ->
|
||||
if (createRelayIfAbsent(relay)) {
|
||||
atLeastOne = true
|
||||
}
|
||||
}
|
||||
|
||||
if (atLeastOne) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +206,7 @@ class RelayPool(
|
||||
|
||||
fun removeRelay(relay: NormalizedRelayUrl) {
|
||||
if (removeRelayInner(relay)) {
|
||||
updateStatus()
|
||||
_availableRelays.update { relays.keys() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +214,7 @@ class RelayPool(
|
||||
if (relays.size() > 0) {
|
||||
disconnect()
|
||||
relays.clear()
|
||||
updateStatus()
|
||||
_availableRelays.update { emptySet() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,12 +228,12 @@ class RelayPool(
|
||||
pingMillis: Int,
|
||||
compressed: Boolean,
|
||||
) {
|
||||
updateStatus()
|
||||
_connectedRelays.update { it + relay.url }
|
||||
listener.onConnected(relay, pingMillis, compressed)
|
||||
}
|
||||
|
||||
override fun onDisconnected(relay: IRelayClient) {
|
||||
updateStatus()
|
||||
_connectedRelays.update { it - relay.url }
|
||||
listener.onDisconnected(relay)
|
||||
}
|
||||
|
||||
@@ -251,33 +255,5 @@ class RelayPool(
|
||||
success: Boolean,
|
||||
) = listener.onSent(relay, cmdStr, cmd, success)
|
||||
|
||||
// ---------------
|
||||
// STATUS Reports
|
||||
// ---------------
|
||||
|
||||
fun availableRelays(): Set<NormalizedRelayUrl> = relays.keys()
|
||||
|
||||
fun connectedRelays(): Set<NormalizedRelayUrl> =
|
||||
relays.mapNotNullIntoSet { url, relay ->
|
||||
if (relay.isConnected()) {
|
||||
url
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateStatus() {
|
||||
val connected = connectedRelays()
|
||||
val available = availableRelays()
|
||||
if (_statusFlow.value.connected != connected || _statusFlow.value.available != available) {
|
||||
_statusFlow.tryEmit(RelayPoolStatus(connected, available))
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class RelayPoolStatus(
|
||||
val connected: Set<NormalizedRelayUrl> = emptySet(),
|
||||
val available: Set<NormalizedRelayUrl> = emptySet(),
|
||||
val isConnected: Boolean = connected.isNotEmpty(),
|
||||
)
|
||||
fun connectedRelaysCount(): Int = relays.count { url, relay -> relay.isConnected() }
|
||||
}
|
||||
|
||||
@@ -89,10 +89,7 @@ object Nip19Parser {
|
||||
if (uri == null) return null
|
||||
|
||||
try {
|
||||
val matcher = nip19PlusNip46regex.find(uri)
|
||||
if (matcher == null) {
|
||||
return null
|
||||
}
|
||||
val matcher = nip19PlusNip46regex.find(uri) ?: return null
|
||||
|
||||
val type = matcher.groups[3]?.value ?: matcher.groups[5]?.value // npub1
|
||||
val key = matcher.groups[4]?.value ?: matcher.groups[6]?.value // bech32
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* 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.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip04DecryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<DecryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseDecrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext))
|
||||
}
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip04EncryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<EncryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseEncrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip44DecryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<DecryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseDecrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
|
||||
class Nip44EncryptResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<EncryptionResult> =
|
||||
when (response) {
|
||||
is BunkerResponseEncrypt -> {
|
||||
SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* 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.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.req
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestConnect
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestGetPublicKey
|
||||
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
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
class NostrSignerRemote(
|
||||
val signer: NostrSignerInternal,
|
||||
val remotePubkey: HexKey,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
val client: INostrClient,
|
||||
val permissions: String? = null,
|
||||
val secret: String? = null,
|
||||
) : NostrSigner(signer.pubKey) {
|
||||
private val manager =
|
||||
RemoteSignerManager(
|
||||
signer = signer,
|
||||
remoteKey = remotePubkey,
|
||||
relayList = relays,
|
||||
client = client,
|
||||
)
|
||||
|
||||
val subscription =
|
||||
client.req(
|
||||
relays = relays.toList(),
|
||||
filter =
|
||||
Filter(
|
||||
kinds = listOf(NostrConnectEvent.KIND),
|
||||
tags = mapOf("p" to listOf(signer.pubKey)),
|
||||
),
|
||||
) { event ->
|
||||
manager.newResponse(event)
|
||||
}
|
||||
|
||||
fun openSubscription() {
|
||||
subscription.updateFilter()
|
||||
}
|
||||
|
||||
fun closeSubscription() {
|
||||
subscription.close()
|
||||
}
|
||||
|
||||
override fun isWriteable(): Boolean = true
|
||||
|
||||
override suspend fun <T : Event> sign(
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
): T {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
val template =
|
||||
EventTemplate<Event>(
|
||||
createdAt = createdAt,
|
||||
kind = kind,
|
||||
tags = tags,
|
||||
content = content,
|
||||
)
|
||||
|
||||
BunkerRequestSign(
|
||||
event = template,
|
||||
)
|
||||
},
|
||||
parser = SignResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<SignResult>) {
|
||||
(result.result.event as? T)?.let {
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not sign", result)
|
||||
}
|
||||
|
||||
override suspend fun nip04Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip04Encrypt(
|
||||
message = plaintext,
|
||||
pubKey = toPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip04EncryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<EncryptionResult>) {
|
||||
return result.result.ciphertext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not encrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip04Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip04Decrypt(
|
||||
ciphertext = ciphertext,
|
||||
pubKey = fromPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip04DecryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<DecryptionResult>) {
|
||||
return result.result.plaintext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not decrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip44Encrypt(
|
||||
plaintext: String,
|
||||
toPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip44Encrypt(
|
||||
message = plaintext,
|
||||
pubKey = toPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip44EncryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<EncryptionResult>) {
|
||||
return result.result.ciphertext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not encrypt", result)
|
||||
}
|
||||
|
||||
override suspend fun nip44Decrypt(
|
||||
ciphertext: String,
|
||||
fromPublicKey: HexKey,
|
||||
): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestNip44Decrypt(
|
||||
ciphertext = ciphertext,
|
||||
pubKey = fromPublicKey,
|
||||
)
|
||||
},
|
||||
parser = Nip44DecryptResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<DecryptionResult>) {
|
||||
return result.result.plaintext
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not decrypt", result)
|
||||
}
|
||||
|
||||
suspend fun ping(): String {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestPing()
|
||||
},
|
||||
parser = PingResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PingResult>) {
|
||||
return result.result.pong
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not ping", result)
|
||||
}
|
||||
|
||||
suspend fun connect(): HexKey {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestConnect(
|
||||
remoteKey = remotePubkey,
|
||||
permissions = permissions,
|
||||
secret = secret,
|
||||
)
|
||||
},
|
||||
parser = PubKeyResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PublicKeyResult>) {
|
||||
return result.result.pubkey
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not connect", result)
|
||||
}
|
||||
|
||||
suspend fun getPublicKey(): HexKey {
|
||||
val result =
|
||||
manager.launchWaitAndParse(
|
||||
bunkerRequestBuilder = {
|
||||
BunkerRequestGetPublicKey()
|
||||
},
|
||||
parser = PubKeyResponse::parse,
|
||||
)
|
||||
|
||||
if (result is SignerResult.RequestAddressed.Successful<PublicKeyResult>) {
|
||||
return result.result.pubkey
|
||||
}
|
||||
|
||||
throw convertExceptions("Could not get public key", result)
|
||||
}
|
||||
|
||||
override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun deriveKey(nonce: HexKey): HexKey {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun hasForegroundSupport(): Boolean = true
|
||||
|
||||
fun convertExceptions(
|
||||
title: String,
|
||||
result: SignerResult.RequestAddressed<*>,
|
||||
): Exception =
|
||||
when (result) {
|
||||
is SignerResult.RequestAddressed.Successful<*> -> IllegalStateException("$title: This should not happen. There is a bug on Quartz.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotParseEventFromResult<*> -> IllegalStateException("$title: Failed to parse event: ${result.eventJson}.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent<*> -> IllegalStateException("$title: Failed to verify event: ${result.invalidEvent.toJson()}.")
|
||||
is SignerResult.RequestAddressed.ReceivedButCouldNotPerform<*> -> SignerExceptions.CouldNotPerformException("$title: ${result.message}")
|
||||
is SignerResult.RequestAddressed.Rejected<*> -> SignerExceptions.ManuallyUnauthorizedException("$title: User has rejected the request.")
|
||||
is SignerResult.RequestAddressed.TimedOut<*> -> SignerExceptions.TimedOutException("$title: User didn't accept or reject in time.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromBunkerUri(
|
||||
bunkerUri: String,
|
||||
signer: NostrSignerInternal,
|
||||
client: INostrClient,
|
||||
permissions: String? = null,
|
||||
): NostrSignerRemote {
|
||||
if (!bunkerUri.startsWith("bunker://")) throw Exception("Invalid bunker uri")
|
||||
val splitData = bunkerUri.split("?")
|
||||
val remotePubkey = splitData[0].removePrefix("bunker://")
|
||||
if (!Hex.isHex(remotePubkey)) throw Exception("Invalid pubkey in bunker uri")
|
||||
val params = splitData[1].split("&")
|
||||
val relays = mutableSetOf<NormalizedRelayUrl>()
|
||||
var secret: String? = null
|
||||
for (param in params) {
|
||||
val splitParam = param.split("=")
|
||||
if (splitParam.size < 2) continue
|
||||
if (splitParam.first() == "relay") {
|
||||
relays.add(NormalizedRelayUrl(splitParam[1]))
|
||||
}
|
||||
if (splitParam.first() == "secret") {
|
||||
secret = splitParam[1]
|
||||
}
|
||||
}
|
||||
return NostrSignerRemote(
|
||||
signer = signer,
|
||||
remotePubkey = remotePubkey,
|
||||
relays = relays,
|
||||
client = client,
|
||||
permissions = permissions,
|
||||
secret = secret,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
|
||||
|
||||
class PingResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<PingResult> =
|
||||
when (response) {
|
||||
is BunkerResponsePong -> {
|
||||
SignerResult.RequestAddressed.Successful(PingResult(response.id))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
|
||||
|
||||
class PubKeyResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<PublicKeyResult> =
|
||||
when (response) {
|
||||
is BunkerResponsePublicKey -> {
|
||||
SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey))
|
||||
}
|
||||
|
||||
is BunkerResponseError -> {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequest
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent
|
||||
import com.vitorpamplona.quartz.utils.cache.LargeCache
|
||||
import com.vitorpamplona.quartz.utils.tryAndWait
|
||||
import kotlin.coroutines.Continuation
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
class RemoteSignerManager(
|
||||
val timeout: Long = 30000,
|
||||
val client: INostrClient,
|
||||
val signer: NostrSignerInternal,
|
||||
val remoteKey: String,
|
||||
val relayList: Set<NormalizedRelayUrl>,
|
||||
) {
|
||||
private val awaitingRequests = LargeCache<String, Continuation<BunkerResponse>>()
|
||||
|
||||
fun newResponse(responseEvent: Event) {
|
||||
val bunkerResponse = OptimizedJsonMapper.fromJsonTo<BunkerResponse>(responseEvent.content)
|
||||
awaitingRequests.get(bunkerResponse.id)?.resume(bunkerResponse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the signer, waits and parses the result
|
||||
*
|
||||
* @param bunkerRequestBuilder The BunkerRequest to be sent.
|
||||
* @param parser A function that parses the BunkerResponse into a [SignerResult.RequestAddressed<T>].
|
||||
* @return The result after parsing the BunkerResponse using the provided parser.
|
||||
*
|
||||
* This function uses the [tryAndWait] utility to implement a timeout on the Bunker Request approval.
|
||||
* It assigns a unique ID to the request and keeps a continuation to resume once the result is received.
|
||||
* If the timeout occurs or the continuation is cancelled, the request ID is cleaned up from [awaitingRequests].
|
||||
*/
|
||||
suspend fun <T : IResult> launchWaitAndParse(
|
||||
bunkerRequestBuilder: () -> BunkerRequest,
|
||||
parser: (response: BunkerResponse) -> SignerResult.RequestAddressed<T>,
|
||||
): SignerResult.RequestAddressed<T> {
|
||||
val request = bunkerRequestBuilder()
|
||||
val event =
|
||||
NostrConnectEvent.create(
|
||||
message = request,
|
||||
remoteKey = remoteKey,
|
||||
signer = signer,
|
||||
)
|
||||
val result =
|
||||
tryAndWait(timeout) { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
awaitingRequests.remove(request.id)
|
||||
}
|
||||
|
||||
awaitingRequests.put(request.id, continuation)
|
||||
|
||||
client.send(event, relayList = relayList)
|
||||
}
|
||||
|
||||
return when (result) {
|
||||
null -> SignerResult.RequestAddressed.TimedOut()
|
||||
else -> parser(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -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.nip46RemoteSigner.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.verify
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
|
||||
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent
|
||||
|
||||
class SignResponse {
|
||||
companion object {
|
||||
fun parse(response: BunkerResponse): SignerResult.RequestAddressed<SignResult> =
|
||||
if (response is BunkerResponseEvent) {
|
||||
if (!response.event.verify()) {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(response.event)
|
||||
} else {
|
||||
SignerResult.RequestAddressed.Successful(SignResult(response.event))
|
||||
}
|
||||
} else if (response is BunkerResponseError) {
|
||||
SignerResult.RequestAddressed.Rejected()
|
||||
} else {
|
||||
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 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.signer
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
|
||||
sealed interface SignerResult<T : IResult> {
|
||||
sealed interface RequestAddressed<T : IResult> : SignerResult<T> {
|
||||
class Successful<T : IResult>(
|
||||
val result: T,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class Rejected<T : IResult> : RequestAddressed<T>
|
||||
|
||||
class TimedOut<T : IResult> : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotPerform<T : IResult>(
|
||||
val message: String? = null,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
|
||||
val eventJson: String,
|
||||
) : RequestAddressed<T>
|
||||
|
||||
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
|
||||
val invalidEvent: Event,
|
||||
) : RequestAddressed<T>
|
||||
}
|
||||
}
|
||||
|
||||
interface IResult
|
||||
|
||||
data class SignResult(
|
||||
val event: Event,
|
||||
) : IResult
|
||||
|
||||
data class EncryptionResult(
|
||||
val ciphertext: String,
|
||||
) : IResult
|
||||
|
||||
data class DecryptionResult(
|
||||
val plaintext: String,
|
||||
) : IResult
|
||||
|
||||
data class PingResult(
|
||||
val pong: String,
|
||||
) : IResult
|
||||
|
||||
data class PublicKeyResult(
|
||||
val pubkey: String,
|
||||
) : IResult
|
||||
@@ -24,6 +24,7 @@ import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.utils.startsWith
|
||||
import com.vitorpamplona.quartz.utils.startsWithAny
|
||||
import com.vitorpamplona.quartz.utils.startsWithIgnoreCase
|
||||
|
||||
inline fun TagArray.filterToArray(predicate: (Array<String>) -> Boolean): TagArray = filterTo(ArrayList(), predicate).toTypedArray()
|
||||
|
||||
@@ -31,6 +32,8 @@ inline fun TagArray.remove(predicate: (Array<String>) -> Boolean): TagArray = fi
|
||||
|
||||
fun TagArray.remove(startsWith: Array<String>): TagArray = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray()
|
||||
|
||||
fun TagArray.removeIgnoreCase(startsWith: Array<String>): TagArray = filterNotTo(ArrayList(this.size), { it.startsWithIgnoreCase(startsWith) }).toTypedArray()
|
||||
|
||||
fun <R> TagArray.removeParsing(
|
||||
transform: (Tag) -> R,
|
||||
equalsTo: R,
|
||||
|
||||
+3
-3
@@ -36,8 +36,8 @@ import com.vitorpamplona.quartz.nip31Alts.alt
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
|
||||
import com.vitorpamplona.quartz.nip51Lists.encryption.signNip51List
|
||||
import com.vitorpamplona.quartz.nip51Lists.remove
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeAny
|
||||
import com.vitorpamplona.quartz.nip51Lists.removeIgnoreCase
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Immutable
|
||||
@@ -140,8 +140,8 @@ class HashtagListEvent(
|
||||
): HashtagListEvent {
|
||||
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
|
||||
return resign(
|
||||
privateTags = privateTags.remove(HashtagTag.assemble(hashtag)),
|
||||
tags = earlierVersion.tags.remove(HashtagTag.assemble(hashtag)),
|
||||
privateTags = privateTags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
|
||||
tags = earlierVersion.tags.removeIgnoreCase(HashtagTag.assemble(hashtag)),
|
||||
signer = signer,
|
||||
createdAt = createdAt,
|
||||
)
|
||||
|
||||
@@ -42,18 +42,14 @@ class IMetaTag(
|
||||
const val TAG_NAME = "imeta"
|
||||
const val ANCHOR_PROPERTY = "url"
|
||||
|
||||
fun parse(tag: Array<String>): IMetaTag? {
|
||||
fun parse(tag: Array<String>): List<IMetaTag>? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
val allTags = parseIMeta(tag)
|
||||
val url = allTags[ANCHOR_PROPERTY]?.firstOrNull()
|
||||
|
||||
return if (url != null) {
|
||||
IMetaTag(url, allTags.minus(ANCHOR_PROPERTY))
|
||||
} else {
|
||||
null
|
||||
return allTags[ANCHOR_PROPERTY]?.map {
|
||||
IMetaTag(it, allTags.minus(ANCHOR_PROPERTY))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ package com.vitorpamplona.quartz.nip92IMeta
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
fun TagArray.imetas() = this.mapNotNull(IMetaTag::parse)
|
||||
fun TagArray.imetas() = this.mapNotNull(IMetaTag::parse).flatten()
|
||||
|
||||
fun TagArray.imetasByUrl() = this.imetas().associateBy { it.url }
|
||||
|
||||
@@ -40,6 +40,14 @@ fun Array<String>.startsWith(startsWith: Array<String>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun Array<String>.startsWithIgnoreCase(startsWith: Array<String>): Boolean {
|
||||
if (startsWith.size > this.size) return false
|
||||
for (tagIdx in startsWith.indices) {
|
||||
if (!startsWith[tagIdx].equals(this[tagIdx], ignoreCase = true)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun Array<String>.startsWithAny(startsWithList: List<Array<String>>): Boolean = startsWithList.any { startsWith(it) }
|
||||
|
||||
public inline fun <T, R> Array<out T>.lastNotNullOfOrNull(transform: (T) -> R?): R? {
|
||||
|
||||
@@ -147,6 +147,18 @@ class NIP19ParserTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nAddrIncompleteParser() {
|
||||
val result =
|
||||
Nip19Parser.uriToRoute(
|
||||
"nostr:naddr1qqqqzxthwden5te0wfjkccte9ejxjanfdejjuanfv3jk7tczyrv4428upmlcujyf2fy4hqrynywj07ukakr99ufvmmw95n5tttj5qqcyqqqgt0qeresua",
|
||||
)
|
||||
assertEquals(
|
||||
"34236:d95aa8fc0eff8e488952495b8064991d27fb96ed8652f12cdedc5a4e8b5ae540:",
|
||||
(result?.entity as? NAddress)?.aTag(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nAddrParser2() {
|
||||
val result =
|
||||
|
||||
Reference in New Issue
Block a user