@solana-mobile/mobile-wallet-adapter-protocol 2.2.8 → 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
- import { getBase58Decoder } from "@solana/codecs-strings";
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,17 +51,55 @@ var SolanaMobileWalletAdapterProtocolError = class extends Error {
47
51
  }
48
52
  };
49
53
  //#endregion
50
- //#region src/base64Utils.ts
51
- function encode(input) {
52
- return window.btoa(input);
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
+ ]);
53
77
  }
54
- function fromUint8Array$1(byteArray, urlsafe) {
55
- const base64 = window.btoa(String.fromCharCode.call(null, ...byteArray));
56
- if (urlsafe) return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
57
- else return base64;
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)));
58
81
  }
59
- function toUint8Array(base64EncodedByteArray) {
60
- return new Uint8Array(window.atob(base64EncodedByteArray).split("").map((c) => c.charCodeAt(0)));
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
+ }
61
103
  }
62
104
  //#endregion
63
105
  //#region src/createHelloReq.ts
@@ -73,14 +115,6 @@ async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
73
115
  return response;
74
116
  }
75
117
  //#endregion
76
- //#region src/base58Utils.ts
77
- function fromUint8Array(byteArray) {
78
- return getBase58Decoder().decode(byteArray);
79
- }
80
- function base64ToBase58(base64EncodedString) {
81
- return fromUint8Array(toUint8Array(base64EncodedString));
82
- }
83
- //#endregion
84
118
  //#region src/createSIWSMessage.ts
85
119
  function createSIWSMessage(payload) {
86
120
  return createSignInMessageText(payload);
@@ -159,9 +193,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
159
193
  case "devnet":
160
194
  chain = `solana:${chain}`;
161
195
  break;
162
- case "mainnet-beta":
163
- chain = "solana:mainnet";
164
- break;
196
+ case "mainnet-beta": chain = "solana:mainnet";
165
197
  }
166
198
  authorizeParams.chain = chain;
167
199
  params = authorizeParams;
@@ -177,9 +209,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
177
209
  identity
178
210
  };
179
211
  break;
180
- default:
181
- method = "authorize";
182
- break;
212
+ default: method = "authorize";
183
213
  }
184
214
  break;
185
215
  }
@@ -228,12 +258,13 @@ async function signInFallback(signInPayload, authorizationResult, protocolReques
228
258
  domain,
229
259
  address: base64ToBase58(address)
230
260
  });
231
- const signedPayload = toUint8Array((await protocolRequestHandler("sign_messages", {
261
+ const signMessageResult = await protocolRequestHandler("sign_messages", {
232
262
  addresses: [address],
233
263
  payloads: [siwsMessage]
234
- })).signed_payloads[0]);
235
- const signedMessage = fromUint8Array$1(signedPayload.slice(0, signedPayload.length - 64));
236
- const signature = fromUint8Array$1(signedPayload.slice(signedPayload.length - 64));
264
+ });
265
+ const signedPayload = toUint8Array(signMessageResult.signed_payloads[0]);
266
+ const signedMessage = fromUint8Array(signedPayload.slice(0, signedPayload.length - 64));
267
+ const signature = fromUint8Array(signedPayload.slice(signedPayload.length - 64));
237
268
  return {
238
269
  address,
239
270
  signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
@@ -253,7 +284,7 @@ async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
253
284
  const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
254
285
  const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
255
286
  crypto.getRandomValues(initializationVector);
256
- const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, new TextEncoder().encode(plaintext));
287
+ const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, utf8ToUint8Array(plaintext));
257
288
  const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
258
289
  response.set(new Uint8Array(sequenceNumberVector), 0);
259
290
  response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
@@ -262,10 +293,10 @@ async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
262
293
  }
263
294
  async function decryptMessage(message, sharedSecret) {
264
295
  const sequenceNumberVector = message.slice(0, 4);
265
- const initializationVector = message.slice(4, 4 + INITIALIZATION_VECTOR_BYTES);
266
- const ciphertext = message.slice(4 + INITIALIZATION_VECTOR_BYTES);
296
+ const initializationVector = message.slice(4, 16);
297
+ const ciphertext = message.slice(16);
267
298
  const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
268
- return getUtf8Decoder().decode(plaintextBuffer);
299
+ return utf8FromUint8Array(new Uint8Array(plaintextBuffer));
269
300
  }
270
301
  function getAlgorithmParams(sequenceNumber, initializationVector) {
271
302
  return {
@@ -275,11 +306,6 @@ function getAlgorithmParams(sequenceNumber, initializationVector) {
275
306
  tagLength: 128
276
307
  };
277
308
  }
278
- let _utf8Decoder;
279
- function getUtf8Decoder() {
280
- if (_utf8Decoder === void 0) _utf8Decoder = new TextDecoder("utf-8");
281
- return _utf8Decoder;
282
- }
283
309
  //#endregion
284
310
  //#region src/generateAssociationKeypair.ts
285
311
  async function generateAssociationKeypair() {
@@ -297,15 +323,6 @@ async function generateECDHKeypair() {
297
323
  }, false, ["deriveKey", "deriveBits"]);
298
324
  }
