@solana-mobile/mobile-wallet-adapter-protocol 2.2.9 → 2.3.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/lib/cjs/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_encoding = require("./chunks/encoding.js");
3
+ let _noble_curves_secp256k1_js = require("@noble/curves/secp256k1.js");
4
+ let _noble_hashes_sha2_js = require("@noble/hashes/sha2.js");
5
+ let _noble_hashes_utils_js = require("@noble/hashes/utils.js");
3
6
  let _solana_wallet_standard_util = require("@solana/wallet-standard-util");
4
7
  //#region src/errors.ts
5
8
  const SolanaMobileWalletAdapterErrorCode = {
@@ -13,7 +16,8 @@ const SolanaMobileWalletAdapterErrorCode = {
13
16
  ERROR_INVALID_PROTOCOL_VERSION: "ERROR_INVALID_PROTOCOL_VERSION",
14
17
  ERROR_BROWSER_NOT_SUPPORTED: "ERROR_BROWSER_NOT_SUPPORTED",
15
18
  ERROR_LOOPBACK_ACCESS_BLOCKED: "ERROR_LOOPBACK_ACCESS_BLOCKED",
16
- ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED"
19
+ ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED",
20
+ ERROR_ILLEGAL_TRANSPORT_STATE: "ERROR_ILLEGAL_TRANSPORT_STATE"
17
21
  };
18
22
  var SolanaMobileWalletAdapterError = class extends Error {
19
23
  data;
@@ -48,6 +52,57 @@ var SolanaMobileWalletAdapterProtocolError = class extends Error {
48
52
  }
49
53
  };
50
54
  //#endregion
55
+ //#region src/nostr.ts
56
+ const NOSTR_EVENT_KIND_MWA = 20012;
57
+ function generateNostrKeypair() {
58
+ const privateKey = _noble_curves_secp256k1_js.schnorr.utils.randomSecretKey();
59
+ return {
60
+ privateKey,
61
+ publicKey: (0, _noble_hashes_utils_js.bytesToHex)(_noble_curves_secp256k1_js.schnorr.getPublicKey(privateKey))
62
+ };
63
+ }
64
+ async function deriveSessionIdentifier(associationPublicKey) {
65
+ const rawKey = await crypto.subtle.exportKey("raw", associationPublicKey);
66
+ const hash = (0, _noble_hashes_sha2_js.sha256)(new Uint8Array(rawKey));
67
+ return (0, _noble_hashes_utils_js.bytesToHex)(hash);
68
+ }
69
+ function serializeEvent(pubkey, createdAt, kind, tags, content) {
70
+ return JSON.stringify([
71
+ 0,
72
+ pubkey,
73
+ createdAt,
74
+ kind,
75
+ tags,
76
+ content
77
+ ]);
78
+ }
79
+ function computeEventId(pubkey, createdAt, kind, tags, content) {
80
+ const serialized = serializeEvent(pubkey, createdAt, kind, tags, content);
81
+ return (0, _noble_hashes_utils_js.bytesToHex)((0, _noble_hashes_sha2_js.sha256)(new TextEncoder().encode(serialized)));
82
+ }
83
+ function createNostrEvent(kind, content, tags, privateKey) {
84
+ const pubkey = (0, _noble_hashes_utils_js.bytesToHex)(_noble_curves_secp256k1_js.schnorr.getPublicKey(privateKey));
85
+ const createdAt = Math.floor(Date.now() / 1e3);
86
+ const id = computeEventId(pubkey, createdAt, kind, tags, content);
87
+ return {
88
+ id,
89
+ pubkey,
90
+ created_at: createdAt,
91
+ kind,
92
+ tags,
93
+ content,
94
+ sig: (0, _noble_hashes_utils_js.bytesToHex)(_noble_curves_secp256k1_js.schnorr.sign((0, _noble_hashes_utils_js.hexToBytes)(id), privateKey))
95
+ };
96
+ }
97
+ function verifyNostrEvent(event) {
98
+ if (computeEventId(event.pubkey, event.created_at, event.kind, event.tags, event.content) !== event.id) return false;
99
+ try {
100
+ return _noble_curves_secp256k1_js.schnorr.verify((0, _noble_hashes_utils_js.hexToBytes)(event.sig), (0, _noble_hashes_utils_js.hexToBytes)(event.id), (0, _noble_hashes_utils_js.hexToBytes)(event.pubkey));
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ //#endregion
51
106
  //#region src/createHelloReq.ts
52
107
  async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
53
108
  const publicKeyBuffer = await crypto.subtle.exportKey("raw", ecdhPublicKey);
@@ -139,9 +194,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
139
194
  case "devnet":
140
195
  chain = `solana:${chain}`;
141
196
  break;
142
- case "mainnet-beta":
143
- chain = "solana:mainnet";
144
- break;
197
+ case "mainnet-beta": chain = "solana:mainnet";
145
198
  }
146
199
  authorizeParams.chain = chain;
147
200
  params = authorizeParams;
@@ -157,9 +210,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
157
210
  identity
158
211
  };
159
212
  break;
160
- default:
161
- method = "authorize";
162
- break;
213
+ default: method = "authorize";
163
214
  }
