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