299
325
  //#endregion
300
- //#region src/arrayBufferToBase64String.ts
301
- function arrayBufferToBase64String(buffer) {
302
- let binary = "";
303
- const bytes = new Uint8Array(buffer);
304
- const len = bytes.byteLength;
305
- for (let ii = 0; ii < len; ii++) binary += String.fromCharCode(bytes[ii]);
306
- return window.btoa(binary);
307
- }
308
- //#endregion
309
326
  //#region src/associationPort.ts
310
327
  function getRandomAssociationPort() {
311
328
  return assertAssociationPort(49152 + Math.floor(Math.random() * 16384));
@@ -316,7 +333,7 @@ function assertAssociationPort(port) {
316
333
  }
317
334
  //#endregion
318
335
  //#region src/getStringWithURLUnsafeBase64CharactersReplaced.ts
319
- function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
336
+ function getStringWithURLUnsafeBase64CharactersReplaced(unsafeBase64EncodedString) {
320
337
  return unsafeBase64EncodedString.replace(/[/+=]/g, (m) => ({
321
338
  "/": "_",
322
339
  "+": "-",
@@ -343,9 +360,10 @@ function getIntentURL(methodPathname, intentUrlBase) {
343
360
  }
344
361
  async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ["v1"]) {
345
362
  const associationPort = assertAssociationPort(putativePort);
346
- const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
363
+ const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
364
+ const encodedKey = arrayBufferToBase64String(exportedKey);
347
365
  const url = getIntentURL("v1/associate/local", associationURLBase);
348
- url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
366
+ url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
349
367
  url.searchParams.set("port", `${associationPort}`);
350
368
  protocolVersions.forEach((version) => {
351
369
  url.searchParams.set("v", version);
@@ -353,11 +371,24 @@ async function getAssociateAndroidIntentURL(associationPublicKey, putativePort,
353
371
  return url;
354
372
  }
355
373
  async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ["v1"]) {
356
- const encodedKey = arrayBufferToBase64String(await crypto.subtle.exportKey("raw", associationPublicKey));
374
+ const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
375
+ const encodedKey = arrayBufferToBase64String(exportedKey);
357
376
  const url = getIntentURL("v1/associate/remote", associationURLBase);
358
- url.searchParams.set("association", getStringWithURLUnsafeCharactersReplaced(encodedKey));
377
+ url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
359
378
  url.searchParams.set("reflector", `${hostAuthority}`);
360
- url.searchParams.set("id", `${fromUint8Array$1(reflectorId, true)}`);
379
+ url.searchParams.set("id", `${fromUint8Array(reflectorId, true)}`);
380
+ protocolVersions.forEach((version) => {
381
+ url.searchParams.set("v", version);
382
+ });
383
+ return url;
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);
361
392
  protocolVersions.forEach((version) => {
362
393
  url.searchParams.set("v", version);
363
394
  });
@@ -392,7 +423,7 @@ async function parseHelloRsp(payloadBuffer, associationPublicKey, ecdhPrivateKey
392
423
  name: "HKDF",
393
424
  hash: "SHA-256",
394
425
  salt: new Uint8Array(associationPublicKeyBuffer),
395
- info: new Uint8Array()
426
+ info: /* @__PURE__ */ new Uint8Array()
396
427
  }, ecdhSecretKey, {
397
428
  name: "AES-GCM",
398
429
  length: 128
@@ -423,9 +454,6 @@ const Browser = {
423
454
  Firefox: 0,
424
455
  Other: 1
425
456
  };
426
- function assertUnreachable(x) {
427
- return x;
428
- }
429
457
  function getBrowser() {
430
458
  return navigator.userAgent.indexOf("Firefox/") !== -1 ? Browser.Firefox : Browser.Other;
431
459
  }
@@ -448,7 +476,7 @@ function getDetectionPromise() {
448
476
  }
449
477
  let _frame = null;
450
478
  function launchUrlThroughHiddenFrame(url) {
451
- if (_frame == null) {
479
+ if (_frame == null || !_frame.isConnected) {
452
480
  _frame = document.createElement("iframe");
453
481
  _frame.style.display = "none";
454
482
  document.body.appendChild(_frame);
@@ -458,8 +486,7 @@ function launchUrlThroughHiddenFrame(url) {
458
486
  async function launchAssociation(associationUrl) {
459
487
  if (associationUrl.protocol === "https:") window.location.assign(associationUrl);
460
488
  else try {
461
- const browser = getBrowser();
462
- switch (browser) {
489
+ switch (getBrowser()) {
463
490
  case Browser.Firefox:
464
491
  launchUrlThroughHiddenFrame(associationUrl);
465
492
  break;
@@ -469,7 +496,6 @@ async function launchAssociation(associationUrl) {
469
496
  await detectionPromise;
470
497
  break;
471
498
  }
472
- default: assertUnreachable(browser);
473
499
  }
474
500
  } catch {
475
501
  throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, "Found no installed wallet that supports the mobile wallet protocol.");
@@ -480,9 +506,24 @@ async function startSession(associationPublicKey, associationURLBase) {
480
506
  await launchAssociation(await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase));
481
507
  return randomAssociationPort;
482
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
+ }
483
514
  //#endregion
484
515
  //#region src/transact.ts
485
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
+ */
486
527
  retryDelayScheduleMs: [
487
528
  150,
488
529
  150,
@@ -596,7 +637,10 @@ async function startScenario(config) {
596
637
  const responseBuffer = await evt.data.arrayBuffer();
597
638
  switch (state.__type) {
598
639
  case "connecting": {
599
- 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
+ }
600
644
  const ecdhKeypair = await generateECDHKeypair();
601
645
  socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
602
646
  state = {
@@ -609,7 +653,7 @@ async function startScenario(config) {
609
653
  case "connected":
610
654
  try {
611
655
  const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
612
- 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");
613
657
  lastKnownInboundSequenceNumber = sequenceNumber;
614
658
  const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
615
659
  const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
@@ -638,7 +682,11 @@ async function startScenario(config) {
638
682
  const sessionPropertiesBuffer = responseBuffer.slice(65);
639
683
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
640
684
  const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
641
- 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
+ }
642
690
  lastKnownInboundSequenceNumber = sequenceNumber;
643
691
  return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
644
692
  })() : { protocol_version: "legacy" };
@@ -734,8 +782,10 @@ async function startRemoteScenario(config) {
734
782
  let socket;
735
783
  let disposeSocket;
736
784
  const decodeBytes = async (evt) => {
737
- if (encoding == "base64") return toUint8Array(await evt.data).buffer;
738
- 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();
739
789
  };
740
790
  const associationUrl = await new Promise((resolve, reject) => {
741
791
  const handleOpen = async () => {
@@ -766,7 +816,11 @@ async function startRemoteScenario(config) {
766
816
  const handleReflectorIdMessage = async (evt) => {
767
817
  const responseBuffer = await decodeBytes(evt);
768
818
  if (state.__type === "connecting") {
769
- 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
+ }
770
824
  const reflectorId = getReflectorIdFromByteArray(responseBuffer);
771
825
  state = {
772
826
  __type: "reflector_id_received",
@@ -814,10 +868,14 @@ async function startRemoteScenario(config) {
814
868
  const responseBuffer = await decodeBytes(evt);
815
869
  switch (state.__type) {
816
870
  case "reflector_id_received": {
817
- 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
+ }
818
876
  const ecdhKeypair = await generateECDHKeypair();
819
877
  const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
820
- if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
878
+ if (encoding == "base64") socket.send(fromUint8Array(binaryMsg));
821
879
  else socket.send(binaryMsg);
822
880
  state = {
823
881
  __type: "hello_req_sent",
@@ -829,7 +887,7 @@ async function startRemoteScenario(config) {
829
887
  case "connected":
830
888
  try {
831
889
  const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
832
- 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");
833
891
  lastKnownInboundSequenceNumber = sequenceNumber;
834
892
  const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
835
893
  const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
@@ -848,7 +906,11 @@ async function startRemoteScenario(config) {
848
906
  const sessionPropertiesBuffer = responseBuffer.slice(65);
849
907
  const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
850
908
  const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
851
- 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
+ }
852
914
  lastKnownInboundSequenceNumber = sequenceNumber;
853
915
  return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
854
916
  })() : { protocol_version: "legacy" };
@@ -865,7 +927,7 @@ async function startRemoteScenario(config) {
865
927
  method,
866
928
  params: params ?? {}
867
929
  }, sharedSecret);
868
- if (encoding == "base64") socket.send(fromUint8Array$1(binaryMsg));
930
+ if (encoding == "base64") socket.send(fromUint8Array(binaryMsg));
869
931
  else socket.send(binaryMsg);
870
932
  return new Promise((resolve, reject) => {
871
933
  jsonRpcResponsePromises[id] = {
@@ -908,7 +970,238 @@ async function startRemoteScenario(config) {
908
970
  })
909
971
  };
910
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
+ }
911
1204
  //#endregion
912
- 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 };
913
1206
 
914
1207
  //# sourceMappingURL=index.browser.js.map