@solana-mobile/mobile-wallet-adapter-protocol 0.0.1-alpha.7 → 0.0.1-alpha.8
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 +87 -79
- package/lib/cjs/index.js +87 -79
- package/lib/cjs/index.native.js +60 -62
- package/lib/esm/index.browser.mjs +85 -72
- package/lib/esm/index.mjs +85 -72
- package/lib/types/index.browser.d.mts +70 -54
- package/lib/types/index.browser.d.mts.map +1 -1
- package/lib/types/index.browser.d.ts +70 -54
- package/lib/types/index.browser.d.ts.map +1 -1
- package/lib/types/index.d.mts +70 -54
- package/lib/types/index.d.mts.map +1 -1
- package/lib/types/index.d.ts +70 -54
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.native.d.ts +70 -54
- package/lib/types/index.native.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@solana-mobile/mobile-wallet-adapter-protocol`
|
|
2
2
|
|
|
3
|
-
This is a reference implementation of the [Mobile Wallet Adapter specification](https://github.com/solana-mobile/mobile-wallet-adapter/blob/main/spec/spec.md) in JavaScript. Use this to start a session with a mobile wallet in which you can issue API calls to it (eg. `
|
|
3
|
+
This is a reference implementation of the [Mobile Wallet Adapter specification](https://github.com/solana-mobile/mobile-wallet-adapter/blob/main/spec/spec.md) in JavaScript. Use this to start a session with a mobile wallet in which you can issue API calls to it (eg. `sign_messages`) as per the spec.
|
|
4
4
|
|
|
5
5
|
If you are simply looking to integrate a JavaScript application with mobile wallets, see [`@solana-mobile/wallet-adapter-mobile`](https://www.npmjs.com/package/@solana-mobile/wallet-adapter-mobile) instead.
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ The callback you provide will be called once a session has been established with
|
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
const signedPayloads = await transact(async (wallet) => {
|
|
23
|
-
const {signed_payloads} = await wallet.
|
|
23
|
+
const {signed_payloads} = await wallet.signMessages({
|
|
24
24
|
auth_token,
|
|
25
25
|
payloads: [/* ... */],
|
|
26
26
|
});
|
|
@@ -38,9 +38,12 @@ You can catch exceptions at any level. See `errors.ts` for a list of exceptions
|
|
|
38
38
|
try {
|
|
39
39
|
await transact(async (wallet) => {
|
|
40
40
|
try {
|
|
41
|
-
await wallet.
|
|
41
|
+
await wallet.signTransactions(/* ... */);
|
|
42
42
|
} catch (e) {
|
|
43
|
-
if (
|
|
43
|
+
if (
|
|
44
|
+
e instanceof SolanaMobileWalletAdapterProtocolError &&
|
|
45
|
+
e.code === SolanaMobileWalletAdapterProtocolErrorCode.ERROR_REAUTHORIZE
|
|
46
|
+
) {
|
|
44
47
|
console.error('The auth token has gone stale');
|
|
45
48
|
await wallet.reauthorize({auth_token});
|
|
46
49
|
// Retry...
|
|
@@ -49,7 +52,10 @@ try {
|
|
|
49
52
|
}
|
|
50
53
|
});
|
|
51
54
|
} catch(e) {
|
|
52
|
-
if (
|
|
55
|
+
if (
|
|
56
|
+
e instanceof SolanaMobileWalletAdapterError &&
|
|
57
|
+
e.code === SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND
|
|
58
|
+
) {
|
|
53
59
|
/* ... */
|
|
54
60
|
}
|
|
55
61
|
throw e;
|
package/android/build.gradle
CHANGED
|
@@ -132,7 +132,7 @@ def kotlin_version = getExtOrDefault('kotlinVersion')
|
|
|
132
132
|
dependencies {
|
|
133
133
|
//noinspection GradleDynamicVersion
|
|
134
134
|
implementation "com.facebook.react:react-native:+" // From node_modules
|
|
135
|
-
implementation "com.solanamobile:mobile-wallet-adapter-clientlib:0.
|
|
135
|
+
implementation "com.solanamobile:mobile-wallet-adapter-clientlib:0.2.0"
|
|
136
136
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
137
137
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2"
|
|
138
138
|
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
package com.solanamobile.mobilewalletadapter.reactnative
|
|
2
2
|
|
|
3
|
+
import android.content.ActivityNotFoundException
|
|
3
4
|
import android.net.Uri
|
|
4
|
-
import android.os.Looper
|
|
5
5
|
import android.util.Log
|
|
6
6
|
import com.facebook.react.bridge.*
|
|
7
7
|
import com.solana.mobilewalletadapter.clientlib.protocol.JsonRpc20Client
|
|
8
8
|
import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient
|
|
9
|
+
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
|
|
9
10
|
import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario
|
|
10
|
-
import com.solana.mobilewalletadapter.clientlib.scenario.Scenario
|
|
11
11
|
import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertJsonToMap
|
|
12
12
|
import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertMapToJson
|
|
13
13
|
import kotlinx.coroutines.*
|
|
14
14
|
import kotlinx.coroutines.sync.Mutex
|
|
15
|
-
import kotlinx.coroutines.sync.Semaphore
|
|
16
15
|
import org.json.JSONObject
|
|
17
16
|
import java.util.concurrent.ExecutionException
|
|
17
|
+
import java.util.concurrent.TimeUnit
|
|
18
|
+
import java.util.concurrent.TimeoutException
|
|
18
19
|
|
|
19
20
|
class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
|
|
20
21
|
ReactContextBaseJavaModule(reactContext), CoroutineScope {
|
|
@@ -22,7 +23,6 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
|
|
|
22
23
|
data class SessionState(
|
|
23
24
|
val client: MobileWalletAdapterClient,
|
|
24
25
|
val localAssociation: LocalAssociationScenario,
|
|
25
|
-
val semSessionTermination: Semaphore,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
override val coroutineContext =
|
|
@@ -46,38 +46,37 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
|
|
|
46
46
|
mutex.lock()
|
|
47
47
|
Log.d(name, "startSession with config $config")
|
|
48
48
|
try {
|
|
49
|
-
val semConnectedOrFailed = Semaphore(1, 1)
|
|
50
|
-
val semTerminated = Semaphore(1, 1)
|
|
51
49
|
val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
override fun onScenarioError() = semConnectedOrFailed.release()
|
|
60
|
-
override fun onScenarioComplete() = semConnectedOrFailed.release()
|
|
61
|
-
override fun onScenarioTeardownComplete() = semTerminated.release()
|
|
62
|
-
}
|
|
63
|
-
localAssociation = LocalAssociationScenario(
|
|
64
|
-
Looper.getMainLooper(),
|
|
65
|
-
ASSOCIATION_TIMEOUT_MS,
|
|
66
|
-
scenarioCallbacks,
|
|
67
|
-
uriPrefix
|
|
50
|
+
val localAssociation = LocalAssociationScenario(
|
|
51
|
+
CLIENT_TIMEOUT_MS,
|
|
52
|
+
)
|
|
53
|
+
val intent = LocalAssociationIntentCreator.createAssociationIntent(
|
|
54
|
+
uriPrefix,
|
|
55
|
+
localAssociation.port,
|
|
56
|
+
localAssociation.session
|
|
68
57
|
)
|
|
69
|
-
val intent = localAssociation.createAssociationIntent()
|
|
70
58
|
currentActivity?.startActivityForResult(intent, 0)
|
|
71
59
|
?: throw NullPointerException("Could not find a current activity from which to launch a local association")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
60
|
+
val client =
|
|
61
|
+
localAssociation.start().get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
|
|
62
|
+
sessionState = SessionState(client, localAssociation)
|
|
76
63
|
promise.resolve(true)
|
|
77
|
-
} catch (e:
|
|
64
|
+
} catch (e: ActivityNotFoundException) {
|
|
65
|
+
Log.e(name, "Found no installed wallet that supports the mobile wallet protocol", e)
|
|
66
|
+
cleanup()
|
|
67
|
+
promise.reject("ERROR_WALLET_NOT_FOUND", e)
|
|
68
|
+
} catch (e: TimeoutException) {
|
|
78
69
|
Log.e(name, "Timed out waiting for local association to be ready", e)
|
|
79
70
|
cleanup()
|
|
80
71
|
promise.reject("Timed out waiting for local association to be ready", e)
|
|
72
|
+
} catch (e: InterruptedException) {
|
|
73
|
+
Log.w(name, "Interrupted while waiting for local association to be ready", e)
|
|
74
|
+
cleanup()
|
|
75
|
+
promise.reject(e)
|
|
76
|
+
} catch (e: ExecutionException) {
|
|
77
|
+
Log.e(name, "Failed establishing local association with wallet", e.cause)
|
|
78
|
+
cleanup()
|
|
79
|
+
promise.reject(e)
|
|
81
80
|
} catch (e: Throwable) {
|
|
82
81
|
Log.e(name, "Failed to start session", e)
|
|
83
82
|
cleanup()
|
|
@@ -116,12 +115,10 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
|
|
|
116
115
|
Log.d(name, "endSession")
|
|
117
116
|
try {
|
|
118
117
|
it.localAssociation.close()
|
|
119
|
-
|
|
120
|
-
it.semSessionTermination.acquire()
|
|
121
|
-
}
|
|
118
|
+
.get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
|
|
122
119
|
cleanup()
|
|
123
120
|
promise.resolve(true)
|
|
124
|
-
} catch (e:
|
|
121
|
+
} catch (e: TimeoutException) {
|
|
125
122
|
Log.e(name, "Timed out waiting for local association to close", e)
|
|
126
123
|
cleanup()
|
|
127
124
|
promise.reject("Failed to end session", e)
|
package/lib/cjs/index.browser.js
CHANGED
|
@@ -2,65 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.name = 'SolanaMobileWalletAdapterWalletNotInstalledError';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
class SolanaMobileWalletAdapterProtocolSessionEstablishmentError extends Error {
|
|
24
|
-
constructor(port) {
|
|
25
|
-
super(`Failed to connect to the wallet websocket on port ${port}.`);
|
|
26
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionEstablishmentError';
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
class SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError extends Error {
|
|
30
|
-
constructor(port) {
|
|
31
|
-
super(`Association port number must be between 49152 and 65535. ${port} given.`);
|
|
32
|
-
this.name = 'SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError';
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
class SolanaMobileWalletAdapterProtocolSessionClosedError extends Error {
|
|
36
|
-
constructor(code, reason) {
|
|
37
|
-
super(`The wallet session dropped unexpectedly (${code}: ${reason}).`);
|
|
38
|
-
this.name = 'SolanaMobileWalletAdapterProtocolSessionClosedError';
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
class SolanaMobileWalletAdapterProtocolReauthorizeError extends Error {
|
|
42
|
-
constructor() {
|
|
43
|
-
super('The auth token provided has gone stale and needs reauthorization.');
|
|
44
|
-
this.name = 'SolanaMobileWalletAdapterProtocolReauthorizeError';
|
|
5
|
+
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
6
|
+
const SolanaMobileWalletAdapterErrorCode = {
|
|
7
|
+
ERROR_ASSOCIATION_PORT_OUT_OF_RANGE: 'ERROR_ASSOCIATION_PORT_OUT_OF_RANGE',
|
|
8
|
+
ERROR_FORBIDDEN_WALLET_BASE_URL: 'ERROR_FORBIDDEN_WALLET_BASE_URL',
|
|
9
|
+
ERROR_SECURE_CONTEXT_REQUIRED: 'ERROR_SECURE_CONTEXT_REQUIRED',
|
|
10
|
+
ERROR_SESSION_CLOSED: 'ERROR_SESSION_CLOSED',
|
|
11
|
+
ERROR_WALLET_NOT_FOUND: 'ERROR_WALLET_NOT_FOUND',
|
|
12
|
+
};
|
|
13
|
+
class SolanaMobileWalletAdapterError extends Error {
|
|
14
|
+
constructor(...args) {
|
|
15
|
+
const [code, message, data] = args;
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.data = data;
|
|
19
|
+
this.name = 'SolanaMobileWalletAdapterError';
|
|
45
20
|
}
|
|
46
21
|
}
|
|
47
22
|
// Typescript `enums` thwart tree-shaking. See https://bargsten.org/jsts/enums/
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
ERROR_AUTHORIZATION_FAILED: -
|
|
51
|
-
|
|
52
|
-
ERROR_NOT_SIGNED: -
|
|
53
|
-
ERROR_NOT_COMMITTED: -
|
|
23
|
+
const SolanaMobileWalletAdapterProtocolErrorCode = {
|
|
24
|
+
// Keep these in sync with `mobilewalletadapter/common/ProtocolContract.java`.
|
|
25
|
+
ERROR_AUTHORIZATION_FAILED: -1,
|
|
26
|
+
ERROR_INVALID_PAYLOADS: -2,
|
|
27
|
+
ERROR_NOT_SIGNED: -3,
|
|
28
|
+
ERROR_NOT_COMMITTED: -4,
|
|
29
|
+
ERROR_TOO_MANY_PAYLOADS: -5,
|
|
54
30
|
ERROR_ATTEST_ORIGIN_ANDROID: -100,
|
|
55
31
|
};
|
|
56
|
-
class
|
|
32
|
+
class SolanaMobileWalletAdapterProtocolError extends Error {
|
|
57
33
|
constructor(...args) {
|
|
58
34
|
const [jsonRpcMessageId, code, message, data] = args;
|
|
59
35
|
super(message);
|
|
60
36
|
this.code = code;
|
|
61
37
|
this.data = data;
|
|
62
38
|
this.jsonRpcMessageId = jsonRpcMessageId;
|
|
63
|
-
this.name = '
|
|
39
|
+
this.name = 'SolanaMobileWalletAdapterProtocolError';
|
|
64
40
|
}
|
|
65
41
|
}
|
|
66
42
|
|
|
@@ -100,6 +76,17 @@ function createHelloReq(ecdhPublicKey, associationKeypairPrivateKey) {
|
|
|
100
76
|
});
|
|
101
77
|
}
|
|
102
78
|
|
|
79
|
+
const SEQUENCE_NUMBER_BYTES = 4;
|
|
80
|
+
function createSequenceNumberVector(sequenceNumber) {
|
|
81
|
+
if (sequenceNumber >= 4294967296) {
|
|
82
|
+
throw new Error('Outbound sequence number overflow. The maximum sequence number is 32-bytes.');
|
|
83
|
+
}
|
|
84
|
+
const byteArray = new ArrayBuffer(SEQUENCE_NUMBER_BYTES);
|
|
85
|
+
const view = new DataView(byteArray);
|
|
86
|
+
view.setUint32(0, sequenceNumber, /* littleEndian */ false);
|
|
87
|
+
return new Uint8Array(byteArray);
|
|
88
|
+
}
|
|
89
|
+
|
|
103
90
|
function generateAssociationKeypair() {
|
|
104
91
|
return __awaiter(this, void 0, void 0, function* () {
|
|
105
92
|
return yield crypto.subtle.generateKey({
|
|
@@ -122,30 +109,34 @@ const INITIALIZATION_VECTOR_BYTES = 12;
|
|
|
122
109
|
function encryptJsonRpcMessage(jsonRpcMessage, sharedSecret) {
|
|
123
110
|
return __awaiter(this, void 0, void 0, function* () {
|
|
124
111
|
const plaintext = JSON.stringify(jsonRpcMessage);
|
|
112
|
+
const sequenceNumberVector = createSequenceNumberVector(jsonRpcMessage.id);
|
|
125
113
|
const initializationVector = new Uint8Array(INITIALIZATION_VECTOR_BYTES);
|
|
126
114
|
crypto.getRandomValues(initializationVector);
|
|
127
|
-
const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(initializationVector), sharedSecret, Buffer.from(plaintext));
|
|
128
|
-
const response = new Uint8Array(initializationVector.byteLength + ciphertext.byteLength);
|
|
129
|
-
response.set(new Uint8Array(
|
|
130
|
-
response.set(new Uint8Array(
|
|
115
|
+
const ciphertext = yield crypto.subtle.encrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, Buffer.from(plaintext));
|
|
116
|
+
const response = new Uint8Array(sequenceNumberVector.byteLength + initializationVector.byteLength + ciphertext.byteLength);
|
|
117
|
+
response.set(new Uint8Array(sequenceNumberVector), 0);
|
|
118
|
+
response.set(new Uint8Array(initializationVector), sequenceNumberVector.byteLength);
|
|
119
|
+
response.set(new Uint8Array(ciphertext), sequenceNumberVector.byteLength + initializationVector.byteLength);
|
|
131
120
|
return response;
|
|
132
121
|
});
|
|
133
122
|
}
|
|
134
123
|
function decryptJsonRpcMessage(message, sharedSecret) {
|
|
135
124
|
return __awaiter(this, void 0, void 0, function* () {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
const
|
|
125
|
+
const sequenceNumberVector = message.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
126
|
+
const initializationVector = message.slice(SEQUENCE_NUMBER_BYTES, SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
127
|
+
const ciphertext = message.slice(SEQUENCE_NUMBER_BYTES + INITIALIZATION_VECTOR_BYTES);
|
|
128
|
+
const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(sequenceNumberVector, initializationVector), sharedSecret, ciphertext);
|
|
139
129
|
const plaintext = getUtf8Decoder().decode(plaintextBuffer);
|
|
140
130
|
const jsonRpcMessage = JSON.parse(plaintext);
|
|
141
131
|
if (Object.hasOwnProperty.call(jsonRpcMessage, 'error')) {
|
|
142
|
-
throw new
|
|
132
|
+
throw new SolanaMobileWalletAdapterProtocolError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
|
|
143
133
|
}
|
|
144
134
|
return jsonRpcMessage;
|
|
145
135
|
});
|
|
146
136
|
}
|
|
147
|
-
function getAlgorithmParams(initializationVector) {
|
|
137
|
+
function getAlgorithmParams(sequenceNumber, initializationVector) {
|
|
148
138
|
return {
|
|
139
|
+
additionalData: sequenceNumber,
|
|
149
140
|
iv: initializationVector,
|
|
150
141
|
name: 'AES-GCM',
|
|
151
142
|
tagLength: 128, // 16 byte tag => 128 bits
|
|
@@ -183,7 +174,7 @@ function getRandomAssociationPort() {
|
|
|
183
174
|
}
|
|
184
175
|
function assertAssociationPort(port) {
|
|
185
176
|
if (port < 49152 || port > 65535) {
|
|
186
|
-
throw new
|
|
177
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE, `Association port number must be between 49152 and 65535. ${port} given.`, { port });
|
|
187
178
|
}
|
|
188
179
|
return port;
|
|
189
180
|
}
|
|
@@ -223,7 +214,7 @@ function getIntentURL(methodPathname, intentUrlBase) {
|
|
|
223
214
|
}
|
|
224
215
|
catch (_a) { } // eslint-disable-line no-empty
|
|
225
216
|
if ((baseUrl === null || baseUrl === void 0 ? void 0 : baseUrl.protocol) !== 'https:') {
|
|
226
|
-
throw new
|
|
217
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
227
218
|
}
|
|
228
219
|
}
|
|
229
220
|
baseUrl || (baseUrl = new URL(`${INTENT_NAME}:/`));
|
|
@@ -318,7 +309,7 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
318
309
|
}
|
|
319
310
|
}
|
|
320
311
|
catch (e) {
|
|
321
|
-
throw new
|
|
312
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, 'Found no installed wallet that supports the mobile wallet protocol.');
|
|
322
313
|
}
|
|
323
314
|
}
|
|
324
315
|
return randomAssociationPort;
|
|
@@ -326,32 +317,44 @@ function startSession(associationPublicKey, associationURLBase) {
|
|
|
326
317
|
}
|
|
327
318
|
|
|
328
319
|
const WEBSOCKET_CONNECTION_CONFIG = {
|
|
329
|
-
maxAttempts: 34,
|
|
330
320
|
/**
|
|
331
321
|
* 300 milliseconds is a generally accepted threshold for what someone
|
|
332
322
|
* would consider an acceptable response time for a user interface
|
|
333
|
-
* after having performed a low-attention tapping task. We set the
|
|
323
|
+
* after having performed a low-attention tapping task. We set the initial
|
|
334
324
|
* interval at which we wait for the wallet to set up the websocket at
|
|
335
|
-
* half this, as per the Nyquist frequency
|
|
325
|
+
* half this, as per the Nyquist frequency, with a progressive backoff
|
|
326
|
+
* sequence from there. The total wait time is 30s, which allows for the
|
|
327
|
+
* user to be presented with a disambiguation dialog, select a wallet, and
|
|
328
|
+
* for the wallet app to subsequently start.
|
|
336
329
|
*/
|
|
337
|
-
|
|
330
|
+
retryDelayScheduleMs: [150, 150, 200, 500, 500, 750, 750, 1000],
|
|
331
|
+
timeoutMs: 30000,
|
|
338
332
|
};
|
|
339
333
|
const WEBSOCKET_PROTOCOL = 'com.solana.mobilewalletadapter.v1';
|
|
340
334
|
function assertSecureContext() {
|
|
341
335
|
if (typeof window === 'undefined' || window.isSecureContext !== true) {
|
|
342
|
-
throw new
|
|
336
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SECURE_CONTEXT_REQUIRED, 'The mobile wallet adapter protocol must be used in a secure context (`https`).');
|
|
343
337
|
}
|
|
344
338
|
}
|
|
339
|
+
function getSequenceNumberFromByteArray(byteArray) {
|
|
340
|
+
const view = new DataView(byteArray);
|
|
341
|
+
return view.getUint32(0, /* littleEndian */ false);
|
|
342
|
+
}
|
|
345
343
|
function transact(callback, config) {
|
|
346
344
|
return __awaiter(this, void 0, void 0, function* () {
|
|
347
345
|
assertSecureContext();
|
|
348
346
|
const associationKeypair = yield generateAssociationKeypair();
|
|
349
347
|
const sessionPort = yield startSession(associationKeypair.publicKey, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
350
348
|
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
349
|
+
let connectionStartTime;
|
|
350
|
+
const getNextRetryDelayMs = (() => {
|
|
351
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
352
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
353
|
+
})();
|
|
351
354
|
let nextJsonRpcMessageId = 1;
|
|
355
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
352
356
|
let state = { __type: 'disconnected' };
|
|
353
357
|
return new Promise((resolve, reject) => {
|
|
354
|
-
let attempts = 0;
|
|
355
358
|
let socket;
|
|
356
359
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
360
|
const jsonRpcResponsePromises = {};
|
|
@@ -376,18 +379,19 @@ function transact(callback, config) {
|
|
|
376
379
|
state = { __type: 'disconnected' };
|
|
377
380
|
}
|
|
378
381
|
else {
|
|
379
|
-
reject(new
|
|
382
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
380
383
|
}
|
|
381
384
|
disposeSocket();
|
|
382
385
|
};
|
|
383
386
|
const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
|
|
384
387
|
disposeSocket();
|
|
385
|
-
if (
|
|
386
|
-
reject(new
|
|
388
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
389
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
|
|
387
390
|
}
|
|
388
391
|
else {
|
|
389
392
|
yield new Promise((resolve) => {
|
|
390
|
-
|
|
393
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
394
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
391
395
|
});
|
|
392
396
|
attemptSocketConnection();
|
|
393
397
|
}
|
|
@@ -397,13 +401,19 @@ function transact(callback, config) {
|
|
|
397
401
|
switch (state.__type) {
|
|
398
402
|
case 'connected':
|
|
399
403
|
try {
|
|
404
|
+
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
405
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
406
|
+
if (sequenceNumber <= lastKnownInboundSequenceNumber) {
|
|
407
|
+
throw new Error('Encrypted message has invalid sequence number');
|
|
408
|
+
}
|
|
409
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
400
410
|
const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
401
411
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
402
412
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
403
413
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
404
414
|
}
|
|
405
415
|
catch (e) {
|
|
406
|
-
if (e instanceof
|
|
416
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
407
417
|
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
408
418
|
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
409
419
|
responsePromise.reject(e);
|
|
@@ -468,6 +478,9 @@ function transact(callback, config) {
|
|
|
468
478
|
disposeSocket();
|
|
469
479
|
}
|
|
470
480
|
state = { __type: 'connecting', associationKeypair };
|
|
481
|
+
if (connectionStartTime === undefined) {
|
|
482
|
+
connectionStartTime = Date.now();
|
|
483
|
+
}
|
|
471
484
|
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
|
|
472
485
|
socket.addEventListener('open', handleOpen);
|
|
473
486
|
socket.addEventListener('close', handleClose);
|
|
@@ -486,13 +499,8 @@ function transact(callback, config) {
|
|
|
486
499
|
});
|
|
487
500
|
}
|
|
488
501
|
|
|
489
|
-
exports.
|
|
490
|
-
exports.
|
|
502
|
+
exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
|
|
503
|
+
exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
|
|
491
504
|
exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtocolError;
|
|
492
|
-
exports.
|
|
493
|
-
exports.SolanaMobileWalletAdapterProtocolReauthorizeError = SolanaMobileWalletAdapterProtocolReauthorizeError;
|
|
494
|
-
exports.SolanaMobileWalletAdapterProtocolSessionClosedError = SolanaMobileWalletAdapterProtocolSessionClosedError;
|
|
495
|
-
exports.SolanaMobileWalletAdapterProtocolSessionEstablishmentError = SolanaMobileWalletAdapterProtocolSessionEstablishmentError;
|
|
496
|
-
exports.SolanaMobileWalletAdapterSecureContextRequiredError = SolanaMobileWalletAdapterSecureContextRequiredError;
|
|
497
|
-
exports.SolanaMobileWalletAdapterWalletNotInstalledError = SolanaMobileWalletAdapterWalletNotInstalledError;
|
|
505
|
+
exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
|
|
498
506
|
exports.transact = transact;
|