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.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/android-template/app/build.gradle.kts +4 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +273 -0
  4. package/android-template/app/src/main/assets/www/index.html +0 -6
  5. package/android-template/app/src/main/java/com/pakstr/app/LocalServer.kt +20 -4
  6. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +136 -12
  7. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Hash.kt +56 -0
  8. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Signer.kt +104 -0
  9. package/android-template/app/src/main/java/com/pakstr/app/crypto/Bip340Verifier.kt +20 -170
  10. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip44Crypto.kt +513 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip46ClientKey.kt +140 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventHasher.kt +3 -28
  13. package/android-template/app/src/main/java/com/pakstr/app/crypto/NostrEventSerializer.kt +74 -0
  14. package/android-template/app/src/main/java/com/pakstr/app/crypto/Secp256k1.kt +100 -0
  15. package/android-template/app/src/main/java/com/pakstr/app/signer/BunkerSigner.kt +32 -7
  16. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +36 -9
  17. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +240 -163
  18. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConfig.kt +175 -0
  19. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/BunkerConnectionStore.kt +144 -0
  20. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Client.kt +79 -0
  21. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Models.kt +319 -0
  22. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingClient.kt +159 -0
  23. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingQrCode.kt +35 -0
  24. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46PairingUri.kt +104 -0
  25. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46Protocol.kt +695 -0
  26. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/Nip46RelayTransport.kt +489 -0
  27. package/android-template/app/src/main/java/com/pakstr/app/signer/bunker/RelayNip46Client.kt +403 -0
  28. package/android-template/app/src/main/java/com/pakstr/app/utils/BunkerPairingDialog.kt +82 -0
  29. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +78 -17
  30. package/android-template/app/src/main/res/layout/activity_layout.xml +11 -0
  31. package/android-template/app/src/main/res/layout/dialog_bunker_pairing.xml +75 -0
  32. package/android-template/app/src/main/res/layout/dialog_picker.xml +60 -31
  33. package/android-template/app/src/main/res/values/strings.xml +27 -0
  34. package/android-template/app/src/test/java/com/pakstr/app/LocalServerQueryKeepAliveTest.kt +118 -0
  35. package/android-template/app/src/test/java/com/pakstr/app/crypto/Bip340SignerTest.kt +133 -0
  36. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip44CryptoTest.kt +199 -0
  37. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyProtocolTest.kt +82 -0
  38. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip46ClientKeyTest.kt +52 -0
  39. package/android-template/app/src/test/java/com/pakstr/app/crypto/NostrEventSerializerTest.kt +42 -0
  40. package/android-template/app/src/test/java/com/pakstr/app/signer/NostrBridgeErrorTest.kt +32 -0
  41. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/BunkerConfigTest.kt +120 -0
  42. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ModelsTest.kt +111 -0
  43. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingClientTest.kt +271 -0
  44. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingQrCodeTest.kt +54 -0
  45. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46PairingUriTest.kt +80 -0
  46. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46ProtocolTest.kt +578 -0
  47. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/Nip46RelayTransportTest.kt +508 -0
  48. package/android-template/app/src/test/java/com/pakstr/app/signer/bunker/RelayNip46ClientTest.kt +678 -0
  49. package/android-template/gradle/libs.versions.toml +8 -0
  50. package/dist/cli.js +8 -0
  51. package/dist/commands/init.js +16 -0
  52. package/dist/commands/nsite.js +38 -0
  53. package/dist/commands/publish.js +50 -2
  54. package/dist/commands/run.js +5 -5
  55. package/dist/core/blossom.js +2 -2
  56. package/dist/core/nsite.js +176 -0
  57. package/dist/core/pakstrConfig.js +85 -6
  58. package/package.json +1 -1
@@ -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
+ }