@solana-mobile/mobile-wallet-adapter-protocol 0.0.1-alpha.6 → 0.9.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/README.md +11 -5
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/solanamobile/mobilewalletadapter/reactnative/SolanaMobileWalletAdapterModule.kt +29 -32
- package/lib/cjs/index.browser.js +121 -80
- package/lib/cjs/index.js +121 -80
- package/lib/cjs/index.native.js +60 -62
- package/lib/esm/index.browser.mjs +119 -73
- package/lib/esm/index.mjs +119 -73
- package/lib/types/index.browser.d.mts +76 -54
- package/lib/types/index.browser.d.mts.map +1 -1
- package/lib/types/index.browser.d.ts +76 -54
- package/lib/types/index.browser.d.ts.map +1 -1
- package/lib/types/index.d.mts +76 -54
- package/lib/types/index.d.mts.map +1 -1
- package/lib/types/index.d.ts +76 -54
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.native.d.ts +76 -54
- package/lib/types/index.native.d.ts.map +1 -1
- package/package.json +5 -4
|
@@ -1,62 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.name = 'SolanaMobileWalletAdapterWalletNotInstalledError';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
class SolanaMobileWalletAdapterProtocolSessionEstablishmentError extends Error {
|
|
20
|
-
constructor(port) {
|
|
21
|
-
super(`Failed to connect to the wallet websocket on port ${port}.`);
|
|
22
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionEstablishmentError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
class SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError extends Error {
|
|
26
|
-
constructor(port) {
|
|
27
|
-
super(`Association port number must be between 49152 and 65535. ${port} given.`);
|
|
28
|
-
this.name = 'SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
class SolanaMobileWalletAdapterProtocolSessionClosedError extends Error {
|
|
32
|
-
constructor(code, reason) {
|
|
33
|
-
super(`The wallet session dropped unexpectedly (${code}: ${reason}).`);
|
|
34
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionClosedError';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
class SolanaMobileWalletAdapterProtocolReauthorizeError extends Error {
|
|
38
|
-
constructor() {
|
|
39
|
-
super('The auth token provided has gone stale and needs reauthorization.');
|
|
40
|
-
this.name = 'SolanaMobileWalletAdapterProtocolReauthorizeError';
|
|
1
|
+
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
2
|
+
const SolanaMobileWalletAdapterErrorCode = {
|
|
3
|
+
ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
|
|
4
|
+
ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
|
|
5
|
+
ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
|
|
6
|
+
ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
|
|
7
|
+
ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
|
|
8
|
+
};
|
|
9
|
+
class SolanaMobileWalletAdapterError extends Error {
|
|
10
|
+
constructor(...args) {
|
|
11
|
+
const [code, message, data] = args;
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.data = data;
|
|
15
|
+
this.name = 'SolanaMobileWalletAdapterError';
|
|
41
16
|
}
|
|
42
17
|
}
|
|
43
18
|
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
ERROR_AUTHORIZATION_FAILED: -
|
|
47
|
-
|
|
48
|
-
ERROR_NOT_SIGNED: -
|
|
49
|
-
|
|
19
|
+
const SolanaMobileWalletAdapterProtocolErrorCode = {
|
|
20
|
+
// Keep these in sync with `mobilewalletadapter/common/ProtocolContract.java`.
|
|
21
|
+
ERROR_AUTHORIZATION_FAILED: -1,
|
|
22
|
+
ERROR_INVALID_PAYLOADS: -2,
|
|
23
|
+
ERROR_NOT_SIGNED: -3,
|
|
24
|
+
ERROR_NOT_SUBMITTED: -4,
|
|
25
|
+
ERROR_TOO_MANY_PAYLOADS: -5,
|
|
50
26
|
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
51
27
|
};
|
|
52
|
-
class
|
|
28
|
+
class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
53
29
|
constructor(...args) {
|
|
54
30
|
const [jsonRpcMessageId, code, message, data] = args;
|
|
55
31
|
super(message);
|
|
56
32
|
this.code = code;
|
|
57
33
|
this.data = data;
|
|
58
34
|
this.jsonRpcMessageId = jsonRpcMessageId;
|
|
59
|
-
this.name = '
|
|
35
|
+
this.name = 'SolanaMobileWalletAdapterProtocolError';
|
|
60
36
|
}
|
|
61
37
|
}
|
|
62
38
|
|
|
@@ -96,6 +72,17 @@ function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
|
96
72
|
});
|
|
97
73
|
}
|
|
98
74
|
|
|
75
|
+
const SEQUENCE_NUMBER_BYTES = 4;
|
|
76
|
+
function createSequenceNumberVector(sequenceNumber) {
|
|
77
|
+
if (sequenceNumber >= 4294967296) {
|
|
78
|
+
throw new Error('Outbound sequence number overflow. The maximum sequence number is 32-bytes.');
|
|
79
|
+
}
|
|
80
|
+
const byteArray = new ArrayBuffer(SEQUENCE_NUMBER_BYTES);
|
|
81
|
+
const view = new DataView(byteArray);
|
|
82
|
+
view.setUint32(0, sequenceNumber, /* littleEndian */ false);
|
|
83
|
+
return new Uint8Array(byteArray);
|
|
84
|
+
}
|
|
85
|
+
|
|
99
86
|
function generateAssociationKeypair() {
|
|
100
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
101
88
|
return yield crypto.subtle.generateKey({
|
|
@@ -118,30 +105,34 @@ const INITIALIZATION_VECTOR_BYTES = 12;
|
|
|
118
105
|
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
119
106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
107
|
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
108
|
+
const sequenceNumberVector = createSequenceNumberVector(jsonRpcMessage.id);
|
|
121
109
|
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
122
110
|
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(
|
|
126
|
-
response.set(new Uint8Array(
|
|
111
|
+
const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, Buffer.from(plaintext));
|
|
112
|
+
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
|
|
113
|
+
response.set(new Uint8Array(sequenceNumberVector), 0);
|
|
114
|
+
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
|
|
115
|
+
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
|
|
127
116
|
return response;
|
|
128
117
|
});
|
|
129
118
|
}
|
|
130
119
|
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
131
120
|
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
121
|
+
const sequenceNumberVector = message.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
122
|
+
const initializationVector = message.slice(SEQUENCE_NUMBER_BYTES, SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
123
|
+
const ciphertext = message.slice(SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
124
|
+
const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
|
|
135
125
|
const plaintext = getUtf8Decoder().decode(plaintextBuffer);
|
|
136
126
|
const jsonRpcMessage = JSON.parse(plaintext);
|
|
137
127
|
if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
|
|
138
|
-
throw new
|
|
128
|
+
throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
|
|
139
129
|
}
|
|
140
130
|
return jsonRpcMessage;
|
|
141
131
|
});
|
|
142
132
|
}
|
|
143
|
-
function getAlgorithmParams(initializationVector) {
|
|
133
|
+
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
144
134
|
return {
|
|
135
|
+
additionalData: sequenceNumber,
|
|
145
136
|
iv: initializationVector,
|
|
146
137
|
name: 'AES-GCM',
|
|
147
138
|
tagLength: 128, // 16 byte tag => 128 bits
|
|
@@ -179,7 +170,7 @@ function getRandomAssociationPort() {
|
|
|
179
170
|
}
|
|
180
171
|
function assertAssociationPort(port) {
|
|
181
172
|
if (port < 49152 || port > 65535) {
|
|
182
|
-
throw new
|
|
173
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${port} given.`, { port });
|
|
183
174
|
}
|
|
184
175
|
return port;
|
|
185
176
|
}
|
|
@@ -219,7 +210,7 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
219
210
|
}
|
|
220
211
|
catch (_a) { } // eslint-disable-line no-empty
|
|
221
212
|
if ((baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.protocol) !== 'https:') {
|
|
222
|
-
throw new
|
|
213
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
223
214
|
}
|
|
224
215
|
}
|
|
225
216
|
baseUrl || (baseUrl = new URL(`${INTENT_NAME}:/`));
|
|
@@ -314,7 +305,7 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
314
305
|
}
|
|
315
306
|
}
|
|
316
307
|
catch (e) {
|
|
317
|
-
throw new
|
|
308
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
318
309
|
}
|
|
319
310
|
}
|
|
320
311
|
return randomAssociationPort;
|
|
@@ -322,32 +313,56 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
322
313
|
}
|
|
323
314
|
|
|
324
315
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
325
|
-
maxAttempts: 34,
|
|
326
316
|
/**
|
|
327
317
|
* 300 milliseconds is a generally accepted threshold for what someone
|
|
328
318
|
* would consider an acceptable response time for a user interface
|
|
329
|
-
* after having performed a low-attention tapping task. We set the
|
|
319
|
+
* after having performed a low-attention tapping task. We set the initial
|
|
330
320
|
* interval at which we wait for the wallet to set up the websocket at
|
|
331
|
-
* half this, as per the Nyquist frequency
|
|
321
|
+
* half this, as per the Nyquist frequency, with a progressive backoff
|
|
322
|
+
* sequence from there. The total wait time is 30s, which allows for the
|
|
323
|
+
* user to be presented with a disambiguation dialog, select a wallet, and
|
|
324
|
+
* for the wallet app to subsequently start.
|
|
332
325
|
*/
|
|
333
|
-
|
|
326
|
+
retryDelayScheduleMs: [150, 150, 200, 500, 500, 750, 750, 1000],
|
|
327
|
+
timeoutMs: 30000,
|
|
334
328
|
};
|
|
335
329
|
const WEBSOCKET_PROTOCOL = 'com.solana.mobilewalletadapter.v1';
|
|
336
330
|
function assertSecureContext() {
|
|
337
331
|
if (typeof window === 'undefined' || window.isSecureContext !== true) {
|
|
338
|
-
throw new
|
|
332
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED, 'The mobile wallet adapter protocol must be used in a secure context (`https`).');
|
|
339
333
|
}
|
|
340
334
|
}
|
|
335
|
+
function assertSecureEndpointSpecificURI(walletUriBase) {
|
|
336
|
+
let url;
|
|
337
|
+
try {
|
|
338
|
+
url = new URL(walletUriBase);
|
|
339
|
+
}
|
|
340
|
+
catch (_a) {
|
|
341
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Invalid base URL supplied by wallet');
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol !== 'https:') {
|
|
344
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function getSequenceNumberFromByteArray(byteArray) {
|
|
348
|
+
const view = new DataView(byteArray);
|
|
349
|
+
return view.getUint32(0, /* littleEndian */ false);
|
|
350
|
+
}
|
|
341
351
|
function transact(callback, config) {
|
|
342
352
|
return __awaiter(this, void 0, void 0, function* () {
|
|
343
353
|
assertSecureContext();
|
|
344
354
|
const associationKeypair = yield generateAssociationKeypair();
|
|
345
355
|
const sessionPort = yield startSession(associationKeypair.publicKey, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
346
356
|
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
357
|
+
let connectionStartTime;
|
|
358
|
+
const getNextRetryDelayMs = (() => {
|
|
359
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
360
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
361
|
+
})();
|
|
347
362
|
let nextJsonRpcMessageId = 1;
|
|
363
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
348
364
|
let state = { __type: 'disconnected' };
|
|
349
365
|
return new Promise((resolve, reject) => {
|
|
350
|
-
let attempts = 0;
|
|
351
366
|
let socket;
|
|
352
367
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
368
|
const jsonRpcResponsePromises = {};
|
|
@@ -372,18 +387,19 @@ function transact(callback, config) {
|
|
|
372
387
|
state = { __type: 'disconnected' };
|
|
373
388
|
}
|
|
374
389
|
else {
|
|
375
|
-
reject(new
|
|
390
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
376
391
|
}
|
|
377
392
|
disposeSocket();
|
|
378
393
|
};
|
|
379
394
|
const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
|
|
380
395
|
disposeSocket();
|
|
381
|
-
if (
|
|
382
|
-
reject(new
|
|
396
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
397
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
|
|
383
398
|
}
|
|
384
399
|
else {
|
|
385
400
|
yield new Promise((resolve) => {
|
|
386
|
-
|
|
401
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
402
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
387
403
|
});
|
|
388
404
|
attemptSocketConnection();
|
|
389
405
|
}
|
|
@@ -393,13 +409,19 @@ function transact(callback, config) {
|
|
|
393
409
|
switch (state.__type) {
|
|
394
410
|
case 'connected':
|
|
395
411
|
try {
|
|
412
|
+
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
413
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
414
|
+
if (sequenceNumber <= lastKnownInboundSequenceNumber) {
|
|
415
|
+
throw new Error('Encrypted message has invalid sequence number');
|
|
416
|
+
}
|
|
417
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
396
418
|
const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
397
419
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
398
420
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
399
421
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
400
422
|
}
|
|
401
423
|
catch (e) {
|
|
402
|
-
if (e instanceof
|
|
424
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
403
425
|
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
404
426
|
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
405
427
|
responsePromise.reject(e);
|
|
@@ -429,7 +451,28 @@ function transact(callback, config) {
|
|
|
429
451
|
params,
|
|
430
452
|
}, sharedSecret));
|
|
431
453
|
return new Promise((resolve, reject) => {
|
|
432
|
-
jsonRpcResponsePromises[id] = {
|
|
454
|
+
jsonRpcResponsePromises[id] = {
|
|
455
|
+
resolve(result) {
|
|
456
|
+
switch (p) {
|
|
457
|
+
case 'authorize':
|
|
458
|
+
case 'reauthorize': {
|
|
459
|
+
const { wallet_uri_base } = result;
|
|
460
|
+
if (wallet_uri_base != null) {
|
|
461
|
+
try {
|
|
462
|
+
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
reject(e);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
resolve(result);
|
|
473
|
+
},
|
|
474
|
+
reject,
|
|
475
|
+
};
|
|
433
476
|
});
|
|
434
477
|
});
|
|
435
478
|
};
|
|
@@ -464,6 +507,9 @@ function transact(callback, config) {
|
|
|
464
507
|
disposeSocket();
|
|
465
508
|
}
|
|
466
509
|
state = { __type: 'connecting', associationKeypair };
|
|
510
|
+
if (connectionStartTime === undefined) {
|
|
511
|
+
connectionStartTime = Date.now();
|
|
512
|
+
}
|
|
467
513
|
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
|
|
468
514
|
socket.addEventListener('open', handleOpen);
|
|
469
515
|
socket.addEventListener('close', handleClose);
|
|
@@ -482,4 +528,4 @@ function transact(callback, config) {
|
|
|
482
528
|
});
|
|
483
529
|
}
|
|
484
530
|
|
|
485
|
-
export {
|
|
531
|
+
export { SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, transact };
|
package/lib/esm/index.mjs
CHANGED
|
@@ -1,62 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this.name = 'SolanaMobileWalletAdapterWalletNotInstalledError';
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
class SolanaMobileWalletAdapterProtocolSessionEstablishmentError extends Error {
|
|
20
|
-
constructor(port) {
|
|
21
|
-
super(`Failed to connect to the wallet websocket on port ${port}.`);
|
|
22
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionEstablishmentError';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
class SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError extends Error {
|
|
26
|
-
constructor(port) {
|
|
27
|
-
super(`Association port number must be between 49152 and 65535. ${port} given.`);
|
|
28
|
-
this.name = 'SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError';
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
class SolanaMobileWalletAdapterProtocolSessionClosedError extends Error {
|
|
32
|
-
constructor(code, reason) {
|
|
33
|
-
super(`The wallet session dropped unexpectedly (${code}: ${reason}).`);
|
|
34
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionClosedError';
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
class SolanaMobileWalletAdapterProtocolReauthorizeError extends Error {
|
|
38
|
-
constructor() {
|
|
39
|
-
super('The auth token provided has gone stale and needs reauthorization.');
|
|
40
|
-
this.name = 'SolanaMobileWalletAdapterProtocolReauthorizeError';
|
|
1
|
+
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
2
|
+
const SolanaMobileWalletAdapterErrorCode = {
|
|
3
|
+
ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
|
|
4
|
+
ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
|
|
5
|
+
ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
|
|
6
|
+
ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
|
|
7
|
+
ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
|
|
8
|
+
};
|
|
9
|
+
class SolanaMobileWalletAdapterError extends Error {
|
|
10
|
+
constructor(...args) {
|
|
11
|
+
const [code, message, data] = args;
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.data = data;
|
|
15
|
+
this.name = 'SolanaMobileWalletAdapterError';
|
|
41
16
|
}
|
|
42
17
|
}
|
|
43
18
|
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
ERROR_AUTHORIZATION_FAILED: -
|
|
47
|
-
|
|
48
|
-
ERROR_NOT_SIGNED: -
|
|
49
|
-
|
|
19
|
+
const SolanaMobileWalletAdapterProtocolErrorCode = {
|
|
20
|
+
// Keep these in sync with `mobilewalletadapter/common/ProtocolContract.java`.
|
|
21
|
+
ERROR_AUTHORIZATION_FAILED: -1,
|
|
22
|
+
ERROR_INVALID_PAYLOADS: -2,
|
|
23
|
+
ERROR_NOT_SIGNED: -3,
|
|
24
|
+
ERROR_NOT_SUBMITTED: -4,
|
|
25
|
+
ERROR_TOO_MANY_PAYLOADS: -5,
|
|
50
26
|
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
51
27
|
};
|
|
52
|
-
class
|
|
28
|
+
class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
53
29
|
constructor(...args) {
|
|
54
30
|
const [jsonRpcMessageId, code, message, data] = args;
|
|
55
31
|
super(message);
|
|
56
32
|
this.code = code;
|
|
57
33
|
this.data = data;
|
|
58
34
|
this.jsonRpcMessageId = jsonRpcMessageId;
|
|
59
|
-
this.name = '
|
|
35
|
+
this.name = 'SolanaMobileWalletAdapterProtocolError';
|
|
60
36
|
}
|
|
61
37
|
}
|
|
62
38
|
|
|
@@ -96,6 +72,17 @@ function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
|
96
72
|
});
|
|
97
73
|
}
|
|
98
74
|
|
|
75
|
+
const SEQUENCE_NUMBER_BYTES = 4;
|
|
76
|
+
function createSequenceNumberVector(sequenceNumber) {
|
|
77
|
+
if (sequenceNumber >= 4294967296) {
|
|
78
|
+
throw new Error('Outbound sequence number overflow. The maximum sequence number is 32-bytes.');
|
|
79
|
+
}
|
|
80
|
+
const byteArray = new ArrayBuffer(SEQUENCE_NUMBER_BYTES);
|
|
81
|
+
const view = new DataView(byteArray);
|
|
82
|
+
view.setUint32(0, sequenceNumber, /* littleEndian */ false);
|
|
83
|
+
return new Uint8Array(byteArray);
|
|
84
|
+
}
|
|
85
|
+
|
|
99
86
|
function generateAssociationKeypair() {
|
|
100
87
|
return __awaiter(this, void 0, void 0, function* () {
|
|
101
88
|
return yield crypto.subtle.generateKey({
|
|
@@ -118,30 +105,34 @@ const INITIALIZATION_VECTOR_BYTES = 12;
|
|
|
118
105
|
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
119
106
|
return __awaiter(this, void 0, void 0, function* () {
|
|
120
107
|
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
108
|
+
const sequenceNumberVector = createSequenceNumberVector(jsonRpcMessage.id);
|
|
121
109
|
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
122
110
|
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(
|
|
126
|
-
response.set(new Uint8Array(
|
|
111
|
+
const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, Buffer.from(plaintext));
|
|
112
|
+
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
|
|
113
|
+
response.set(new Uint8Array(sequenceNumberVector), 0);
|
|
114
|
+
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
|
|
115
|
+
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
|
|
127
116
|
return response;
|
|
128
117
|
});
|
|
129
118
|
}
|
|
130
119
|
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
131
120
|
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
121
|
+
const sequenceNumberVector = message.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
122
|
+
const initializationVector = message.slice(SEQUENCE_NUMBER_BYTES, SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
123
|
+
const ciphertext = message.slice(SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
124
|
+
const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
|
|
135
125
|
const plaintext = getUtf8Decoder().decode(plaintextBuffer);
|
|
136
126
|
const jsonRpcMessage = JSON.parse(plaintext);
|
|
137
127
|
if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
|
|
138
|
-
throw new
|
|
128
|
+
throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
|
|
139
129
|
}
|
|
140
130
|
return jsonRpcMessage;
|
|
141
131
|
});
|
|
142
132
|
}
|
|
143
|
-
function getAlgorithmParams(initializationVector) {
|
|
133
|
+
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
144
134
|
return {
|
|
135
|
+
additionalData: sequenceNumber,
|
|
145
136
|
iv: initializationVector,
|
|
146
137
|
name: 'AES-GCM',
|
|
147
138
|
tagLength: 128, // 16 byte tag => 128 bits
|
|
@@ -179,7 +170,7 @@ function getRandomAssociationPort() {
|
|
|
179
170
|
}
|
|
180
171
|
function assertAssociationPort(port) {
|
|
181
172
|
if (port < 49152 || port > 65535) {
|
|
182
|
-
throw new
|
|
173
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${port} given.`, { port });
|
|
183
174
|
}
|
|
184
175
|
return port;
|
|
185
176
|
}
|
|
@@ -219,7 +210,7 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
219
210
|
}
|
|
220
211
|
catch (_a) { } // eslint-disable-line no-empty
|
|
221
212
|
if ((baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.protocol) !== 'https:') {
|
|
222
|
-
throw new
|
|
213
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
223
214
|
}
|
|
224
215
|
}
|
|
225
216
|
baseUrl || (baseUrl = new URL(`${INTENT_NAME}:/`));
|
|
@@ -314,7 +305,7 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
314
305
|
}
|
|
315
306
|
}
|
|
316
307
|
catch (e) {
|
|
317
|
-
throw new
|
|
308
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
318
309
|
}
|
|
319
310
|
}
|
|
320
311
|
return randomAssociationPort;
|
|
@@ -322,32 +313,56 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
322
313
|
}
|
|
323
314
|
|
|
324
315
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
325
|
-
maxAttempts: 34,
|
|
326
316
|
/**
|
|
327
317
|
* 300 milliseconds is a generally accepted threshold for what someone
|
|
328
318
|
* would consider an acceptable response time for a user interface
|
|
329
|
-
* after having performed a low-attention tapping task. We set the
|
|
319
|
+
* after having performed a low-attention tapping task. We set the initial
|
|
330
320
|
* interval at which we wait for the wallet to set up the websocket at
|
|
331
|
-
* half this, as per the Nyquist frequency
|
|
321
|
+
* half this, as per the Nyquist frequency, with a progressive backoff
|
|
322
|
+
* sequence from there. The total wait time is 30s, which allows for the
|
|
323
|
+
* user to be presented with a disambiguation dialog, select a wallet, and
|
|
324
|
+
* for the wallet app to subsequently start.
|
|
332
325
|
*/
|
|
333
|
-
|
|
326
|
+
retryDelayScheduleMs: [150, 150, 200, 500, 500, 750, 750, 1000],
|
|
327
|
+
timeoutMs: 30000,
|
|
334
328
|
};
|
|
335
329
|
const WEBSOCKET_PROTOCOL = 'com.solana.mobilewalletadapter.v1';
|
|
336
330
|
function assertSecureContext() {
|
|
337
331
|
if (typeof window === 'undefined' || window.isSecureContext !== true) {
|
|
338
|
-
throw new
|
|
332
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED, 'The mobile wallet adapter protocol must be used in a secure context (`https`).');
|
|
339
333
|
}
|
|
340
334
|
}
|
|
335
|
+
function assertSecureEndpointSpecificURI(walletUriBase) {
|
|
336
|
+
let url;
|
|
337
|
+
try {
|
|
338
|
+
url = new URL(walletUriBase);
|
|
339
|
+
}
|
|
340
|
+
catch (_a) {
|
|
341
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Invalid base URL supplied by wallet');
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol !== 'https:') {
|
|
344
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function getSequenceNumberFromByteArray(byteArray) {
|
|
348
|
+
const view = new DataView(byteArray);
|
|
349
|
+
return view.getUint32(0, /* littleEndian */ false);
|
|
350
|
+
}
|
|
341
351
|
function transact(callback, config) {
|
|
342
352
|
return __awaiter(this, void 0, void 0, function* () {
|
|
343
353
|
assertSecureContext();
|
|
344
354
|
const associationKeypair = yield generateAssociationKeypair();
|
|
345
355
|
const sessionPort = yield startSession(associationKeypair.publicKey, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
346
356
|
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
357
|
+
let connectionStartTime;
|
|
358
|
+
const getNextRetryDelayMs = (() => {
|
|
359
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
360
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
361
|
+
})();
|
|
347
362
|
let nextJsonRpcMessageId = 1;
|
|
363
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
348
364
|
let state = { __type: 'disconnected' };
|
|
349
365
|
return new Promise((resolve, reject) => {
|
|
350
|
-
let attempts = 0;
|
|
351
366
|
let socket;
|
|
352
367
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
368
|
const jsonRpcResponsePromises = {};
|
|
@@ -372,18 +387,19 @@ function transact(callback, config) {
|
|
|
372
387
|
state = { __type: 'disconnected' };
|
|
373
388
|
}
|
|
374
389
|
else {
|
|
375
|
-
reject(new
|
|
390
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
376
391
|
}
|
|
377
392
|
disposeSocket();
|
|
378
393
|
};
|
|
379
394
|
const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
|
|
380
395
|
disposeSocket();
|
|
381
|
-
if (
|
|
382
|
-
reject(new
|
|
396
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
397
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
|
|
383
398
|
}
|
|
384
399
|
else {
|
|
385
400
|
yield new Promise((resolve) => {
|
|
386
|
-
|
|
401
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
402
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
387
403
|
});
|
|
388
404
|
attemptSocketConnection();
|
|
389
405
|
}
|
|
@@ -393,13 +409,19 @@ function transact(callback, config) {
|
|
|
393
409
|
switch (state.__type) {
|
|
394
410
|
case 'connected':
|
|
395
411
|
try {
|
|
412
|
+
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
413
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
414
|
+
if (sequenceNumber <= lastKnownInboundSequenceNumber) {
|
|
415
|
+
throw new Error('Encrypted message has invalid sequence number');
|
|
416
|
+
}
|
|
417
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
396
418
|
const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
397
419
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
398
420
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
399
421
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
400
422
|
}
|
|
401
423
|
catch (e) {
|
|
402
|
-
if (e instanceof
|
|
424
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
403
425
|
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
404
426
|
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
405
427
|
responsePromise.reject(e);
|
|
@@ -429,7 +451,28 @@ function transact(callback, config) {
|
|
|
429
451
|
params,
|
|
430
452
|
}, sharedSecret));
|
|
431
453
|
return new Promise((resolve, reject) => {
|
|
432
|
-
jsonRpcResponsePromises[id] = {
|
|
454
|
+
jsonRpcResponsePromises[id] = {
|
|
455
|
+
resolve(result) {
|
|
456
|
+
switch (p) {
|
|
457
|
+
case 'authorize':
|
|
458
|
+
case 'reauthorize': {
|
|
459
|
+
const { wallet_uri_base } = result;
|
|
460
|
+
if (wallet_uri_base != null) {
|
|
461
|
+
try {
|
|
462
|
+
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
463
|
+
}
|
|
464
|
+
catch (e) {
|
|
465
|
+
reject(e);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
resolve(result);
|
|
473
|
+
},
|
|
474
|
+
reject,
|
|
475
|
+
};
|
|
433
476
|
});
|
|
434
477
|
});
|
|
435
478
|
};
|
|
@@ -464,6 +507,9 @@ function transact(callback, config) {
|
|
|
464
507
|
disposeSocket();
|
|
465
508
|
}
|
|
466
509
|
state = { __type: 'connecting', associationKeypair };
|
|
510
|
+
if (connectionStartTime === undefined) {
|
|
511
|
+
connectionStartTime = Date.now();
|
|
512
|
+
}
|
|
467
513
|
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
|
|
468
514
|
socket.addEventListener('open', handleOpen);
|
|
469
515
|
socket.addEventListener('close', handleClose);
|
|
@@ -482,4 +528,4 @@ function transact(callback, config) {
|
|
|
482
528
|
});
|
|
483
529
|
}
|
|
484
530
|
|
|
485
|
-
export {
|
|
531
|
+
export { SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode, SolanaMobileWalletAdapterProtocolError, SolanaMobileWalletAdapterProtocolErrorCode, transact };
|