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