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