@solana-mobile/mobile-wallet-adapter-protocol 2.2.4 → 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 +465 -477
- package/lib/cjs/index.js +465 -477
- package/lib/cjs/index.native.js +80 -83
- package/lib/esm/index.browser.js +465 -476
- package/lib/esm/index.js +465 -476
- package/lib/types/index.browser.d.ts +9 -2
- package/lib/types/index.browser.d.ts.map +1 -1
- package/lib/types/index.d.ts +9 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.native.d.ts +9 -2
- package/lib/types/index.native.d.ts.map +1 -1
- package/package.json +74 -74
package/lib/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSignInMessageText } from '@solana/wallet-standard-util';
|
|
2
|
+
import { getBase58Decoder } from '@solana/codecs-strings';
|
|
2
3
|
|
|
3
4
|
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
4
5
|
const SolanaMobileWalletAdapterErrorCode = {
|
|
@@ -11,8 +12,12 @@ const SolanaMobileWalletAdapterErrorCode = {
|
|
|
11
12
|
ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
|
|
12
13
|
ERROR_INVALID_PROTOCOL_VERSION: 'ERROR_INVALID_PROTOCOL_VERSION',
|
|
13
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',
|
|
14
17
|
};
|
|
15
18
|
class SolanaMobileWalletAdapterError extends Error {
|
|
19
|
+
data;
|
|
20
|
+
code;
|
|
16
21
|
constructor(...args) {
|
|
17
22
|
const [code, message, data] = args;
|
|
18
23
|
super(message);
|
|
@@ -32,6 +37,9 @@ const SolanaMobileWalletAdapterProtocolErrorCode = {
|
|
|
32
37
|
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
33
38
|
};
|
|
34
39
|
class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
40
|
+
data;
|
|
41
|
+
code;
|
|
42
|
+
jsonRpcMessageId;
|
|
35
43
|
constructor(...args) {
|
|
36
44
|
const [jsonRpcMessageId, code, message, data] = args;
|
|
37
45
|
super(message);
|
|
@@ -42,35 +50,10 @@ class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
45
|
-
/******************************************************************************
|
|
46
|
-
Copyright (c) Microsoft Corporation.
|
|
47
|
-
|
|
48
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
49
|
-
purpose with or without fee is hereby granted.
|
|
50
|
-
|
|
51
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
52
|
-
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
53
|
-
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
54
|
-
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
55
|
-
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
56
|
-
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
57
|
-
PERFORMANCE OF THIS SOFTWARE.
|
|
58
|
-
***************************************************************************** */
|
|
59
|
-
|
|
60
|
-
function __awaiter(thisArg, _arguments, P, generator) {
|
|
61
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
62
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
63
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
64
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
65
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
66
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
53
|
function encode(input) {
|
|
71
54
|
return window.btoa(input);
|
|
72
55
|
}
|
|
73
|
-
function fromUint8Array(byteArray, urlsafe) {
|
|
56
|
+
function fromUint8Array$1(byteArray, urlsafe) {
|
|
74
57
|
const base64 = window.btoa(String.fromCharCode.call(null, ...byteArray));
|
|
75
58
|
if (urlsafe) {
|
|
76
59
|
return base64
|
|
@@ -88,22 +71,23 @@ function toUint8Array(base64EncodedByteArray) {
|
|
|
88
71
|
.map((c) => c.charCodeAt(0)));
|
|
89
72
|
}
|
|
90
73
|
|
|
91
|
-
function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return response;
|
|
99
|
-
});
|
|
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;
|
|
100
81
|
}
|
|
101
82
|
|
|
102
83
|
function createSIWSMessage(payload) {
|
|
103
84
|
return createSignInMessageText(payload);
|
|
104
85
|
}
|
|
105
|
-
function
|
|
106
|
-
return encode(createSIWSMessage(payload))
|
|
86
|
+
function createSIWSMessageBase64Url(payload) {
|
|
87
|
+
return encode(createSIWSMessage(payload))
|
|
88
|
+
.replace(/\+/g, '-')
|
|
89
|
+
.replace(/\//g, '_')
|
|
90
|
+
.replace(/=+$/, ''); // convert to base64url encoding;
|
|
107
91
|
}
|
|
108
92
|
|
|
109
93
|
// optional features
|
|
@@ -111,6 +95,13 @@ const SolanaSignTransactions = 'solana:signTransactions';
|
|
|
111
95
|
const SolanaCloneAuthorization = 'solana:cloneAuthorization';
|
|
112
96
|
const SolanaSignInWithSolana = 'solana:signInWithSolana';
|
|
113
97
|
|
|
98
|
+
function fromUint8Array(byteArray) {
|
|
99
|
+
return getBase58Decoder().decode(byteArray);
|
|
100
|
+
}
|
|
101
|
+
function base64ToBase58(base64EncodedString) {
|
|
102
|
+
return fromUint8Array(toUint8Array(base64EncodedString));
|
|
103
|
+
}
|
|
104
|
+
|
|
114
105
|
/**
|
|
115
106
|
* Creates a {@link MobileWallet} proxy that handles backwards compatibility and API to RPC conversion.
|
|
116
107
|
*
|
|
@@ -129,16 +120,14 @@ function createMobileWalletProxy(protocolVersion, protocolRequestHandler) {
|
|
|
129
120
|
return null;
|
|
130
121
|
}
|
|
131
122
|
if (target[p] == null) {
|
|
132
|
-
target[p] = function (inputParams) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return handleMobileWalletResponse(p, result, protocolVersion);
|
|
141
|
-
});
|
|
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);
|
|
142
131
|
};
|
|
143
132
|
}
|
|
144
133
|
return target[p];
|
|
@@ -242,33 +231,43 @@ function handleMobileWalletResponse(method, response, protocolVersion) {
|
|
|
242
231
|
if (capabilities.supports_clone_authorization === true) {
|
|
243
232
|
features.push(SolanaCloneAuthorization);
|
|
244
233
|
}
|
|
245
|
-
return
|
|
234
|
+
return {
|
|
235
|
+
...capabilities,
|
|
236
|
+
features: features,
|
|
237
|
+
};
|
|
246
238
|
}
|
|
247
239
|
case 'v1': {
|
|
248
|
-
return
|
|
240
|
+
return {
|
|
241
|
+
...capabilities,
|
|
242
|
+
supports_sign_and_send_transactions: true,
|
|
243
|
+
supports_clone_authorization: capabilities.features.includes(SolanaCloneAuthorization)
|
|
244
|
+
};
|
|
249
245
|
}
|
|
250
246
|
}
|
|
251
247
|
}
|
|
252
248
|
}
|
|
253
249
|
return response;
|
|
254
250
|
}
|
|
255
|
-
function signInFallback(signInPayload, authorizationResult, protocolRequestHandler) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
addresses: [address],
|
|
263
|
-
payloads: [siwsMessage]
|
|
264
|
-
});
|
|
265
|
-
const signInResult = {
|
|
266
|
-
address: address,
|
|
267
|
-
signed_message: siwsMessage,
|
|
268
|
-
signature: signMessageResult.signed_payloads[0].slice(siwsMessage.length)
|
|
269
|
-
};
|
|
270
|
-
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]
|
|
271
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;
|
|
272
271
|
}
|
|
273
272
|
|
|
274
273
|
const SEQUENCE_NUMBER_BYTES = 4;
|
|
@@ -284,28 +283,24 @@ function createSequenceNumberVector(sequenceNumber) {
|
|
|
284
283
|
|
|
285
284
|
const INITIALIZATION_VECTOR_BYTES = 12;
|
|
286
285
|
const ENCODED_PUBLIC_KEY_LENGTH_BYTES = 65;
|
|
287
|
-
function encryptMessage(plaintext, sequenceNumber, sharedSecret) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return response;
|
|
298
|
-
});
|
|
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;
|
|
299
296
|
}
|
|
300
|
-
function decryptMessage(message, sharedSecret) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
return plaintext;
|
|
308
|
-
});
|
|
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;
|
|
309
304
|
}
|
|
310
305
|
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
311
306
|
return {
|
|
@@ -323,22 +318,18 @@ function getUtf8Decoder() {
|
|
|
323
318
|
return _utf8Decoder;
|
|
324
319
|
}
|
|
325
320
|
|
|
326
|
-
function generateAssociationKeypair() {
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}, false /* extractable */, ['sign'] /* keyUsages */);
|
|
332
|
-
});
|
|
321
|
+
async function generateAssociationKeypair() {
|
|
322
|
+
return await crypto.subtle.generateKey({
|
|
323
|
+
name: 'ECDSA',
|
|
324
|
+
namedCurve: 'P-256',
|
|
325
|
+
}, false /* extractable */, ['sign'] /* keyUsages */);
|
|
333
326
|
}
|
|
334
327
|
|
|
335
|
-
function generateECDHKeypair() {
|
|
336
|
-
return
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
|
|
341
|
-
});
|
|
328
|
+
async function generateECDHKeypair() {
|
|
329
|
+
return await crypto.subtle.generateKey({
|
|
330
|
+
name: 'ECDH',
|
|
331
|
+
namedCurve: 'P-256',
|
|
332
|
+
}, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
|
|
342
333
|
}
|
|
343
334
|
|
|
344
335
|
// https://stackoverflow.com/a/9458996/802047
|
|
@@ -384,12 +375,12 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
384
375
|
try {
|
|
385
376
|
baseUrl = new URL(intentUrlBase);
|
|
386
377
|
}
|
|
387
|
-
catch
|
|
388
|
-
if (
|
|
378
|
+
catch { } // eslint-disable-line no-empty
|
|
379
|
+
if (baseUrl?.protocol !== 'https:') {
|
|
389
380
|
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
390
381
|
}
|
|
391
382
|
}
|
|
392
|
-
baseUrl
|
|
383
|
+
baseUrl ||= new URL(`${INTENT_NAME}:/`);
|
|
393
384
|
const pathname = methodPathname.startsWith('/')
|
|
394
385
|
? // Method is an absolute path. Replace it wholesale.
|
|
395
386
|
methodPathname
|
|
@@ -397,94 +388,82 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
397
388
|
[...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join('/');
|
|
398
389
|
return new URL(pathname, baseUrl);
|
|
399
390
|
}
|
|
400
|
-
function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase, protocolVersions = ['v1']) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
url.searchParams.set('v', version);
|
|
410
|
-
});
|
|
411
|
-
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);
|
|
412
400
|
});
|
|
401
|
+
return url;
|
|
413
402
|
}
|
|
414
|
-
function getRemoteAssociateAndroidIntentURL(associationPublicKey, hostAuthority, reflectorId, associationURLBase, protocolVersions = ['v1']) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
url.searchParams.set('v', version);
|
|
424
|
-
});
|
|
425
|
-
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);
|
|
426
412
|
});
|
|
413
|
+
return url;
|
|
427
414
|
}
|
|
428
415
|
|
|
429
|
-
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
|
|
434
|
-
});
|
|
416
|
+
async function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
417
|
+
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
418
|
+
const sequenceNumber = jsonRpcMessage.id;
|
|
419
|
+
return encryptMessage(plaintext, sequenceNumber, sharedSecret);
|
|
435
420
|
}
|
|
436
|
-
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
return jsonRpcMessage;
|
|
444
|
-
});
|
|
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;
|
|
445
428
|
}
|
|
446
429
|
|
|
447
|
-
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.
|
|
448
431
|
associationPublicKey, ecdhPrivateKey) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return aesKeyMaterialVal;
|
|
463
|
-
});
|
|
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;
|
|
464
445
|
}
|
|
465
446
|
|
|
466
|
-
function parseSessionProps(message, sharedSecret) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_INVALID_PROTOCOL_VERSION, `Unknown/unsupported protocol version: ${jsonProperties.v}`);
|
|
483
|
-
}
|
|
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}`);
|
|
484
463
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
464
|
+
}
|
|
465
|
+
return ({
|
|
466
|
+
protocol_version: protocolVersion
|
|
488
467
|
});
|
|
489
468
|
}
|
|
490
469
|
|
|
@@ -529,47 +508,43 @@ function launchUrlThroughHiddenFrame(url) {
|
|
|
529
508
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
530
509
|
_frame.contentWindow.location.href = url.toString();
|
|
531
510
|
}
|
|
532
|
-
function launchAssociation(associationUrl) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
break;
|
|
555
|
-
}
|
|
556
|
-
default:
|
|
557
|
-
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;
|
|
558
533
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
534
|
+
default:
|
|
535
|
+
assertUnreachable(browser);
|
|
562
536
|
}
|
|
563
537
|
}
|
|
564
|
-
|
|
538
|
+
catch (e) {
|
|
539
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
540
|
+
}
|
|
541
|
+
}
|
|
565
542
|
}
|
|
566
|
-
function startSession(associationPublicKey, associationURLBase) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return randomAssociationPort;
|
|
572
|
-
});
|
|
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;
|
|
573
548
|
}
|
|
574
549
|
|
|
575
550
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
@@ -598,7 +573,7 @@ function assertSecureEndpointSpecificURI(walletUriBase) {
|
|
|
598
573
|
try {
|
|
599
574
|
url = new URL(walletUriBase);
|
|
600
575
|
}
|
|
601
|
-
catch
|
|
576
|
+
catch {
|
|
602
577
|
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Invalid base URL supplied by wallet');
|
|
603
578
|
}
|
|
604
579
|
if (url.protocol !== 'https:') {
|
|
@@ -623,25 +598,38 @@ function getReflectorIdFromByteArray(byteArray) {
|
|
|
623
598
|
let { value: length, offset } = decodeVarLong(byteArray);
|
|
624
599
|
return new Uint8Array(byteArray.slice(offset, offset + length));
|
|
625
600
|
}
|
|
626
|
-
function transact(callback, config) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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) => {
|
|
642
630
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
643
631
|
const jsonRpcResponsePromises = {};
|
|
644
|
-
const handleOpen = () =>
|
|
632
|
+
const handleOpen = async () => {
|
|
645
633
|
if (state.__type !== 'connecting') {
|
|
646
634
|
console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
|
|
647
635
|
`Got \`${state.__type}\`.`);
|
|
@@ -655,14 +643,14 @@ function transact(callback, config) {
|
|
|
655
643
|
// APP_PING was sent by the wallet/websocket server. We must continue to support this behavior
|
|
656
644
|
// in case the user is using a wallet that has not updated their walletlib implementation.
|
|
657
645
|
const { associationKeypair } = state;
|
|
658
|
-
const ecdhKeypair =
|
|
659
|
-
socket.send(
|
|
646
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
647
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
660
648
|
state = {
|
|
661
649
|
__type: 'hello_req_sent',
|
|
662
650
|
associationPublicKey: associationKeypair.publicKey,
|
|
663
651
|
ecdhPrivateKey: ecdhKeypair.privateKey,
|
|
664
652
|
};
|
|
665
|
-
}
|
|
653
|
+
};
|
|
666
654
|
const handleClose = (evt) => {
|
|
667
655
|
if (evt.wasClean) {
|
|
668
656
|
state = { __type: 'disconnected' };
|
|
@@ -672,28 +660,28 @@ function transact(callback, config) {
|
|
|
672
660
|
}
|
|
673
661
|
disposeSocket();
|
|
674
662
|
};
|
|
675
|
-
const handleError = (_evt) =>
|
|
663
|
+
const handleError = async (_evt) => {
|
|
676
664
|
disposeSocket();
|
|
677
665
|
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
678
666
|
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
|
|
679
667
|
}
|
|
680
668
|
else {
|
|
681
|
-
|
|
669
|
+
await new Promise((resolve) => {
|
|
682
670
|
const retryDelayMs = getNextRetryDelayMs();
|
|
683
671
|
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
684
672
|
});
|
|
685
673
|
attemptSocketConnection();
|
|
686
674
|
}
|
|
687
|
-
}
|
|
688
|
-
const handleMessage = (evt) =>
|
|
689
|
-
const responseBuffer =
|
|
675
|
+
};
|
|
676
|
+
const handleMessage = async (evt) => {
|
|
677
|
+
const responseBuffer = await evt.data.arrayBuffer();
|
|
690
678
|
switch (state.__type) {
|
|
691
679
|
case 'connecting':
|
|
692
680
|
if (responseBuffer.byteLength !== 0) {
|
|
693
681
|
throw new Error('Encountered unexpected message while connecting');
|
|
694
682
|
}
|
|
695
|
-
const ecdhKeypair =
|
|
696
|
-
socket.send(
|
|
683
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
684
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
697
685
|
state = {
|
|
698
686
|
__type: 'hello_req_sent',
|
|
699
687
|
associationPublicKey: associationKeypair.publicKey,
|
|
@@ -708,7 +696,7 @@ function transact(callback, config) {
|
|
|
708
696
|
throw new Error('Encrypted message has invalid sequence number');
|
|
709
697
|
}
|
|
710
698
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
711
|
-
const jsonRpcMessage =
|
|
699
|
+
const jsonRpcMessage = await decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
712
700
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
713
701
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
714
702
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
@@ -727,8 +715,8 @@ function transact(callback, config) {
|
|
|
727
715
|
case 'hello_req_sent': {
|
|
728
716
|
// if we receive an APP_PING message (empty message), resend the HELLO_REQ (see above)
|
|
729
717
|
if (responseBuffer.byteLength === 0) {
|
|
730
|
-
const ecdhKeypair =
|
|
731
|
-
socket.send(
|
|
718
|
+
const ecdhKeypair = await generateECDHKeypair();
|
|
719
|
+
socket.send(await createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
732
720
|
state = {
|
|
733
721
|
__type: 'hello_req_sent',
|
|
734
722
|
associationPublicKey: associationKeypair.publicKey,
|
|
@@ -736,10 +724,10 @@ function transact(callback, config) {
|
|
|
736
724
|
};
|
|
737
725
|
break;
|
|
738
726
|
}
|
|
739
|
-
const sharedSecret =
|
|
727
|
+
const sharedSecret = await parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
|
|
740
728
|
const sessionPropertiesBuffer = responseBuffer.slice(ENCODED_PUBLIC_KEY_LENGTH_BYTES);
|
|
741
729
|
const sessionProperties = sessionPropertiesBuffer.byteLength !== 0
|
|
742
|
-
?
|
|
730
|
+
? await (async () => {
|
|
743
731
|
const sequenceNumberVector = sessionPropertiesBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
744
732
|
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
745
733
|
if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
|
|
@@ -747,15 +735,15 @@ function transact(callback, config) {
|
|
|
747
735
|
}
|
|
748
736
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
749
737
|
return parseSessionProps(sessionPropertiesBuffer, sharedSecret);
|
|
750
|
-
})
|
|
738
|
+
})() : { protocol_version: 'legacy' };
|
|
751
739
|
state = { __type: 'connected', sharedSecret, sessionProperties };
|
|
752
|
-
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) =>
|
|
740
|
+
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
|
|
753
741
|
const id = nextJsonRpcMessageId++;
|
|
754
|
-
socket.send(
|
|
742
|
+
socket.send(await encryptJsonRpcMessage({
|
|
755
743
|
id,
|
|
756
744
|
jsonrpc: '2.0',
|
|
757
745
|
method,
|
|
758
|
-
params: params
|
|
746
|
+
params: params ?? {},
|
|
759
747
|
}, sharedSecret));
|
|
760
748
|
return new Promise((resolve, reject) => {
|
|
761
749
|
jsonRpcResponsePromises[id] = {
|
|
@@ -781,21 +769,25 @@ function transact(callback, config) {
|
|
|
781
769
|
reject,
|
|
782
770
|
};
|
|
783
771
|
});
|
|
784
|
-
})
|
|
772
|
+
});
|
|
773
|
+
sessionEstablished = true;
|
|
785
774
|
try {
|
|
786
|
-
resolve(
|
|
775
|
+
resolve(wallet);
|
|
787
776
|
}
|
|
788
777
|
catch (e) {
|
|
789
778
|
reject(e);
|
|
790
779
|
}
|
|
791
|
-
finally {
|
|
792
|
-
disposeSocket();
|
|
793
|
-
socket.close();
|
|
794
|
-
}
|
|
795
780
|
break;
|
|
796
781
|
}
|
|
797
782
|
}
|
|
798
|
-
}
|
|
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
|
+
};
|
|
799
791
|
let disposeSocket;
|
|
800
792
|
let retryWaitTimeoutId;
|
|
801
793
|
const attemptSocketConnection = () => {
|
|
@@ -820,244 +812,241 @@ function transact(callback, config) {
|
|
|
820
812
|
};
|
|
821
813
|
};
|
|
822
814
|
attemptSocketConnection();
|
|
823
|
-
});
|
|
824
|
-
});
|
|
815
|
+
}) };
|
|
825
816
|
}
|
|
826
|
-
function startRemoteScenario(config) {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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';
|
|
846
853
|
}
|
|
847
854
|
else {
|
|
848
|
-
|
|
855
|
+
encoding = 'binary';
|
|
849
856
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
disposeSocket();
|
|
880
|
-
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
881
|
-
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_TIMEOUT, `Failed to connect to the wallet websocket at ${websocketURL}.`));
|
|
882
|
-
}
|
|
883
|
-
else {
|
|
884
|
-
yield new Promise((resolve) => {
|
|
885
|
-
const retryDelayMs = getNextRetryDelayMs();
|
|
886
|
-
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
887
|
-
});
|
|
888
|
-
attemptSocketConnection();
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
|
-
const handleReflectorIdMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
892
|
-
const responseBuffer = yield decodeBytes(evt);
|
|
893
|
-
if (state.__type === 'connecting') {
|
|
894
|
-
if (responseBuffer.byteLength == 0) {
|
|
895
|
-
throw new Error('Encountered unexpected message while connecting');
|
|
896
|
-
}
|
|
897
|
-
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
|
|
898
|
-
state = {
|
|
899
|
-
__type: 'reflector_id_received',
|
|
900
|
-
reflectorId: reflectorId
|
|
901
|
-
};
|
|
902
|
-
const associationUrl = yield getRemoteAssociateAndroidIntentURL(associationKeypair.publicKey, config.remoteHostAuthority, reflectorId, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
903
|
-
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
904
|
-
resolve(associationUrl);
|
|
905
|
-
}
|
|
906
|
-
});
|
|
907
|
-
let retryWaitTimeoutId;
|
|
908
|
-
const attemptSocketConnection = () => {
|
|
909
|
-
if (disposeSocket) {
|
|
910
|
-
disposeSocket();
|
|
911
|
-
}
|
|
912
|
-
state = { __type: 'connecting', associationKeypair };
|
|
913
|
-
if (connectionStartTime === undefined) {
|
|
914
|
-
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');
|
|
915
886
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
socket.addEventListener('message', handleReflectorIdMessage);
|
|
921
|
-
disposeSocket = () => {
|
|
922
|
-
window.clearTimeout(retryWaitTimeoutId);
|
|
923
|
-
socket.removeEventListener('open', handleOpen);
|
|
924
|
-
socket.removeEventListener('close', handleClose);
|
|
925
|
-
socket.removeEventListener('error', handleError);
|
|
926
|
-
socket.removeEventListener('message', handleReflectorIdMessage);
|
|
887
|
+
const reflectorId = getReflectorIdFromByteArray(responseBuffer);
|
|
888
|
+
state = {
|
|
889
|
+
__type: 'reflector_id_received',
|
|
890
|
+
reflectorId: reflectorId
|
|
927
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);
|
|
928
917
|
};
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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');
|
|
948
959
|
}
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
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);
|
|
953
971
|
}
|
|
954
972
|
else {
|
|
955
|
-
|
|
973
|
+
throw e;
|
|
956
974
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
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);
|
|
966
983
|
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
967
984
|
if (sequenceNumber !== (lastKnownInboundSequenceNumber + 1)) {
|
|
968
985
|
throw new Error('Encrypted message has invalid sequence number');
|
|
969
986
|
}
|
|
970
987
|
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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));
|
|
975
1001
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
979
|
-
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
980
|
-
responsePromise.reject(e);
|
|
981
|
-
}
|
|
982
|
-
else {
|
|
983
|
-
throw e;
|
|
984
|
-
}
|
|
1002
|
+
else {
|
|
1003
|
+
socket.send(binaryMsg);
|
|
985
1004
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
state = { __type: 'connected', sharedSecret, sessionProperties };
|
|
1001
|
-
const wallet = createMobileWalletProxy(sessionProperties.protocol_version, (method, params) => __awaiter(this, void 0, void 0, function* () {
|
|
1002
|
-
const id = nextJsonRpcMessageId++;
|
|
1003
|
-
const binaryMsg = yield encryptJsonRpcMessage({
|
|
1004
|
-
id,
|
|
1005
|
-
jsonrpc: '2.0',
|
|
1006
|
-
method,
|
|
1007
|
-
params: params !== null && params !== void 0 ? params : {},
|
|
1008
|
-
}, sharedSecret);
|
|
1009
|
-
if (encoding == 'base64') {
|
|
1010
|
-
socket.send(fromUint8Array(binaryMsg));
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
socket.send(binaryMsg);
|
|
1014
|
-
}
|
|
1015
|
-
return new Promise((resolve, reject) => {
|
|
1016
|
-
jsonRpcResponsePromises[id] = {
|
|
1017
|
-
resolve(result) {
|
|
1018
|
-
switch (method) {
|
|
1019
|
-
case 'authorize':
|
|
1020
|
-
case 'reauthorize': {
|
|
1021
|
-
const { wallet_uri_base } = result;
|
|
1022
|
-
if (wallet_uri_base != null) {
|
|
1023
|
-
try {
|
|
1024
|
-
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
1025
|
-
}
|
|
1026
|
-
catch (e) {
|
|
1027
|
-
reject(e);
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
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;
|
|
1030
1019
|
}
|
|
1031
|
-
break;
|
|
1032
1020
|
}
|
|
1021
|
+
break;
|
|
1033
1022
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
}
|
|
1039
|
-
})
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
catch (e) {
|
|
1045
|
-
reject(e);
|
|
1046
|
-
}
|
|
1047
|
-
break;
|
|
1023
|
+
}
|
|
1024
|
+
resolve(result);
|
|
1025
|
+
},
|
|
1026
|
+
reject,
|
|
1027
|
+
};
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
sessionEstablished = true;
|
|
1031
|
+
try {
|
|
1032
|
+
resolve(wallet);
|
|
1048
1033
|
}
|
|
1034
|
+
catch (e) {
|
|
1035
|
+
reject(e);
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1049
1038
|
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
|
|
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
|
+
}) };
|
|
1061
1050
|
}
|
|
1062
1051
|
|
|
1063
|
-
export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, startRemoteScenario, transact };
|
|
1052
|
+
export { SolanaCloneAuthorization, SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, SolanaSignInWithSolana, SolanaSignTransactions, startRemoteScenario, startScenario, transact };
|