@solana-mobile/mobile-wallet-adapter-protocol 0.0.1-alpha.5 → 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 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. `sign_message`) as per the spec.
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('sign_message', {
23
+ const {signed_payloads} = await wallet.signMessages({
24
24
  auth_token,
25
25
  payloads: [/* ... */],
26
26
  });
@@ -38,18 +38,24 @@ 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('sign_transaction', /* ... */);
41
+ await wallet.signTransactions(/* ... */);
42
42
  } catch (e) {
43
- if (e instanceof SolanaMobileWalletAdapterProtocolReauthorizeError) {
43
+ if (
44
+ e instanceof SolanaMobileWalletAdapterProtocolError &&
45
+ e.code === SolanaMobileWalletAdapterProtocolErrorCode.ERROR_REAUTHORIZE
46
+ ) {
44
47
  console.error('The auth token has gone stale');
45
- await wallet('reauthorize', {auth_token});
48
+ await wallet.reauthorize({auth_token});
46
49
  // Retry...
47
50
  }
48
51
  throw e;
49
52
  }
50
53
  });
51
54
  } catch(e) {
52
- if (e instanceof SolanaMobileWalletAdapterWalletNotInstalledError) {
55
+ if (
56
+ e instanceof SolanaMobileWalletAdapterError &&
57
+ e.code === SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND
58
+ ) {
53
59
  /* ... */
54
60
  }
55
61
  throw e;
@@ -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.1.2"
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
- lateinit var localAssociation: LocalAssociationScenario
53
- val scenarioCallbacks = object : Scenario.Callbacks {
54
- override fun onScenarioReady(client: MobileWalletAdapterClient) {
55
- sessionState = SessionState(client, localAssociation, semTerminated)
56
- semConnectedOrFailed.release()
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
- localAssociation.start()
73
- withTimeout(ASSOCIATION_TIMEOUT_MS.toLong()) {
74
- semConnectedOrFailed.acquire()
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: TimeoutCancellationException) {
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
- withTimeout(ASSOCIATION_TIMEOUT_MS.toLong()) {
120
- it.semSessionTermination.acquire()
121
- }
118
+ .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
122
119
  cleanup()
123
120
  promise.resolve(true)
124
- } catch (e: TimeoutCancellationException) {
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)
@@ -2,65 +2,41 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- class SolanaMobileWalletAdapterSecureContextRequiredError extends Error {
6
- constructor() {
7
- super('The mobile wallet adapter protocol must be used in a secure context (`https`).');
8
- this.name = 'SolanaMobileWalletAdapterSecureContextRequiredError';
9
- }
10
- }
11
- class SolanaMobileWalletAdapterForbiddenWalletBaseURLError extends Error {
12
- constructor() {
13
- super('Base URLs supplied by wallets must be valid `https` URLs');
14
- this.name = 'SolanaMobileWalletAdapterForbiddenWalletBaseURLError';
15
- }
16
- }
17
- class SolanaMobileWalletAdapterWalletNotInstalledError extends Error {
18
- constructor() {
19
- super(`Found no installed wallet that supports the mobile wallet protocol.`);
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 SolanaMobileWalletAdapterProtocolError = {
49
- ERROR_REAUTHORIZE: -1,
50
- ERROR_AUTHORIZATION_FAILED: -2,
51
- ERROR_INVALID_PAYLOAD: -3,
52
- ERROR_NOT_SIGNED: -4,
53
- ERROR_NOT_COMMITTED: -5,
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 SolanaMobileWalletAdapterProtocolJsonRpcError extends Error {
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 = 'SolanaNativeWalletAdapterJsonRpcError';
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(initializationVector), 0);
130
- response.set(new Uint8Array(ciphertext), initializationVector.byteLength);
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 initializationVector = message.slice(0, INITIALIZATION_VECTOR_BYTES);
137
- const ciphertext = message.slice(INITIALIZATION_VECTOR_BYTES);
138
- const plaintextBuffer = yield crypto.subtle.decrypt(getAlgorithmParams(initializationVector), sharedSecret, ciphertext);
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 SolanaMobileWalletAdapterProtocolJsonRpcError(jsonRpcMessage.id, jsonRpcMessage.error.code, jsonRpcMessage.error.message);
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 SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError(port);
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 SolanaMobileWalletAdapterForbiddenWalletBaseURLError();
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 SolanaMobileWalletAdapterWalletNotInstalledError();
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
- retryDelayMs: 150,
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 SolanaMobileWalletAdapterSecureContextRequiredError();
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 SolanaMobileWalletAdapterProtocolSessionClosedError(evt.code, evt.reason));
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 (++attempts >= WEBSOCKET_CONNECTION_CONFIG.maxAttempts) {
386
- reject(new SolanaMobileWalletAdapterProtocolSessionEstablishmentError(sessionPort));
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
- retryWaitTimeoutId = window.setTimeout(resolve, WEBSOCKET_CONNECTION_CONFIG.retryDelayMs);
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 SolanaMobileWalletAdapterProtocolJsonRpcError) {
416
+ if (e instanceof SolanaMobileWalletAdapterProtocolError) {
407
417
  const responsePromise = jsonRpcResponsePromises[e.jsonRpcMessageId];
408
418
  delete jsonRpcResponsePromises[e.jsonRpcMessageId];
409
419
  responsePromise.reject(e);
@@ -416,20 +426,39 @@ function transact(callback, config) {
416
426
  case 'hello_req_sent': {
417
427
  const sharedSecret = yield parseHelloRsp(responseBuffer, state.associationPublicKey, state.ecdhPrivateKey);
418
428
  state = { __type: 'connected', sharedSecret };
419
- const walletAPI = (method, params) => __awaiter(this, void 0, void 0, function* () {
420
- const id = nextJsonRpcMessageId++;
421
- socket.send(yield encryptJsonRpcMessage({
422
- id,
423
- jsonrpc: '2.0',
424
- method,
425
- params,
426
- }, sharedSecret));
427
- return new Promise((resolve, reject) => {
428
- jsonRpcResponsePromises[id] = { resolve, reject };
429
- });
429
+ const wallet = new Proxy({}, {
430
+ get(target, p) {
431
+ if (target[p] == null) {
432
+ const method = p
433
+ .toString()
434
+ .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
435
+ .toLowerCase();
436
+ target[p] = function (params) {
437
+ return __awaiter(this, void 0, void 0, function* () {
438
+ const id = nextJsonRpcMessageId++;
439
+ socket.send(yield encryptJsonRpcMessage({
440
+ id,
441
+ jsonrpc: '2.0',
442
+ method,
443
+ params,
444
+ }, sharedSecret));
445
+ return new Promise((resolve, reject) => {
446
+ jsonRpcResponsePromises[id] = { resolve, reject };
447
+ });
448
+ });
449
+ };
450
+ }
451
+ return target[p];
452
+ },
453
+ defineProperty() {
454
+ return false;
455
+ },
456
+ deleteProperty() {
457
+ return false;
458
+ },
430
459
  });
431
460
  try {
432
- resolve(yield callback(walletAPI));
461
+ resolve(yield callback(wallet));
433
462
  }
434
463
  catch (e) {
435
464
  reject(e);
@@ -449,6 +478,9 @@ function transact(callback, config) {
449
478
  disposeSocket();
450
479
  }
451
480
  state = { __type: 'connecting', associationKeypair };
481
+ if (connectionStartTime === undefined) {
482
+ connectionStartTime = Date.now();
483
+ }
452
484
  socket = new WebSocket(websocketURL, [WEBSOCKET_PROTOCOL]);
453
485
  socket.addEventListener('open', handleOpen);
454
486
  socket.addEventListener('close', handleClose);
@@ -467,13 +499,8 @@ function transact(callback, config) {
467
499
  });
468
500
  }
469
501
 
470
- exports.SolanaMobileWalletAdapterForbiddenWalletBaseURLError = SolanaMobileWalletAdapterForbiddenWalletBaseURLError;
471
- exports.SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError = SolanaMobileWalletAdapterProtocolAssociationPortOutOfRangeError;
502
+ exports.SolanaMobileWalletAdapterError = SolanaMobileWalletAdapterError;
503
+ exports.SolanaMobileWalletAdapterErrorCode = SolanaMobileWalletAdapterErrorCode;
472
504
  exports.SolanaMobileWalletAdapterProtocolError = SolanaMobileWalletAdapterProtocolError;
473
- exports.SolanaMobileWalletAdapterProtocolJsonRpcError = SolanaMobileWalletAdapterProtocolJsonRpcError;
474
- exports.SolanaMobileWalletAdapterProtocolReauthorizeError = SolanaMobileWalletAdapterProtocolReauthorizeError;
475
- exports.SolanaMobileWalletAdapterProtocolSessionClosedError = SolanaMobileWalletAdapterProtocolSessionClosedError;
476
- exports.SolanaMobileWalletAdapterProtocolSessionEstablishmentError = SolanaMobileWalletAdapterProtocolSessionEstablishmentError;
477
- exports.SolanaMobileWalletAdapterSecureContextRequiredError = SolanaMobileWalletAdapterSecureContextRequiredError;
478
- exports.SolanaMobileWalletAdapterWalletNotInstalledError = SolanaMobileWalletAdapterWalletNotInstalledError;
505
+ exports.SolanaMobileWalletAdapterProtocolErrorCode = SolanaMobileWalletAdapterProtocolErrorCode;
479
506
  exports.transact = transact;