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