From df1bf82e764ad2afbd1d592134f78d192af53cd9 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 10 Nov 2025 15:03:45 -0300 Subject: [PATCH 01/11] Create NostrSignerRemote --- .../signer/NostrSignerRemote.kt | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt new file mode 100644 index 000000000..613c0a1bc --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -0,0 +1,108 @@ +/** + * 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.NostrSigner +import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal +import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent +import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent + +class NostrSignerRemote( + signer: NostrSignerInternal, + val remotePubkey: HexKey, + val relays: List, + val client: INostrClient, +) : NostrSigner(signer.pubKey) { + val subscription = + client.req( + relays = relays, + filter = + Filter( + kinds = listOf(NostrConnectEvent.KIND), + tags = mapOf("p" to listOf(remotePubkey)), + ), + ) { event -> + } + + fun openSubscription() { + subscription.updateFilter() + } + + fun closeSubscription() { + subscription.close() + } + + override fun isWriteable(): Boolean = true + + override suspend fun sign( + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + ): T { + TODO("Not yet implemented") + } + + override suspend fun nip04Encrypt( + plaintext: String, + toPublicKey: HexKey, + ): String { + TODO("Not yet implemented") + } + + override suspend fun nip04Decrypt( + ciphertext: String, + fromPublicKey: HexKey, + ): String { + TODO("Not yet implemented") + } + + override suspend fun nip44Encrypt( + plaintext: String, + toPublicKey: HexKey, + ): String { + TODO("Not yet implemented") + } + + override suspend fun nip44Decrypt( + ciphertext: String, + fromPublicKey: HexKey, + ): String { + TODO("Not yet implemented") + } + + 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 +} From e6f109de740ca0323b85fe4fbc5c21ce3e97dcc5 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 10 Nov 2025 15:29:43 -0300 Subject: [PATCH 02/11] - Add connect, getPublivKey and ping - send NostrConnectEvent to relays --- .../signer/NostrSignerRemote.kt | 103 +++++++++++++++++- 1 file changed, 100 insertions(+), 3 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 613c0a1bc..193cf8656 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -22,31 +22,44 @@ 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.core.OptimizedJsonMapper 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.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 class NostrSignerRemote( - signer: NostrSignerInternal, + val signer: NostrSignerInternal, val remotePubkey: HexKey, - val relays: List, + val relays: Set, val client: INostrClient, + val permissions: String? = null, + val secret: String? = null, ) : NostrSigner(signer.pubKey) { val subscription = client.req( - relays = relays, + relays = relays.toList(), filter = Filter( kinds = listOf(NostrConnectEvent.KIND), tags = mapOf("p" to listOf(remotePubkey)), ), ) { event -> + } fun openSubscription() { @@ -65,6 +78,20 @@ class NostrSignerRemote( tags: Array>, content: String, ): T { + val template = EventTemplate( + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + ) + val event = NostrConnectEvent.create( + message = BunkerRequestSign( + event = template, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -72,6 +99,15 @@ class NostrSignerRemote( plaintext: String, toPublicKey: HexKey, ): String { + val event = NostrConnectEvent.create( + message = BunkerRequestNip04Encrypt( + message = plaintext, + pubKey = toPublicKey, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -79,6 +115,15 @@ class NostrSignerRemote( ciphertext: String, fromPublicKey: HexKey, ): String { + val event = NostrConnectEvent.create( + message = BunkerRequestNip04Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -86,6 +131,15 @@ class NostrSignerRemote( plaintext: String, toPublicKey: HexKey, ): String { + val event = NostrConnectEvent.create( + message = BunkerRequestNip44Encrypt( + message = plaintext, + pubKey = toPublicKey, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -93,6 +147,49 @@ class NostrSignerRemote( ciphertext: String, fromPublicKey: HexKey, ): String { + val event = NostrConnectEvent.create( + message = BunkerRequestNip44Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) + TODO("Not yet implemented") + } + + suspend fun ping(): String { + val event = NostrConnectEvent.create( + message = BunkerRequestPing(), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) + TODO("Not yet implemented") + } + + suspend fun connect(): String { + val event = NostrConnectEvent.create( + message = BunkerRequestConnect( + remoteKey = remotePubkey, + permissions = permissions, + secret = secret, + ), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) + TODO("Not yet implemented") + } + + suspend fun getPublicKey(): String { + val event = NostrConnectEvent.create( + message = BunkerRequestGetPublicKey(), + remoteKey = remotePubkey, + signer = signer + ) + client.send(event, relayList = relays) TODO("Not yet implemented") } From da85e59424dce37ced5d3badb47949923366d8a8 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 10 Nov 2025 15:36:57 -0300 Subject: [PATCH 03/11] - Add connect, getPublivKey and ping - send NostrConnectEvent to relays --- .../signer/NostrSignerRemote.kt | 143 ++++++++++-------- 1 file changed, 78 insertions(+), 65 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 193cf8656..518bfebd6 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -22,7 +22,6 @@ 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.core.OptimizedJsonMapper 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 @@ -59,7 +58,6 @@ class NostrSignerRemote( tags = mapOf("p" to listOf(remotePubkey)), ), ) { event -> - } fun openSubscription() { @@ -78,19 +76,22 @@ class NostrSignerRemote( tags: Array>, content: String, ): T { - val template = EventTemplate( - createdAt = createdAt, - kind = kind, - tags = tags, - content = content, - ) - val event = NostrConnectEvent.create( - message = BunkerRequestSign( - event = template, - ), - remoteKey = remotePubkey, - signer = signer - ) + val template = + EventTemplate( + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + ) + val event = + NostrConnectEvent.create( + message = + BunkerRequestSign( + event = template, + ), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -99,14 +100,16 @@ class NostrSignerRemote( plaintext: String, toPublicKey: HexKey, ): String { - val event = NostrConnectEvent.create( - message = BunkerRequestNip04Encrypt( - message = plaintext, - pubKey = toPublicKey, - ), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = + BunkerRequestNip04Encrypt( + message = plaintext, + pubKey = toPublicKey, + ), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -115,14 +118,16 @@ class NostrSignerRemote( ciphertext: String, fromPublicKey: HexKey, ): String { - val event = NostrConnectEvent.create( - message = BunkerRequestNip04Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = + BunkerRequestNip04Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -131,14 +136,16 @@ class NostrSignerRemote( plaintext: String, toPublicKey: HexKey, ): String { - val event = NostrConnectEvent.create( - message = BunkerRequestNip44Encrypt( - message = plaintext, - pubKey = toPublicKey, - ), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = + BunkerRequestNip44Encrypt( + message = plaintext, + pubKey = toPublicKey, + ), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } @@ -147,48 +154,54 @@ class NostrSignerRemote( ciphertext: String, fromPublicKey: HexKey, ): String { - val event = NostrConnectEvent.create( - message = BunkerRequestNip44Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = + BunkerRequestNip44Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } suspend fun ping(): String { - val event = NostrConnectEvent.create( - message = BunkerRequestPing(), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = BunkerRequestPing(), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } suspend fun connect(): String { - val event = NostrConnectEvent.create( - message = BunkerRequestConnect( + val event = + NostrConnectEvent.create( + message = + BunkerRequestConnect( + remoteKey = remotePubkey, + permissions = permissions, + secret = secret, + ), remoteKey = remotePubkey, - permissions = permissions, - secret = secret, - ), - remoteKey = remotePubkey, - signer = signer - ) + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } suspend fun getPublicKey(): String { - val event = NostrConnectEvent.create( - message = BunkerRequestGetPublicKey(), - remoteKey = remotePubkey, - signer = signer - ) + val event = + NostrConnectEvent.create( + message = BunkerRequestGetPublicKey(), + remoteKey = remotePubkey, + signer = signer, + ) client.send(event, relayList = relays) TODO("Not yet implemented") } From aee6ffdbf5bc14c7651d21e72bd814d966a3765a Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Mon, 10 Nov 2025 17:26:39 -0300 Subject: [PATCH 04/11] return BunkerResponse from connect --- .../signer/NostrSignerRemote.kt | 59 ++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 518bfebd6..61b3171bb 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -22,6 +22,7 @@ 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.core.OptimizedJsonMapper 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 @@ -37,9 +38,14 @@ 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.BunkerResponse import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.utils.cache.LargeCache +import com.vitorpamplona.quartz.utils.tryAndWait +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume class NostrSignerRemote( val signer: NostrSignerInternal, @@ -49,15 +55,23 @@ class NostrSignerRemote( val permissions: String? = null, val secret: String? = null, ) : NostrSigner(signer.pubKey) { + private val timeout = 30_000L + private val awaitingRequests = LargeCache>() + val subscription = client.req( relays = relays.toList(), filter = Filter( kinds = listOf(NostrConnectEvent.KIND), - tags = mapOf("p" to listOf(remotePubkey)), + tags = mapOf("p" to listOf(signer.pubKey)), ), ) { event -> + val message = signer.signerSync.nip44Decrypt(event.content, remotePubkey) + val bunkerResponse = OptimizedJsonMapper.fromJsonTo(message) + + awaitingRequests.get(bunkerResponse.id)?.resume(bunkerResponse) + awaitingRequests.remove(bunkerResponse.id) } fun openSubscription() { @@ -83,16 +97,21 @@ class NostrSignerRemote( tags = tags, content = content, ) + + val request = + BunkerRequestSign( + event = template, + ) + val event = NostrConnectEvent.create( - message = - BunkerRequestSign( - event = template, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) + client.send(event, relayList = relays) + TODO("Not yet implemented") } @@ -179,20 +198,32 @@ class NostrSignerRemote( TODO("Not yet implemented") } - suspend fun connect(): String { + suspend fun connect(): BunkerResponse? { + val request = + BunkerRequestConnect( + remoteKey = remotePubkey, + permissions = permissions, + secret = secret, + ) val event = NostrConnectEvent.create( - message = - BunkerRequestConnect( - remoteKey = remotePubkey, - permissions = permissions, - secret = secret, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + + return result } suspend fun getPublicKey(): String { From b2f2a409c30d3e8ec604463ce1d9c31691ca9b9d Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 08:17:36 -0300 Subject: [PATCH 05/11] implement the functions of nip46 --- .../signer/NostrSignerRemote.kt | 154 +++++++++++++----- 1 file changed, 114 insertions(+), 40 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 61b3171bb..4f9cc2832 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestNip44Encrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -110,95 +111,156 @@ class NostrSignerRemote( signer = signer, ) - client.send(event, relayList = relays) + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } - TODO("Not yet implemented") + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + + return (result as BunkerResponseEvent).event as T } override suspend fun nip04Encrypt( plaintext: String, toPublicKey: HexKey, ): String { + val request = + BunkerRequestNip04Encrypt( + message = plaintext, + pubKey = toPublicKey, + ) val event = NostrConnectEvent.create( - message = - BunkerRequestNip04Encrypt( - message = plaintext, - pubKey = toPublicKey, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + return result?.result ?: "" } override suspend fun nip04Decrypt( ciphertext: String, fromPublicKey: HexKey, ): String { + val request = + BunkerRequestNip04Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ) val event = NostrConnectEvent.create( - message = - BunkerRequestNip04Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + return result?.result ?: "" } override suspend fun nip44Encrypt( plaintext: String, toPublicKey: HexKey, ): String { + val request = + BunkerRequestNip44Encrypt( + message = plaintext, + pubKey = toPublicKey, + ) val event = NostrConnectEvent.create( - message = - BunkerRequestNip44Encrypt( - message = plaintext, - pubKey = toPublicKey, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + return result?.result ?: "" } override suspend fun nip44Decrypt( ciphertext: String, fromPublicKey: HexKey, ): String { + val request = + BunkerRequestNip44Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ) val event = NostrConnectEvent.create( - message = - BunkerRequestNip44Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + return result?.result ?: "" } - suspend fun ping(): String { + suspend fun ping(): String? { + val request = BunkerRequestPing() val event = NostrConnectEvent.create( - message = BunkerRequestPing(), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + + return result?.result } - suspend fun connect(): BunkerResponse? { + suspend fun connect(): HexKey? { val request = BunkerRequestConnect( remoteKey = remotePubkey, @@ -223,18 +285,30 @@ class NostrSignerRemote( client.send(event, relayList = relays) } - return result + return result?.result } - suspend fun getPublicKey(): String { + suspend fun getPublicKey(): HexKey? { + val request = BunkerRequestGetPublicKey() val event = NostrConnectEvent.create( - message = BunkerRequestGetPublicKey(), + message = request, remoteKey = remotePubkey, signer = signer, ) - client.send(event, relayList = relays) - TODO("Not yet implemented") + + val result = + tryAndWait(timeout) { continuation -> + continuation.invokeOnCancellation { + awaitingRequests.remove(request.id) + } + + awaitingRequests.put(request.id, continuation) + + client.send(event, relayList = relays) + } + + return result?.result } override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { From 8d529e1ea969c29bfe5031b0fd0f204778272d86 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 08:56:36 -0300 Subject: [PATCH 06/11] add a signer result interface for remote signer --- .../nip46RemoteSigner/signer/SignerResult.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt new file mode 100644 index 000000000..f86f0f8d8 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt @@ -0,0 +1,49 @@ +/** + * 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 { + sealed interface RequestAddressed : SignerResult { + class Successful( + val result: T, + ) : RequestAddressed + + class Rejected : RequestAddressed + + class TimedOut : RequestAddressed + + class ReceivedButCouldNotPerform( + val message: String? = null, + ) : RequestAddressed + + class ReceivedButCouldNotParseEventFromResult( + val eventJson: String, + ) : RequestAddressed + + class ReceivedButCouldNotVerifyResultingEvent( + val invalidEvent: Event, + ) : RequestAddressed + } +} + +interface IResult From 55b4ddc453494d7c71ede29a248303bf9e72a80a Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 09:29:32 -0300 Subject: [PATCH 07/11] add a fromBunkerUri helper --- .../signer/NostrSignerRemote.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 4f9cc2832..fe0a27c7b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.utils.Hex import com.vitorpamplona.quartz.utils.cache.LargeCache import com.vitorpamplona.quartz.utils.tryAndWait import kotlin.coroutines.Continuation @@ -320,4 +321,39 @@ class NostrSignerRemote( } override fun hasForegroundSupport(): Boolean = true + + 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() + 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, + ) + } + } } From e02dfce152f0e7ab8dcd9741b442e6827a8b647f Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 10:45:03 -0300 Subject: [PATCH 08/11] create a RemoteSignerManager --- .../signer/NostrSignerRemote.kt | 375 ++++++++++-------- .../signer/RemoteSignerManager.kt | 88 ++++ .../nip46RemoteSigner/signer/SignerResult.kt | 20 + 3 files changed, 309 insertions(+), 174 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/RemoteSignerManager.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index fe0a27c7b..b2aee390c 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -22,7 +22,7 @@ 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.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.crypto.verify 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 @@ -38,16 +38,15 @@ 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.BunkerResponse +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.utils.Hex -import com.vitorpamplona.quartz.utils.cache.LargeCache -import com.vitorpamplona.quartz.utils.tryAndWait -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume class NostrSignerRemote( val signer: NostrSignerInternal, @@ -57,8 +56,13 @@ class NostrSignerRemote( val permissions: String? = null, val secret: String? = null, ) : NostrSigner(signer.pubKey) { - private val timeout = 30_000L - private val awaitingRequests = LargeCache>() + private val manager = + RemoteSignerManager( + signer = signer, + remoteKey = remotePubkey, + relayList = relays, + client = client, + ) val subscription = client.req( @@ -69,11 +73,7 @@ class NostrSignerRemote( tags = mapOf("p" to listOf(signer.pubKey)), ), ) { event -> - val message = signer.signerSync.nip44Decrypt(event.content, remotePubkey) - val bunkerResponse = OptimizedJsonMapper.fromJsonTo(message) - - awaitingRequests.get(bunkerResponse.id)?.resume(bunkerResponse) - awaitingRequests.remove(bunkerResponse.id) + manager.newResponse(event) } fun openSubscription() { @@ -92,224 +92,251 @@ class NostrSignerRemote( tags: Array>, content: String, ): T { - val template = - EventTemplate( - createdAt = createdAt, - kind = kind, - tags = tags, - content = content, - ) - - val request = - BunkerRequestSign( - event = template, - ) - - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) - val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + val template = + EventTemplate( + createdAt = createdAt, + kind = kind, + tags = tags, + content = content, + ) - awaitingRequests.put(request.id, continuation) + BunkerRequestSign( + event = template, + ) + }, + parser = { response -> + if (response is BunkerResponseEvent) { + if (!response.event.verify()) { + SignerResult.RequestAddressed.ReceivedButCouldNotVerifyResultingEvent(response.event) + } else { + SignerResult.RequestAddressed.Successful(SignResult(response.event)) + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - client.send(event, relayList = relays) + if (result is SignerResult.RequestAddressed.Successful) { + (result.result.event as? T)?.let { + return it } + } - return (result as BunkerResponseEvent).event as T + throw Exception("Could not sign") } override suspend fun nip04Encrypt( plaintext: String, toPublicKey: HexKey, ): String { - val request = - BunkerRequestNip04Encrypt( - message = plaintext, - pubKey = toPublicKey, - ) - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestNip04Encrypt( + message = plaintext, + pubKey = toPublicKey, + ) + }, + parser = { response -> + if (response is BunkerResponseEncrypt) { + if (response.error != null) { + SignerResult.RequestAddressed.Rejected() + } else { + SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - awaitingRequests.put(request.id, continuation) + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.ciphertext + } - client.send(event, relayList = relays) - } - return result?.result ?: "" + throw Exception("Could not encrypt") } override suspend fun nip04Decrypt( ciphertext: String, fromPublicKey: HexKey, ): String { - val request = - BunkerRequestNip04Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ) - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestNip04Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ) + }, + parser = { response -> + if (response is BunkerResponseDecrypt) { + if (response.error != null) { + SignerResult.RequestAddressed.Rejected() + } else { + SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - awaitingRequests.put(request.id, continuation) + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.plaintext + } - client.send(event, relayList = relays) - } - return result?.result ?: "" + throw Exception("Could not decrypt") } override suspend fun nip44Encrypt( plaintext: String, toPublicKey: HexKey, ): String { - val request = - BunkerRequestNip44Encrypt( - message = plaintext, - pubKey = toPublicKey, - ) - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestNip44Encrypt( + message = plaintext, + pubKey = toPublicKey, + ) + }, + parser = { response -> + if (response is BunkerResponseEncrypt) { + if (response.error != null) { + SignerResult.RequestAddressed.Rejected() + } else { + SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - awaitingRequests.put(request.id, continuation) + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.ciphertext + } - client.send(event, relayList = relays) - } - return result?.result ?: "" + throw Exception("Could not encrypt") } override suspend fun nip44Decrypt( ciphertext: String, fromPublicKey: HexKey, ): String { - val request = - BunkerRequestNip44Decrypt( - ciphertext = ciphertext, - pubKey = fromPublicKey, - ) - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestNip44Decrypt( + ciphertext = ciphertext, + pubKey = fromPublicKey, + ) + }, + parser = { response -> + if (response is BunkerResponseDecrypt) { + if (response.error != null) { + SignerResult.RequestAddressed.Rejected() + } else { + SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - awaitingRequests.put(request.id, continuation) + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.plaintext + } - client.send(event, relayList = relays) - } - return result?.result ?: "" + throw Exception("Could not decrypt") } suspend fun ping(): String? { - val request = BunkerRequestPing() - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, + val result = + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestPing() + }, + parser = { response -> + if (response is BunkerResponsePong) { + if (response.result != null) { + SignerResult.RequestAddressed.Successful(PingResult(response.result)) + } else { + SignerResult.RequestAddressed.Rejected() + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, ) - val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.pong + } - awaitingRequests.put(request.id, continuation) - - client.send(event, relayList = relays) - } - - return result?.result + throw Exception("Could not ping") } - suspend fun connect(): HexKey? { - val request = - BunkerRequestConnect( - remoteKey = remotePubkey, - permissions = permissions, - secret = secret, - ) - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, - ) - + suspend fun connect(): HexKey { val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestConnect( + remoteKey = remotePubkey, + permissions = permissions, + secret = secret, + ) + }, + parser = { response -> + if (response is BunkerResponsePublicKey) { + if (response.result != null) { + SignerResult.RequestAddressed.Successful(PublicKeyResult(response.result)) + } else { + SignerResult.RequestAddressed.Rejected() + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, + ) - awaitingRequests.put(request.id, continuation) + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.pubkey + } - client.send(event, relayList = relays) - } - - return result?.result + throw Exception("Could not connect") } - suspend fun getPublicKey(): HexKey? { - val request = BunkerRequestGetPublicKey() - val event = - NostrConnectEvent.create( - message = request, - remoteKey = remotePubkey, - signer = signer, + suspend fun getPublicKey(): HexKey { + val result = + manager.launchWaitAndParse( + bunkerRequestBuilder = { + BunkerRequestGetPublicKey() + }, + parser = { response -> + if (response is BunkerResponsePublicKey) { + if (response.result != null) { + SignerResult.RequestAddressed.Successful(PublicKeyResult(response.result)) + } else { + SignerResult.RequestAddressed.Rejected() + } + } else { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + }, ) - val result = - tryAndWait(timeout) { continuation -> - continuation.invokeOnCancellation { - awaitingRequests.remove(request.id) - } + if (result is SignerResult.RequestAddressed.Successful) { + return result.result.pubkey + } - awaitingRequests.put(request.id, continuation) - - client.send(event, relayList = relays) - } - - return result?.result + throw Exception("Could not get public key") } override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/RemoteSignerManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/RemoteSignerManager.kt new file mode 100644 index 000000000..eb07ca232 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/RemoteSignerManager.kt @@ -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, +) { + private val awaitingRequests = LargeCache>() + + fun newResponse(responseEvent: Event) { + val bunkerResponse = OptimizedJsonMapper.fromJsonTo(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]. + * @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 launchWaitAndParse( + bunkerRequestBuilder: () -> BunkerRequest, + parser: (response: BunkerResponse) -> SignerResult.RequestAddressed, + ): SignerResult.RequestAddressed { + 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) + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt index f86f0f8d8..1bb20fc06 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt @@ -47,3 +47,23 @@ sealed interface SignerResult { } 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 From c2f2dc46d91ab574ea2746ccd52080df57da34fa Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 11:49:00 -0300 Subject: [PATCH 09/11] check for error result --- .../signer/NostrSignerRemote.kt | 115 +++++++++++------- 1 file changed, 72 insertions(+), 43 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index b2aee390c..1633590f9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -40,6 +40,7 @@ import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestPing import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerRequestSign import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseDecrypt import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt +import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey @@ -114,6 +115,8 @@ class NostrSignerRemote( } else { SignerResult.RequestAddressed.Successful(SignResult(response.event)) } + } else if (response is BunkerResponseError) { + SignerResult.RequestAddressed.Rejected() } else { SignerResult.RequestAddressed.ReceivedButCouldNotPerform() } @@ -142,14 +145,18 @@ class NostrSignerRemote( ) }, parser = { response -> - if (response is BunkerResponseEncrypt) { - if (response.error != null) { - SignerResult.RequestAddressed.Rejected() - } else { + when (response) { + is BunkerResponseEncrypt -> { SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -174,14 +181,16 @@ class NostrSignerRemote( ) }, parser = { response -> - if (response is BunkerResponseDecrypt) { - if (response.error != null) { - SignerResult.RequestAddressed.Rejected() - } else { + when (response) { + is BunkerResponseDecrypt -> { SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -206,14 +215,18 @@ class NostrSignerRemote( ) }, parser = { response -> - if (response is BunkerResponseEncrypt) { - if (response.error != null) { - SignerResult.RequestAddressed.Rejected() - } else { + when (response) { + is BunkerResponseEncrypt -> { SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -238,14 +251,18 @@ class NostrSignerRemote( ) }, parser = { response -> - if (response is BunkerResponseDecrypt) { - if (response.error != null) { - SignerResult.RequestAddressed.Rejected() - } else { + when (response) { + is BunkerResponseDecrypt -> { SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -257,21 +274,25 @@ class NostrSignerRemote( throw Exception("Could not decrypt") } - suspend fun ping(): String? { + suspend fun ping(): String { val result = manager.launchWaitAndParse( bunkerRequestBuilder = { BunkerRequestPing() }, parser = { response -> - if (response is BunkerResponsePong) { - if (response.result != null) { - SignerResult.RequestAddressed.Successful(PingResult(response.result)) - } else { + when (response) { + is BunkerResponsePong -> { + SignerResult.RequestAddressed.Successful(PingResult(response.id)) + } + + is BunkerResponseError -> { SignerResult.RequestAddressed.Rejected() } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -294,14 +315,18 @@ class NostrSignerRemote( ) }, parser = { response -> - if (response is BunkerResponsePublicKey) { - if (response.result != null) { - SignerResult.RequestAddressed.Successful(PublicKeyResult(response.result)) - } else { + when (response) { + is BunkerResponsePublicKey -> { + SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey)) + } + + is BunkerResponseError -> { SignerResult.RequestAddressed.Rejected() } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) @@ -320,14 +345,18 @@ class NostrSignerRemote( BunkerRequestGetPublicKey() }, parser = { response -> - if (response is BunkerResponsePublicKey) { - if (response.result != null) { - SignerResult.RequestAddressed.Successful(PublicKeyResult(response.result)) - } else { + when (response) { + is BunkerResponsePublicKey -> { + SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey)) + } + + is BunkerResponseError -> { SignerResult.RequestAddressed.Rejected() } - } else { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } } }, ) From 7dce71d8fad5e98586ae610f913abb46c53156a8 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 13:33:48 -0300 Subject: [PATCH 10/11] added response helper --- .../signer/Nip04DecryptResponse.kt | 42 ++++++ .../signer/Nip04EncryptResponse.kt | 44 ++++++ .../signer/Nip44DecryptResponse.kt | 44 ++++++ .../signer/Nip44EncryptResponse.kt | 44 ++++++ .../signer/NostrSignerRemote.kt | 131 ++---------------- .../nip46RemoteSigner/signer/PingResponse.kt | 44 ++++++ .../signer/PubKeyResponse.kt | 44 ++++++ .../nip46RemoteSigner/signer/SignResponse.kt | 43 ++++++ 8 files changed, 313 insertions(+), 123 deletions(-) create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04DecryptResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04EncryptResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44DecryptResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44EncryptResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PingResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PubKeyResponse.kt create mode 100644 quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignResponse.kt diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04DecryptResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04DecryptResponse.kt new file mode 100644 index 000000000..a671fbc58 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04DecryptResponse.kt @@ -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 = + when (response) { + is BunkerResponseDecrypt -> { + SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) + } + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04EncryptResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04EncryptResponse.kt new file mode 100644 index 000000000..39e3d9ef9 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip04EncryptResponse.kt @@ -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 = + when (response) { + is BunkerResponseEncrypt -> { + SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) + } + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44DecryptResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44DecryptResponse.kt new file mode 100644 index 000000000..7349f8991 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44DecryptResponse.kt @@ -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 = + when (response) { + is BunkerResponseDecrypt -> { + SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) + } + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44EncryptResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44EncryptResponse.kt new file mode 100644 index 000000000..255fe5f46 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/Nip44EncryptResponse.kt @@ -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 = + when (response) { + is BunkerResponseEncrypt -> { + SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) + } + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 1633590f9..44873b8f9 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -22,7 +22,6 @@ 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.crypto.verify 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 @@ -38,12 +37,6 @@ 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.BunkerResponseDecrypt -import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEncrypt -import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError -import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseEvent -import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong -import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey import com.vitorpamplona.quartz.nip46RemoteSigner.NostrConnectEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent @@ -108,19 +101,7 @@ class NostrSignerRemote( event = template, ) }, - parser = { response -> - 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() - } - }, + parser = SignResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -144,21 +125,7 @@ class NostrSignerRemote( pubKey = toPublicKey, ) }, - parser = { response -> - when (response) { - is BunkerResponseEncrypt -> { - SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = Nip04EncryptResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -180,19 +147,7 @@ class NostrSignerRemote( pubKey = fromPublicKey, ) }, - parser = { response -> - when (response) { - is BunkerResponseDecrypt -> { - SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) - } - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = Nip04DecryptResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -214,21 +169,7 @@ class NostrSignerRemote( pubKey = toPublicKey, ) }, - parser = { response -> - when (response) { - is BunkerResponseEncrypt -> { - SignerResult.RequestAddressed.Successful(EncryptionResult(response.ciphertext)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = Nip44EncryptResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -250,21 +191,7 @@ class NostrSignerRemote( pubKey = fromPublicKey, ) }, - parser = { response -> - when (response) { - is BunkerResponseDecrypt -> { - SignerResult.RequestAddressed.Successful(DecryptionResult(response.plaintext)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = Nip44DecryptResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -280,21 +207,7 @@ class NostrSignerRemote( bunkerRequestBuilder = { BunkerRequestPing() }, - parser = { response -> - when (response) { - is BunkerResponsePong -> { - SignerResult.RequestAddressed.Successful(PingResult(response.id)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = PingResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -314,21 +227,7 @@ class NostrSignerRemote( secret = secret, ) }, - parser = { response -> - when (response) { - is BunkerResponsePublicKey -> { - SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = PubKeyResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { @@ -344,21 +243,7 @@ class NostrSignerRemote( bunkerRequestBuilder = { BunkerRequestGetPublicKey() }, - parser = { response -> - when (response) { - is BunkerResponsePublicKey -> { - SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey)) - } - - is BunkerResponseError -> { - SignerResult.RequestAddressed.Rejected() - } - - else -> { - SignerResult.RequestAddressed.ReceivedButCouldNotPerform() - } - } - }, + parser = PubKeyResponse::parse, ) if (result is SignerResult.RequestAddressed.Successful) { diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PingResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PingResponse.kt new file mode 100644 index 000000000..754ce1c16 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PingResponse.kt @@ -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 = + when (response) { + is BunkerResponsePong -> { + SignerResult.RequestAddressed.Successful(PingResult(response.id)) + } + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PubKeyResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PubKeyResponse.kt new file mode 100644 index 000000000..43fed5d03 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/PubKeyResponse.kt @@ -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 = + when (response) { + is BunkerResponsePublicKey -> { + SignerResult.RequestAddressed.Successful(PublicKeyResult(response.pubkey)) + } + + is BunkerResponseError -> { + SignerResult.RequestAddressed.Rejected() + } + + else -> { + SignerResult.RequestAddressed.ReceivedButCouldNotPerform() + } + } + } +} diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignResponse.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignResponse.kt new file mode 100644 index 000000000..35f47498b --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignResponse.kt @@ -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 = + 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() + } + } +} From a6f3e09a6ca8f177c5d3f3bb93ef121e0a96cb37 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 12 Nov 2025 13:47:26 -0300 Subject: [PATCH 11/11] add a convertExeptions function --- .../signer/NostrSignerRemote.kt | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt index 44873b8f9..58811230b 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/NostrSignerRemote.kt @@ -29,6 +29,7 @@ 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 @@ -110,7 +111,7 @@ class NostrSignerRemote( } } - throw Exception("Could not sign") + throw convertExceptions("Could not sign", result) } override suspend fun nip04Encrypt( @@ -132,7 +133,7 @@ class NostrSignerRemote( return result.result.ciphertext } - throw Exception("Could not encrypt") + throw convertExceptions("Could not encrypt", result) } override suspend fun nip04Decrypt( @@ -154,7 +155,7 @@ class NostrSignerRemote( return result.result.plaintext } - throw Exception("Could not decrypt") + throw convertExceptions("Could not decrypt", result) } override suspend fun nip44Encrypt( @@ -176,7 +177,7 @@ class NostrSignerRemote( return result.result.ciphertext } - throw Exception("Could not encrypt") + throw convertExceptions("Could not encrypt", result) } override suspend fun nip44Decrypt( @@ -198,7 +199,7 @@ class NostrSignerRemote( return result.result.plaintext } - throw Exception("Could not decrypt") + throw convertExceptions("Could not decrypt", result) } suspend fun ping(): String { @@ -214,7 +215,7 @@ class NostrSignerRemote( return result.result.pong } - throw Exception("Could not ping") + throw convertExceptions("Could not ping", result) } suspend fun connect(): HexKey { @@ -234,7 +235,7 @@ class NostrSignerRemote( return result.result.pubkey } - throw Exception("Could not connect") + throw convertExceptions("Could not connect", result) } suspend fun getPublicKey(): HexKey { @@ -250,7 +251,7 @@ class NostrSignerRemote( return result.result.pubkey } - throw Exception("Could not get public key") + throw convertExceptions("Could not get public key", result) } override suspend fun decryptZapEvent(event: LnZapRequestEvent): LnZapPrivateEvent { @@ -263,6 +264,19 @@ class NostrSignerRemote( 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,