164
215
  break;
165
216
  }
@@ -208,10 +259,11 @@ async function signInFallback(signInPayload, authorizationResult, protocolReques
208
259
  domain,
209
260
  address: require_encoding.base64ToBase58$1(address)
210
261
  });
211
- const signedPayload = require_encoding.toUint8Array((await protocolRequestHandler("sign_messages", {
262
+ const signMessageResult = await protocolRequestHandler("sign_messages", {
212
263
  addresses: [address],
213
264
  payloads: [siwsMessage]
214
- })).signed_payloads[0]);
265
+ });
266
+ const signedPayload = require_encoding.toUint8Array(signMessageResult.signed_payloads[0]);
215
267
  const signedMessage = require_encoding.fromUint8Array(signedPayload.slice(0, signedPayload.length - 64));
216
268
  const signature = require_encoding.fromUint8Array(signedPayload.slice(signedPayload.length - 64));
217
269
  return {
@@ -242,8 +294,8 @@ async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
242
294
  }
243
295
  async function decryptMessage(message, sharedSecret) {
244
296
  const sequenceNumberVector = message.slice(0, 4);
245
- const initializationVector = message.slice(4, 4 + INITIALIZATION_VECTOR_BYTES);
246
- const ciphertext = message.slice(4 + INITIALIZATION_VECTOR_BYTES);
297
+ const initializationVector = message.slice(4, 16);
298
+ const ciphertext = message.slice(16);
247
299
  const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
248
300
  return require_encoding.utf8FromUint8Array(new Uint8Array(plaintextBuffer));
249
301
  }
@@ -309,7 +361,8 @@ function getIntentURL(methodPathname, intentUrlBase) {
309
361
  }
310
362
  async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ["v1"]) {
311
363
  const associationPort = assertAssociationPort(putativePort);
312
- const encodedKey = require_encoding.arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
364
+ const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
365
+ const encodedKey = require_encoding.arrayBufferToBase64String(exportedKey);
313
366
  const url = getIntentURL("v1/associate/local", associationURLBase);
314
367
  url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
315
368
  url.searchParams.set("port", `${associationPort}`);
@@ -319,7 +372,8 @@ async function getAssociateAndroidIntentURL(associationPublicKey, putativePort,
319
372
  return url;
320
373
  }
321
374
  async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ["v1"]) {
322
- const encodedKey = require_encoding.arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
375
+ const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
376
+ const encodedKey = require_encoding.arrayBufferToBase64String(exportedKey);
323
377
  const url = getIntentURL("v1/associate/remote", associationURLBase);
324
378
  url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
325
379
  url.searchParams.set("reflector", `${hostAuthority}`);
@@ -329,6 +383,18 @@ async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuth
329
383
  });
330
384
  return url;
331
385
  }
386
+ async function getNostrAssociateAndroidIntentURL(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase, protocolVersions = ["v1"]) {
387
+ const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
388
+ const encodedKey = require_encoding.arrayBufferToBase64String(exportedKey);
389
+ const url = getIntentURL(`v1/associate/${connectionType}/nostr`, associationURLBase);
390
+ url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
391
+ url.searchParams.set("relay", relayDomain);
392
+ url.searchParams.set("pubkey", dappNostrPubkey);
393
+ protocolVersions.forEach((version) => {
394
+ url.searchParams.set("v", version);
395
+ });
396
+ return url;
397
+ }
332
398
  //#endregion
333
399
  //#region src/jsonRpcMessage.ts
334
400
  async function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
