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