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
package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
package com.pakstr.app.signer.bunker
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.CompletableDeferred
|
|
4
|
+
import kotlinx.coroutines.withTimeoutOrNull
|
|
5
|
+
import okhttp3.OkHttpClient
|
|
6
|
+
import okhttp3.Request
|
|
7
|
+
import okhttp3.Response
|
|
8
|
+
import okhttp3.WebSocket
|
|
9
|
+
import okhttp3.WebSocketListener
|
|
10
|
+
import org.json.JSONArray
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
import java.util.LinkedHashMap
|
|
13
|
+
import java.util.concurrent.Executors
|
|
14
|
+
import java.util.concurrent.ScheduledExecutorService
|
|
15
|
+
import java.util.concurrent.ScheduledFuture
|
|
16
|
+
import java.util.concurrent.TimeUnit
|
|
17
|
+
import kotlin.math.min
|
|
18
|
+
|
|
19
|
+
internal interface Nip46RelayConnection {
|
|
20
|
+
fun send(message: String): Boolean
|
|
21
|
+
|
|
22
|
+
fun close()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
internal fun interface Nip46RelayConnectionFactory : AutoCloseable {
|
|
26
|
+
fun connect(
|
|
27
|
+
relayUrl: String,
|
|
28
|
+
listener: Nip46RelayConnectionListener
|
|
29
|
+
): Nip46RelayConnection
|
|
30
|
+
|
|
31
|
+
override fun close() = Unit
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
internal interface Nip46RelayConnectionListener {
|
|
35
|
+
fun onOpen()
|
|
36
|
+
|
|
37
|
+
fun onMessage(message: String)
|
|
38
|
+
|
|
39
|
+
fun onClosed()
|
|
40
|
+
|
|
41
|
+
fun onFailure()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
internal class OkHttpNip46RelayConnectionFactory(
|
|
45
|
+
private val client: OkHttpClient =
|
|
46
|
+
OkHttpClient.Builder()
|
|
47
|
+
.pingInterval(30, TimeUnit.SECONDS)
|
|
48
|
+
.build(),
|
|
49
|
+
private val diagnosticLogger: (String) -> Unit = {}
|
|
50
|
+
) : Nip46RelayConnectionFactory {
|
|
51
|
+
override fun connect(
|
|
52
|
+
relayUrl: String,
|
|
53
|
+
listener: Nip46RelayConnectionListener
|
|
54
|
+
): Nip46RelayConnection {
|
|
55
|
+
val connection = OkHttpRelayConnection()
|
|
56
|
+
val socket =
|
|
57
|
+
client.newWebSocket(
|
|
58
|
+
Request.Builder()
|
|
59
|
+
.url(relayUrl)
|
|
60
|
+
.build(),
|
|
61
|
+
object : WebSocketListener() {
|
|
62
|
+
override fun onOpen(
|
|
63
|
+
webSocket: WebSocket,
|
|
64
|
+
response: Response
|
|
65
|
+
) {
|
|
66
|
+
diagnosticLogger("WebSocket onOpen relay=$relayUrl")
|
|
67
|
+
connection.attach(webSocket)
|
|
68
|
+
listener.onOpen()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun onMessage(
|
|
72
|
+
webSocket: WebSocket,
|
|
73
|
+
text: String
|
|
74
|
+
) {
|
|
75
|
+
listener.onMessage(text)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onClosed(
|
|
79
|
+
webSocket: WebSocket,
|
|
80
|
+
code: Int,
|
|
81
|
+
reason: String
|
|
82
|
+
) {
|
|
83
|
+
listener.onClosed()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun onFailure(
|
|
87
|
+
webSocket: WebSocket,
|
|
88
|
+
t: Throwable,
|
|
89
|
+
response: Response?
|
|
90
|
+
) {
|
|
91
|
+
diagnosticLogger(
|
|
92
|
+
"WebSocket onFailure relay=$relayUrl " +
|
|
93
|
+
"exception=${t.javaClass.name}: ${t.message.orEmpty()} " +
|
|
94
|
+
"httpCode=${response?.code ?: "none"}"
|
|
95
|
+
)
|
|
96
|
+
listener.onFailure()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
connection.attach(socket)
|
|
101
|
+
return connection
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
override fun close() {
|
|
105
|
+
client.dispatcher.executorService.shutdown()
|
|
106
|
+
client.connectionPool.evictAll()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private class OkHttpRelayConnection : Nip46RelayConnection {
|
|
110
|
+
@Volatile
|
|
111
|
+
private var socket: WebSocket? = null
|
|
112
|
+
|
|
113
|
+
fun attach(webSocket: WebSocket) {
|
|
114
|
+
socket = webSocket
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
override fun send(message: String): Boolean {
|
|
118
|
+
return socket?.send(message) == true
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
override fun close() {
|
|
122
|
+
socket?.close(NORMAL_CLOSURE, "")
|
|
123
|
+
socket = null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private companion object {
|
|
128
|
+
const val NORMAL_CLOSURE = 1000
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
internal interface Nip46Transport : AutoCloseable {
|
|
133
|
+
fun start(listener: (String) -> Unit)
|
|
134
|
+
|
|
135
|
+
fun publish(requestId: String, eventJson: String)
|
|
136
|
+
|
|
137
|
+
fun cancel(requestId: String)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
internal class Nip46RelayTransport(
|
|
141
|
+
relayUrls: List<String>,
|
|
142
|
+
private val remoteSignerPubkey: String?,
|
|
143
|
+
private val clientPubkey: String,
|
|
144
|
+
private val connectionFactory: Nip46RelayConnectionFactory,
|
|
145
|
+
private val scheduler: ScheduledExecutorService =
|
|
146
|
+
Executors.newSingleThreadScheduledExecutor(),
|
|
147
|
+
private val reconnectBaseDelayMillis: Long = 1_000L,
|
|
148
|
+
private val reconnectMaximumDelayMillis: Long = 30_000L,
|
|
149
|
+
private val diagnosticLogger: (String) -> Unit = {}
|
|
150
|
+
) : Nip46Transport {
|
|
151
|
+
private val lock = Any()
|
|
152
|
+
private val relays =
|
|
153
|
+
relayUrls.distinct().associateWith(::RelayState)
|
|
154
|
+
private val publications = LinkedHashMap<String, String>()
|
|
155
|
+
private val rememberedEventIds =
|
|
156
|
+
object : LinkedHashMap<String, Unit>(16, 0.75f, true) {
|
|
157
|
+
override fun removeEldestEntry(
|
|
158
|
+
eldest: MutableMap.MutableEntry<String, Unit>
|
|
159
|
+
): Boolean {
|
|
160
|
+
return size > MAXIMUM_REMEMBERED_EVENT_IDS
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private val readiness = CompletableDeferred<Boolean>()
|
|
165
|
+
private var eventListener: ((String) -> Unit)? = null
|
|
166
|
+
private var closed = false
|
|
167
|
+
|
|
168
|
+
override fun start(listener: (String) -> Unit) {
|
|
169
|
+
val relayStates =
|
|
170
|
+
synchronized(lock) {
|
|
171
|
+
check(!closed) { "NIP-46 relay transport is closed" }
|
|
172
|
+
eventListener = listener
|
|
173
|
+
relays.values.toList()
|
|
174
|
+
}
|
|
175
|
+
relayStates.forEach(::connect)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
suspend fun awaitReady(timeoutMillis: Long): Boolean {
|
|
179
|
+
require(timeoutMillis > 0) { "Readiness timeout must be positive" }
|
|
180
|
+
val result = withTimeoutOrNull(timeoutMillis) {
|
|
181
|
+
readiness.await()
|
|
182
|
+
}
|
|
183
|
+
if (result == null) {
|
|
184
|
+
diagnosticLogger("Relay readiness timed out after ${timeoutMillis}ms")
|
|
185
|
+
}
|
|
186
|
+
return result ?: false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
override fun publish(
|
|
190
|
+
requestId: String,
|
|
191
|
+
eventJson: String
|
|
192
|
+
) {
|
|
193
|
+
val sockets =
|
|
194
|
+
synchronized(lock) {
|
|
195
|
+
check(!closed) { "NIP-46 relay transport is closed" }
|
|
196
|
+
publications[requestId] = eventJson
|
|
197
|
+
relays.values.mapNotNull { state ->
|
|
198
|
+
state.connection.takeIf { state.connected }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
val message = publishMessage(eventJson)
|
|
202
|
+
sockets.forEach { connection ->
|
|
203
|
+
connection.send(message)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override fun cancel(requestId: String) {
|
|
208
|
+
synchronized(lock) {
|
|
209
|
+
publications.remove(requestId)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
override fun close() {
|
|
214
|
+
val states =
|
|
215
|
+
synchronized(lock) {
|
|
216
|
+
if (closed) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
closed = true
|
|
220
|
+
readiness.complete(false)
|
|
221
|
+
eventListener = null
|
|
222
|
+
publications.clear()
|
|
223
|
+
rememberedEventIds.clear()
|
|
224
|
+
relays.values.toList()
|
|
225
|
+
}
|
|
226
|
+
states.forEach { state ->
|
|
227
|
+
state.reconnect?.cancel(false)
|
|
228
|
+
state.connection?.send(closeSubscriptionMessage(state.subscriptionId))
|
|
229
|
+
state.connection?.close()
|
|
230
|
+
}
|
|
231
|
+
scheduler.shutdownNow()
|
|
232
|
+
connectionFactory.close()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private fun connect(state: RelayState) {
|
|
236
|
+
val generation =
|
|
237
|
+
synchronized(lock) {
|
|
238
|
+
if (closed || state.connecting || state.connected) {
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
state.connecting = true
|
|
242
|
+
state.opened = false
|
|
243
|
+
state.reconnect = null
|
|
244
|
+
++state.generation
|
|
245
|
+
}
|
|
246
|
+
diagnosticLogger(
|
|
247
|
+
"Relay connection attempt relay=${state.relayUrl} " +
|
|
248
|
+
"attempt=${state.reconnectAttempts + 1}"
|
|
249
|
+
)
|
|
250
|
+
val listener = listener(state, generation)
|
|
251
|
+
val connection =
|
|
252
|
+
try {
|
|
253
|
+
connectionFactory.connect(state.relayUrl, listener)
|
|
254
|
+
} catch (error: Exception) {
|
|
255
|
+
diagnosticLogger(
|
|
256
|
+
"Relay connection failed relay=${state.relayUrl} " +
|
|
257
|
+
"exception=${error.javaClass.name}: ${error.message.orEmpty()}"
|
|
258
|
+
)
|
|
259
|
+
synchronized(lock) {
|
|
260
|
+
if (state.generation == generation) {
|
|
261
|
+
state.connecting = false
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
scheduleReconnect(state)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
synchronized(lock) {
|
|
268
|
+
if (closed || state.generation != generation) {
|
|
269
|
+
connection.close()
|
|
270
|
+
} else {
|
|
271
|
+
state.connection = connection
|
|
272
|
+
completeOpen(state, connection)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun listener(
|
|
278
|
+
state: RelayState,
|
|
279
|
+
generation: Long
|
|
280
|
+
): Nip46RelayConnectionListener {
|
|
281
|
+
return object : Nip46RelayConnectionListener {
|
|
282
|
+
override fun onOpen() {
|
|
283
|
+
synchronized(lock) {
|
|
284
|
+
if (closed || state.generation != generation) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
state.opened = true
|
|
288
|
+
state.connection?.let { connection ->
|
|
289
|
+
completeOpen(state, connection)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
override fun onMessage(message: String) {
|
|
295
|
+
receiveRelayMessage(state, generation, message)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
override fun onClosed() {
|
|
299
|
+
disconnected(state, generation)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
override fun onFailure() {
|
|
303
|
+
disconnected(state, generation)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private fun completeOpen(
|
|
309
|
+
state: RelayState,
|
|
310
|
+
connection: Nip46RelayConnection
|
|
311
|
+
) {
|
|
312
|
+
if (!state.opened || state.connected) {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
val subscribed =
|
|
316
|
+
try {
|
|
317
|
+
connection.send(subscriptionMessage(state.subscriptionId)).also { sent ->
|
|
318
|
+
diagnosticLogger(
|
|
319
|
+
"Pairing REQ send relay=${state.relayUrl} result=$sent"
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
} catch (error: Exception) {
|
|
323
|
+
diagnosticLogger(
|
|
324
|
+
"Pairing REQ send exception relay=${state.relayUrl} " +
|
|
325
|
+
"exception=${error.javaClass.name}: ${error.message.orEmpty()}"
|
|
326
|
+
)
|
|
327
|
+
false
|
|
328
|
+
}
|
|
329
|
+
if (!subscribed) {
|
|
330
|
+
disconnected(state, state.generation)
|
|
331
|
+
connection.close()
|
|
332
|
+
return
|
|
333
|
+
}
|
|
334
|
+
state.connecting = false
|
|
335
|
+
state.connected = true
|
|
336
|
+
state.reconnectAttempts = 0
|
|
337
|
+
readiness.complete(true)
|
|
338
|
+
publications.values.forEach { eventJson ->
|
|
339
|
+
connection.send(publishMessage(eventJson))
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private fun receiveRelayMessage(
|
|
344
|
+
state: RelayState,
|
|
345
|
+
generation: Long,
|
|
346
|
+
message: String
|
|
347
|
+
) {
|
|
348
|
+
val eventJson = parseEventMessage(message, state.subscriptionId)
|
|
349
|
+
?: return
|
|
350
|
+
val eventId =
|
|
351
|
+
try {
|
|
352
|
+
JSONObject(eventJson).getString("id")
|
|
353
|
+
} catch (_: Exception) {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
if (!eventId.matches(EVENT_ID_PATTERN)) {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
val listener =
|
|
360
|
+
synchronized(lock) {
|
|
361
|
+
if (
|
|
362
|
+
closed ||
|
|
363
|
+
state.generation != generation ||
|
|
364
|
+
rememberedEventIds.put(eventId, Unit) != null
|
|
365
|
+
) {
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
eventListener
|
|
369
|
+
}
|
|
370
|
+
listener?.invoke(eventJson)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun disconnected(
|
|
374
|
+
state: RelayState,
|
|
375
|
+
generation: Long
|
|
376
|
+
) {
|
|
377
|
+
val shouldReconnect =
|
|
378
|
+
synchronized(lock) {
|
|
379
|
+
if (closed || state.generation != generation) {
|
|
380
|
+
false
|
|
381
|
+
} else {
|
|
382
|
+
state.connecting = false
|
|
383
|
+
state.opened = false
|
|
384
|
+
state.connected = false
|
|
385
|
+
state.connection = null
|
|
386
|
+
true
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (shouldReconnect) {
|
|
390
|
+
scheduleReconnect(state)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private fun scheduleReconnect(state: RelayState) {
|
|
395
|
+
synchronized(lock) {
|
|
396
|
+
if (closed || state.reconnect?.isDone == false) {
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
val exponent = min(state.reconnectAttempts, MAX_BACKOFF_EXPONENT)
|
|
400
|
+
val multiplier = 1L shl exponent
|
|
401
|
+
val delay = min(
|
|
402
|
+
reconnectMaximumDelayMillis,
|
|
403
|
+
reconnectBaseDelayMillis * multiplier
|
|
404
|
+
)
|
|
405
|
+
state.reconnectAttempts++
|
|
406
|
+
diagnosticLogger(
|
|
407
|
+
"Relay reconnect scheduled relay=${state.relayUrl} " +
|
|
408
|
+
"attempt=${state.reconnectAttempts + 1} delayMs=$delay"
|
|
409
|
+
)
|
|
410
|
+
state.reconnect =
|
|
411
|
+
scheduler.schedule(
|
|
412
|
+
{ connect(state) },
|
|
413
|
+
delay,
|
|
414
|
+
TimeUnit.MILLISECONDS
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private fun subscriptionMessage(subscriptionId: String): String {
|
|
420
|
+
return JSONArray()
|
|
421
|
+
.put("REQ")
|
|
422
|
+
.put(subscriptionId)
|
|
423
|
+
.put(
|
|
424
|
+
JSONObject()
|
|
425
|
+
.put("kinds", JSONArray().put(NIP46_EVENT_KIND))
|
|
426
|
+
.put("#p", JSONArray().put(clientPubkey))
|
|
427
|
+
.apply {
|
|
428
|
+
remoteSignerPubkey?.let { pubkey ->
|
|
429
|
+
put("authors", JSONArray().put(pubkey))
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
)
|
|
433
|
+
.toString()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun publishMessage(eventJson: String): String {
|
|
437
|
+
return JSONArray()
|
|
438
|
+
.put("EVENT")
|
|
439
|
+
.put(JSONObject(eventJson))
|
|
440
|
+
.toString()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun closeSubscriptionMessage(subscriptionId: String): String {
|
|
444
|
+
return JSONArray()
|
|
445
|
+
.put("CLOSE")
|
|
446
|
+
.put(subscriptionId)
|
|
447
|
+
.toString()
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private fun parseEventMessage(
|
|
451
|
+
message: String,
|
|
452
|
+
subscriptionId: String
|
|
453
|
+
): String? {
|
|
454
|
+
return try {
|
|
455
|
+
val frame = JSONArray(message)
|
|
456
|
+
if (
|
|
457
|
+
frame.length() != 3 ||
|
|
458
|
+
frame.optString(0) != "EVENT" ||
|
|
459
|
+
frame.optString(1) != subscriptionId
|
|
460
|
+
) {
|
|
461
|
+
null
|
|
462
|
+
} else {
|
|
463
|
+
frame.optJSONObject(2)?.toString()
|
|
464
|
+
}
|
|
465
|
+
} catch (_: Exception) {
|
|
466
|
+
null
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private class RelayState(
|
|
471
|
+
val relayUrl: String
|
|
472
|
+
) {
|
|
473
|
+
val subscriptionId = "nip46-${relayUrl.hashCode().toUInt().toString(16)}"
|
|
474
|
+
var generation = 0L
|
|
475
|
+
var connection: Nip46RelayConnection? = null
|
|
476
|
+
var reconnect: ScheduledFuture<*>? = null
|
|
477
|
+
var reconnectAttempts = 0
|
|
478
|
+
var connecting = false
|
|
479
|
+
var opened = false
|
|
480
|
+
var connected = false
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private companion object {
|
|
484
|
+
const val NIP46_EVENT_KIND = 24133
|
|
485
|
+
const val MAX_BACKOFF_EXPONENT = 20
|
|
486
|
+
const val MAXIMUM_REMEMBERED_EVENT_IDS = 2_048
|
|
487
|
+
val EVENT_ID_PATTERN = Regex("[0-9a-f]{64}")
|
|
488
|
+
}
|
|
489
|
+
}
|