@solana-mobile/mobile-wallet-adapter-protocol 2.2.5 → 2.2.6
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 +3 -3
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +2 -1
- package/android/gradle.properties +1 -1
- package/android/gradlew +173 -110
- package/android/gradlew.bat +22 -18
- package/lib/cjs/index.browser.js +451 -480
- package/lib/cjs/index.js +451 -480
- package/lib/cjs/index.native.js +66 -86
- package/lib/esm/index.browser.js +451 -479
- package/lib/esm/index.js +451 -479
- package/lib/types/index.browser.d.ts +9 -2
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.native.d.ts +9 -2
- package/package.json +74 -75
package/lib/cjs/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
var walletStandardUtil = require('@solana/wallet-standard-util');
|
|
6
4
|
var codecsStrings = require('@solana/codecs-strings');
|
|
7
5
|
|
|
@@ -16,8 +14,12 @@ const SolanaMobileWalletAdapterErrorCode = {
|
|
|
16
14
|
ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
|
|
17
15
|
ERROR_INVALID_PROTOCOL_VERSION: 'ERROR_INVALID_PROTOCOL_VERSION',
|
|
18
16
|
ERROR_BROWSER_NOT_SUPPORTED: 'ERROR_BROWSER_NOT_SUPPORTED',
|
|
17
|
+
ERROR_LOOPBACK_ACCESS_BLOCKED: 'ERROR_LOOPBACK_ACCESS_BLOCKED',
|
|
18
|
+
ERROR_ASSOCIATION_CANCELLED: 'ERROR_ASSOCIATION_CANCELLED',
|
|
19
19
|
};
|
|
20
20
|
class SolanaMobileWalletAdapterError extends Error {
|
|
21
|
+
data;
|
|
22
|
+
code;
|
|
21
23
|
constructor(...args) {
|
|
22
24
|
const [code, message, data] = args;
|
|
23
25
|
super(message);
|
|
@@ -37,6 +39,9 @@ const SolanaMobileWalletAdapterProtocolErrorCode = {
|
|
|
37
39
|
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
38
40
|
};
|
|
39
41
|
class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
42
|
+
data;
|
|
43
|
+
code;
|
|
44
|
+
jsonRpcMessageId;
|
|
40
45
|
constructor(...args) {
|
|
41
46
|
const [jsonRpcMessageId, code, message, data] = args;
|
|
42
47
|
super(message);
|
|
@@ -47,31 +52,6 @@ class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
/******************************************************************************
|
|
51
|
-
Copyright (c) Microsoft Corporation.
|
|
52
|
-
|
|
53
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
54
|
-
purpose with or without fee is hereby granted.
|
|
55
|
-
|
|
56
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
57
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
58
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
59
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
60
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
61
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
62
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
63
|
-
***************************************************************************** */
|
|
64
|
-
|
|
65
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
66
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
67
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
68
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
69
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
70
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
71
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
55
|
function encode(input) {
|
|
76
56
|
return window.btoa(input);
|
|
77
57
|
}
|
|
@@ -93,15 +73,13 @@ function toUint8Array(base64EncodedByteArray) {
|
|
|
93
73
|
.map((c) => c.charCodeAt(0)));
|
|
94
74
|
}
|
|
95
75
|
|
|
96
|
-
function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return response;
|
|
104
|
-
});
|
|
76
|
+
async function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
77
|
+
const publicKeyBuffer = await crypto.subtle.exportKey('raw', ecdhPublicKey);
|
|
78
|
+
const signatureBuffer = await crypto.subtle.sign({ hash: 'SHA-256', name: 'ECDSA' }, associationKeypairPrivateKey, publicKeyBuffer);
|
|
79
|
+
const response = new Uint8Array(publicKeyBuffer.byteLength + signatureBuffer.byteLength);
|
|
80
|
+
response.set(new Uint8Array(publicKeyBuffer), 0);
|
|
81
|
+
response.set(new Uint8Array(signatureBuffer), publicKeyBuffer.byteLength);
|
|
82
|
+
return response;
|
|
105
83
|
}
|
|
106
84
|
|
|
107
85
|
function createSIWSMessage(payload) {
|
|
@@ -144,16 +122,14 @@ function createMobileWalletProxy(protocolVersion, protocolRequestHandler) {
|
|
|
144
122
|
return null;
|
|
145
123
|
}
|
|
146
124
|
if (target[p] == null) {
|
|
147
|
-
target[p] = function (inputParams) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return handleMobileWalletResponse(p, result, protocolVersion);
|
|
156
|
-
});
|
|
125
|
+
target[p] = async function (inputParams) {
|
|
126
|
+
const { method, params } = handleMobileWalletRequest(p, inputParams, protocolVersion);
|
|
127
|
+
const result = await protocolRequestHandler(method, params);
|
|
128
|
+
// if the request tried to sign in but the wallet did not return a sign in result, fallback on message signing
|
|
129
|
+
if (method === 'authorize' && params.sign_in_payload && !result.sign_in_result) {
|
|
130
|
+
result['sign_in_result'] = await signInFallback(params.sign_in_payload, result, protocolRequestHandler);
|
|
131
|
+
}
|
|
132
|
+
return handleMobileWalletResponse(p, result, protocolVersion);
|
|
157
133
|
};
|
|
158
134
|
}
|
|
159
135
|
return target[p];
|
|
@@ -257,39 +233,43 @@ function handleMobileWalletResponse(method, response, protocolVersion) {
|
|
|
257
233
|
if (capabilities.supports_clone_authorization === true) {
|
|
258
234
|
features.push(SolanaCloneAuthorization);
|
|
259
235
|
}
|
|
260
|
-
return
|
|
236
|
+
return {
|
|
237
|
+
...capabilities,
|
|
238
|
+
features: features,
|
|
239
|
+
};
|
|
261
240
|
}
|
|
262
241
|
case 'v1': {
|
|
263
|
-
return
|
|
242
|
+
return {
|
|
243
|
+
...capabilities,
|
|
244
|
+
supports_sign_and_send_transactions: true,
|
|
245
|
+
supports_clone_authorization: capabilities.features.includes(SolanaCloneAuthorization)
|
|
246
|
+
};
|
|
264
247
|
}
|
|
265
248
|
}
|
|
266
249
|
}
|
|
267
250
|
}
|
|
268
251
|
return response;
|
|
269
252
|
}
|
|
270
|
-
function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
addresses: [address],
|
|
278
|
-
payloads: [siwsMessage]
|
|
279
|
-
});
|
|
280
|
-
const signedPayload = toUint8Array(signMessageResult.signed_payloads[0]);
|
|
281
|
-
const signedMessage = fromUint8Array$1(signedPayload.slice(0, signedPayload.length - 64));
|
|
282
|
-
const signature = fromUint8Array$1(signedPayload.slice(signedPayload.length - 64));
|
|
283
|
-
const signInResult = {
|
|
284
|
-
address: address,
|
|
285
|
-
// Workaround: some wallets have been observed to only reply with the message signature.
|
|
286
|
-
// This is non-compliant with the spec, but in the interest of maximizing compatibility,
|
|
287
|
-
// detect this case and reuse the original message.
|
|
288
|
-
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
|
|
289
|
-
signature
|
|
290
|
-
};
|
|
291
|
-
return signInResult;
|
|
253
|
+
async function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
|
|
254
|
+
const domain = signInPayload.domain ?? window.location.host;
|
|
255
|
+
const address = authorizationResult.accounts[0].address;
|
|
256
|
+
const siwsMessage = createSIWSMessageBase64Url({ ...signInPayload, domain, address: base64ToBase58(address) });
|
|
257
|
+
const signMessageResult = await protocolRequestHandler('sign_messages', {
|
|
258
|
+
addresses: [address],
|
|
259
|
+
payloads: [siwsMessage]
|
|
292
260
|
});
|
|
261
|
+
const signedPayload = toUint8Array(signMessageResult.signed_payloads[0]);
|
|
262
|
+
const signedMessage = fromUint8Array$1(signedPayload.slice(0, signedPayload.length - 64));
|
|
263
|
+
const signature = fromUint8Array$1(signedPayload.slice(signedPayload.length - 64));
|
|
264
|
+
const signInResult = {
|
|
265
|
+
address: address,
|
|
266
|
+
// Workaround: some wallets have been observed to only reply with the message signature.
|
|
267
|
+
// This is non-compliant with the spec, but in the interest of maximizing compatibility,
|
|
268
|
+
// detect this case and reuse the original message.
|
|
269
|
+
signed_message: signedMessage.length == 0 ? siwsMessage : signedMessage,
|
|
270
|
+
signature
|
|
271
|
+
};
|
|
272
|
+
return signInResult;
|
|
293
273
|
}
|
|
294
274
|
|
|
295
275
|
const SEQUENCE_NUMBER_BYTES = 4;
|
|
@@ -305,28 +285,24 @@ function createSequenceNumberVector(sequenceNumber) {
|
|
|
305
285
|
|
|
306
286
|
const INITIALIZATION_VECTOR_BYTES = 12;
|
|
307
287
|
const ENCODED_PUBLIC_KEY_LENGTH_BYTES = 65;
|
|
308
|
-
function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return response;
|
|
319
|
-
});
|
|
288
|
+
async function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
|
|
289
|
+
const sequenceNumberVector = createSequenceNumberVector(sequenceNumber);
|
|
290
|
+
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
291
|
+
crypto.getRandomValues(initializationVector);
|
|
292
|
+
const ciphertext = await crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, new TextEncoder().encode(plaintext));
|
|
293
|
+
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
|
|
294
|
+
response.set(new Uint8Array(sequenceNumberVector), 0);
|
|
295
|
+
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
|
|
296
|
+
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
|
|
297
|
+
return response;
|
|
320
298
|
}
|
|
321
|
-
function decryptMessage(message, sharedSecret) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
return plaintext;
|
|
329
|
-
});
|
|
299
|
+
async function decryptMessage(message, sharedSecret) {
|
|
300
|
+
const sequenceNumberVector = message.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
301
|
+
const initializationVector = message.slice(SEQUENCE_NUMBER_BYTES, SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
302
|
+
const ciphertext = message.slice(SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
303
|
+
const plaintextBuffer = await crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
|
|
304
|
+
const plaintext = getUtf8Decoder().decode(plaintextBuffer);
|
|
305
|
+
return plaintext;
|
|
330
306
|
}
|
|
331
307
|
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
332
308
|
return {
|
|
@@ -344,22 +320,18 @@ function getUtf8Decoder() {
|
|
|
344
320
|
return _utf8Decoder;
|
|
345
321
|
}
|
|
346
322
|
|
|
347
|
-
function generateAssociationKeypair() {
|
|
348
|
-
return
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}, false /* extractable */, ['sign'] /* keyUsages */);
|
|
353
|
-
});
|
|
323
|
+
async function generateAssociationKeypair() {
|
|
324
|
+
return await crypto.subtle.generateKey({
|
|
325
|
+
name: 'ECDSA',
|
|
326
|
+
namedCurve: 'P-256',
|
|
327
|
+
}, false /* extractable */, ['sign'] /* keyUsages */);
|
|
354
328
|
}
|
|
355
329
|
|
|
356
|
-
function generateECDHKeypair() {
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
|
|
362
|
-
});
|
|
330
|
+
async function generateECDHKeypair() {
|
|
331
|
+
return await crypto.subtle.generateKey({
|
|
332
|
+
name: 'ECDH',
|
|
333
|
+
namedCurve: 'P-256',
|
|
334
|
+
}, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
|
|
363
335
|
}
|
|
364
336
|
|
|
365
337
|
// https://stackoverflow.com/a/9458996/802047
|
|
@@ -405,12 +377,12 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
405
377
|
try {
|
|
406
378
|
baseUrl = new URL(intentUrlBase);
|
|
407
379
|
}
|
|
408
|
-
catch
|
|
409
|
-
if (
|
|
380
|
+
catch { } // eslint-disable-line no-empty
|
|
381
|
+
if (baseUrl?.protocol !== 'https:') {
|
|
410
382
|
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
411
383
|
}
|
|
412
384
|
}
|
|
413
|
-
baseUrl
|
|
385
|
+
baseUrl ||= new URL(`${INTENT_NAME}:/`);
|
|
414
386
|
const pathname = methodPathname.startsWith('/')
|
|
415
387
|
? // Method is an absolute path. Replace it wholesale.
|
|
416
388
|
methodPathname
|
|
@@ -418,94 +390,82 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
418
390
|
[...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join('/');
|
|
419
391
|
return new URL(pathname, baseUrl);
|
|
420
392
|
}
|
|
421
|
-
function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ['v1']) {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
url.searchParams.set('v', version);
|
|
431
|
-
});
|
|
432
|
-
return url;
|
|
393
|
+
async function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ['v1']) {
|
|
394
|
+
const associationPort = assertAssociationPort(putativePort);
|
|
395
|
+
const exportedKey = await crypto.subtle.exportKey('raw', associationPublicKey);
|
|
396
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
397
|
+
const url = getIntentURL('v1/associate/local', associationURLBase);
|
|
398
|
+
url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
|
|
399
|
+
url.searchParams.set('port', `${associationPort}`);
|
|
400
|
+
protocolVersions.forEach((version) => {
|
|
401
|
+
url.searchParams.set('v', version);
|
|
433
402
|
});
|
|
403
|
+
return url;
|
|
434
404
|
}
|
|
435
|
-
function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ['v1']) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
url.searchParams.set('v', version);
|
|
445
|
-
});
|
|
446
|
-
return url;
|
|
405
|
+
async function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ['v1']) {
|
|
406
|
+
const exportedKey = await crypto.subtle.exportKey('raw', associationPublicKey);
|
|
407
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
408
|
+
const url = getIntentURL('v1/associate/remote', associationURLBase);
|
|
409
|
+
url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
|
|
410
|
+
url.searchParams.set('reflector', `${hostAuthority}`);
|
|
411
|
+
url.searchParams.set('id', `${fromUint8Array$1(reflectorId, true)}`);
|
|
412
|
+
protocolVersions.forEach((version) => {
|
|
413
|
+
url.searchParams.set('v', version);
|
|
447
414
|
});
|
|
415
|
+
return url;
|
|
448
416
|
}
|
|
449
417
|
|
|
450
|
-
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
|
|
455
|
-
});
|
|
418
|
+
async function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
419
|
+
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
420
|
+
const sequenceNumber = jsonRpcMessage.id;
|
|
421
|
+
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
|
|
456
422
|
}
|
|
457
|
-
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return jsonRpcMessage;
|
|
465
|
-
});
|
|
423
|
+
async function decryptJsonRpcMessage(message, sharedSecret) {
|
|
424
|
+
const plaintext = await decryptMessage(message, sharedSecret);
|
|
425
|
+
const jsonRpcMessage = JSON.parse(plaintext);
|
|
426
|
+
if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
|
|
427
|
+
throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
|
|
428
|
+
}
|
|
429
|
+
return jsonRpcMessage;
|
|
466
430
|
}
|
|
467
431
|
|
|
468
|
-
function parseHelloRsp(payloadBuffer, // The X9.62-encoded wallet endpoint ephemeral ECDH public keypoint.
|
|
432
|
+
async function parseHelloRsp(payloadBuffer, // The X9.62-encoded wallet endpoint ephemeral ECDH public keypoint.
|
|
469
433
|
associationPublicKey, ecdhPrivateKey) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
return aesKeyMaterialVal;
|
|
484
|
-
});
|
|
434
|
+
const [associationPublicKeyBuffer, walletPublicKey] = await Promise.all([
|
|
435
|
+
crypto.subtle.exportKey('raw', associationPublicKey),
|
|
436
|
+
crypto.subtle.importKey('raw', payloadBuffer.slice(0, ENCODED_PUBLIC_KEY_LENGTH_BYTES), { name: 'ECDH', namedCurve: 'P-256' }, false /* extractable */, [] /* keyUsages */),
|
|
437
|
+
]);
|
|
438
|
+
const sharedSecret = await crypto.subtle.deriveBits({ name: 'ECDH', public: walletPublicKey }, ecdhPrivateKey, 256);
|
|
439
|
+
const ecdhSecretKey = await crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false /* extractable */, ['deriveKey'] /* keyUsages */);
|
|
440
|
+
const aesKeyMaterialVal = await crypto.subtle.deriveKey({
|
|
441
|
+
name: 'HKDF',
|
|
442
|
+
hash: 'SHA-256',
|
|
443
|
+
salt: new Uint8Array(associationPublicKeyBuffer),
|
|
444
|
+
info: new Uint8Array(),
|
|
445
|
+
}, ecdhSecretKey, { name: 'AES-GCM', length: 128 }, false /* extractable */, ['encrypt', 'decrypt']);
|
|
446
|
+
return aesKeyMaterialVal;
|
|
485
447
|
}
|
|
486
448
|
|
|
487
|
-
function parseSessionProps(message, sharedSecret) {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
|
|
504
|
-
}
|
|
449
|
+
async function parseSessionProps(message, sharedSecret) {
|
|
450
|
+
const plaintext = await decryptMessage(message, sharedSecret);
|
|
451
|
+
const jsonProperties = JSON.parse(plaintext);
|
|
452
|
+
let protocolVersion = 'legacy';
|
|
453
|
+
if (Object.hasOwnProperty.call(jsonProperties, 'v')) {
|
|
454
|
+
switch (jsonProperties.v) {
|
|
455
|
+
case 1:
|
|
456
|
+
case '1':
|
|
457
|
+
case 'v1':
|
|
458
|
+
protocolVersion = 'v1';
|
|
459
|
+
break;
|
|
460
|
+
case 'legacy':
|
|
461
|
+
protocolVersion = 'legacy';
|
|
462
|
+
break;
|
|
463
|
+
default:
|
|
464
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
|
|
505
465
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
466
|
+
}
|
|
467
|
+
return ({
|
|
468
|
+
protocol_version: protocolVersion
|
|
509
469
|
});
|
|
510
470
|
}
|
|
511
471
|
|
|
@@ -550,47 +510,43 @@ function launchUrlThroughHiddenFrame(url) {
|
|
|
550
510
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
551
511
|
_frame.contentWindow.location.href = url.toString();
|
|
552
512
|
}
|
|
553
|
-
function launchAssociation(associationUrl) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
default:
|
|
578
|
-
assertUnreachable(browser);
|
|
513
|
+
async function launchAssociation(associationUrl) {
|
|
514
|
+
if (associationUrl.protocol === 'https:') {
|
|
515
|
+
// The association URL is an Android 'App Link' or iOS 'Universal Link'.
|
|
516
|
+
// These are regular web URLs that are designed to launch an app if it
|
|
517
|
+
// is installed or load the actual target webpage if not.
|
|
518
|
+
window.location.assign(associationUrl);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// The association URL has a custom protocol (eg. `solana-wallet:`)
|
|
522
|
+
try {
|
|
523
|
+
const browser = getBrowser();
|
|
524
|
+
switch (browser) {
|
|
525
|
+
case Browser.Firefox:
|
|
526
|
+
// If a custom protocol is not supported in Firefox, it throws.
|
|
527
|
+
launchUrlThroughHiddenFrame(associationUrl);
|
|
528
|
+
// If we reached this line, it's supported.
|
|
529
|
+
break;
|
|
530
|
+
case Browser.Other: {
|
|
531
|
+
const detectionPromise = getDetectionPromise();
|
|
532
|
+
window.location.assign(associationUrl);
|
|
533
|
+
await detectionPromise;
|
|
534
|
+
break;
|
|
579
535
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
536
|
+
default:
|
|
537
|
+
assertUnreachable(browser);
|
|
583
538
|
}
|
|
584
539
|
}
|
|
585
|
-
|
|
540
|
+
catch (e) {
|
|
541
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
542
|
+
}
|
|
543
|
+
}
|
|
586
544
|
}
|
|
587
|
-
function startSession(associationPublicKey, associationURLBase) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
return randomAssociationPort;
|
|
593
|
-
});
|
|
545
|
+
async function startSession(associationPublicKey, associationURLBase) {
|
|
546
|
+
const randomAssociationPort = getRandomAssociationPort();
|
|
547
|
+
const associationUrl = await getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
|
|
548
|
+
await launchAssociation(associationUrl);
|
|
549
|
+
return randomAssociationPort;
|
|
594
550
|
}
|
|
595
551
|
|
|
596
552
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
@@ -619,7 +575,7 @@ function assertSecureEndpointSpecificURI(walletUriBase) {
|
|
|
619
575
|
try {
|
|
620
576
|
url = new URL(walletUriBase);
|
|
621
577
|
}
|
|
622
|
-
catch
|
|
578
|
+
catch {
|
|
623
579
|
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Invalid base URL supplied by wallet');
|
|
624
580
|
}
|
|
625
581
|
if (url.protocol !== 'https:') {
|
|
@@ -644,25 +600,38 @@ function getReflectorIdFromByteArray(byteArray) {
|
|
|
644
600
|
let { value: length, offset } = decodeVarLong(byteArray);
|
|
645
601
|
return new Uint8Array(byteArray.slice(offset, offset + length));
|
|
646
602
|
}
|
|
647
|
-
function transact(callback, config) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
603
|
+
async function transact(callback, config) {
|
|
604
|
+
const { wallet, close } = await startScenario(config);
|
|
605
|
+
try {
|
|
606
|
+
return await callback(await wallet);
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
609
|
+
close();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function startScenario(config) {
|
|
613
|
+
assertSecureContext();
|
|
614
|
+
const associationKeypair = await generateAssociationKeypair();
|
|
615
|
+
const sessionPort = await startSession(associationKeypair.publicKey, config?.baseUri);
|
|
616
|
+
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
617
|
+
let connectionStartTime;
|
|
618
|
+
const getNextRetryDelayMs = (() => {
|
|
619
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
620
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
621
|
+
})();
|
|
622
|
+
let nextJsonRpcMessageId = 1;
|
|
623
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
624
|
+
let state = { __type: 'disconnected' };
|
|
625
|
+
let socket;
|
|
626
|
+
let sessionEstablished = false;
|
|
627
|
+
let handleForceClose;
|
|
628
|
+
return { close: () => {
|
|
629
|
+
socket.close();
|
|
630
|
+
handleForceClose();
|
|
631
|
+
}, wallet: new Promise((resolve, reject) => {
|
|
663
632
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
664
633
|
const jsonRpcResponsePromises = {};
|
|
665
|
-
const handleOpen = () =>
|
|
634
|
+
const handleOpen = async () => {
|
|
666
635
|
if (state.__type !== 'connecting') {
|
|
667
636
|
console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
|
|
668
637
|
`Got \`${state.__type}\`.`);
|
|
@@ -676,14 +645,14 @@ function transact(callback, config) {
|
|
|
676
645
|
// APP_PING was sent by the wallet/websocket server. We must continue to support this behavior
|
|
677
646
|
// in case the user is using a wallet that has not updated their walletlib implementation.
|
|
678
647
|
const { associationKeypair } = state;
|
|
679
|
-
const ecdhKeypair =
|
|
680
|
-
socket.send(
|
|
648
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
649
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
681
650
|
state = {
|
|
682
651
|
__type: 'hello_req_sent',
|
|
683
652
|
associationPublicKey: associationKeypair.publicKey,
|
|
684
653
|
ecdhPrivateKey: ecdhKeypair.privateKey,
|
|
685
654
|
};
|
|
686
|
-
}
|
|
655
|
+
};
|
|
687
656
|
const handleClose = (evt) => {
|
|
688
657
|
if (evt.wasClean) {
|
|
689
658
|
state = { __type: 'disconnected' };
|
|
@@ -693,28 +662,28 @@ function transact(callback, config) {
|
|
|
693
662
|
}
|
|
694
663
|
disposeSocket();
|
|
695
664
|
};
|
|
696
|
-
const handleError = (_evt) =>
|
|
665
|
+
const handleError = async (_evt) => {
|
|
697
666
|
disposeSocket();
|
|
698
667
|
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
699
668
|
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
|
|
700
669
|
}
|
|
701
670
|
else {
|
|
702
|
-
|
|
671
|
+
await new Promise((resolve) => {
|
|
703
672
|
const retryDelayMs = getNextRetryDelayMs();
|
|
704
673
|
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
705
674
|
});
|
|
706
675
|
attemptSocketConnection();
|
|
707
676
|
}
|
|
708
|
-
}
|
|
709
|
-
const handleMessage = (evt) =>
|
|
710
|
-
const responseBuffer =
|
|
677
|
+
};
|
|
678
|
+
const handleMessage = async (evt) => {
|
|
679
|
+
const responseBuffer = await evt.data.arrayBuffer();
|
|
711
680
|
switch (state.__type) {
|
|
712
681
|
case 'connecting':
|
|
713
682
|
if (responseBuffer.byteLength !== 0) {
|
|
714
683
|
throw new Error('Encountered unexpected message while connecting');
|
|
715
684
|
}
|
|
716
|
-
const ecdhKeypair =
|
|
717
|
-
socket.send(
|
|
685
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
686
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
718
687
|
state = {
|
|
719
688
|
__type: 'hello_req_sent',
|
|
720
689
|
associationPublicKey: associationKeypair.publicKey,
|
|
@@ -729,7 +698,7 @@ function transact(callback, config) {
|
|
|
729
698
|
throw new Error('Encrypted message has invalid sequence number');
|
|
730
699
|
}
|
|
731
700
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
732
|
-
const jsonRpcMessage =
|
|
701
|
+
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
733
702
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
734
703
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
735
704
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
@@ -748,8 +717,8 @@ function transact(callback, config) {
|
|
|
748
717
|
case 'hello_req_sent': {
|
|
749
718
|
// if we receive an APP_PING message (empty message), resend the HELLO_REQ (see above)
|
|
750
719
|
if (responseBuffer.byteLength === 0) {
|
|
751
|
-
const ecdhKeypair =
|
|
752
|
-
socket.send(
|
|
720
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
721
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
753
722
|
state = {
|
|
754
723
|
__type: 'hello_req_sent',
|
|
755
724
|
associationPublicKey: associationKeypair.publicKey,
|
|
@@ -757,10 +726,10 @@ function transact(callback, config) {
|
|
|
757
726
|
};
|
|
758
727
|
break;
|
|
759
728
|
}
|
|
760
|
-
const sharedSecret =
|
|
729
|
+
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
|
|
761
730
|
const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
|
|
762
731
|
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
|
|
763
|
-
?
|
|
732
|
+
? await (async () => {
|
|
764
733
|
const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
765
734
|
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
766
735
|
if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
|
|
@@ -768,15 +737,15 @@ function transact(callback, config) {
|
|
|
768
737
|
}
|
|
769
738
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
770
739
|
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
771
|
-
})
|
|
740
|
+
})() : { protocol_version: 'legacy' };
|
|
772
741
|
state = { __type: 'connected', sharedSecret, sessionProperties };
|
|
773
|
-
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) =>
|
|
742
|
+
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
|
|
774
743
|
const id = nextJsonRpcMessageId++;
|
|
775
|
-
socket.send(
|
|
744
|
+
socket.send(await encryptJsonRpcMessage({
|
|
776
745
|
id,
|
|
777
746
|
jsonrpc: '2.0',
|
|
778
747
|
method,
|
|
779
|
-
params: params
|
|
748
|
+
params: params ?? {},
|
|
780
749
|
}, sharedSecret));
|
|
781
750
|
return new Promise((resolve, reject) => {
|
|
782
751
|
jsonRpcResponsePromises[id] = {
|
|
@@ -802,21 +771,25 @@ function transact(callback, config) {
|
|
|
802
771
|
reject,
|
|
803
772
|
};
|
|
804
773
|
});
|
|
805
|
-
})
|
|
774
|
+
});
|
|
775
|
+
sessionEstablished = true;
|
|
806
776
|
try {
|
|
807
|
-
resolve(
|
|
777
|
+
resolve(wallet);
|
|
808
778
|
}
|
|
809
779
|
catch (e) {
|
|
810
780
|
reject(e);
|
|
811
781
|
}
|
|
812
|
-
finally {
|
|
813
|
-
disposeSocket();
|
|
814
|
-
socket.close();
|
|
815
|
-
}
|
|
816
782
|
break;
|
|
817
783
|
}
|
|
818
784
|
}
|
|
819
|
-
}
|
|
785
|
+
};
|
|
786
|
+
handleForceClose = () => {
|
|
787
|
+
socket.removeEventListener('message', handleMessage);
|
|
788
|
+
disposeSocket();
|
|
789
|
+
if (!sessionEstablished) {
|
|
790
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent('socket was closed before connection') }));
|
|
791
|
+
}
|
|
792
|
+
};
|
|
820
793
|
let disposeSocket;
|
|
821
794
|
let retryWaitTimeoutId;
|
|
822
795
|
const attemptSocketConnection = () => {
|
|
@@ -841,244 +814,241 @@ function transact(callback, config) {
|
|
|
841
814
|
};
|
|
842
815
|
};
|
|
843
816
|
attemptSocketConnection();
|
|
844
|
-
});
|
|
845
|
-
});
|
|
817
|
+
}) };
|
|
846
818
|
}
|
|
847
|
-
function startRemoteScenario(config) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
819
|
+
async function startRemoteScenario(config) {
|
|
820
|
+
assertSecureContext();
|
|
821
|
+
const associationKeypair = await generateAssociationKeypair();
|
|
822
|
+
const websocketURL = `wss://${config?.remoteHostAuthority}/reflect`;
|
|
823
|
+
let connectionStartTime;
|
|
824
|
+
const getNextRetryDelayMs = (() => {
|
|
825
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
826
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
827
|
+
})();
|
|
828
|
+
let nextJsonRpcMessageId = 1;
|
|
829
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
830
|
+
let encoding;
|
|
831
|
+
let state = { __type: 'disconnected' };
|
|
832
|
+
let socket;
|
|
833
|
+
let disposeSocket;
|
|
834
|
+
let decodeBytes = async (evt) => {
|
|
835
|
+
if (encoding == 'base64') { // base64 encoding
|
|
836
|
+
const message = await evt.data;
|
|
837
|
+
return toUint8Array(message).buffer;
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
return await evt.data.arrayBuffer();
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
// Reflector Connection Phase
|
|
844
|
+
// here we connect to the reflector and wait for the REFLECTOR_ID message
|
|
845
|
+
// so we build the association URL and return that back to the caller
|
|
846
|
+
const associationUrl = await new Promise((resolve, reject) => {
|
|
847
|
+
const handleOpen = async () => {
|
|
848
|
+
if (state.__type !== 'connecting') {
|
|
849
|
+
console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
|
|
850
|
+
`Got \`${state.__type}\`.`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (socket.protocol.includes(WEBSOCKET_PROTOCOL_BASE64)) {
|
|
854
|
+
encoding = 'base64';
|
|
867
855
|
}
|
|
868
856
|
else {
|
|
869
|
-
|
|
857
|
+
encoding = 'binary';
|
|
870
858
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
disposeSocket();
|
|
901
|
-
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
902
|
-
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
|
|
903
|
-
}
|
|
904
|
-
else {
|
|
905
|
-
yield new Promise((resolve) => {
|
|
906
|
-
const retryDelayMs = getNextRetryDelayMs();
|
|
907
|
-
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
908
|
-
});
|
|
909
|
-
attemptSocketConnection();
|
|
910
|
-
}
|
|
911
|
-
});
|
|
912
|
-
const handleReflectorIdMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
913
|
-
const responseBuffer = yield decodeBytes(evt);
|
|
914
|
-
if (state.__type === 'connecting') {
|
|
915
|
-
if (responseBuffer.byteLength == 0) {
|
|
916
|
-
throw new Error('Encountered unexpected message while connecting');
|
|
917
|
-
}
|
|
918
|
-
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
|
|
919
|
-
state = {
|
|
920
|
-
__type: 'reflector_id_received',
|
|
921
|
-
reflectorId: reflectorId
|
|
922
|
-
};
|
|
923
|
-
const associationUrl = yield getRemoteAssociateAndroidIntentURL(associationKeypair.publicKey, config.remoteHostAuthority, reflectorId, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
924
|
-
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
925
|
-
resolve(associationUrl);
|
|
926
|
-
}
|
|
927
|
-
});
|
|
928
|
-
let retryWaitTimeoutId;
|
|
929
|
-
const attemptSocketConnection = () => {
|
|
930
|
-
if (disposeSocket) {
|
|
931
|
-
disposeSocket();
|
|
932
|
-
}
|
|
933
|
-
state = { __type: 'connecting', associationKeypair };
|
|
934
|
-
if (connectionStartTime === undefined) {
|
|
935
|
-
connectionStartTime = Date.now();
|
|
859
|
+
socket.removeEventListener('open', handleOpen);
|
|
860
|
+
};
|
|
861
|
+
const handleClose = (evt) => {
|
|
862
|
+
if (evt.wasClean) {
|
|
863
|
+
state = { __type: 'disconnected' };
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
867
|
+
}
|
|
868
|
+
disposeSocket();
|
|
869
|
+
};
|
|
870
|
+
const handleError = async (_evt) => {
|
|
871
|
+
disposeSocket();
|
|
872
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
873
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
await new Promise((resolve) => {
|
|
877
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
878
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
879
|
+
});
|
|
880
|
+
attemptSocketConnection();
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
const handleReflectorIdMessage = async (evt) => {
|
|
884
|
+
const responseBuffer = await decodeBytes(evt);
|
|
885
|
+
if (state.__type === 'connecting') {
|
|
886
|
+
if (responseBuffer.byteLength == 0) {
|
|
887
|
+
throw new Error('Encountered unexpected message while connecting');
|
|
936
888
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
socket.addEventListener('message', handleReflectorIdMessage);
|
|
942
|
-
disposeSocket = () => {
|
|
943
|
-
window.clearTimeout(retryWaitTimeoutId);
|
|
944
|
-
socket.removeEventListener('open', handleOpen);
|
|
945
|
-
socket.removeEventListener('close', handleClose);
|
|
946
|
-
socket.removeEventListener('error', handleError);
|
|
947
|
-
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
889
|
+
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
|
|
890
|
+
state = {
|
|
891
|
+
__type: 'reflector_id_received',
|
|
892
|
+
reflectorId: reflectorId
|
|
948
893
|
};
|
|
894
|
+
const associationUrl = await getRemoteAssociateAndroidIntentURL(associationKeypair.publicKey, config.remoteHostAuthority, reflectorId, config?.baseUri);
|
|
895
|
+
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
896
|
+
resolve(associationUrl);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
let retryWaitTimeoutId;
|
|
900
|
+
const attemptSocketConnection = () => {
|
|
901
|
+
if (disposeSocket) {
|
|
902
|
+
disposeSocket();
|
|
903
|
+
}
|
|
904
|
+
state = { __type: 'connecting', associationKeypair };
|
|
905
|
+
if (connectionStartTime === undefined) {
|
|
906
|
+
connectionStartTime = Date.now();
|
|
907
|
+
}
|
|
908
|
+
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL_BINARY, WEBSOCKET_PROTOCOL_BASE64]);
|
|
909
|
+
socket.addEventListener('open', handleOpen);
|
|
910
|
+
socket.addEventListener('close', handleClose);
|
|
911
|
+
socket.addEventListener('error', handleError);
|
|
912
|
+
socket.addEventListener('message', handleReflectorIdMessage);
|
|
913
|
+
disposeSocket = () => {
|
|
914
|
+
window.clearTimeout(retryWaitTimeoutId);
|
|
915
|
+
socket.removeEventListener('open', handleOpen);
|
|
916
|
+
socket.removeEventListener('close', handleClose);
|
|
917
|
+
socket.removeEventListener('error', handleError);
|
|
918
|
+
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
949
919
|
};
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
920
|
+
};
|
|
921
|
+
attemptSocketConnection();
|
|
922
|
+
});
|
|
923
|
+
// Wallet Connection Phase
|
|
924
|
+
// here we return the association URL (containing the reflector ID) to the caller +
|
|
925
|
+
// a promise that will resolve the MobileWallet object once the wallet connects.
|
|
926
|
+
let sessionEstablished = false;
|
|
927
|
+
let handleClose;
|
|
928
|
+
return { associationUrl, close: () => {
|
|
929
|
+
socket.close();
|
|
930
|
+
handleClose();
|
|
931
|
+
}, wallet: new Promise((resolve, reject) => {
|
|
932
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
933
|
+
const jsonRpcResponsePromises = {};
|
|
934
|
+
const handleMessage = async (evt) => {
|
|
935
|
+
const responseBuffer = await decodeBytes(evt);
|
|
936
|
+
switch (state.__type) {
|
|
937
|
+
case 'reflector_id_received':
|
|
938
|
+
if (responseBuffer.byteLength !== 0) {
|
|
939
|
+
throw new Error('Encountered unexpected message while awaiting reflection');
|
|
940
|
+
}
|
|
941
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
942
|
+
const binaryMsg = await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey);
|
|
943
|
+
if (encoding == 'base64') {
|
|
944
|
+
socket.send(fromUint8Array$1(binaryMsg));
|
|
945
|
+
}
|
|
946
|
+
else {
|
|
947
|
+
socket.send(binaryMsg);
|
|
948
|
+
}
|
|
949
|
+
state = {
|
|
950
|
+
__type: 'hello_req_sent',
|
|
951
|
+
associationPublicKey: associationKeypair.publicKey,
|
|
952
|
+
ecdhPrivateKey: ecdhKeypair.privateKey,
|
|
953
|
+
};
|
|
954
|
+
break;
|
|
955
|
+
case 'connected':
|
|
956
|
+
try {
|
|
957
|
+
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
958
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
959
|
+
if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
|
|
960
|
+
throw new Error('Encrypted message has invalid sequence number');
|
|
969
961
|
}
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
|
|
962
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
963
|
+
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
964
|
+
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
965
|
+
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
966
|
+
responsePromise.resolve(jsonRpcMessage.result);
|
|
967
|
+
}
|
|
968
|
+
catch (e) {
|
|
969
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
970
|
+
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
971
|
+
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
972
|
+
responsePromise.reject(e);
|
|
974
973
|
}
|
|
975
974
|
else {
|
|
976
|
-
|
|
975
|
+
throw e;
|
|
977
976
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
977
|
+
}
|
|
978
|
+
break;
|
|
979
|
+
case 'hello_req_sent': {
|
|
980
|
+
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
|
|
981
|
+
const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
|
|
982
|
+
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
|
|
983
|
+
? await (async () => {
|
|
984
|
+
const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
987
985
|
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
988
986
|
if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
|
|
989
987
|
throw new Error('Encrypted message has invalid sequence number');
|
|
990
988
|
}
|
|
991
989
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
990
|
+
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
991
|
+
})() : { protocol_version: 'legacy' };
|
|
992
|
+
state = { __type: 'connected', sharedSecret, sessionProperties };
|
|
993
|
+
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
|
|
994
|
+
const id = nextJsonRpcMessageId++;
|
|
995
|
+
const binaryMsg = await encryptJsonRpcMessage({
|
|
996
|
+
id,
|
|
997
|
+
jsonrpc: '2.0',
|
|
998
|
+
method,
|
|
999
|
+
params: params ?? {},
|
|
1000
|
+
}, sharedSecret);
|
|
1001
|
+
if (encoding == 'base64') {
|
|
1002
|
+
socket.send(fromUint8Array$1(binaryMsg));
|
|
996
1003
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
1000
|
-
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
1001
|
-
responsePromise.reject(e);
|
|
1002
|
-
}
|
|
1003
|
-
else {
|
|
1004
|
-
throw e;
|
|
1005
|
-
}
|
|
1004
|
+
else {
|
|
1005
|
+
socket.send(binaryMsg);
|
|
1006
1006
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
state = { __type: 'connected', sharedSecret, sessionProperties };
|
|
1022
|
-
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) => __awaiter(this, void 0, void 0, function* () {
|
|
1023
|
-
const id = nextJsonRpcMessageId++;
|
|
1024
|
-
const binaryMsg = yield encryptJsonRpcMessage({
|
|
1025
|
-
id,
|
|
1026
|
-
jsonrpc: '2.0',
|
|
1027
|
-
method,
|
|
1028
|
-
params: params !== null && params !== void 0 ? params : {},
|
|
1029
|
-
}, sharedSecret);
|
|
1030
|
-
if (encoding == 'base64') {
|
|
1031
|
-
socket.send(fromUint8Array$1(binaryMsg));
|
|
1032
|
-
}
|
|
1033
|
-
else {
|
|
1034
|
-
socket.send(binaryMsg);
|
|
1035
|
-
}
|
|
1036
|
-
return new Promise((resolve, reject) => {
|
|
1037
|
-
jsonRpcResponsePromises[id] = {
|
|
1038
|
-
resolve(result) {
|
|
1039
|
-
switch (method) {
|
|
1040
|
-
case 'authorize':
|
|
1041
|
-
case 'reauthorize': {
|
|
1042
|
-
const { wallet_uri_base } = result;
|
|
1043
|
-
if (wallet_uri_base != null) {
|
|
1044
|
-
try {
|
|
1045
|
-
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
1046
|
-
}
|
|
1047
|
-
catch (e) {
|
|
1048
|
-
reject(e);
|
|
1049
|
-
return;
|
|
1050
|
-
}
|
|
1007
|
+
return new Promise((resolve, reject) => {
|
|
1008
|
+
jsonRpcResponsePromises[id] = {
|
|
1009
|
+
resolve(result) {
|
|
1010
|
+
switch (method) {
|
|
1011
|
+
case 'authorize':
|
|
1012
|
+
case 'reauthorize': {
|
|
1013
|
+
const { wallet_uri_base } = result;
|
|
1014
|
+
if (wallet_uri_base != null) {
|
|
1015
|
+
try {
|
|
1016
|
+
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
1017
|
+
}
|
|
1018
|
+
catch (e) {
|
|
1019
|
+
reject(e);
|
|
1020
|
+
return;
|
|
1051
1021
|
}
|
|
1052
|
-
break;
|
|
1053
1022
|
}
|
|
1023
|
+
break;
|
|
1054
1024
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
}
|
|
1060
|
-
})
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
catch (e) {
|
|
1066
|
-
reject(e);
|
|
1067
|
-
}
|
|
1068
|
-
break;
|
|
1025
|
+
}
|
|
1026
|
+
resolve(result);
|
|
1027
|
+
},
|
|
1028
|
+
reject,
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
sessionEstablished = true;
|
|
1033
|
+
try {
|
|
1034
|
+
resolve(wallet);
|
|
1069
1035
|
}
|
|
1036
|
+
catch (e) {
|
|
1037
|
+
reject(e);
|
|
1038
|
+
}
|
|
1039
|
+
break;
|
|
1070
1040
|
}
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
socket.addEventListener('message', handleMessage);
|
|
1044
|
+
handleClose = () => {
|
|
1045
|
+
socket.removeEventListener('message', handleMessage);
|
|
1046
|
+
disposeSocket();
|
|
1047
|
+
if (!sessionEstablished) {
|
|
1048
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session was closed before connection.`, { closeEvent: new CloseEvent('socket was closed before connection') }));
|
|
1049
|
+
}
|
|
1050
|
+
};
|
|
1051
|
+
}) };
|
|
1082
1052
|
}
|
|
1083
1053
|
|
|
1084
1054
|
exports.SolanaCloneAuthorization = SolanaCloneAuthorization;
|
|
@@ -1089,4 +1059,5 @@ exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterPr
|
|
|
1089
1059
|
exports.SolanaSignInWithSolana = SolanaSignInWithSolana;
|
|
1090
1060
|
exports.SolanaSignTransactions = SolanaSignTransactions;
|
|
1091
1061
|
exports.startRemoteScenario = startRemoteScenario;
|
|
1062
|
+
exports.startScenario = startScenario;
|
|
1092
1063
|
exports.transact = transact;
|