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/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
package com.pakstr.app.signer.bunker
|
|
2
|
+
|
|
3
|
+
import org.json.JSONArray
|
|
4
|
+
import org.json.JSONObject
|
|
5
|
+
import org.junit.After
|
|
6
|
+
import org.junit.Assert.assertEquals
|
|
7
|
+
import org.junit.Assert.assertFalse
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import kotlinx.coroutines.CoroutineStart
|
|
10
|
+
import kotlinx.coroutines.async
|
|
11
|
+
import kotlinx.coroutines.runBlocking
|
|
12
|
+
import org.junit.Test
|
|
13
|
+
import java.util.concurrent.Executors
|
|
14
|
+
import java.util.concurrent.TimeUnit
|
|
15
|
+
|
|
16
|
+
class Nip46RelayTransportTest {
|
|
17
|
+
private val factories = mutableListOf<FakeConnectionFactory>()
|
|
18
|
+
private val transports = mutableListOf<Nip46RelayTransport>()
|
|
19
|
+
|
|
20
|
+
@After
|
|
21
|
+
fun teardown() {
|
|
22
|
+
transports.forEach(Nip46RelayTransport::close)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test
|
|
26
|
+
fun connects_subscribes_and_publishes_to_every_available_relay() {
|
|
27
|
+
val factory = factory()
|
|
28
|
+
val transport = transport(factory, RELAY_ONE, RELAY_TWO)
|
|
29
|
+
val event = eventJson(EVENT_ID)
|
|
30
|
+
|
|
31
|
+
transport.start { }
|
|
32
|
+
factory.connection(RELAY_ONE).open()
|
|
33
|
+
factory.connection(RELAY_TWO).open()
|
|
34
|
+
transport.publish(REQUEST_ID, event)
|
|
35
|
+
|
|
36
|
+
listOf(RELAY_ONE, RELAY_TWO).forEach { relayUrl ->
|
|
37
|
+
val messages = factory.connection(relayUrl).messages
|
|
38
|
+
val subscription = JSONArray(messages[0])
|
|
39
|
+
assertEquals("REQ", subscription.getString(0))
|
|
40
|
+
assertEquals(
|
|
41
|
+
24133,
|
|
42
|
+
subscription.getJSONObject(2)
|
|
43
|
+
.getJSONArray("kinds")
|
|
44
|
+
.getInt(0)
|
|
45
|
+
)
|
|
46
|
+
assertEquals(
|
|
47
|
+
REMOTE_PUBKEY,
|
|
48
|
+
subscription.getJSONObject(2)
|
|
49
|
+
.getJSONArray("authors")
|
|
50
|
+
.getString(0)
|
|
51
|
+
)
|
|
52
|
+
val publication = JSONArray(messages[1])
|
|
53
|
+
assertEquals("EVENT", publication.getString(0))
|
|
54
|
+
assertEquals(EVENT_ID, publication.getJSONObject(1).getString("id"))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
fun readiness_waits_until_pairing_subscription_is_sent() = runBlocking {
|
|
60
|
+
val factory = factory()
|
|
61
|
+
val transport = transport(factory, RELAY_ONE)
|
|
62
|
+
transport.start { }
|
|
63
|
+
val readiness = async(start = CoroutineStart.UNDISPATCHED) {
|
|
64
|
+
transport.awaitReady(1_000)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
assertFalse(readiness.isCompleted)
|
|
68
|
+
val connection = factory.connection(RELAY_ONE)
|
|
69
|
+
assertTrue(connection.messages.isEmpty())
|
|
70
|
+
|
|
71
|
+
connection.open()
|
|
72
|
+
|
|
73
|
+
assertTrue(readiness.await())
|
|
74
|
+
assertEquals("REQ", JSONArray(connection.messages.single()).getString(0))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun failed_subscription_send_reconnects_without_reporting_readiness() = runBlocking {
|
|
79
|
+
val factory = factory()
|
|
80
|
+
val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
81
|
+
val transport = Nip46RelayTransport(
|
|
82
|
+
relayUrls = listOf(RELAY_ONE),
|
|
83
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
84
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
85
|
+
connectionFactory = factory,
|
|
86
|
+
scheduler = scheduler,
|
|
87
|
+
reconnectBaseDelayMillis = 1,
|
|
88
|
+
reconnectMaximumDelayMillis = 1
|
|
89
|
+
).also(transports::add)
|
|
90
|
+
transport.start { }
|
|
91
|
+
val first = factory.connection(RELAY_ONE)
|
|
92
|
+
first.sendResult = false
|
|
93
|
+
|
|
94
|
+
first.open()
|
|
95
|
+
|
|
96
|
+
assertFalse(transport.awaitReady(25))
|
|
97
|
+
assertTrue(first.closed)
|
|
98
|
+
assertEquals("REQ", JSONArray(first.messages.single()).getString(0))
|
|
99
|
+
assertTrue(factory.awaitConnectionCount(RELAY_ONE, 2))
|
|
100
|
+
val second = factory.connection(RELAY_ONE, 1)
|
|
101
|
+
second.open()
|
|
102
|
+
assertTrue(transport.awaitReady(100))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun any_successful_pairing_subscription_reports_readiness() = runBlocking {
|
|
107
|
+
val factory = factory()
|
|
108
|
+
val transport = transport(factory, RELAY_ONE, RELAY_TWO)
|
|
109
|
+
transport.start { }
|
|
110
|
+
val first = factory.connection(RELAY_ONE)
|
|
111
|
+
val second = factory.connection(RELAY_TWO)
|
|
112
|
+
first.sendResult = false
|
|
113
|
+
|
|
114
|
+
first.open()
|
|
115
|
+
assertFalse(transport.awaitReady(25))
|
|
116
|
+
|
|
117
|
+
second.open()
|
|
118
|
+
|
|
119
|
+
assertTrue(transport.awaitReady(100))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Test
|
|
123
|
+
fun close_before_subscription_unblocks_readiness_waiter() = runBlocking {
|
|
124
|
+
val factory = factory()
|
|
125
|
+
val transport = transport(factory, RELAY_ONE)
|
|
126
|
+
transport.start { }
|
|
127
|
+
val readiness = async(start = CoroutineStart.UNDISPATCHED) {
|
|
128
|
+
transport.awaitReady(1_000)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
transport.close()
|
|
132
|
+
|
|
133
|
+
assertFalse(readiness.await())
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
fun pairing_subscription_filters_recipient_without_known_author() {
|
|
138
|
+
val factory = factory()
|
|
139
|
+
val transport =
|
|
140
|
+
Nip46RelayTransport(
|
|
141
|
+
relayUrls = listOf(RELAY_ONE),
|
|
142
|
+
remoteSignerPubkey = null,
|
|
143
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
144
|
+
connectionFactory = factory
|
|
145
|
+
).also(transports::add)
|
|
146
|
+
|
|
147
|
+
transport.start { }
|
|
148
|
+
val connection = factory.connection(RELAY_ONE)
|
|
149
|
+
connection.open()
|
|
150
|
+
val filter = JSONArray(connection.messages.single()).getJSONObject(2)
|
|
151
|
+
|
|
152
|
+
assertEquals(24133, filter.getJSONArray("kinds").getInt(0))
|
|
153
|
+
assertEquals(CLIENT_PUBKEY, filter.getJSONArray("#p").getString(0))
|
|
154
|
+
assertFalse(filter.has("authors"))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
@Test
|
|
158
|
+
fun early_open_callback_still_subscribes_and_flushes_requests() {
|
|
159
|
+
val factory = EarlyOpenConnectionFactory()
|
|
160
|
+
val transport =
|
|
161
|
+
Nip46RelayTransport(
|
|
162
|
+
relayUrls = listOf(RELAY_ONE),
|
|
163
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
164
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
165
|
+
connectionFactory = factory
|
|
166
|
+
).also(transports::add)
|
|
167
|
+
val event = eventJson(EVENT_ID)
|
|
168
|
+
|
|
169
|
+
transport.start { }
|
|
170
|
+
transport.publish(REQUEST_ID, event)
|
|
171
|
+
|
|
172
|
+
assertEquals(2, factory.connection.messages.size)
|
|
173
|
+
assertEquals(
|
|
174
|
+
"REQ",
|
|
175
|
+
JSONArray(factory.connection.messages[0]).getString(0)
|
|
176
|
+
)
|
|
177
|
+
assertEquals(
|
|
178
|
+
"EVENT",
|
|
179
|
+
JSONArray(factory.connection.messages[1]).getString(0)
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@Test
|
|
184
|
+
fun relay_acknowledgements_and_malformed_frames_are_not_delivered() {
|
|
185
|
+
val factory = factory()
|
|
186
|
+
val transport = transport(factory, RELAY_ONE)
|
|
187
|
+
val received = mutableListOf<String>()
|
|
188
|
+
transport.start(received::add)
|
|
189
|
+
val connection = factory.connection(RELAY_ONE)
|
|
190
|
+
connection.open()
|
|
191
|
+
|
|
192
|
+
connection.receive("not-json")
|
|
193
|
+
connection.receive(JSONArray().put("OK").put(EVENT_ID).put(true).toString())
|
|
194
|
+
connection.receive(
|
|
195
|
+
JSONArray()
|
|
196
|
+
.put("EVENT")
|
|
197
|
+
.put(connection.subscriptionId())
|
|
198
|
+
.put(JSONObject(eventJson("invalid")))
|
|
199
|
+
.toString()
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
assertTrue(received.isEmpty())
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Test
|
|
206
|
+
fun duplicate_event_ids_are_delivered_once_across_relays() {
|
|
207
|
+
val factory = factory()
|
|
208
|
+
val transport = transport(factory, RELAY_ONE, RELAY_TWO)
|
|
209
|
+
val received = mutableListOf<String>()
|
|
210
|
+
transport.start(received::add)
|
|
211
|
+
val first = factory.connection(RELAY_ONE)
|
|
212
|
+
val second = factory.connection(RELAY_TWO)
|
|
213
|
+
first.open()
|
|
214
|
+
second.open()
|
|
215
|
+
val event = eventJson(EVENT_ID)
|
|
216
|
+
|
|
217
|
+
first.receiveEvent(event)
|
|
218
|
+
second.receiveEvent(event)
|
|
219
|
+
|
|
220
|
+
assertEquals(1, received.size)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
@Test
|
|
224
|
+
fun reconnect_recreates_subscription_and_republishes_pending_events() {
|
|
225
|
+
val factory = factory()
|
|
226
|
+
val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
227
|
+
val transport = Nip46RelayTransport(
|
|
228
|
+
relayUrls = listOf(RELAY_ONE),
|
|
229
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
230
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
231
|
+
connectionFactory = factory,
|
|
232
|
+
scheduler = scheduler,
|
|
233
|
+
reconnectBaseDelayMillis = 1,
|
|
234
|
+
reconnectMaximumDelayMillis = 1
|
|
235
|
+
)
|
|
236
|
+
transports += transport
|
|
237
|
+
transport.start { }
|
|
238
|
+
val first = factory.connection(RELAY_ONE)
|
|
239
|
+
first.open()
|
|
240
|
+
transport.publish(REQUEST_ID, eventJson(EVENT_ID))
|
|
241
|
+
first.fail()
|
|
242
|
+
|
|
243
|
+
assertTrue(factory.awaitConnectionCount(RELAY_ONE, 2))
|
|
244
|
+
val second = factory.connection(RELAY_ONE, 1)
|
|
245
|
+
second.open()
|
|
246
|
+
|
|
247
|
+
assertEquals("REQ", JSONArray(second.messages[0]).getString(0))
|
|
248
|
+
assertEquals("EVENT", JSONArray(second.messages[1]).getString(0))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
@Test
|
|
252
|
+
fun clean_relay_close_reconnects_and_republishes_pending_events() {
|
|
253
|
+
val factory = factory()
|
|
254
|
+
val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
255
|
+
val transport = Nip46RelayTransport(
|
|
256
|
+
relayUrls = listOf(RELAY_ONE),
|
|
257
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
258
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
259
|
+
connectionFactory = factory,
|
|
260
|
+
scheduler = scheduler,
|
|
261
|
+
reconnectBaseDelayMillis = 1,
|
|
262
|
+
reconnectMaximumDelayMillis = 1
|
|
263
|
+
)
|
|
264
|
+
transports += transport
|
|
265
|
+
transport.start { }
|
|
266
|
+
val first = factory.connection(RELAY_ONE)
|
|
267
|
+
first.open()
|
|
268
|
+
transport.publish(REQUEST_ID, eventJson(EVENT_ID))
|
|
269
|
+
first.disconnect()
|
|
270
|
+
|
|
271
|
+
assertTrue(factory.awaitConnectionCount(RELAY_ONE, 2))
|
|
272
|
+
val second = factory.connection(RELAY_ONE, 1)
|
|
273
|
+
second.open()
|
|
274
|
+
|
|
275
|
+
assertEquals("REQ", JSONArray(second.messages[0]).getString(0))
|
|
276
|
+
assertEquals("EVENT", JSONArray(second.messages[1]).getString(0))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
@Test
|
|
280
|
+
fun connection_factory_failure_is_retried() {
|
|
281
|
+
val factory = FailingOnceConnectionFactory()
|
|
282
|
+
val scheduler = Executors.newSingleThreadScheduledExecutor()
|
|
283
|
+
val transport = Nip46RelayTransport(
|
|
284
|
+
relayUrls = listOf(RELAY_ONE),
|
|
285
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
286
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
287
|
+
connectionFactory = factory,
|
|
288
|
+
scheduler = scheduler,
|
|
289
|
+
reconnectBaseDelayMillis = 1,
|
|
290
|
+
reconnectMaximumDelayMillis = 1
|
|
291
|
+
)
|
|
292
|
+
transports += transport
|
|
293
|
+
|
|
294
|
+
transport.start { }
|
|
295
|
+
|
|
296
|
+
assertTrue(factory.awaitSuccessfulConnection())
|
|
297
|
+
factory.connection.open()
|
|
298
|
+
assertEquals(
|
|
299
|
+
"REQ",
|
|
300
|
+
JSONArray(factory.connection.messages.single()).getString(0)
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
@Test
|
|
305
|
+
fun close_is_idempotent_and_closes_every_connection() {
|
|
306
|
+
val factory = factory()
|
|
307
|
+
val transport = transport(factory, RELAY_ONE, RELAY_TWO)
|
|
308
|
+
transport.start { }
|
|
309
|
+
val first = factory.connection(RELAY_ONE)
|
|
310
|
+
val second = factory.connection(RELAY_TWO)
|
|
311
|
+
first.open()
|
|
312
|
+
second.open()
|
|
313
|
+
|
|
314
|
+
transport.close()
|
|
315
|
+
transport.close()
|
|
316
|
+
|
|
317
|
+
assertTrue(first.closed)
|
|
318
|
+
assertTrue(second.closed)
|
|
319
|
+
assertTrue(factory.closed)
|
|
320
|
+
assertFalse(first.messages.isEmpty())
|
|
321
|
+
assertEquals(
|
|
322
|
+
"CLOSE",
|
|
323
|
+
JSONArray(first.messages.last()).getString(0)
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private fun transport(
|
|
328
|
+
factory: FakeConnectionFactory,
|
|
329
|
+
vararg relayUrls: String
|
|
330
|
+
): Nip46RelayTransport {
|
|
331
|
+
return Nip46RelayTransport(
|
|
332
|
+
relayUrls = relayUrls.toList(),
|
|
333
|
+
remoteSignerPubkey = REMOTE_PUBKEY,
|
|
334
|
+
clientPubkey = CLIENT_PUBKEY,
|
|
335
|
+
connectionFactory = factory
|
|
336
|
+
).also(transports::add)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private fun factory(): FakeConnectionFactory {
|
|
340
|
+
return FakeConnectionFactory().also(factories::add)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private fun eventJson(id: String): String {
|
|
344
|
+
return JSONObject()
|
|
345
|
+
.put("id", id)
|
|
346
|
+
.put("pubkey", REMOTE_PUBKEY)
|
|
347
|
+
.put("created_at", 1_700_000_000L)
|
|
348
|
+
.put("kind", 24133)
|
|
349
|
+
.put("tags", JSONArray())
|
|
350
|
+
.put("content", "redacted")
|
|
351
|
+
.put("sig", "0".repeat(128))
|
|
352
|
+
.toString()
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private class EarlyOpenConnectionFactory : Nip46RelayConnectionFactory {
|
|
356
|
+
lateinit var connection: FakeConnection
|
|
357
|
+
private set
|
|
358
|
+
|
|
359
|
+
override fun connect(
|
|
360
|
+
relayUrl: String,
|
|
361
|
+
listener: Nip46RelayConnectionListener
|
|
362
|
+
): Nip46RelayConnection {
|
|
363
|
+
connection = FakeConnection(listener)
|
|
364
|
+
connection.open()
|
|
365
|
+
return connection
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private class FailingOnceConnectionFactory : Nip46RelayConnectionFactory {
|
|
370
|
+
private var attempts = 0
|
|
371
|
+
private val monitor = java.lang.Object()
|
|
372
|
+
lateinit var connection: FakeConnection
|
|
373
|
+
private set
|
|
374
|
+
|
|
375
|
+
override fun connect(
|
|
376
|
+
relayUrl: String,
|
|
377
|
+
listener: Nip46RelayConnectionListener
|
|
378
|
+
): Nip46RelayConnection {
|
|
379
|
+
synchronized(monitor) {
|
|
380
|
+
attempts++
|
|
381
|
+
if (attempts == 1) {
|
|
382
|
+
throw IllegalStateException("connection failed")
|
|
383
|
+
}
|
|
384
|
+
connection = FakeConnection(listener)
|
|
385
|
+
monitor.notifyAll()
|
|
386
|
+
return connection
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fun awaitSuccessfulConnection(): Boolean {
|
|
391
|
+
synchronized(monitor) {
|
|
392
|
+
val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2)
|
|
393
|
+
while (!::connection.isInitialized) {
|
|
394
|
+
val remaining = deadline - System.nanoTime()
|
|
395
|
+
if (remaining <= 0) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
monitor.wait(
|
|
399
|
+
TimeUnit.NANOSECONDS.toMillis(remaining).coerceAtLeast(1)
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
return true
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private class FakeConnectionFactory : Nip46RelayConnectionFactory {
|
|
408
|
+
private val connections = mutableMapOf<String, MutableList<FakeConnection>>()
|
|
409
|
+
var closed = false
|
|
410
|
+
|
|
411
|
+
@Synchronized
|
|
412
|
+
override fun connect(
|
|
413
|
+
relayUrl: String,
|
|
414
|
+
listener: Nip46RelayConnectionListener
|
|
415
|
+
): Nip46RelayConnection {
|
|
416
|
+
return FakeConnection(listener).also { connection ->
|
|
417
|
+
connections.getOrPut(relayUrl, ::mutableListOf).add(connection)
|
|
418
|
+
(this as java.lang.Object).notifyAll()
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@Synchronized
|
|
423
|
+
fun connection(
|
|
424
|
+
relayUrl: String,
|
|
425
|
+
index: Int = 0
|
|
426
|
+
): FakeConnection {
|
|
427
|
+
return connections.getValue(relayUrl)[index]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
@Synchronized
|
|
431
|
+
fun awaitConnectionCount(
|
|
432
|
+
relayUrl: String,
|
|
433
|
+
count: Int
|
|
434
|
+
): Boolean {
|
|
435
|
+
val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2)
|
|
436
|
+
while ((connections[relayUrl]?.size ?: 0) < count) {
|
|
437
|
+
val remaining = deadline - System.nanoTime()
|
|
438
|
+
if (remaining <= 0) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
(this as java.lang.Object).wait(
|
|
442
|
+
TimeUnit.NANOSECONDS.toMillis(remaining).coerceAtLeast(1)
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
override fun close() {
|
|
449
|
+
closed = true
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private class FakeConnection(
|
|
454
|
+
private val listener: Nip46RelayConnectionListener
|
|
455
|
+
) : Nip46RelayConnection {
|
|
456
|
+
val messages = mutableListOf<String>()
|
|
457
|
+
var closed = false
|
|
458
|
+
var sendResult = true
|
|
459
|
+
|
|
460
|
+
override fun send(message: String): Boolean {
|
|
461
|
+
messages += message
|
|
462
|
+
return !closed && sendResult
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
override fun close() {
|
|
466
|
+
closed = true
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fun open() {
|
|
470
|
+
listener.onOpen()
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
fun fail() {
|
|
474
|
+
listener.onFailure()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
fun disconnect() {
|
|
478
|
+
listener.onClosed()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fun receive(message: String) {
|
|
482
|
+
listener.onMessage(message)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fun receiveEvent(eventJson: String) {
|
|
486
|
+
receive(
|
|
487
|
+
JSONArray()
|
|
488
|
+
.put("EVENT")
|
|
489
|
+
.put(subscriptionId())
|
|
490
|
+
.put(JSONObject(eventJson))
|
|
491
|
+
.toString()
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
fun subscriptionId(): String {
|
|
496
|
+
return JSONArray(messages.first()).getString(1)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private companion object {
|
|
501
|
+
const val RELAY_ONE = "wss://relay.one"
|
|
502
|
+
const val RELAY_TWO = "wss://relay.two"
|
|
503
|
+
val REQUEST_ID = "a".repeat(64)
|
|
504
|
+
val EVENT_ID = "b".repeat(64)
|
|
505
|
+
val REMOTE_PUBKEY = "c".repeat(64)
|
|
506
|
+
val CLIENT_PUBKEY = "d".repeat(64)
|
|
507
|
+
}
|
|
508
|
+
}
|