@@ -358,7 +424,7 @@ async function parseHelloRsp(payloadBuffer, associationPublicKey, ecdhPrivateKey
358
424
  name: "HKDF",
359
425
  hash: "SHA-256",
360
426
  salt: new Uint8Array(associationPublicKeyBuffer),
361
- info: new Uint8Array()
427
+ info: /* @__PURE__ */ new Uint8Array()
362
428
  }, ecdhSecretKey, {
363
429
  name: "AES-GCM",
364
430
  length: 128
@@ -389,9 +455,6 @@ const Browser = {
389
455
  Firefox: 0,
390
456
  Other: 1
391
457
  };
392
- function assertUnreachable(x) {
393
- return x;
394
- }
395
458
  function getBrowser() {
396
459
  return navigator.userAgent.indexOf("Firefox/") !== -1 ? Browser.Firefox : Browser.Other;
397
460
  }
@@ -414,7 +477,7 @@ function getDetectionPromise() {
414
477
  }
415
478
  let _frame = null;
416
479
  function launchUrlThroughHiddenFrame(url) {
417
- if (_frame == null) {
480
+ if (_frame == null || !_frame.isConnected) {
418
481
  _frame = document.createElement("iframe");
419
482
  _frame.style.display = "none";
420
483
  document.body.appendChild(_frame);
@@ -424,8 +487,7 @@ function launchUrlThroughHiddenFrame(url) {
424
487
  async function launchAssociation(associationUrl) {
425
488
  if (associationUrl.protocol === "https:") window.location.assign(associationUrl);
426
489
  else try {
427
- const browser = getBrowser();
428
- switch (browser) {
490
+ switch (getBrowser()) {
429
491
  case Browser.Firefox:
430
492
  launchUrlThroughHiddenFrame(associationUrl);
431
493
  break;
@@ -435,7 +497,6 @@ async function launchAssociation(associationUrl) {
435
497
  await detectionPromise;
436
498
  break;
437
499
  }
438
- default: assertUnreachable(browser);
439
500
  }
440
501
  } catch {
441
502
  throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, "Found no installed wallet that supports the mobile wallet protocol.");
@@ -446,9 +507,24 @@ async function startSession(associationPublicKey, associationURLBase) {
446
507
  await launchAssociation(await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase));
447
508
  return randomAssociationPort;
448
509
  }
510
+ async function startNostrSession(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase) {
511
+ const associationUrl = await getNostrAssociateAndroidIntentURL(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase);
512
+ if (connectionType == "local") await launchAssociation(associationUrl);
513
+ return associationUrl;
514
+ }
449
515
  //#endregion
450
516
  //#region src/transact.ts
451
517
  const WEBSOCKET_CONNECTION_CONFIG = {
518
+ /**
519
+ * 300 milliseconds is a generally accepted threshold for what someone
520
+ * would consider an acceptable response time for a user interface
521
+ * after having performed a low-attention tapping task. We set the initial
522
+ * interval at which we wait for the wallet to set up the websocket at
523
+ * half this, as per the Nyquist frequency, with a progressive backoff
524
+ * sequence from there. The total wait time is 30s, which allows for the
525
+ * user to be presented with a disambiguation dialog, select a wallet, and
526
+ * for the wallet app to subsequently start.
527
+ */
452
528
  retryDelayScheduleMs: [
453
529
  150,
454
530
  150,
@@ -562,7 +638,10 @@ async function startScenario(config) {
562
638
  const responseBuffer = await evt.data.arrayBuffer();
563
639
  switch (state.__type) {
564
640
  case "connecting": {
565
- if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while connecting");
641
+ if (responseBuffer.byteLength !== 0) {
642
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while connecting"));
643
+ return;
644
+ }
566
645
  const ecdhKeypair = await generateECDHKeypair();
567
646
  socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
568
647
  state = {
@@ -575,7 +654,7 @@ async function startScenario(config) {
575
654
  case "connected":
576
655
  try {
577
656
  const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
578
- if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
657
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
579
658
  lastKnownInboundSequenceNumber = sequenceNumber;
580
659
  const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
581
660
  const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
@@ -604,7 +683,11 @@ async function startScenario(config) {
604
683
  const sessionPropertiesBuffer = responseBuffer.slice(65);
605
684
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
606
685
  const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
607
- if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
686
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
687
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
688
+ socket.close();
689
+ return { protocol_version: "v1" };
690
+ }
608
691
  lastKnownInboundSequenceNumber = sequenceNumber;
609
692
  return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
610
693
  })() : { protocol_version: "legacy" };
@@ -700,8 +783,10 @@ async function startRemoteScenario(config) {
700
783
  let socket;
701
784
  let disposeSocket;
702
785
  const decodeBytes = async (evt) => {
703
- if (encoding == "base64") return require_encoding.toUint8Array(await evt.data).buffer;
704
- else return await evt.data.arrayBuffer();
786
+ if (encoding == "base64") {
787
+ const message = await evt.data;
788
+ return require_encoding.toUint8Array(message).buffer;
789
+ } else return await evt.data.arrayBuffer();
705
790
  };
706
791
  const associationUrl = await new Promise((resolve, reject) => {
707
792
  const handleOpen = async () => {
@@ -732,7 +817,11 @@ async function startRemoteScenario(config) {
732
817
  const handleReflectorIdMessage = async (evt) => {
733
818
  const responseBuffer = await decodeBytes(evt);
734
819
  if (state.__type === "connecting") {
735
- if (responseBuffer.byteLength == 0) throw new Error("Encountered unexpected message while connecting");
820
+ if (responseBuffer.byteLength == 0) {
821
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while connecting"));
822
+ socket.close();
823
+ return;
824
+ }
736
825
  const reflectorId = getReflectorIdFromByteArray(responseBuffer);
737
826
  state = {
738
827
  __type: "reflector_id_received",
@@ -780,7 +869,11 @@ async function startRemoteScenario(config) {
780
869
  const responseBuffer = await decodeBytes(evt);
781
870
  switch (state.__type) {
782
871
  case "reflector_id_received": {
783
- if (responseBuffer.byteLength !== 0) throw new Error("Encountered unexpected message while awaiting reflection");
872
+ if (responseBuffer.byteLength !== 0) {
873
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while awaiting reflection"));
874
+ socket.close();
875
+ return;
876
+ }
784
877
  const ecdhKeypair = await generateECDHKeypair();
785
878
  const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
786
879
  if (encoding == "base64") socket.send(require_encoding.fromUint8Array(binaryMsg));
@@ -795,7 +888,7 @@ async function startRemoteScenario(config) {
795
888
  case "connected":
796
889
  try {
797
890
  const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
798
- if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
891
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
799
892
  lastKnownInboundSequenceNumber = sequenceNumber;
800
893
  const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
801
894
  const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
@@ -814,7 +907,11 @@ async function startRemoteScenario(config) {
814
907
  const sessionPropertiesBuffer = responseBuffer.slice(65);
815
908
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
816
909
  const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
817
- if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new Error("Encrypted message has invalid sequence number");
910
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
911
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
912
+ socket.close();
913
+ return { protocol_version: "v1" };
914
+ }
818
915
  lastKnownInboundSequenceNumber = sequenceNumber;
819
916
  return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
820
917
  })() : { protocol_version: "legacy" };
@@ -874,7 +971,239 @@ async function startRemoteScenario(config) {
874
971
  })
875
972
  };
876
973
  }
974
+ async function startNostrScenario(config) {
975
+ assertSecureContext();
976
+ const associationKeypair = await generateAssociationKeypair();
977
+ const { privateKey: dappNostrPrivateKey, publicKey: dappNostrPubkey } = generateNostrKeypair();
978
+ const associationUrl = await startNostrSession(associationKeypair.publicKey, config.connectionType, config.relayDomain, dappNostrPubkey, config.baseUri);
979
+ const sessionIdentifier = await deriveSessionIdentifier(associationKeypair.publicKey);
980
+ const subscriptionId = crypto.randomUUID();
981
+ const webSocketURL = `wss://${config.relayDomain}`;
982
+ const connectionStartTime = Date.now();
983
+ const getNextRetryDelayMs = (() => {
984
+ const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
985
+ return () => schedule.length > 1 ? schedule.shift() : schedule[0];
986
+ })();
987
+ let nextJsonRpcMessageId = 1;
988
+ let lastKnownInboundSequenceNumber = 0;
989
+ let state = { __type: "disconnected" };
990
+ let socket;
991
+ let sessionEstablished = false;
992
+ let handleForceClose;
993
+ const sendNostrEvent = (content, walletPubkey) => {
994
+ const event = createNostrEvent(NOSTR_EVENT_KIND_MWA, content, [["d", sessionIdentifier], ["p", walletPubkey]], dappNostrPrivateKey);
995
+ socket.send(JSON.stringify(["EVENT", event]));
996
+ };
997
+ const doClose = () => {
998
+ if (state.__type == "connected" || state.__type == "hello_req_sent") {
999
+ const event = createNostrEvent(NOSTR_EVENT_KIND_MWA, "", [
1000
+ ["d", sessionIdentifier],
1001
+ ["p", state.walletNostrPubkey],
1002
+ ["msg", "SESSION_END"]
1003
+ ], dappNostrPrivateKey);
1004
+ socket.send(JSON.stringify(["EVENT", event]));
1005
+ }
1006
+ socket.close();
1007
+ };
1008
+ const scenario = {
1009
+ close: () => {
1010
+ doClose();
1011
+ handleForceClose();
1012
+ },
1013
+ wallet: new Promise((resolve, reject) => {
1014
+ const jsonRpcResponsePromises = {};
1015
+ const handleOpen = async () => {
1016
+ if (state.__type !== "connecting") {
1017
+ console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
1018
+ return;
1019
+ }
1020
+ socket.removeEventListener("open", handleOpen);
1021
+ socket.send(JSON.stringify([
1022
+ "REQ",
1023
+ subscriptionId,
1024
+ {
1025
+ kinds: [NOSTR_EVENT_KIND_MWA],
1026
+ "#d": [sessionIdentifier]
1027
+ }
1028
+ ]));
1029
+ state = {
1030
+ __type: "subscribed",
1031
+ dappNostrPrivateKey,
1032
+ dappNostrPubkey,
1033
+ sessionIdentifier
1034
+ };
1035
+ };
1036
+ const handleClose = (evt) => {
1037
+ if (evt.wasClean) state = { __type: "disconnected" };
1038
+ else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The Nostr subscription dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
1039
+ disposeSocket();
1040
+ };
1041
+ const handleError = async () => {
1042
+ disposeSocket();
1043
+ if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the Nostr relay at ${config.relayDomain}.`));
1044
+ else {
1045
+ await new Promise((resolve) => {
1046
+ const retryDelayMs = getNextRetryDelayMs();
1047
+ retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
1048
+ });
1049
+ attemptSocketConnection();
1050
+ }
1051
+ };
1052
+ const handleMessage = async (evt) => {
1053
+ let msg;
1054
+ try {
1055
+ msg = JSON.parse(evt.data);
1056
+ if (!Array.isArray(msg)) throw new Error();
1057
+ } catch {
1058
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Invalid Nostr message received: " + evt.data));
1059
+ doClose();
1060
+ return;
1061
+ }
1062
+ const type = msg[0];
1063
+ if (type === "CLOSED") disposeSocket();
1064
+ else if (type === "EVENT") {
1065
+ const event = msg[2];
1066
+ if (!event || !verifyNostrEvent(event)) return;
1067
+ switch (state.__type) {
1068
+ case "subscribed": {
1069
+ if (event.content.length !== 0) {
1070
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while awaiting reflection"));
1071
+ doClose();
1072
+ return;
1073
+ }
1074
+ const walletNostrPubkey = event.pubkey;
1075
+ const ecdhKeypair = await generateECDHKeypair();
1076
+ const helloReq = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
1077
+ const base64Content = require_encoding.fromUint8Array(helloReq);
1078
+ sendNostrEvent(base64Content, walletNostrPubkey);
1079
+ state = {
1080
+ __type: "hello_req_sent",
1081
+ associationPublicKey: associationKeypair.publicKey,
1082
+ ecdhPrivateKey: ecdhKeypair.privateKey,
1083
+ walletNostrPubkey
1084
+ };
1085
+ break;
1086
+ }
1087
+ case "hello_req_sent": {
1088
+ if (event.pubkey !== state.walletNostrPubkey) return;
1089
+ if (event.content === "") return;
1090
+ const responseBuffer = require_encoding.toUint8Array(event.content).buffer;
1091
+ const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
1092
+ const sessionPropertiesBuffer = responseBuffer.slice(65);
1093
+ const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
1094
+ const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
1095
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
1096
+ reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
1097
+ doClose();
1098
+ return { protocol_version: "v1" };
1099
+ }
1100
+ lastKnownInboundSequenceNumber = sequenceNumber;
1101
+ return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
1102
+ })() : { protocol_version: "legacy" };
1103
+ state = {
1104
+ __type: "connected",
1105
+ sharedSecret,
1106
+ sessionProperties,
1107
+ walletNostrPubkey: state.walletNostrPubkey
1108
+ };
1109
+ const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
1110
+ const id = nextJsonRpcMessageId++;
1111
+ const binaryMsg = await encryptJsonRpcMessage({
1112
+ id,
1113
+ jsonrpc: "2.0",
1114
+ method,
1115
+ params: params ?? {}
1116
+ }, sharedSecret);
1117
+ sendNostrEvent(require_encoding.fromUint8Array(binaryMsg), state.walletNostrPubkey);
1118
+ return new Promise((resolve, reject) => {
1119
+ jsonRpcResponsePromises[id] = {
1120
+ resolve(result) {
1121
+ switch (method) {
1122
+ case "authorize":
1123
+ case "reauthorize": {
1124
+ const { wallet_uri_base } = result;
1125
+ if (wallet_uri_base != null) try {
1126
+ assertSecureEndpointSpecificURI(wallet_uri_base);
1127
+ } catch (e) {
1128
+ reject(e);
1129
+ return;
1130
+ }
1131
+ break;
1132
+ }
1133
+ }
1134
+ resolve(result);
1135
+ },
1136
+ reject
1137
+ };
1138
+ });
1139
+ });
1140
+ sessionEstablished = true;
1141
+ try {
1142
+ resolve(wallet);
1143
+ } catch (e) {
1144
+ reject(e);
1145
+ }
1146
+ break;
1147
+ }
1148
+ case "connected":
1149
+ if (event.pubkey !== state.walletNostrPubkey) return;
1150
+ if (event.content === "") return;
1151
+ try {
1152
+ const responseBuffer = require_encoding.toUint8Array(event.content).buffer;
1153
+ const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
1154
+ if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
1155
+ lastKnownInboundSequenceNumber = sequenceNumber;
1156
+ const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
1157
+ const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
1158
+ delete jsonRpcResponsePromises[jsonRpcMessage.id];
1159
+ responsePromise.resolve(jsonRpcMessage.result);
1160
+ } catch (e) {
1161
+ if (e instanceof SolanaMobileWalletAdapterProtocolError) {
1162
+ const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
1163
+ delete jsonRpcResponsePromises[e.jsonRpcMessageId];
1164
+ responsePromise.reject(e);
1165
+ } else throw e;
1166
+ }
1167
+ }
1168
+ }
1169
+ };
1170
+ handleForceClose = () => {
1171
+ socket.removeEventListener("message", handleMessage);
1172
+ disposeSocket();
1173
+ if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
1174
+ };
1175
+ let disposeSocket;
1176
+ let retryWaitTimeoutId;
1177
+ const attemptSocketConnection = () => {
1178
+ if (disposeSocket) disposeSocket();
1179
+ state = {
1180
+ __type: "connecting",
1181
+ associationKeypair
1182
+ };
1183
+ socket = new WebSocket(webSocketURL);
1184
+ socket.addEventListener("open", handleOpen);
1185
+ socket.addEventListener("close", handleClose);
1186
+ socket.addEventListener("error", handleError);
1187
+ socket.addEventListener("message", handleMessage);
1188
+ disposeSocket = () => {
1189
+ window.clearTimeout(retryWaitTimeoutId);
1190
+ socket.removeEventListener("open", handleOpen);
1191
+ socket.removeEventListener("close", handleClose);
1192
+ socket.removeEventListener("error", handleError);
1193
+ socket.removeEventListener("message", handleMessage);
1194
+ };
1195
+ };
1196
+ attemptSocketConnection();
1197
+ })
1198
+ };
1199
+ if (config.connectionType == "local") return scenario;
1200
+ else return {
1201
+ ...scenario,
1202
+ associationUrl
1203
+ };
1204
+ }
877
1205
  //#endregion
1206
+ exports.NOSTR_EVENT_KIND_MWA = NOSTR_EVENT_KIND_MWA;
878
1207
  exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
879
1208
  exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
880
1209
  exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
@@ -882,8 +1211,13 @@ exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtoc
882
1211
  exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
883
1212
  exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
884
1213
  exports.SolanaSignTransactions = SolanaSignTransactions;
1214
+ exports.createNostrEvent = createNostrEvent;
1215
+ exports.deriveSessionIdentifier = deriveSessionIdentifier;
1216
+ exports.generateNostrKeypair = generateNostrKeypair;
1217
+ exports.startNostrScenario = startNostrScenario;
885
1218
  exports.startRemoteScenario = startRemoteScenario;
886
1219
  exports.startScenario = startScenario;
887
1220
  exports.transact = transact;
1221
+ exports.verifyNostrEvent = verifyNostrEvent;
888
1222
 
889
1223
  //# sourceMappingURL=index.js.map