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