@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
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
|
-
|
|
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_SUBMITTED: -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,56 @@ 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 assertSecureEndpointSpecificURI(walletUriBase) {
|
|
340
|
+
let url;
|
|
341
|
+
try {
|
|
342
|
+
url = new URL(walletUriBase);
|
|
343
|
+
}
|
|
344
|
+
catch (_a) {
|
|
345
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Invalid base URL supplied by wallet');
|
|
346
|
+
}
|
|
347
|
+
if (url.protocol !== 'https:') {
|
|
348
|
+
throw new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_FORBIDDEN_WALLET_BASE_URL, 'Base URLs supplied by wallets must be valid `https` URLs');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function getSequenceNumberFromByteArray(byteArray) {
|
|
352
|
+
const view = new DataView(byteArray);
|
|
353
|
+
return view.getUint32(0, /* littleEndian */ false);
|
|
354
|
+
}
|
|
345
355
|
function transact(callback, config) {
|
|
346
356
|
return __awaiter(this, void 0, void 0, function* () {
|
|
347
357
|
assertSecureContext();
|
|
348
358
|
const associationKeypair = yield generateAssociationKeypair();
|
|
349
359
|
const sessionPort = yield startSession(associationKeypair.publicKey, config === null || config === void 0 ? void 0 : config.baseUri);
|
|
350
360
|
const websocketURL = `ws://localhost:${sessionPort}/solana-wallet`;
|
|
361
|
+
let connectionStartTime;
|
|
362
|
+
const getNextRetryDelayMs = (() => {
|
|
363
|
+
const schedule = [...WEBSOCKET_CONNECTION_CONFIG.retryDelayScheduleMs];
|
|
364
|
+
return () => (schedule.length > 1 ? schedule.shift() : schedule[0]);
|
|
365
|
+
})();
|
|
351
366
|
let nextJsonRpcMessageId = 1;
|
|
367
|
+
let lastKnownInboundSequenceNumber = 0;
|
|
352
368
|
let state = { __type: 'disconnected' };
|
|
353
369
|
return new Promise((resolve, reject) => {
|
|
354
|
-
let attempts = 0;
|
|
355
370
|
let socket;
|
|
356
371
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
372
|
const jsonRpcResponsePromises = {};
|
|
@@ -376,18 +391,19 @@ function transact(callback, config) {
|
|
|
376
391
|
state = { __type: 'disconnected' };
|
|
377
392
|
}
|
|
378
393
|
else {
|
|
379
|
-
reject(new
|
|
394
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_SESSION_CLOSED, `The wallet session dropped unexpectedly (${evt.code}: ${evt.reason}).`, { closeEvent: evt }));
|
|
380
395
|
}
|
|
381
396
|
disposeSocket();
|
|
382
397
|
};
|
|
383
398
|
const handleError = (_evt) => __awaiter(this, void 0, void 0, function* () {
|
|
384
399
|
disposeSocket();
|
|
385
|
-
if (
|
|
386
|
-
reject(new
|
|
400
|
+
if (Date.now() - connectionStartTime >= WEBSOCKET_CONNECTION_CONFIG.timeoutMs) {
|
|
401
|
+
reject(new SolanaMobileWalletAdapterError(SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND, `Failed to connect to the wallet websocket on port ${sessionPort}.`));
|
|
387
402
|
}
|
|
388
403
|
else {
|
|
389
404
|
yield new Promise((resolve) => {
|
|
390
|
-
|
|
405
|
+
const retryDelayMs = getNextRetryDelayMs();
|
|
406
|
+
retryWaitTimeoutId = window.setTimeout(resolve, retryDelayMs);
|
|
391
407
|
});
|
|
392
408
|
attemptSocketConnection();
|
|
393
409
|
}
|
|
@@ -397,13 +413,19 @@ function transact(callback, config) {
|
|
|
397
413
|
switch (state.__type) {
|
|
398
414
|
case 'connected':
|
|
399
415
|
try {
|
|
416
|
+
const sequenceNumberVector = responseBuffer.slice(0, SEQUENCE_NUMBER_BYTES);
|
|
417
|
+
const sequenceNumber = getSequenceNumberFromByteArray(sequenceNumberVector);
|
|
418
|
+
if (sequenceNumber <= lastKnownInboundSequenceNumber) {
|
|
419
|
+
throw new Error('Encrypted message has invalid sequence number');
|
|
420
|
+
}
|
|
421
|
+
lastKnownInboundSequenceNumber = sequenceNumber;
|
|
400
422
|
const jsonRpcMessage = yield decryptJsonRpcMessage(responseBuffer, state.sharedSecret);
|
|
401
423
|
const responsePromise = jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
402
424
|
delete jsonRpcResponsePromises[jsonRpcMessage.id];
|
|
403
425
|
responsePromise.resolve(jsonRpcMessage.result);
|
|
404
426
|
}
|
|
405
427
|
catch (e) {
|
|
406
|
-
if (e instanceof
|
|
428
|
+
if (e instanceof SolanaMobileWalletAdapterProtocolError) {
|
|
407
429
|
const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
408
430
|
delete jsonRpcResponsePromises[e.jsonRpcMessageId];
|
|
409
431
|
responsePromise.reject(e);
|
|
@@ -433,7 +455,28 @@ function transact(callback, config) {
|
|
|
433
455
|
params,
|
|
434
456
|
}, sharedSecret));
|
|
435
457
|
return new Promise((resolve, reject) => {
|
|
436
|
-
jsonRpcResponsePromises[id] = {
|
|
458
|
+
jsonRpcResponsePromises[id] = {
|
|
459
|
+
resolve(result) {
|
|
460
|
+
switch (p) {
|
|
461
|
+
case 'authorize':
|
|
462
|
+
case 'reauthorize': {
|
|
463
|
+
const { wallet_uri_base } = result;
|
|
464
|
+
if (wallet_uri_base != null) {
|
|
465
|
+
try {
|
|
466
|
+
assertSecureEndpointSpecificURI(wallet_uri_base);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
reject(e);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
resolve(result);
|
|
477
|
+
},
|
|
478
|
+
reject,
|
|
479
|
+
};
|
|
437
480
|
});
|
|
438
481
|
});
|
|
439
482
|
};
|
|
@@ -468,6 +511,9 @@ function transact(callback, config) {
|
|
|
468
511
|
disposeSocket();
|
|
469
512
|
}
|
|
470
513
|
state = { __type: 'connecting', associationKeypair };
|
|
514
|
+
if (connectionStartTime === undefined) {
|
|
515
|
+
connectionStartTime = Date.now();
|
|
516
|
+
}
|
|
471
517
|
socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
|
|
472
518
|
socket.addEventListener('open', handleOpen);
|
|
473
519
|
socket.addEventListener('close', handleClose);
|
|
@@ -486,13 +532,8 @@ function transact(callback, config) {
|
|
|
486
532
|
});
|
|
487
533
|
}
|
|
488
534
|
|
|
489
|
-
exports.
|
|
490
|
-
exports.
|
|
535
|
+
exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
|
|
536
|
+
exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
|
|
491
537
|
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;
|
|
538
|
+
exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
|
|
498
539
|
exports.transact = transact;
|