@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.
- package/android/build.gradle +2 -2
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +3 -1
- package/android/gradlew +1 -1
- package/android/gradlew.bat +10 -21
- package/lib/cjs/chunks/base58Utils.js +63 -0
- package/lib/cjs/chunks/base58Utils.js.map +1 -0
- package/lib/cjs/chunks/encoding.js +145 -0
- package/lib/cjs/chunks/encoding.js.map +1 -0
- package/lib/cjs/encoding.browser.js +12 -0
- package/lib/cjs/encoding.js +12 -0
- package/lib/cjs/encoding.native.js +49 -0
- package/lib/cjs/encoding.native.js.map +1 -0
- package/lib/cjs/index.browser.js +373 -74
- package/lib/cjs/index.browser.js.map +1 -1
- package/lib/cjs/index.js +373 -74
- package/lib/cjs/index.js.map +1 -1
- package/lib/cjs/index.native.js +71 -23
- package/lib/cjs/index.native.js.map +1 -1
- package/lib/esm/chunks/encoding.js +62 -0
- package/lib/esm/chunks/encoding.js.map +1 -0
- package/lib/esm/encoding.browser.js +2 -0
- package/lib/esm/encoding.js +2 -0
- package/lib/esm/index.browser.js +366 -73
- package/lib/esm/index.browser.js.map +1 -1
- package/lib/esm/index.js +366 -73
- package/lib/esm/index.js.map +1 -1
- package/lib/types/encoding.d.ts +16 -0
- package/lib/types/encoding.d.ts.map +1 -0
- package/lib/types/index.d.ts +28 -6
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +46 -21
package/lib/esm/index.browser.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as utf8FromUint8Array, d as base64ToBase58, f as encode, l as utf8ToUint8Array, m as toUint8Array, p as fromUint8Array, u as arrayBufferToBase64String } from "./chunks/encoding.js";
|
|
2
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
4
|
+
import { bytesToHex, hexToBytes } from "@noble/hashes/utils.js";
|
|
2
5
|
import { createSignInMessageText } from "@solana/wallet-standard-util";
|
|
3
6
|
//#region src/errors.ts
|
|
4
7
|
const SolanaMobileWalletAdapterErrorCode = {
|
|
@@ -12,7 +15,8 @@ const SolanaMobileWalletAdapterErrorCode = {
|
|
|
12
15
|
ERROR_INVALID_PROTOCOL_VERSION: "ERROR_INVALID_PROTOCOL_VERSION",
|
|
13
16
|
ERROR_BROWSER_NOT_SUPPORTED: "ERROR_BROWSER_NOT_SUPPORTED",
|
|
14
17
|
ERROR_LOOPBACK_ACCESS_BLOCKED: "ERROR_LOOPBACK_ACCESS_BLOCKED",
|
|
15
|
-
ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED"
|
|
18
|
+
ERROR_ASSOCIATION_CANCELLED: "ERROR_ASSOCIATION_CANCELLED",
|
|
19
|
+
ERROR_ILLEGAL_TRANSPORT_STATE: "ERROR_ILLEGAL_TRANSPORT_STATE"
|
|
16
20
|
};
|
|
17
21
|
var SolanaMobileWalletAdapterError = class extends Error {
|
|
18
22
|
data;
|
|
@@ -47,17 +51,55 @@ var SolanaMobileWalletAdapterProtocolError = class extends Error {
|
|
|
47
51
|
}
|
|
48
52
|
};
|
|
49
53
|
//#endregion
|
|
50
|
-
//#region src/
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
//#region src/nostr.ts
|
|
55
|
+
const NOSTR_EVENT_KIND_MWA = 20012;
|
|
56
|
+
function generateNostrKeypair() {
|
|
57
|
+
const privateKey = schnorr.utils.randomSecretKey();
|
|
58
|
+
return {
|
|
59
|
+
privateKey,
|
|
60
|
+
publicKey: bytesToHex(schnorr.getPublicKey(privateKey))
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function deriveSessionIdentifier(associationPublicKey) {
|
|
64
|
+
const rawKey = await crypto.subtle.exportKey("raw", associationPublicKey);
|
|
65
|
+
const hash = sha256(new Uint8Array(rawKey));
|
|
66
|
+
return bytesToHex(hash);
|
|
67
|
+
}
|
|
68
|
+
function serializeEvent(pubkey, createdAt, kind, tags, content) {
|
|
69
|
+
return JSON.stringify([
|
|
70
|
+
0,
|
|
71
|
+
pubkey,
|
|
72
|
+
createdAt,
|
|
73
|
+
kind,
|
|
74
|
+
tags,
|
|
75
|
+
content
|
|
76
|
+
]);
|
|
53
77
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
else return base64;
|
|
78
|
+
function computeEventId(pubkey, createdAt, kind, tags, content) {
|
|
79
|
+
const serialized = serializeEvent(pubkey, createdAt, kind, tags, content);
|
|
80
|
+
return bytesToHex(sha256(new TextEncoder().encode(serialized)));
|
|
58
81
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
82
|
+
function createNostrEvent(kind, content, tags, privateKey) {
|
|
83
|
+
const pubkey = bytesToHex(schnorr.getPublicKey(privateKey));
|
|
84
|
+
const createdAt = Math.floor(Date.now() / 1e3);
|
|
85
|
+
const id = computeEventId(pubkey, createdAt, kind, tags, content);
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
pubkey,
|
|
89
|
+
created_at: createdAt,
|
|
90
|
+
kind,
|
|
91
|
+
tags,
|
|
92
|
+
content,
|
|
93
|
+
sig: bytesToHex(schnorr.sign(hexToBytes(id), privateKey))
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function verifyNostrEvent(event) {
|
|
97
|
+
if (computeEventId(event.pubkey, event.created_at, event.kind, event.tags, event.content) !== event.id) return false;
|
|
98
|
+
try {
|
|
99
|
+
return schnorr.verify(hexToBytes(event.sig), hexToBytes(event.id), hexToBytes(event.pubkey));
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
61
103
|
}
|
|
62
104
|
//#endregion
|
|
63
105
|
//#region src/createHelloReq.ts
|
|
@@ -73,14 +115,6 @@ async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
|
73
115
|
return response;
|
|
74
116
|
}
|
|
75
117
|
//#endregion
|
|
76
|
-
//#region src/base58Utils.ts
|
|
77
|
-
function fromUint8Array(byteArray) {
|
|
78
|
-
return getBase58Decoder().decode(byteArray);
|
|
79
|
-
}
|
|
80
|
-
function base64ToBase58(base64EncodedString) {
|
|
81
|
-
return fromUint8Array(toUint8Array(base64EncodedString));
|
|
82
|
-
}
|
|
83
|
-
//#endregion
|
|
84
118
|
//#region src/createSIWSMessage.ts
|
|
85
119
|
function createSIWSMessage(payload) {
|
|
86
120
|
return createSignInMessageText(payload);
|
|
@@ -159,9 +193,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
|
|
|
159
193
|
case "devnet":
|
|
160
194
|
chain = `solana:${chain}`;
|
|
161
195
|
break;
|
|
162
|
-
case "mainnet-beta":
|
|
163
|
-
chain = "solana:mainnet";
|
|
164
|
-
break;
|
|
196
|
+
case "mainnet-beta": chain = "solana:mainnet";
|
|
165
197
|
}
|
|
166
198
|
authorizeParams.chain = chain;
|
|
167
199
|
params = authorizeParams;
|
|
@@ -177,9 +209,7 @@ function handleMobileWalletRequest(methodName, methodParams, protocolVersion) {
|
|
|
177
209
|
identity
|
|
178
210
|
};
|
|
179
211
|
break;
|
|
180
|
-
default:
|
|
181
|
-
method = "authorize";
|
|
182
|
-
break;
|
|
212
|
+
default: method = "authorize";
|
|
183
213
|
}
|
|
184
214
|
break;
|
|
185
215
|
}
|
|
@@ -228,12 +258,13 @@ async function signInFallback(signInPayload, authorizationResult, protocolReques
|
|
|
228
258
|
domain,
|
|
229
259
|
address: base64ToBase58(address)
|
|
230
260
|
});
|
|
231
|
-
const
|
|
261
|
+
const signMessageResult = await protocolRequestHandler("sign_messages", {
|
|
232
262
|
addresses: [address],
|
|
233
263
|
payloads: [siwsMessage]
|
|
234
|
-
})
|
|
235
|
-
const
|
|
236
|
-
const
|
|
264
|
+
});
|
|
265
|
+
const signedPayload = toUint8Array(signMessageResult.signed_payloads[0]);
|
|
266
|
+
const signedMessage = fromUint8Array(signedPayload.slice(0, signedPayload.length - 64));
|
|
267
|
+
const signature = fromUint8Array(signedPayload.slice(signedPayload.length - 64));
|
|
237
268
|
return {
|
|
238
269
|
address,
|
|
239
270
|
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
|
|
@@ -253,7 +284,7 @@ async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
|
|
|
253
284
|
const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
|
|
254
285
|
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
255
286
|
crypto.getRandomValues(initializationVector);
|
|
256
|
-
const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret,
|
|
287
|
+
const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, utf8ToUint8Array(plaintext));
|
|
257
288
|
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
|
|
258
289
|
response.set(new Uint8Array(sequenceNumberVector), 0);
|
|
259
290
|
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
|
|
@@ -262,10 +293,10 @@ async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
|
|
|
262
293
|
}
|
|
263
294
|
async function decryptMessage(message, sharedSecret) {
|
|
264
295
|
const sequenceNumberVector = message.slice(0, 4);
|
|
265
|
-
const initializationVector = message.slice(4,
|
|
266
|
-
const ciphertext = message.slice(
|
|
296
|
+
const initializationVector = message.slice(4, 16);
|
|
297
|
+
const ciphertext = message.slice(16);
|
|
267
298
|
const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
|
|
268
|
-
return
|
|
299
|
+
return utf8FromUint8Array(new Uint8Array(plaintextBuffer));
|
|
269
300
|
}
|
|
270
301
|
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
271
302
|
return {
|
|
@@ -275,11 +306,6 @@ function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
|
275
306
|
tagLength: 128
|
|
276
307
|
};
|
|
277
308
|
}
|
|
278
|
-
let _utf8Decoder;
|
|
279
|
-
function getUtf8Decoder() {
|
|
280
|
-
if (_utf8Decoder === void 0) _utf8Decoder = new TextDecoder("utf-8");
|
|
281
|
-
return _utf8Decoder;
|
|
282
|
-
}
|
|
283
309
|
//#endregion
|
|
284
310
|
//#region src/generateAssociationKeypair.ts
|
|
285
311
|
async function generateAssociationKeypair() {
|
|
@@ -297,15 +323,6 @@ async function generateECDHKeypair() {
|
|
|
297
323
|
}, false, ["deriveKey", "deriveBits"]);
|
|
298
324
|
}
|
|
299
325
|
//#endregion
|
|
300
|
-
//#region src/arrayBufferToBase64String.ts
|
|
301
|
-
function arrayBufferToBase64String(buffer) {
|
|
302
|
-
let binary = "";
|
|
303
|
-
const bytes = new Uint8Array(buffer);
|
|
304
|
-
const len = bytes.byteLength;
|
|
305
|
-
for (let ii = 0; ii < len; ii++) binary += String.fromCharCode(bytes[ii]);
|
|
306
|
-
return window.btoa(binary);
|
|
307
|
-
}
|
|
308
|
-
//#endregion
|
|
309
326
|
//#region src/associationPort.ts
|
|
310
327
|
function getRandomAssociationPort() {
|
|
311
328
|
return assertAssociationPort(49152 + Math.floor(Math.random() * 16384));
|
|
@@ -316,7 +333,7 @@ function assertAssociationPort(port) {
|
|
|
316
333
|
}
|
|
317
334
|
//#endregion
|
|
318
335
|
//#region src/getStringWithURLUnsafeBase64CharactersReplaced.ts
|
|
319
|
-
function
|
|
336
|
+
function getStringWithURLUnsafeBase64CharactersReplaced(unsafeBase64EncodedString) {
|
|
320
337
|
return unsafeBase64EncodedString.replace(/[/+=]/g, (m) => ({
|
|
321
338
|
"/": "_",
|
|
322
339
|
"+": "-",
|
|
@@ -343,9 +360,10 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
343
360
|
}
|
|
344
361
|
async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ["v1"]) {
|
|
345
362
|
const associationPort = assertAssociationPort(putativePort);
|
|
346
|
-
const
|
|
363
|
+
const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
|
|
364
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
347
365
|
const url = getIntentURL("v1/associate/local", associationURLBase);
|
|
348
|
-
url.searchParams.set("association",
|
|
366
|
+
url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
|
|
349
367
|
url.searchParams.set("port", `${associationPort}`);
|
|
350
368
|
protocolVersions.forEach((version) => {
|
|
351
369
|
url.searchParams.set("v", version);
|
|
@@ -353,11 +371,24 @@ async function getAssociateAndroidIntentURL(associationPublicKey, putativePort,
|
|
|
353
371
|
return url;
|
|
354
372
|
}
|
|
355
373
|
async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ["v1"]) {
|
|
356
|
-
const
|
|
374
|
+
const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
|
|
375
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
357
376
|
const url = getIntentURL("v1/associate/remote", associationURLBase);
|
|
358
|
-
url.searchParams.set("association",
|
|
377
|
+
url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
|
|
359
378
|
url.searchParams.set("reflector", `${hostAuthority}`);
|
|
360
|
-
url.searchParams.set("id", `${fromUint8Array
|
|
379
|
+
url.searchParams.set("id", `${fromUint8Array(reflectorId, true)}`);
|
|
380
|
+
protocolVersions.forEach((version) => {
|
|
381
|
+
url.searchParams.set("v", version);
|
|
382
|
+
});
|
|
383
|
+
return url;
|
|
384
|
+
}
|
|
385
|
+
async function getNostrAssociateAndroidIntentURL(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase, protocolVersions = ["v1"]) {
|
|
386
|
+
const exportedKey = await crypto.subtle.exportKey("raw", associationPublicKey);
|
|
387
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
388
|
+
const url = getIntentURL(`v1/associate/${connectionType}/nostr`, associationURLBase);
|
|
389
|
+
url.searchParams.set("association", getStringWithURLUnsafeBase64CharactersReplaced(encodedKey));
|
|
390
|
+
url.searchParams.set("relay", relayDomain);
|
|
391
|
+
url.searchParams.set("pubkey", dappNostrPubkey);
|
|
361
392
|
protocolVersions.forEach((version) => {
|
|
362
393
|
url.searchParams.set("v", version);
|
|
363
394
|
});
|
|
@@ -392,7 +423,7 @@ async function parseHelloRsp(payloadBuffer, associationPublicKey, ecdhPrivateKey
|
|
|
392
423
|
name: "HKDF",
|
|
393
424
|
hash: "SHA-256",
|
|
394
425
|
salt: new Uint8Array(associationPublicKeyBuffer),
|
|
395
|
-
info: new Uint8Array()
|
|
426
|
+
info: /* @__PURE__ */ new Uint8Array()
|
|
396
427
|
}, ecdhSecretKey, {
|
|
397
428
|
name: "AES-GCM",
|
|
398
429
|
length: 128
|
|
@@ -423,9 +454,6 @@ const Browser = {
|
|
|
423
454
|
Firefox: 0,
|
|
424
455
|
Other: 1
|
|
425
456
|
};
|
|
426
|
-
function assertUnreachable(x) {
|
|
427
|
-
return x;
|
|
428
|
-
}
|
|
429
457
|
function getBrowser() {
|
|
430
458
|
return navigator.userAgent.indexOf("Firefox/") !== -1 ? Browser.Firefox : Browser.Other;
|
|
431
459
|
}
|
|
@@ -448,7 +476,7 @@ function getDetectionPromise() {
|
|
|
448
476
|
}
|
|
449
477
|
let _frame = null;
|
|
450
478
|
function launchUrlThroughHiddenFrame(url) {
|
|
451
|
-
if (_frame == null) {
|
|
479
|
+
if (_frame == null || !_frame.isConnected) {
|
|
452
480
|
_frame = document.createElement("iframe");
|
|
453
481
|
_frame.style.display = "none";
|
|
454
482
|
document.body.appendChild(_frame);
|
|
@@ -458,8 +486,7 @@ function launchUrlThroughHiddenFrame(url) {
|
|
|
458
486
|
async function launchAssociation(associationUrl) {
|
|
459
487
|
if (associationUrl.protocol === "https:") window.location.assign(associationUrl);
|
|
460
488
|
else try {
|
|
461
|
-
|
|
462
|
-
switch (browser) {
|
|
489
|
+
switch (getBrowser()) {
|
|
463
490
|
case Browser.Firefox:
|
|
464
491
|
launchUrlThroughHiddenFrame(associationUrl);
|
|
465
492
|
break;
|
|
@@ -469,7 +496,6 @@ async function launchAssociation(associationUrl) {
|
|
|
469
496
|
await detectionPromise;
|
|
470
497
|
break;
|
|
471
498
|
}
|
|
472
|
-
default: assertUnreachable(browser);
|
|
473
499
|
}
|
|
474
500
|
} catch {
|
|
475
501
|
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, "Found no installed wallet that supports the mobile wallet protocol.");
|
|
@@ -480,9 +506,24 @@ async function startSession(associationPublicKey, associationURLBase) {
|
|
|
480
506
|
await launchAssociation(await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase));
|
|
481
507
|
return randomAssociationPort;
|
|
482
508
|
}
|
|
509
|
+
async function startNostrSession(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase) {
|
|
510
|
+
const associationUrl = await getNostrAssociateAndroidIntentURL(associationPublicKey, connectionType, relayDomain, dappNostrPubkey, associationURLBase);
|
|
511
|
+
if (connectionType == "local") await launchAssociation(associationUrl);
|
|
512
|
+
return associationUrl;
|
|
513
|
+
}
|
|
483
514
|
//#endregion
|
|
484
515
|
//#region src/transact.ts
|
|
485
516
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
517
|
+
/**
|
|
518
|
+
* 300 milliseconds is a generally accepted threshold for what someone
|
|
519
|
+
* would consider an acceptable response time for a user interface
|
|
520
|
+
* after having performed a low-attention tapping task. We set the initial
|
|
521
|
+
* interval at which we wait for the wallet to set up the websocket at
|
|
522
|
+
* half this, as per the Nyquist frequency, with a progressive backoff
|
|
523
|
+
* sequence from there. The total wait time is 30s, which allows for the
|
|
524
|
+
* user to be presented with a disambiguation dialog, select a wallet, and
|
|
525
|
+
* for the wallet app to subsequently start.
|
|
526
|
+
*/
|
|
486
527
|
retryDelayScheduleMs: [
|
|
487
528
|
150,
|
|
488
529
|
150,
|
|
@@ -596,7 +637,10 @@ async function startScenario(config) {
|
|
|
596
637
|
const responseBuffer = await evt.data.arrayBuffer();
|
|
597
638
|
switch (state.__type) {
|
|
598
639
|
case "connecting": {
|
|
599
|
-
if (responseBuffer.byteLength !== 0)
|
|
640
|
+
if (responseBuffer.byteLength !== 0) {
|
|
641
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while connecting"));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
600
644
|
const ecdhKeypair = await generateECDHKeypair();
|
|
601
645
|
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
602
646
|
state = {
|
|
@@ -609,7 +653,7 @@ async function startScenario(config) {
|
|
|
609
653
|
case "connected":
|
|
610
654
|
try {
|
|
611
655
|
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
|
|
612
|
-
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new
|
|
656
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
|
|
613
657
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
614
658
|
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
615
659
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
@@ -638,7 +682,11 @@ async function startScenario(config) {
|
|
|
638
682
|
const sessionPropertiesBuffer = responseBuffer.slice(65);
|
|
639
683
|
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
|
|
640
684
|
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
|
|
641
|
-
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1)
|
|
685
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
|
|
686
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
|
|
687
|
+
socket.close();
|
|
688
|
+
return { protocol_version: "v1" };
|
|
689
|
+
}
|
|
642
690
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
643
691
|
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
644
692
|
})() : { protocol_version: "legacy" };
|
|
@@ -734,8 +782,10 @@ async function startRemoteScenario(config) {
|
|
|
734
782
|
let socket;
|
|
735
783
|
let disposeSocket;
|
|
736
784
|
const decodeBytes = async (evt) => {
|
|
737
|
-
if (encoding == "base64")
|
|
738
|
-
|
|
785
|
+
if (encoding == "base64") {
|
|
786
|
+
const message = await evt.data;
|
|
787
|
+
return toUint8Array(message).buffer;
|
|
788
|
+
} else return await evt.data.arrayBuffer();
|
|
739
789
|
};
|
|
740
790
|
const associationUrl = await new Promise((resolve, reject) => {
|
|
741
791
|
const handleOpen = async () => {
|
|
@@ -766,7 +816,11 @@ async function startRemoteScenario(config) {
|
|
|
766
816
|
const handleReflectorIdMessage = async (evt) => {
|
|
767
817
|
const responseBuffer = await decodeBytes(evt);
|
|
768
818
|
if (state.__type === "connecting") {
|
|
769
|
-
if (responseBuffer.byteLength == 0)
|
|
819
|
+
if (responseBuffer.byteLength == 0) {
|
|
820
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while connecting"));
|
|
821
|
+
socket.close();
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
770
824
|
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
|
|
771
825
|
state = {
|
|
772
826
|
__type: "reflector_id_received",
|
|
@@ -814,10 +868,14 @@ async function startRemoteScenario(config) {
|
|
|
814
868
|
const responseBuffer = await decodeBytes(evt);
|
|
815
869
|
switch (state.__type) {
|
|
816
870
|
case "reflector_id_received": {
|
|
817
|
-
if (responseBuffer.byteLength !== 0)
|
|
871
|
+
if (responseBuffer.byteLength !== 0) {
|
|
872
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while awaiting reflection"));
|
|
873
|
+
socket.close();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
818
876
|
const ecdhKeypair = await generateECDHKeypair();
|
|
819
877
|
const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
|
|
820
|
-
if (encoding == "base64") socket.send(fromUint8Array
|
|
878
|
+
if (encoding == "base64") socket.send(fromUint8Array(binaryMsg));
|
|
821
879
|
else socket.send(binaryMsg);
|
|
822
880
|
state = {
|
|
823
881
|
__type: "hello_req_sent",
|
|
@@ -829,7 +887,7 @@ async function startRemoteScenario(config) {
|
|
|
829
887
|
case "connected":
|
|
830
888
|
try {
|
|
831
889
|
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
|
|
832
|
-
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new
|
|
890
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
|
|
833
891
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
834
892
|
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
835
893
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
@@ -848,7 +906,11 @@ async function startRemoteScenario(config) {
|
|
|
848
906
|
const sessionPropertiesBuffer = responseBuffer.slice(65);
|
|
849
907
|
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
|
|
850
908
|
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
|
|
851
|
-
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1)
|
|
909
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
|
|
910
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
|
|
911
|
+
socket.close();
|
|
912
|
+
return { protocol_version: "v1" };
|
|
913
|
+
}
|
|
852
914
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
853
915
|
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
854
916
|
})() : { protocol_version: "legacy" };
|
|
@@ -865,7 +927,7 @@ async function startRemoteScenario(config) {
|
|
|
865
927
|
method,
|
|
866
928
|
params: params ?? {}
|
|
867
929
|
}, sharedSecret);
|
|
868
|
-
if (encoding == "base64") socket.send(fromUint8Array
|
|
930
|
+
if (encoding == "base64") socket.send(fromUint8Array(binaryMsg));
|
|
869
931
|
else socket.send(binaryMsg);
|
|
870
932
|
return new Promise((resolve, reject) => {
|
|
871
933
|
jsonRpcResponsePromises[id] = {
|
|
@@ -908,7 +970,238 @@ async function startRemoteScenario(config) {
|
|
|
908
970
|
})
|
|
909
971
|
};
|
|
910
972
|
}
|
|
973
|
+
async function startNostrScenario(config) {
|
|
974
|
+
assertSecureContext();
|
|
975
|
+
const associationKeypair = await generateAssociationKeypair();
|
|
976
|
+
const { privateKey: dappNostrPrivateKey, publicKey: dappNostrPubkey } = generateNostrKeypair();
|
|
977
|
+
const associationUrl = await startNostrSession(associationKeypair.publicKey, config.connectionType, config.relayDomain, dappNostrPubkey, config.baseUri);
|
|
978
|
+
const sessionIdentifier = await deriveSessionIdentifier(associationKeypair.publicKey);
|
|
979
|
+
const subscriptionId = crypto.randomUUID();
|
|
980
|
+
const webSocketURL = `wss://${config.relayDomain}`;
|
|
981
|
+
const connectionStartTime = Date.now();
|
|
982
|
+
const getNextRetryDelayMs = (() => {
|
|
983
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
984
|
+
return () => schedule.length > 1 ? schedule.shift() : schedule[0];
|
|
985
|
+
})();
|
|
986
|
+
let nextJsonRpcMessageId = 1;
|
|
987
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
988
|
+
let state = { __type: "disconnected" };
|
|
989
|
+
let socket;
|
|
990
|
+
let sessionEstablished = false;
|
|
991
|
+
let handleForceClose;
|
|
992
|
+
const sendNostrEvent = (content, walletPubkey) => {
|
|
993
|
+
const event = createNostrEvent(NOSTR_EVENT_KIND_MWA, content, [["d", sessionIdentifier], ["p", walletPubkey]], dappNostrPrivateKey);
|
|
994
|
+
socket.send(JSON.stringify(["EVENT", event]));
|
|
995
|
+
};
|
|
996
|
+
const doClose = () => {
|
|
997
|
+
if (state.__type == "connected" || state.__type == "hello_req_sent") {
|
|
998
|
+
const event = createNostrEvent(NOSTR_EVENT_KIND_MWA, "", [
|
|
999
|
+
["d", sessionIdentifier],
|
|
1000
|
+
["p", state.walletNostrPubkey],
|
|
1001
|
+
["msg", "SESSION_END"]
|
|
1002
|
+
], dappNostrPrivateKey);
|
|
1003
|
+
socket.send(JSON.stringify(["EVENT", event]));
|
|
1004
|
+
}
|
|
1005
|
+
socket.close();
|
|
1006
|
+
};
|
|
1007
|
+
const scenario = {
|
|
1008
|
+
close: () => {
|
|
1009
|
+
doClose();
|
|
1010
|
+
handleForceClose();
|
|
1011
|
+
},
|
|
1012
|
+
wallet: new Promise((resolve, reject) => {
|
|
1013
|
+
const jsonRpcResponsePromises = {};
|
|
1014
|
+
const handleOpen = async () => {
|
|
1015
|
+
if (state.__type !== "connecting") {
|
|
1016
|
+
console.warn(`Expected adapter state to be \`connecting\` at the moment the websocket opens. Got \`${state.__type}\`.`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
socket.removeEventListener("open", handleOpen);
|
|
1020
|
+
socket.send(JSON.stringify([
|
|
1021
|
+
"REQ",
|
|
1022
|
+
subscriptionId,
|
|
1023
|
+
{
|
|
1024
|
+
kinds: [NOSTR_EVENT_KIND_MWA],
|
|
1025
|
+
"#d": [sessionIdentifier]
|
|
1026
|
+
}
|
|
1027
|
+
]));
|
|
1028
|
+
state = {
|
|
1029
|
+
__type: "subscribed",
|
|
1030
|
+
dappNostrPrivateKey,
|
|
1031
|
+
dappNostrPubkey,
|
|
1032
|
+
sessionIdentifier
|
|
1033
|
+
};
|
|
1034
|
+
};
|
|
1035
|
+
const handleClose = (evt) => {
|
|
1036
|
+
if (evt.wasClean) state = { __type: "disconnected" };
|
|
1037
|
+
else reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The Nostr subscription dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
1038
|
+
disposeSocket();
|
|
1039
|
+
};
|
|
1040
|
+
const handleError = async () => {
|
|
1041
|
+
disposeSocket();
|
|
1042
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the Nostr relay at ${config.relayDomain}.`));
|
|
1043
|
+
else {
|
|
1044
|
+
await new Promise((resolve) => {
|
|
1045
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
1046
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
1047
|
+
});
|
|
1048
|
+
attemptSocketConnection();
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
const handleMessage = async (evt) => {
|
|
1052
|
+
let msg;
|
|
1053
|
+
try {
|
|
1054
|
+
msg = JSON.parse(evt.data);
|
|
1055
|
+
if (!Array.isArray(msg)) throw new Error();
|
|
1056
|
+
} catch {
|
|
1057
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Invalid Nostr message received: " + evt.data));
|
|
1058
|
+
doClose();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const type = msg[0];
|
|
1062
|
+
if (type === "CLOSED") disposeSocket();
|
|
1063
|
+
else if (type === "EVENT") {
|
|
1064
|
+
const event = msg[2];
|
|
1065
|
+
if (!event || !verifyNostrEvent(event)) return;
|
|
1066
|
+
switch (state.__type) {
|
|
1067
|
+
case "subscribed": {
|
|
1068
|
+
if (event.content.length !== 0) {
|
|
1069
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encountered unexpected message while awaiting reflection"));
|
|
1070
|
+
doClose();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const walletNostrPubkey = event.pubkey;
|
|
1074
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
1075
|
+
const helloReq = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
|
|
1076
|
+
const base64Content = fromUint8Array(helloReq);
|
|
1077
|
+
sendNostrEvent(base64Content, walletNostrPubkey);
|
|
1078
|
+
state = {
|
|
1079
|
+
__type: "hello_req_sent",
|
|
1080
|
+
associationPublicKey: associationKeypair.publicKey,
|
|
1081
|
+
ecdhPrivateKey: ecdhKeypair.privateKey,
|
|
1082
|
+
walletNostrPubkey
|
|
1083
|
+
};
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
case "hello_req_sent": {
|
|
1087
|
+
if (event.pubkey !== state.walletNostrPubkey) return;
|
|
1088
|
+
if (event.content === "") return;
|
|
1089
|
+
const responseBuffer = toUint8Array(event.content).buffer;
|
|
1090
|
+
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
|
|
1091
|
+
const sessionPropertiesBuffer = responseBuffer.slice(65);
|
|
1092
|
+
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0 ? await (async () => {
|
|
1093
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sessionPropertiesBuffer.slice(0, 4));
|
|
1094
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) {
|
|
1095
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number"));
|
|
1096
|
+
doClose();
|
|
1097
|
+
return { protocol_version: "v1" };
|
|
1098
|
+
}
|
|
1099
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
1100
|
+
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
1101
|
+
})() : { protocol_version: "legacy" };
|
|
1102
|
+
state = {
|
|
1103
|
+
__type: "connected",
|
|
1104
|
+
sharedSecret,
|
|
1105
|
+
sessionProperties,
|
|
1106
|
+
walletNostrPubkey: state.walletNostrPubkey
|
|
1107
|
+
};
|
|
1108
|
+
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
|
|
1109
|
+
const id = nextJsonRpcMessageId++;
|
|
1110
|
+
const binaryMsg = await encryptJsonRpcMessage({
|
|
1111
|
+
id,
|
|
1112
|
+
jsonrpc: "2.0",
|
|
1113
|
+
method,
|
|
1114
|
+
params: params ?? {}
|
|
1115
|
+
}, sharedSecret);
|
|
1116
|
+
sendNostrEvent(fromUint8Array(binaryMsg), state.walletNostrPubkey);
|
|
1117
|
+
return new Promise((resolve, reject) => {
|
|
1118
|
+
jsonRpcResponsePromises[id] = {
|
|
1119
|
+
resolve(result) {
|
|
1120
|
+
switch (method) {
|
|
1121
|
+
case "authorize":
|
|
1122
|
+
case "reauthorize": {
|
|
1123
|
+
const { wallet_uri_base } = result;
|
|
1124
|
+
if (wallet_uri_base != null) try {
|
|
1125
|
+
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
reject(e);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
resolve(result);
|
|
1134
|
+
},
|
|
1135
|
+
reject
|
|
1136
|
+
};
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
sessionEstablished = true;
|
|
1140
|
+
try {
|
|
1141
|
+
resolve(wallet);
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
reject(e);
|
|
1144
|
+
}
|
|
1145
|
+
break;
|
|
1146
|
+
}
|
|
1147
|
+
case "connected":
|
|
1148
|
+
if (event.pubkey !== state.walletNostrPubkey) return;
|
|
1149
|
+
if (event.content === "") return;
|
|
1150
|
+
try {
|
|
1151
|
+
const responseBuffer = toUint8Array(event.content).buffer;
|
|
1152
|
+
const sequenceNumber = getSequenceNumberFromByteArray(responseBuffer.slice(0, 4));
|
|
1153
|
+
if (sequenceNumber !== lastKnownInboundSequenceNumber + 1) throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ILLEGAL_TRANSPORT_STATE, "Encrypted message has invalid sequence number");
|
|
1154
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
1155
|
+
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
1156
|
+
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
1157
|
+
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
1158
|
+
responsePromise.resolve(jsonRpcMessage.result);
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
1161
|
+
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
1162
|
+
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
1163
|
+
responsePromise.reject(e);
|
|
1164
|
+
} else throw e;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
};
|
|
1169
|
+
handleForceClose = () => {
|
|
1170
|
+
socket.removeEventListener("message", handleMessage);
|
|
1171
|
+
disposeSocket();
|
|
1172
|
+
if (!sessionEstablished) reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent("socket was closed before connection") }));
|
|
1173
|
+
};
|
|
1174
|
+
let disposeSocket;
|
|
1175
|
+
let retryWaitTimeoutId;
|
|
1176
|
+
const attemptSocketConnection = () => {
|
|
1177
|
+
if (disposeSocket) disposeSocket();
|
|
1178
|
+
state = {
|
|
1179
|
+
__type: "connecting",
|
|
1180
|
+
associationKeypair
|
|
1181
|
+
};
|
|
1182
|
+
socket = new WebSocket(webSocketURL);
|
|
1183
|
+
socket.addEventListener("open", handleOpen);
|
|
1184
|
+
socket.addEventListener("close", handleClose);
|
|
1185
|
+
socket.addEventListener("error", handleError);
|
|
1186
|
+
socket.addEventListener("message", handleMessage);
|
|
1187
|
+
disposeSocket = () => {
|
|
1188
|
+
window.clearTimeout(retryWaitTimeoutId);
|
|
1189
|
+
socket.removeEventListener("open", handleOpen);
|
|
1190
|
+
socket.removeEventListener("close", handleClose);
|
|
1191
|
+
socket.removeEventListener("error", handleError);
|
|
1192
|
+
socket.removeEventListener("message", handleMessage);
|
|
1193
|
+
};
|
|
1194
|
+
};
|
|
1195
|
+
attemptSocketConnection();
|
|
1196
|
+
})
|
|
1197
|
+
};
|
|
1198
|
+
if (config.connectionType == "local") return scenario;
|
|
1199
|
+
else return {
|
|
1200
|
+
...scenario,
|
|
1201
|
+
associationUrl
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
911
1204
|
//#endregion
|
|
912
|
-
export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, startRemoteScenario, startScenario, transact };
|
|
1205
|
+
export { NOSTR_EVENT_KIND_MWA, SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, createNostrEvent, deriveSessionIdentifier, generateNostrKeypair, startNostrScenario, startRemoteScenario, startScenario, transact, verifyNostrEvent };
|
|
913
1206
|
|
|
914
1207
|
//# sourceMappingURL=index.browser.js.map
|