pakstr 0.19.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/android-template/app/build.gradle.kts +4 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
- package/android-template/app/src/main/assets/www/index.html +0 -6
- package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
- package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
- package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
- package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
- package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
- package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
- package/android-template/app/src/main/res/values/strings.xml +27 -0
- package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
- package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
- package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
- package/android-template/gradle/libs.versions.toml +8 -0
- package/dist/cli.js +8 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/nsite.js +38 -0
- package/dist/commands/publish.js +50 -2
- package/dist/commands/run.js +5 -5
- package/dist/core/blossom.js +2 -2
- package/dist/core/nsite.js +176 -0
- package/dist/core/pakstrConfig.js +85 -6
- package/package.json +1 -1
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
package com.pakstr.app.signer.bunker
|
|
2
|
+
|
|
3
|
+
import com.pakstr.app.crypto.Nip46ClientKey
|
|
4
|
+
import com.pakstr.app.signer.exceptions.SignerUnavailableException
|
|
5
|
+
import kotlinx.coroutines.CancellableContinuation
|
|
6
|
+
import kotlinx.coroutines.sync.Mutex
|
|
7
|
+
import kotlinx.coroutines.sync.withLock
|
|
8
|
+
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
9
|
+
import java.util.concurrent.Executors
|
|
10
|
+
import java.util.concurrent.ScheduledExecutorService
|
|
11
|
+
import java.util.concurrent.ScheduledFuture
|
|
12
|
+
import java.util.concurrent.TimeUnit
|
|
13
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
14
|
+
import kotlin.coroutines.resume
|
|
15
|
+
import kotlin.coroutines.resumeWithException
|
|
16
|
+
|
|
17
|
+
internal class RelayNip46Client(
|
|
18
|
+
private val protocol: Nip46Protocol,
|
|
19
|
+
private val transport: Nip46Transport,
|
|
20
|
+
private val requestTimeoutMillis: Long = DEFAULT_REQUEST_TIMEOUT_MILLIS,
|
|
21
|
+
private val scheduler: ScheduledExecutorService =
|
|
22
|
+
Executors.newSingleThreadScheduledExecutor(),
|
|
23
|
+
private val ownedClientKey: Nip46ClientKey? = null,
|
|
24
|
+
private val authChallengeHandler: ((String) -> Unit)? = null,
|
|
25
|
+
initialUserPubkey: String? = null,
|
|
26
|
+
private val userPubkeyHandler: ((String) -> Unit)? = null,
|
|
27
|
+
private val activationHandler: (() -> Unit)? = null,
|
|
28
|
+
private val authChallengeTimeoutMillis: Long =
|
|
29
|
+
DEFAULT_AUTH_CHALLENGE_TIMEOUT_MILLIS,
|
|
30
|
+
private val requestedPermissions: String? = null,
|
|
31
|
+
private val clientMetadataJson: String? = null,
|
|
32
|
+
private var connected: Boolean = false
|
|
33
|
+
) : Nip46Client {
|
|
34
|
+
constructor(
|
|
35
|
+
config: BunkerConfig,
|
|
36
|
+
clientKey: Nip46ClientKey = Nip46ClientKey.generate(),
|
|
37
|
+
connectionFactory: Nip46RelayConnectionFactory =
|
|
38
|
+
OkHttpNip46RelayConnectionFactory(),
|
|
39
|
+
requestTimeoutMillis: Long = DEFAULT_REQUEST_TIMEOUT_MILLIS,
|
|
40
|
+
authChallengeHandler: ((String) -> Unit)? = null,
|
|
41
|
+
initialUserPubkey: String? = null,
|
|
42
|
+
userPubkeyHandler: ((String) -> Unit)? = null,
|
|
43
|
+
activationHandler: (() -> Unit)? = null,
|
|
44
|
+
authChallengeTimeoutMillis: Long =
|
|
45
|
+
DEFAULT_AUTH_CHALLENGE_TIMEOUT_MILLIS,
|
|
46
|
+
requestedPermissions: String? = null,
|
|
47
|
+
clientMetadataJson: String? = null,
|
|
48
|
+
connected: Boolean = false
|
|
49
|
+
) : this(
|
|
50
|
+
protocol = Nip46Protocol(clientKey, config),
|
|
51
|
+
transport =
|
|
52
|
+
Nip46RelayTransport(
|
|
53
|
+
relayUrls = config.relayUrls,
|
|
54
|
+
remoteSignerPubkey = config.remoteSignerPubkey,
|
|
55
|
+
clientPubkey = clientKey.publicKeyHex,
|
|
56
|
+
connectionFactory = connectionFactory
|
|
57
|
+
),
|
|
58
|
+
requestTimeoutMillis = requestTimeoutMillis,
|
|
59
|
+
ownedClientKey = clientKey,
|
|
60
|
+
authChallengeHandler = authChallengeHandler,
|
|
61
|
+
initialUserPubkey = initialUserPubkey,
|
|
62
|
+
userPubkeyHandler = userPubkeyHandler,
|
|
63
|
+
activationHandler = activationHandler,
|
|
64
|
+
authChallengeTimeoutMillis = authChallengeTimeoutMillis,
|
|
65
|
+
requestedPermissions = requestedPermissions,
|
|
66
|
+
clientMetadataJson = clientMetadataJson,
|
|
67
|
+
connected = connected
|
|
68
|
+
)
|
|
69
|
+
private val lock = Any()
|
|
70
|
+
private val activationMutex = Mutex()
|
|
71
|
+
private val pendingRequests = mutableMapOf<String, PendingRequest>()
|
|
72
|
+
private var userSigningPubkey: String? = initialUserPubkey
|
|
73
|
+
private var closed = false
|
|
74
|
+
|
|
75
|
+
init {
|
|
76
|
+
transport.start(::receiveEvent)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override suspend fun getPublicKey(): String {
|
|
80
|
+
return activate()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
override suspend fun signEvent(eventJson: String): String {
|
|
84
|
+
val signingPubkey = activate()
|
|
85
|
+
return execute(
|
|
86
|
+
protocol.prepareSignEvent(
|
|
87
|
+
eventJson,
|
|
88
|
+
signingPubkey
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override suspend fun nip04Encrypt(
|
|
94
|
+
pubkey: String,
|
|
95
|
+
plaintext: String
|
|
96
|
+
): String {
|
|
97
|
+
activate()
|
|
98
|
+
return execute(protocol.prepareNip04Encrypt(pubkey, plaintext))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override suspend fun nip04Decrypt(
|
|
102
|
+
pubkey: String,
|
|
103
|
+
ciphertext: String
|
|
104
|
+
): String {
|
|
105
|
+
activate()
|
|
106
|
+
return execute(protocol.prepareNip04Decrypt(pubkey, ciphertext))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
override suspend fun nip44Encrypt(
|
|
110
|
+
pubkey: String,
|
|
111
|
+
plaintext: String
|
|
112
|
+
): String {
|
|
113
|
+
activate()
|
|
114
|
+
return execute(protocol.prepareNip44Encrypt(pubkey, plaintext))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override suspend fun nip44Decrypt(
|
|
118
|
+
pubkey: String,
|
|
119
|
+
ciphertext: String
|
|
120
|
+
): String {
|
|
121
|
+
activate()
|
|
122
|
+
return execute(protocol.prepareNip44Decrypt(pubkey, ciphertext))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private suspend fun activate(): String {
|
|
126
|
+
synchronized(lock) {
|
|
127
|
+
userSigningPubkey?.let { return it }
|
|
128
|
+
if (closed) {
|
|
129
|
+
throw closedException()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return activationMutex.withLock {
|
|
134
|
+
synchronized(lock) {
|
|
135
|
+
userSigningPubkey?.let { return@withLock it }
|
|
136
|
+
if (closed) {
|
|
137
|
+
throw closedException()
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!connected) {
|
|
142
|
+
execute(
|
|
143
|
+
protocol.prepareConnect(
|
|
144
|
+
requestedPermissions,
|
|
145
|
+
clientMetadataJson
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
connected = true
|
|
149
|
+
activationHandler?.invoke()
|
|
150
|
+
protocol.clearPairingSecret()
|
|
151
|
+
}
|
|
152
|
+
val resolved =
|
|
153
|
+
try {
|
|
154
|
+
execute(protocol.prepareGetPublicKey())
|
|
155
|
+
} catch (_: Nip46RequestTimeoutException) {
|
|
156
|
+
execute(protocol.prepareGetPublicKey())
|
|
157
|
+
}
|
|
158
|
+
synchronized(lock) {
|
|
159
|
+
if (closed) {
|
|
160
|
+
throw closedException()
|
|
161
|
+
}
|
|
162
|
+
userSigningPubkey = resolved
|
|
163
|
+
}
|
|
164
|
+
userPubkeyHandler?.invoke(resolved)
|
|
165
|
+
resolved
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
override fun disconnect() {
|
|
170
|
+
val outbound =
|
|
171
|
+
synchronized(lock) {
|
|
172
|
+
if (closed) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
protocol.prepareDisconnect()
|
|
177
|
+
} catch (_: Exception) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
transport.publish(
|
|
183
|
+
outbound.context.id,
|
|
184
|
+
outbound.eventJson
|
|
185
|
+
)
|
|
186
|
+
} catch (_: Exception) {
|
|
187
|
+
// Disconnect is best-effort; local logout must still complete.
|
|
188
|
+
} finally {
|
|
189
|
+
outbound.context.close()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
override fun close() {
|
|
194
|
+
val requests =
|
|
195
|
+
synchronized(lock) {
|
|
196
|
+
if (closed) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
closed = true
|
|
200
|
+
userSigningPubkey = null
|
|
201
|
+
val snapshot = pendingRequests.values.toList()
|
|
202
|
+
pendingRequests.clear()
|
|
203
|
+
snapshot
|
|
204
|
+
}
|
|
205
|
+
requests.forEach { pending ->
|
|
206
|
+
pending.timeout.get()?.cancel(false)
|
|
207
|
+
pending.outbound.context.close()
|
|
208
|
+
pending.continuation.resumeWithException(closedException())
|
|
209
|
+
}
|
|
210
|
+
transport.close()
|
|
211
|
+
scheduler.shutdownNow()
|
|
212
|
+
ownedClientKey?.close()
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private suspend fun execute(
|
|
216
|
+
outbound: Nip46OutboundRequest
|
|
217
|
+
): String {
|
|
218
|
+
return suspendCancellableCoroutine { continuation ->
|
|
219
|
+
val timeoutReference = AtomicReference<ScheduledFuture<*>?>()
|
|
220
|
+
val pending = PendingRequest(
|
|
221
|
+
outbound,
|
|
222
|
+
continuation,
|
|
223
|
+
timeoutReference
|
|
224
|
+
)
|
|
225
|
+
val accepted =
|
|
226
|
+
synchronized(lock) {
|
|
227
|
+
if (closed) {
|
|
228
|
+
false
|
|
229
|
+
} else {
|
|
230
|
+
pendingRequests[outbound.context.id] = pending
|
|
231
|
+
timeoutReference.set(
|
|
232
|
+
scheduler.schedule(
|
|
233
|
+
{ timeout(outbound.context.id) },
|
|
234
|
+
requestTimeoutMillis,
|
|
235
|
+
TimeUnit.MILLISECONDS
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
true
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!accepted) {
|
|
242
|
+
outbound.context.close()
|
|
243
|
+
continuation.resumeWithException(closedException())
|
|
244
|
+
return@suspendCancellableCoroutine
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
continuation.invokeOnCancellation {
|
|
248
|
+
cancel(outbound.context.id)
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
transport.publish(
|
|
252
|
+
outbound.context.id,
|
|
253
|
+
outbound.eventJson
|
|
254
|
+
)
|
|
255
|
+
} catch (_: Exception) {
|
|
256
|
+
fail(
|
|
257
|
+
outbound.context.id,
|
|
258
|
+
SignerUnavailableException(
|
|
259
|
+
"Unable to publish NIP-46 request"
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private fun receiveEvent(eventJson: String) {
|
|
267
|
+
val verified =
|
|
268
|
+
try {
|
|
269
|
+
protocol.decodeResponseEvent(eventJson)
|
|
270
|
+
} catch (_: Exception) {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
val requestId = verified.response.id
|
|
274
|
+
val pending =
|
|
275
|
+
synchronized(lock) {
|
|
276
|
+
pendingRequests[requestId]
|
|
277
|
+
} ?: return
|
|
278
|
+
|
|
279
|
+
val outcome =
|
|
280
|
+
try {
|
|
281
|
+
protocol.correlateResponse(
|
|
282
|
+
verified,
|
|
283
|
+
pending.outbound.context
|
|
284
|
+
)
|
|
285
|
+
} catch (_: Exception) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
when (outcome) {
|
|
289
|
+
is Nip46ResponseOutcome.Success ->
|
|
290
|
+
complete(requestId, outcome.result)
|
|
291
|
+
is Nip46ResponseOutcome.RemoteError ->
|
|
292
|
+
fail(
|
|
293
|
+
requestId,
|
|
294
|
+
Nip46RemoteException()
|
|
295
|
+
)
|
|
296
|
+
is Nip46ResponseOutcome.AuthChallenge -> {
|
|
297
|
+
val handler = authChallengeHandler
|
|
298
|
+
if (handler == null) {
|
|
299
|
+
fail(
|
|
300
|
+
requestId,
|
|
301
|
+
Nip46AuthChallengeException()
|
|
302
|
+
)
|
|
303
|
+
} else {
|
|
304
|
+
try {
|
|
305
|
+
extendTimeoutForAuthChallenge(
|
|
306
|
+
requestId,
|
|
307
|
+
pending
|
|
308
|
+
)
|
|
309
|
+
handler(outcome.url)
|
|
310
|
+
} catch (_: Exception) {
|
|
311
|
+
fail(
|
|
312
|
+
requestId,
|
|
313
|
+
Nip46AuthChallengeException()
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private fun extendTimeoutForAuthChallenge(
|
|
322
|
+
requestId: String,
|
|
323
|
+
pending: PendingRequest
|
|
324
|
+
) {
|
|
325
|
+
val replacement =
|
|
326
|
+
scheduler.schedule(
|
|
327
|
+
{ timeout(requestId) },
|
|
328
|
+
authChallengeTimeoutMillis,
|
|
329
|
+
TimeUnit.MILLISECONDS
|
|
330
|
+
)
|
|
331
|
+
val previous = pending.timeout.getAndSet(replacement)
|
|
332
|
+
previous?.cancel(false)
|
|
333
|
+
synchronized(lock) {
|
|
334
|
+
if (pendingRequests[requestId] !== pending) {
|
|
335
|
+
replacement.cancel(false)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private fun complete(
|
|
341
|
+
requestId: String,
|
|
342
|
+
result: String
|
|
343
|
+
) {
|
|
344
|
+
val pending = remove(requestId) ?: return
|
|
345
|
+
pending.continuation.resume(result)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private fun fail(
|
|
349
|
+
requestId: String,
|
|
350
|
+
error: Exception
|
|
351
|
+
) {
|
|
352
|
+
val pending = remove(requestId) ?: return
|
|
353
|
+
pending.continuation.resumeWithException(error)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun timeout(requestId: String) {
|
|
357
|
+
fail(requestId, Nip46RequestTimeoutException())
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private fun cancel(requestId: String) {
|
|
361
|
+
remove(requestId)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private fun remove(requestId: String): PendingRequest? {
|
|
365
|
+
val pending =
|
|
366
|
+
synchronized(lock) {
|
|
367
|
+
pendingRequests.remove(requestId)
|
|
368
|
+
} ?: return null
|
|
369
|
+
pending.timeout.get()?.cancel(false)
|
|
370
|
+
transport.cancel(requestId)
|
|
371
|
+
pending.outbound.context.close()
|
|
372
|
+
return pending
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private fun closedException(): SignerUnavailableException {
|
|
376
|
+
return SignerUnavailableException(
|
|
377
|
+
"Bunker signer is closed"
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private class PendingRequest(
|
|
382
|
+
val outbound: Nip46OutboundRequest,
|
|
383
|
+
val continuation: CancellableContinuation<String>,
|
|
384
|
+
val timeout: AtomicReference<ScheduledFuture<*>?>
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
private companion object {
|
|
388
|
+
const val DEFAULT_REQUEST_TIMEOUT_MILLIS = 30_000L
|
|
389
|
+
const val DEFAULT_AUTH_CHALLENGE_TIMEOUT_MILLIS = 5 * 60_000L
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
internal class Nip46AuthChallengeException : Exception(
|
|
394
|
+
"Remote signer requires authorization"
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
internal class Nip46RemoteException : Exception(
|
|
398
|
+
"Remote signer rejected the NIP-46 request"
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
internal class Nip46RequestTimeoutException : Exception(
|
|
402
|
+
"NIP-46 request timed out"
|
|
403
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
package com.pakstr.app.utils
|
|
2
|
+
|
|
3
|
+
import android.content.ClipData
|
|
4
|
+
import android.content.ClipboardManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.graphics.Bitmap
|
|
7
|
+
import android.graphics.Color
|
|
8
|
+
import android.view.LayoutInflater
|
|
9
|
+
import android.widget.ImageView
|
|
10
|
+
import android.widget.TextView
|
|
11
|
+
import android.widget.Toast
|
|
12
|
+
import androidx.appcompat.app.AlertDialog
|
|
13
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
14
|
+
import com.google.android.material.button.MaterialButton
|
|
15
|
+
import com.pakstr.app.R
|
|
16
|
+
import com.pakstr.app.signer.bunker.Nip46PairingQrCode
|
|
17
|
+
|
|
18
|
+
object BunkerPairingDialog {
|
|
19
|
+
fun show(
|
|
20
|
+
activity: AppCompatActivity,
|
|
21
|
+
pairingUri: String,
|
|
22
|
+
onShare: (String) -> Unit
|
|
23
|
+
): AlertDialog {
|
|
24
|
+
val view = LayoutInflater.from(activity)
|
|
25
|
+
.inflate(R.layout.dialog_bunker_pairing, null, false)
|
|
26
|
+
val qrImage = view.findViewById<ImageView>(R.id.bunker_pairing_qr)
|
|
27
|
+
val uriText = view.findViewById<TextView>(R.id.bunker_pairing_uri)
|
|
28
|
+
val copy = view.findViewById<MaterialButton>(R.id.button_copy_pairing_uri)
|
|
29
|
+
val share = view.findViewById<MaterialButton>(R.id.button_share_pairing_uri)
|
|
30
|
+
|
|
31
|
+
uriText.text = pairingUri
|
|
32
|
+
qrImage.setImageBitmap(createQrBitmap(pairingUri, QR_CODE_SIZE_PIXELS))
|
|
33
|
+
|
|
34
|
+
copy.setOnClickListener {
|
|
35
|
+
val clipboard = activity.getSystemService(
|
|
36
|
+
Context.CLIPBOARD_SERVICE
|
|
37
|
+
) as ClipboardManager
|
|
38
|
+
clipboard.setPrimaryClip(
|
|
39
|
+
ClipData.newPlainText(
|
|
40
|
+
activity.getString(R.string.signer_picker_pairing_clip_label),
|
|
41
|
+
pairingUri
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
Toast.makeText(
|
|
45
|
+
activity,
|
|
46
|
+
R.string.signer_picker_pairing_copied,
|
|
47
|
+
Toast.LENGTH_SHORT
|
|
48
|
+
).show()
|
|
49
|
+
}
|
|
50
|
+
share.setOnClickListener {
|
|
51
|
+
onShare(pairingUri)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
val dialog =
|
|
55
|
+
AlertDialog.Builder(activity)
|
|
56
|
+
.setView(view)
|
|
57
|
+
.setNegativeButton(android.R.string.cancel, null)
|
|
58
|
+
.create()
|
|
59
|
+
dialog.show()
|
|
60
|
+
return dialog
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun createQrBitmap(
|
|
64
|
+
value: String,
|
|
65
|
+
sizePixels: Int
|
|
66
|
+
): Bitmap {
|
|
67
|
+
val matrix = Nip46PairingQrCode.encode(value, sizePixels)
|
|
68
|
+
val pixels = IntArray(sizePixels * sizePixels) { index ->
|
|
69
|
+
val x = index % sizePixels
|
|
70
|
+
val y = index / sizePixels
|
|
71
|
+
if (matrix[x, y]) Color.BLACK else Color.WHITE
|
|
72
|
+
}
|
|
73
|
+
return Bitmap.createBitmap(
|
|
74
|
+
pixels,
|
|
75
|
+
sizePixels,
|
|
76
|
+
sizePixels,
|
|
77
|
+
Bitmap.Config.ARGB_8888
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private const val QR_CODE_SIZE_PIXELS = 768
|
|
82
|
+
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
package com.pakstr.app.utils
|
|
2
2
|
|
|
3
|
-
import com.pakstr.app.R
|
|
4
|
-
|
|
5
3
|
import android.os.Bundle
|
|
6
4
|
import android.view.LayoutInflater
|
|
7
5
|
import android.view.View
|
|
@@ -9,31 +7,94 @@ import android.view.ViewGroup
|
|
|
9
7
|
import android.widget.Button
|
|
10
8
|
import android.widget.RadioGroup
|
|
11
9
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|
12
|
-
import com.
|
|
10
|
+
import com.google.android.material.textfield.TextInputEditText
|
|
11
|
+
import com.google.android.material.textfield.TextInputLayout
|
|
12
|
+
import com.pakstr.app.R
|
|
13
13
|
|
|
14
|
-
class SignerPickerBottomSheet
|
|
15
|
-
private val
|
|
14
|
+
class SignerPickerBottomSheet(
|
|
15
|
+
private val hasBunkerSession: Boolean,
|
|
16
|
+
private val onAmberSelected: () -> Unit,
|
|
17
|
+
private val onBunkerSelected: (String) -> Boolean,
|
|
18
|
+
private val onSharePairing: (List<String>) -> Unit,
|
|
19
|
+
private val onBunkerLogout: () -> Unit
|
|
16
20
|
) : BottomSheetDialogFragment() {
|
|
17
21
|
|
|
18
22
|
override fun onCreateView(
|
|
19
|
-
inflater: LayoutInflater,
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
inflater: LayoutInflater,
|
|
24
|
+
container: ViewGroup?,
|
|
25
|
+
savedInstanceState: Bundle?
|
|
26
|
+
): View {
|
|
27
|
+
return inflater.inflate(
|
|
28
|
+
R.layout.dialog_picker,
|
|
29
|
+
container,
|
|
30
|
+
false
|
|
31
|
+
)
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
override fun onViewCreated(
|
|
34
|
+
override fun onViewCreated(
|
|
35
|
+
view: View,
|
|
36
|
+
savedInstanceState: Bundle?
|
|
37
|
+
) {
|
|
25
38
|
super.onViewCreated(view, savedInstanceState)
|
|
26
|
-
|
|
27
39
|
val radioGroup = view.findViewById<RadioGroup>(R.id.options_radio_group)
|
|
28
|
-
val
|
|
40
|
+
val uriContainer = view.findViewById<TextInputLayout>(R.id.bunker_uri_container)
|
|
41
|
+
val uriInput = view.findViewById<TextInputEditText>(R.id.bunker_uri)
|
|
42
|
+
val confirm = view.findViewById<Button>(R.id.button_confirm)
|
|
43
|
+
val sharePairing = view.findViewById<Button>(R.id.button_share_pairing)
|
|
44
|
+
val logout = view.findViewById<Button>(R.id.button_logout_bunker)
|
|
45
|
+
|
|
46
|
+
logout.visibility =
|
|
47
|
+
if (hasBunkerSession) View.VISIBLE else View.GONE
|
|
48
|
+
|
|
49
|
+
fun updateBunkerControls() {
|
|
50
|
+
val bunkerSelected =
|
|
51
|
+
radioGroup.checkedRadioButtonId == R.id.radio_option_two
|
|
52
|
+
uriContainer.visibility =
|
|
53
|
+
if (bunkerSelected) View.VISIBLE else View.GONE
|
|
54
|
+
sharePairing.visibility =
|
|
55
|
+
if (bunkerSelected) View.VISIBLE else View.GONE
|
|
56
|
+
}
|
|
57
|
+
radioGroup.setOnCheckedChangeListener { _, _ ->
|
|
58
|
+
uriContainer.error = null
|
|
59
|
+
updateBunkerControls()
|
|
60
|
+
}
|
|
61
|
+
updateBunkerControls()
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
confirm.setOnClickListener {
|
|
64
|
+
if (radioGroup.checkedRadioButtonId == R.id.radio_option_two) {
|
|
65
|
+
val value = uriInput.text?.toString().orEmpty().trim()
|
|
66
|
+
if (!onBunkerSelected(value)) {
|
|
67
|
+
uriContainer.error =
|
|
68
|
+
getString(R.string.signer_picker_invalid_bunker)
|
|
69
|
+
return@setOnClickListener
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
onAmberSelected()
|
|
35
73
|
}
|
|
36
|
-
|
|
74
|
+
dismiss()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
sharePairing.setOnClickListener {
|
|
78
|
+
val relays =
|
|
79
|
+
uriInput.text?.toString()
|
|
80
|
+
.orEmpty()
|
|
81
|
+
.lineSequence()
|
|
82
|
+
.map(String::trim)
|
|
83
|
+
.filter { it.startsWith("wss://") }
|
|
84
|
+
.toList()
|
|
85
|
+
.ifEmpty {
|
|
86
|
+
getString(R.string.signer_picker_pairing_relays_default)
|
|
87
|
+
.lineSequence()
|
|
88
|
+
.map(String::trim)
|
|
89
|
+
.filter { it.startsWith("wss://") }
|
|
90
|
+
.toList()
|
|
91
|
+
}
|
|
92
|
+
onSharePairing(relays)
|
|
93
|
+
dismiss()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
logout.setOnClickListener {
|
|
97
|
+
onBunkerLogout()
|
|
37
98
|
dismiss()
|
|
38
99
|
}
|
|
39
100
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="utf-8"?>
|
|
2
2
|
<com.pakstr.app.debug.SupportUnlockGestureLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
3
4
|
android:id="@+id/mainContainer"
|
|
4
5
|
android:layout_width="match_parent"
|
|
5
6
|
android:layout_height="match_parent"
|
|
@@ -18,6 +19,16 @@
|
|
|
18
19
|
android:layout_gravity="center"
|
|
19
20
|
android:indeterminateDrawable="@drawable/custom_loader" />
|
|
20
21
|
|
|
22
|
+
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
23
|
+
android:id="@+id/signerSettingsFab"
|
|
24
|
+
android:layout_width="wrap_content"
|
|
25
|
+
android:layout_height="wrap_content"
|
|
26
|
+
android:layout_gravity="top|end"
|
|
27
|
+
android:layout_margin="24dp"
|
|
28
|
+
android:contentDescription="@string/signer_settings"
|
|
29
|
+
android:src="@android:drawable/ic_menu_manage"
|
|
30
|
+
app:fabSize="mini" />
|
|
31
|
+
|
|
21
32
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
22
33
|
android:id="@+id/developerToolsFab"
|
|
23
34
|
android:layout_width="wrap_content"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
android:layout_width="match_parent"
|
|
5
|
+
android:layout_height="wrap_content"
|
|
6
|
+
android:fillViewport="true">
|
|
7
|
+
|
|
8
|
+
<LinearLayout
|
|
9
|
+
android:layout_width="match_parent"
|
|
10
|
+
android:layout_height="wrap_content"
|
|
11
|
+
android:orientation="vertical"
|
|
12
|
+
android:padding="24dp">
|
|
13
|
+
|
|
14
|
+
<TextView
|
|
15
|
+
android:layout_width="wrap_content"
|
|
16
|
+
android:layout_height="wrap_content"
|
|
17
|
+
android:text="@string/signer_picker_pairing_qr_title"
|
|
18
|
+
android:textColor="#1F2937"
|
|
19
|
+
android:textSize="20sp"
|
|
20
|
+
android:textStyle="bold" />
|
|
21
|
+
|
|
22
|
+
<TextView
|
|
23
|
+
android:layout_width="wrap_content"
|
|
24
|
+
android:layout_height="wrap_content"
|
|
25
|
+
android:layout_marginTop="8dp"
|
|
26
|
+
android:text="@string/signer_picker_pairing_qr_instructions"
|
|
27
|
+
android:textColor="#4B5563"
|
|
28
|
+
android:textSize="14sp" />
|
|
29
|
+
|
|
30
|
+
<ImageView
|
|
31
|
+
android:id="@+id/bunker_pairing_qr"
|
|
32
|
+
android:layout_width="280dp"
|
|
33
|
+
android:layout_height="280dp"
|
|
34
|
+
android:layout_gravity="center_horizontal"
|
|
35
|
+
android:layout_marginTop="20dp"
|
|
36
|
+
android:adjustViewBounds="true"
|
|
37
|
+
android:contentDescription="@string/signer_picker_pairing_qr_content_description"
|
|
38
|
+
android:importantForAccessibility="yes"
|
|
39
|
+
android:scaleType="fitCenter" />
|
|
40
|
+
|
|
41
|
+
<TextView
|
|
42
|
+
android:id="@+id/bunker_pairing_uri"
|
|
43
|
+
android:layout_width="match_parent"
|
|
44
|
+
android:layout_height="wrap_content"
|
|
45
|
+
android:layout_marginTop="16dp"
|
|
46
|
+
android:background="#F3F4F6"
|
|
47
|
+
android:ellipsize="end"
|
|
48
|
+
android:fontFamily="monospace"
|
|
49
|
+
android:maxLines="4"
|
|
50
|
+
android:padding="12dp"
|
|
51
|
+
android:textColor="#1F2937"
|
|
52
|
+
android:textIsSelectable="true"
|
|
53
|
+
android:textSize="12sp" />
|
|
54
|
+
|
|
55
|
+
<com.google.android.material.button.MaterialButton
|
|
56
|
+
android:id="@+id/button_copy_pairing_uri"
|
|
57
|
+
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
|
58
|
+
android:layout_width="match_parent"
|
|
59
|
+
android:layout_height="48dp"
|
|
60
|
+
android:layout_marginTop="16dp"
|
|
61
|
+
android:text="@string/signer_picker_pairing_copy"
|
|
62
|
+
android:textAllCaps="false" />
|
|
63
|
+
|
|
64
|
+
<com.google.android.material.button.MaterialButton
|
|
65
|
+
android:id="@+id/button_share_pairing_uri"
|
|
66
|
+
android:layout_width="match_parent"
|
|
67
|
+
android:layout_height="48dp"
|
|
68
|
+
android:layout_marginTop="8dp"
|
|
69
|
+
android:text="@string/signer_picker_pairing_share"
|
|
70
|
+
android:textAllCaps="false"
|
|
71
|
+
android:textColor="@android:color/white"
|
|
72
|
+
app:backgroundTint="#2563EB" />
|
|
73
|
+
|
|
74
|
+
</LinearLayout>
|
|
75
|
+
</ScrollView>
|