@solana-mobile/mobile-wallet-adapter-protocol 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +13 -0
- package/lib/cjs/index.browser.js +479 -0
- package/lib/cjs/index.js +479 -0
- package/lib/cjs/package.json +3 -0
- package/lib/esm/index.browser.mjs +466 -0
- package/lib/esm/index.mjs +466 -0
- package/lib/esm/package.json +3 -0
- package/lib/types/index.browser.d.mts +133 -0
- package/lib/types/index.browser.d.mts.map +1 -0
- package/lib/types/index.browser.d.ts +133 -0
- package/lib/types/index.browser.d.ts.map +1 -0
- package/lib/types/index.d.mts +133 -0
- package/lib/types/index.d.mts.map +1 -0
- package/lib/types/index.d.ts +133 -0
- package/lib/types/index.d.ts.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/*! *****************************************************************************
|
|
2
|
+
Copyright (c) Microsoft Corporation.
|
|
3
|
+
|
|
4
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
5
|
+
purpose with or without fee is hereby granted.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
8
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
9
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
10
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
11
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
12
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
13
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
14
|
+
***************************************************************************** */
|
|
15
|
+
|
|
16
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
17
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
18
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
19
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
20
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
21
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
22
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const publicKeyBuffer = yield crypto.subtle.exportKey('raw', ecdhPublicKey);
|
|
29
|
+
const signatureBuffer = yield crypto.subtle.sign({ hash: 'SHA-256', name: 'ECDSA' }, associationKeypairPrivateKey, publicKeyBuffer);
|
|
30
|
+
const response = new Uint8Array(publicKeyBuffer.byteLength + signatureBuffer.byteLength);
|
|
31
|
+
response.set(new Uint8Array(publicKeyBuffer), 0);
|
|
32
|
+
response.set(new Uint8Array(signatureBuffer), publicKeyBuffer.byteLength);
|
|
33
|
+
return response;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SolanaMobileWalletAdapterSecureContextRequiredError extends Error {
|
|
38
|
+
constructor() {
|
|
39
|
+
super('The mobile wallet adapter protocol must be used in a secure context (`https`).');
|
|
40
|
+
this.name = 'SolanaMobileWalletAdapterSecureContextRequiredError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
class SolanaMobileWalletAdapterForbiddenWalletBaseURLError extends Error {
|
|
44
|
+
constructor() {
|
|
45
|
+
super('Base URLs supplied by wallets must be valid `https` URLs');
|
|
46
|
+
this.name = 'SolanaMobileWalletAdapterForbiddenWalletBaseURLError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class SolanaMobileWalletAdapterWalletNotInstalledError extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super(`Found no installed wallet that supports the mobile wallet protocol.`);
|
|
52
|
+
this.name = 'SolanaMobileWalletAdapterWalletNotInstalledError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class SolanaMobileWalletAdapterProtocolSessionEstablishmentError extends Error {
|
|
56
|
+
constructor(port) {
|
|
57
|
+
super(`Failed to connect to the wallet websocket on port ${port}.`);
|
|
58
|
+
this.name = 'SolanaMobileWalletAdapterProtocolSessionEstablishmentError';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
class SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError extends Error {
|
|
62
|
+
constructor(port) {
|
|
63
|
+
super(`Association port number must be between 49152 and 65535. ${port} given.`);
|
|
64
|
+
this.name = 'SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class SolanaMobileWalletAdapterProtocolSessionClosedError extends Error {
|
|
68
|
+
constructor(code, reason) {
|
|
69
|
+
super(`The wallet session dropped unexpectedly (${code}: ${reason}).`);
|
|
70
|
+
this.name = 'SolanaMobileWalletAdapterProtocolSessionClosedError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class SolanaMobileWalletAdapterProtocolReauthorizeError extends Error {
|
|
74
|
+
constructor() {
|
|
75
|
+
super('The auth token provided has gone stale and needs reauthorization.');
|
|
76
|
+
this.name = 'SolanaMobileWalletAdapterProtocolReauthorizeError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
80
|
+
const SolanaMobileWalletAdapterProtocolError = {
|
|
81
|
+
ERROR_REAUTHORIZE: -1,
|
|
82
|
+
ERROR_AUTHORIZATION_FAILED: -2,
|
|
83
|
+
ERROR_INVALID_PAYLOAD: -3,
|
|
84
|
+
ERROR_NOT_SIGNED: -4,
|
|
85
|
+
ERROR_NOT_COMMITTED: -5,
|
|
86
|
+
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
87
|
+
};
|
|
88
|
+
class SolanaMobileWalletAdapterProtocolJsonRpcError extends Error {
|
|
89
|
+
constructor(...args) {
|
|
90
|
+
const [jsonRpcMessageId, code, message, data] = args;
|
|
91
|
+
super(message);
|
|
92
|
+
this.code = code;
|
|
93
|
+
this.data = data;
|
|
94
|
+
this.jsonRpcMessageId = jsonRpcMessageId;
|
|
95
|
+
this.name = 'SolanaNativeWalletAdapterJsonRpcError';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function generateAssociationKeypair() {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
return yield crypto.subtle.generateKey({
|
|
102
|
+
name: 'ECDSA',
|
|
103
|
+
namedCurve: 'P-256',
|
|
104
|
+
}, false /* extractable */, ['sign'] /* keyUsages */);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function generateECDHKeypair() {
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
return yield crypto.subtle.generateKey({
|
|
111
|
+
name: 'ECDH',
|
|
112
|
+
namedCurve: 'P-256',
|
|
113
|
+
}, false /* extractable */, ['deriveKey', 'deriveBits'] /* keyUsages */);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const INITIALIZATION_VECTOR_BYTES = 12;
|
|
118
|
+
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
119
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
121
|
+
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
122
|
+
crypto.getRandomValues(initializationVector);
|
|
123
|
+
const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(initializationVector), sharedSecret, Buffer.from(plaintext));
|
|
124
|
+
const response = new Uint8Array(initializationVector.byteLength + ciphertext.byteLength);
|
|
125
|
+
response.set(new Uint8Array(initializationVector), 0);
|
|
126
|
+
response.set(new Uint8Array(ciphertext), initializationVector.byteLength);
|
|
127
|
+
return response;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
131
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
+
const initializationVector = message.slice(0, INITIALIZATION_VECTOR_BYTES);
|
|
133
|
+
const ciphertext = message.slice(INITIALIZATION_VECTOR_BYTES);
|
|
134
|
+
const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(initializationVector), sharedSecret, ciphertext);
|
|
135
|
+
const plaintext = getUtf8Decoder().decode(plaintextBuffer);
|
|
136
|
+
const jsonRpcMessage = JSON.parse(plaintext);
|
|
137
|
+
if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
|
|
138
|
+
throw new SolanaMobileWalletAdapterProtocolJsonRpcError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
|
|
139
|
+
}
|
|
140
|
+
return jsonRpcMessage;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function getAlgorithmParams(initializationVector) {
|
|
144
|
+
return {
|
|
145
|
+
iv: initializationVector,
|
|
146
|
+
name: 'AES-GCM',
|
|
147
|
+
tagLength: 128, // 16 byte tag => 128 bits
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
let _utf8Decoder;
|
|
151
|
+
function getUtf8Decoder() {
|
|
152
|
+
if (_utf8Decoder === undefined) {
|
|
153
|
+
_utf8Decoder = new TextDecoder('utf-8');
|
|
154
|
+
}
|
|
155
|
+
return _utf8Decoder;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseHelloRsp(payloadBuffer, // The X9.62-encoded wallet endpoint ephemeral ECDH public keypoint.
|
|
159
|
+
associationPublicKey, ecdhPrivateKey) {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
const [associationPublicKeyBuffer, walletPublicKey] = yield Promise.all([
|
|
162
|
+
crypto.subtle.exportKey('raw', associationPublicKey),
|
|
163
|
+
crypto.subtle.importKey('raw', payloadBuffer, { name: 'ECDH', namedCurve: 'P-256' }, false /* extractable */, [] /* keyUsages */),
|
|
164
|
+
]);
|
|
165
|
+
const sharedSecret = yield crypto.subtle.deriveBits({ name: 'ECDH', public: walletPublicKey }, ecdhPrivateKey, 256);
|
|
166
|
+
const ecdhSecretKey = yield crypto.subtle.importKey('raw', sharedSecret, 'HKDF', false /* extractable */, ['deriveKey'] /* keyUsages */);
|
|
167
|
+
const aesKeyMaterialVal = yield crypto.subtle.deriveKey({
|
|
168
|
+
name: 'HKDF',
|
|
169
|
+
hash: 'SHA-256',
|
|
170
|
+
salt: new Uint8Array(associationPublicKeyBuffer),
|
|
171
|
+
info: new Uint8Array(),
|
|
172
|
+
}, ecdhSecretKey, { name: 'AES-GCM', length: 128 }, false /* extractable */, ['encrypt', 'decrypt']);
|
|
173
|
+
return aesKeyMaterialVal;
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getRandomAssociationPort() {
|
|
178
|
+
return assertAssociationPort(49152 + Math.floor(Math.random() * (65535 - 49152 + 1)));
|
|
179
|
+
}
|
|
180
|
+
function assertAssociationPort(port) {
|
|
181
|
+
if (port < 49152 || port > 65535) {
|
|
182
|
+
throw new SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError(port);
|
|
183
|
+
}
|
|
184
|
+
return port;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// https://stackoverflow.com/a/9458996/802047
|
|
188
|
+
function arrayBufferToBase64String(buffer) {
|
|
189
|
+
let binary = '';
|
|
190
|
+
const bytes = new Uint8Array(buffer);
|
|
191
|
+
const len = bytes.byteLength;
|
|
192
|
+
for (let ii = 0; ii < len; ii++) {
|
|
193
|
+
binary += String.fromCharCode(bytes[ii]);
|
|
194
|
+
}
|
|
195
|
+
return window.btoa(binary);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function getStringWithURLUnsafeCharactersReplaced(unsafeBase64EncodedString) {
|
|
199
|
+
return unsafeBase64EncodedString.replace(/[/+=]/g, (m) => ({
|
|
200
|
+
'/': '_',
|
|
201
|
+
'+': '-',
|
|
202
|
+
'=': '.',
|
|
203
|
+
}[m]));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const INTENT_NAME = 'solana-wallet';
|
|
207
|
+
function getPathParts(pathString) {
|
|
208
|
+
return (pathString
|
|
209
|
+
// Strip leading and trailing slashes
|
|
210
|
+
.replace(/(^\/+|\/+$)/g, '')
|
|
211
|
+
// Return an array of directories
|
|
212
|
+
.split('/'));
|
|
213
|
+
}
|
|
214
|
+
function getIntentURL(methodPathname, intentUrlBase) {
|
|
215
|
+
let baseUrl = null;
|
|
216
|
+
if (intentUrlBase) {
|
|
217
|
+
try {
|
|
218
|
+
baseUrl = new URL(intentUrlBase);
|
|
219
|
+
}
|
|
220
|
+
catch (_a) { } // eslint-disable-line no-empty
|
|
221
|
+
if ((baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.protocol) !== 'https:') {
|
|
222
|
+
throw new SolanaMobileWalletAdapterForbiddenWalletBaseURLError();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
baseUrl || (baseUrl = new URL(`${INTENT_NAME}:/`));
|
|
226
|
+
const pathname = methodPathname.startsWith('/')
|
|
227
|
+
? // Method is an absolute path. Replace it wholesale.
|
|
228
|
+
methodPathname
|
|
229
|
+
: // Method is a relative path. Merge it with the existing one.
|
|
230
|
+
[...getPathParts(baseUrl.pathname), ...getPathParts(methodPathname)].join('/');
|
|
231
|
+
return new URL(pathname, baseUrl);
|
|
232
|
+
}
|
|
233
|
+
function getAssociateAndroidIntentURL(associationPublicKey, putativePort, associationURLBase) {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
const associationPort = assertAssociationPort(putativePort);
|
|
236
|
+
const exportedKey = yield crypto.subtle.exportKey('raw', associationPublicKey);
|
|
237
|
+
const encodedKey = arrayBufferToBase64String(exportedKey);
|
|
238
|
+
const url = getIntentURL('v1/associate/local', associationURLBase);
|
|
239
|
+
url.searchParams.set('association', getStringWithURLUnsafeCharactersReplaced(encodedKey));
|
|
240
|
+
url.searchParams.set('port', `${associationPort}`);
|
|
241
|
+
return url;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
246
|
+
const Browser = {
|
|
247
|
+
Firefox: 0,
|
|
248
|
+
Other: 1,
|
|
249
|
+
};
|
|
250
|
+
function assertUnreachable(x) {
|
|
251
|
+
return x;
|
|
252
|
+
}
|
|
253
|
+
function getBrowser() {
|
|
254
|
+
return navigator.userAgent.indexOf('Firefox/') !== -1 ? Browser.Firefox : Browser.Other;
|
|
255
|
+
}
|
|
256
|
+
function getDetectionPromise() {
|
|
257
|
+
// Chrome and others silently fail if a custom protocol is not supported.
|
|
258
|
+
// For these, we wait to see if the browser is navigated away from in
|
|
259
|
+
// a reasonable amount of time (ie. the native wallet opened).
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
function cleanup() {
|
|
262
|
+
clearTimeout(timeoutId);
|
|
263
|
+
window.removeEventListener('blur', handleBlur);
|
|
264
|
+
}
|
|
265
|
+
function handleBlur() {
|
|
266
|
+
cleanup();
|
|
267
|
+
resolve();
|
|
268
|
+
}
|
|
269
|
+
window.addEventListener('blur', handleBlur);
|
|
270
|
+
const timeoutId = setTimeout(() => {
|
|
271
|
+
cleanup();
|
|
272
|
+
reject();
|
|
273
|
+
}, 2000);
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let _frame = null;
|
|
277
|
+
function launchUrlThroughHiddenFrame(url) {
|
|
278
|
+
if (_frame == null) {
|
|
279
|
+
_frame = document.createElement('iframe');
|
|
280
|
+
_frame.style.display = 'none';
|
|
281
|
+
document.body.appendChild(_frame);
|
|
282
|
+
}
|
|
283
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
284
|
+
_frame.contentWindow.location.href = url.toString();
|
|
285
|
+
}
|
|
286
|
+
function startSession(associationPublicKey, associationURLBase) {
|
|
287
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
288
|
+
const randomAssociationPort = getRandomAssociationPort();
|
|
289
|
+
const associationUrl = yield getAssociateAndroidIntentURL(associationPublicKey, randomAssociationPort, associationURLBase);
|
|
290
|
+
if (associationUrl.protocol === 'https:') {
|
|
291
|
+
// The association URL is an Android 'App Link' or iOS 'Universal Link'.
|
|
292
|
+
// These are regular web URLs that are designed to launch an app if it
|
|
293
|
+
// is installed or load the actual target webpage if not.
|
|
294
|
+
window.location.assign(associationUrl);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// The association URL has a custom protocol (eg. `solana-wallet:`)
|
|
298
|
+
try {
|
|
299
|
+
const browser = getBrowser();
|
|
300
|
+
switch (browser) {
|
|
301
|
+
case Browser.Firefox:
|
|
302
|
+
// If a custom protocol is not supported in Firefox, it throws.
|
|
303
|
+
launchUrlThroughHiddenFrame(associationUrl);
|
|
304
|
+
// If we reached this line, it's supported.
|
|
305
|
+
break;
|
|
306
|
+
case Browser.Other: {
|
|
307
|
+
const detectionPromise = getDetectionPromise();
|
|
308
|
+
window.location.assign(associationUrl);
|
|
309
|
+
yield detectionPromise;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
assertUnreachable(browser);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
throw new SolanaMobileWalletAdapterWalletNotInstalledError();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return randomAssociationPort;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
325
|
+
maxAttempts: 34,
|
|
326
|
+
/**
|
|
327
|
+
* 300 milliseconds is a generally accepted threshold for what someone
|
|
328
|
+
* would consider an acceptable response time for a user interface
|
|
329
|
+
* after having performed a low-attention tapping task. We set the
|
|
330
|
+
* interval at which we wait for the wallet to set up the websocket at
|
|
331
|
+
* half this, as per the Nyquist frequency.
|
|
332
|
+
*/
|
|
333
|
+
retryDelayMs: 150,
|
|
334
|
+
};
|
|
335
|
+
const WEBSOCKET_PROTOCOL = 'com.solana.mobilewalletadapter.v1';
|
|
336
|
+
function assertSecureContext() {
|
|
337
|
+
if (typeof window === 'undefined' || window.isSecureContext !== true) {
|
|
338
|
+
throw new SolanaMobileWalletAdapterSecureContextRequiredError();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function withLocalWallet(callback, config) {
|
|
342
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
343
|
+
assertSecureContext();
|
|
344
|
+
const associationKeypair = yield generateAssociationKeypair();
|
|
345
|
+
const sessionPort = yield startSession(associationKeypair.publicKey, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
346
|
+
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
347
|
+
let nextJsonRpcMessageId = 1;
|
|
348
|
+
let state = { __type: 'disconnected' };
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
let attempts = 0;
|
|
351
|
+
let socket;
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
|
+
const jsonRpcResponsePromises = {};
|
|
354
|
+
const handleOpen = () => __awaiter(this, void 0, void 0, function* () {
|
|
355
|
+
if (state.__type !== 'connecting') {
|
|
356
|
+
console.warn('Expected adapter state to be `connecting` at the moment the websocket opens. ' +
|
|
357
|
+
`Got \`${state.__type}\`.`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const { associationKeypair } = state;
|
|
361
|
+
socket.removeEventListener('open', handleOpen);
|
|
362
|
+
const ecdhKeypair = yield generateECDHKeypair();
|
|
363
|
+
socket.send(yield createHelloReq(ecdhKeypair.publicKey, associationKeypair.privateKey));
|
|
364
|
+
state = {
|
|
365
|
+
__type: 'hello_req_sent',
|
|
366
|
+
associationPublicKey: associationKeypair.publicKey,
|
|
367
|
+
ecdhPrivateKey: ecdhKeypair.privateKey,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
const handleClose = (evt) => {
|
|
371
|
+
if (evt.wasClean) {
|
|
372
|
+
state = { __type: 'disconnected' };
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
reject(new SolanaMobileWalletAdapterProtocolSessionClosedError(evt.code, evt.reason));
|
|
376
|
+
}
|
|
377
|
+
disposeSocket();
|
|
378
|
+
};
|
|
379
|
+
const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
|
|
380
|
+
disposeSocket();
|
|
381
|
+
if (++attempts >= WEBSOCKET_CONNECTION_CONFIG.maxAttempts) {
|
|
382
|
+
reject(new SolanaMobileWalletAdapterProtocolSessionEstablishmentError(sessionPort));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
yield new Promise((resolve) => {
|
|
386
|
+
retryWaitTimeoutId = window.setTimeout(resolve, WEBSOCKET_CONNECTION_CONFIG.retryDelayMs);
|
|
387
|
+
});
|
|
388
|
+
attemptSocketConnection();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
const handleMessage = (evt) => __awaiter(this, void 0, void 0, function* () {
|
|
392
|
+
const responseBuffer = yield evt.data.arrayBuffer();
|
|
393
|
+
switch (state.__type) {
|
|
394
|
+
case 'connected':
|
|
395
|
+
try {
|
|
396
|
+
const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
397
|
+
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
398
|
+
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
399
|
+
responsePromise.resolve(jsonRpcMessage.result);
|
|
400
|
+
}
|
|
401
|
+
catch (e) {
|
|
402
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolJsonRpcError) {
|
|
403
|
+
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
404
|
+
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
405
|
+
responsePromise.reject(e);
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
throw e;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
break;
|
|
412
|
+
case 'hello_req_sent': {
|
|
413
|
+
const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
|
|
414
|
+
state = { __type: 'connected', sharedSecret };
|
|
415
|
+
const wallet = (method, params) => __awaiter(this, void 0, void 0, function* () {
|
|
416
|
+
const id = nextJsonRpcMessageId++;
|
|
417
|
+
socket.send(yield encryptJsonRpcMessage({
|
|
418
|
+
id,
|
|
419
|
+
jsonrpc: '2.0',
|
|
420
|
+
method,
|
|
421
|
+
params,
|
|
422
|
+
}, sharedSecret));
|
|
423
|
+
return new Promise((resolve, reject) => {
|
|
424
|
+
jsonRpcResponsePromises[id] = { resolve, reject };
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
resolve(yield callback(wallet));
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
reject(e);
|
|
432
|
+
}
|
|
433
|
+
finally {
|
|
434
|
+
disposeSocket();
|
|
435
|
+
socket.close();
|
|
436
|
+
}
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
let disposeSocket;
|
|
442
|
+
let retryWaitTimeoutId;
|
|
443
|
+
const attemptSocketConnection = () => {
|
|
444
|
+
if (disposeSocket) {
|
|
445
|
+
disposeSocket();
|
|
446
|
+
}
|
|
447
|
+
state = { __type: 'connecting', associationKeypair };
|
|
448
|
+
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
|
|
449
|
+
socket.addEventListener('open', handleOpen);
|
|
450
|
+
socket.addEventListener('close', handleClose);
|
|
451
|
+
socket.addEventListener('error', handleError);
|
|
452
|
+
socket.addEventListener('message', handleMessage);
|
|
453
|
+
disposeSocket = () => {
|
|
454
|
+
window.clearTimeout(retryWaitTimeoutId);
|
|
455
|
+
socket.removeEventListener('open', handleOpen);
|
|
456
|
+
socket.removeEventListener('close', handleClose);
|
|
457
|
+
socket.removeEventListener('error', handleError);
|
|
458
|
+
socket.removeEventListener('message', handleMessage);
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
attemptSocketConnection();
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export { SolanaMobileWalletAdapterForbiddenWalletBaseURLError, SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolJsonRpcError, SolanaMobileWalletAdapterProtocolReauthorizeError, SolanaMobileWalletAdapterProtocolSessionClosedError, SolanaMobileWalletAdapterProtocolSessionEstablishmentError, SolanaMobileWalletAdapterSecureContextRequiredError, SolanaMobileWalletAdapterWalletNotInstalledError, withLocalWallet };
|