@solana-mobile/mobile-wallet-adapter-protocol 2.2.2 → 2.2.3

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.
@@ -8,7 +8,7 @@ buildscript {
8
8
  }
9
9
 
10
10
  dependencies {
11
- classpath 'com.android.tools.build:gradle:8.10.1'
11
+ classpath 'com.android.tools.build:gradle:8.12.0'
12
12
  // noinspection DifferentKotlinGradleVersion
13
13
  classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
14
  }
@@ -144,7 +144,7 @@ def kotlin_version = getExtOrDefault('kotlinVersion')
144
144
  dependencies {
145
145
  //noinspection GradleDynamicVersion
146
146
  implementation "com.facebook.react:react-native:+" // From node_modules
147
- implementation "com.solanamobile:mobile-wallet-adapter-clientlib:2.0.7"
147
+ implementation "com.solanamobile:mobile-wallet-adapter-clientlib:2.0.8"
148
148
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
149
149
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0"
150
150
  }
@@ -6,6 +6,8 @@ import android.content.Intent
6
6
  import android.net.Uri
7
7
  import android.util.Log
8
8
  import com.facebook.react.bridge.*
9
+ import com.facebook.react.jstasks.HeadlessJsTaskConfig
10
+ import com.facebook.react.jstasks.HeadlessJsTaskContext
9
11
  import com.solana.mobilewalletadapter.clientlib.protocol.JsonRpc20Client
10
12
  import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient
11
13
  import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
@@ -21,40 +23,49 @@ import kotlinx.coroutines.sync.Mutex
21
23
  import org.json.JSONObject
22
24
 
23
25
  class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
24
- SolanaMobileWalletAdapterSpec(reactContext), CoroutineScope {
26
+ SolanaMobileWalletAdapterSpec(reactContext), CoroutineScope {
25
27
 
26
28
  data class SessionState(
27
- val client: MobileWalletAdapterClient,
28
- val localAssociation: LocalAssociationScenario,
29
+ val client: MobileWalletAdapterClient,
30
+ val localAssociation: LocalAssociationScenario,
29
31
  )
30
32
 
31
33
  override val coroutineContext =
32
- Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()
34
+ Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()
33
35
 
34
36
  companion object {
35
37
  const val NAME = "SolanaMobileWalletAdapter"
38
+ private const val TAG = "SolanaMobileWalletAdapterModule"
36
39
  private const val ASSOCIATION_TIMEOUT_MS = 10000
37
40
  private const val CLIENT_TIMEOUT_MS = 90000
38
41
  private const val REQUEST_LOCAL_ASSOCIATION = 0
39
-
40
- // Used to ensure that you can't start more than one session at a time.
41
- private val mutex: Mutex = Mutex()
42
- private var sessionState: SessionState? = null
43
- private var associationResultCallback: ((Int) -> Unit)? = null
44
42
  }
45
43
 
44
+ // Used to ensure that you can't start more than one session at a time.
45
+ private val mutex: Mutex = Mutex()
46
+ private var sessionState: SessionState? = null
47
+ private var associationResultCallback: ((Int) -> Unit)? = null
48
+
46
49
  private val mActivityEventListener: ActivityEventListener =
47
- object : BaseActivityEventListener() {
48
- override fun onActivityResult(
49
- activity: Activity?,
50
- requestCode: Int,
51
- resultCode: Int,
52
- data: Intent?
53
- ) {
54
- if (requestCode == REQUEST_LOCAL_ASSOCIATION)
55
- associationResultCallback?.invoke(resultCode)
56
- }
50
+ object : BaseActivityEventListener() {
51
+ override fun onActivityResult(
52
+ activity: Activity?,
53
+ requestCode: Int,
54
+ resultCode: Int,
55
+ data: Intent?
56
+ ) {
57
+ if (requestCode == REQUEST_LOCAL_ASSOCIATION)
58
+ associationResultCallback?.invoke(resultCode)
57
59
  }
60
+ }
61
+
62
+ private val sessionBackgroundTaskConfig
63
+ get() = HeadlessJsTaskConfig(
64
+ taskKey = "SolanaMobileWalletAdapterSessionBackgroundTask",
65
+ timeout = 0,
66
+ data = Arguments.createMap(),
67
+ isAllowedInForeground = true
68
+ )
58
69
 
59
70
  init {
60
71
  reactContext.addActivityEventListener(mActivityEventListener)
@@ -68,67 +79,87 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
68
79
  override fun startSession(config: ReadableMap?, promise: Promise): Unit {
69
80
  launch {
70
81
  mutex.lock()
71
- Log.d(name, "startSession with config $config")
82
+ Log.d(TAG, "startSession with config $config")
83
+ var sessionTaskId: Int? = null
84
+ val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactApplicationContext)
85
+ val finishHeadlessTask = { taskId: Int? ->
86
+ try {
87
+ if (taskId != null && headlessJsTaskContext.isTaskRunning(taskId)) {
88
+ headlessJsTaskContext.finishTask(taskId)
89
+ }
90
+ } catch (e: Exception) {
91
+ Log.w(TAG, "Failed to finish headless JS task", e)
92
+ }
93
+ }
72
94
  try {
73
95
  val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
74
96
  val localAssociation =
75
- LocalAssociationScenario(
76
- CLIENT_TIMEOUT_MS,
77
- )
97
+ LocalAssociationScenario(
98
+ CLIENT_TIMEOUT_MS,
99
+ )
78
100
  val intent =
79
- LocalAssociationIntentCreator.createAssociationIntent(
80
- uriPrefix,
81
- localAssociation.port,
82
- localAssociation.session
83
- )
101
+ LocalAssociationIntentCreator.createAssociationIntent(
102
+ uriPrefix,
103
+ localAssociation.port,
104
+ localAssociation.session
105
+ )
106
+ withContext(Dispatchers.Main) {
107
+ sessionTaskId = headlessJsTaskContext.startTask(sessionBackgroundTaskConfig)
108
+ }
84
109
  associationResultCallback = { resultCode ->
85
110
  if (resultCode == Activity.RESULT_CANCELED) {
86
- Log.d(name, "Local association cancelled by user, ending session")
111
+ Log.d(TAG, "Local association cancelled by user, ending session")
87
112
  promise.reject(
88
- "Session not established: Local association cancelled by user",
89
- LocalAssociationScenario.ConnectionFailedException(
90
- "Local association cancelled by user"
91
- )
113
+ "Session not established: Local association cancelled by user",
114
+ LocalAssociationScenario.ConnectionFailedException(
115
+ "Local association cancelled by user"
116
+ )
92
117
  )
93
118
  localAssociation.close()
94
119
  }
120
+
121
+ // stop the headless js task, regardless if the association was successful or not
122
+ finishHeadlessTask(sessionTaskId)
95
123
  }
96
124
  currentActivity?.startActivityForResult(intent, REQUEST_LOCAL_ASSOCIATION)
97
- ?: throw NullPointerException(
98
- "Could not find a current activity from which to launch a local association"
99
- )
100
- val client =
101
- localAssociation
102
- .start()
103
- .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
125
+ ?: throw NullPointerException(
126
+ "Could not find a current activity from which to launch a local association"
127
+ )
128
+ val client = localAssociation.start()
129
+ .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
104
130
  sessionState = SessionState(client, localAssociation)
105
131
  val sessionPropertiesMap: WritableMap = WritableNativeMap()
106
132
  sessionPropertiesMap.putString(
107
- "protocol_version",
108
- when (localAssociation.session.sessionProperties.protocolVersion) {
109
- ProtocolVersion.LEGACY -> "legacy"
110
- ProtocolVersion.V1 -> "v1"
111
- }
133
+ "protocol_version",
134
+ when (localAssociation.session.sessionProperties.protocolVersion) {
135
+ ProtocolVersion.LEGACY -> "legacy"
136
+ ProtocolVersion.V1 -> "v1"
137
+ }
112
138
  )
113
139
  promise.resolve(sessionPropertiesMap)
114
140
  } catch (e: ActivityNotFoundException) {
115
- Log.e(name, "Found no installed wallet that supports the mobile wallet protocol", e)
141
+ Log.e(TAG, "Found no installed wallet that supports the mobile wallet protocol", e)
142
+ finishHeadlessTask(sessionTaskId)
116
143
  cleanup()
117
144
  promise.reject("ERROR_WALLET_NOT_FOUND", e)
118
145
  } catch (e: TimeoutException) {
119
- Log.e(name, "Timed out waiting for local association to be ready", e)
146
+ Log.e(TAG, "Timed out waiting for local association to be ready", e)
147
+ finishHeadlessTask(sessionTaskId)
120
148
  cleanup()
121
149
  promise.reject("Timed out waiting for local association to be ready", e)
122
150
  } catch (e: InterruptedException) {
123
- Log.w(name, "Interrupted while waiting for local association to be ready", e)
151
+ Log.w(TAG, "Interrupted while waiting for local association to be ready", e)
152
+ finishHeadlessTask(sessionTaskId)
124
153
  cleanup()
125
154
  promise.reject(e)
126
155
  } catch (e: ExecutionException) {
127
- Log.e(name, "Failed establishing local association with wallet", e.cause)
156
+ Log.e(TAG, "Failed establishing local association with wallet", e.cause)
157
+ finishHeadlessTask(sessionTaskId)
128
158
  cleanup()
129
159
  promise.reject(e)
130
160
  } catch (e: Throwable) {
131
- Log.e(name, "Failed to start session", e)
161
+ Log.e(TAG, "Failed to start session", e)
162
+ finishHeadlessTask(sessionTaskId)
132
163
  cleanup()
133
164
  promise.reject(e)
134
165
  }
@@ -137,58 +168,54 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
137
168
 
138
169
  @ReactMethod
139
170
  override fun invoke(method: String, params: ReadableMap?, promise: Promise): Unit =
140
- sessionState?.let {
141
- Log.d(name, "invoke `$method` with params $params")
142
- try {
143
- val result =
144
- it.client
145
- .methodCall(method, convertMapToJson(params), CLIENT_TIMEOUT_MS)
146
- .get() as
147
- JSONObject
148
- promise.resolve(convertJsonToMap(result))
149
- } catch (e: ExecutionException) {
150
- val cause = e.cause
151
- if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
152
- val userInfo = Arguments.createMap()
153
- userInfo.putInt("jsonRpcErrorCode", cause.code)
154
- promise.reject("JSON_RPC_ERROR", cause, userInfo)
155
- } else if (cause is TimeoutException) {
156
- promise.reject("Timed out waiting for response", e)
157
- } else {
158
- throw e
159
- }
160
- } catch (e: Throwable) {
161
- Log.e(name, "Failed to invoke `$method` with params $params", e)
162
- promise.reject(e)
171
+ sessionState?.let {
172
+ Log.d(TAG, "invoke `$method` with params $params")
173
+ try {
174
+ val result = it.client
175
+ .methodCall(method, convertMapToJson(params), CLIENT_TIMEOUT_MS)
176
+ .get() as JSONObject
177
+ promise.resolve(convertJsonToMap(result))
178
+ } catch (e: ExecutionException) {
179
+ val cause = e.cause
180
+ if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
181
+ val userInfo = Arguments.createMap()
182
+ userInfo.putInt("jsonRpcErrorCode", cause.code)
183
+ promise.reject("JSON_RPC_ERROR", cause, userInfo)
184
+ } else if (cause is TimeoutException) {
185
+ promise.reject("Timed out waiting for response", e)
186
+ } else {
187
+ throw e
163
188
  }
189
+ } catch (e: Throwable) {
190
+ Log.e(TAG, "Failed to invoke `$method` with params $params", e)
191
+ promise.reject(e)
164
192
  }
165
- ?: throw NullPointerException(
166
- "Tried to invoke `$method` without an active session"
167
- )
193
+ } ?: throw NullPointerException(
194
+ "Tried to invoke `$method` without an active session"
195
+ )
168
196
 
169
197
  @ReactMethod
170
198
  override fun endSession(promise: Promise): Unit {
171
199
  sessionState?.let {
172
200
  launch {
173
- Log.d(name, "endSession")
201
+ Log.d(TAG, "endSession")
174
202
  try {
175
203
  it.localAssociation
176
- .close()
177
- .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
204
+ .close()
205
+ .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
178
206
  cleanup()
179
207
  promise.resolve(true)
180
208
  } catch (e: TimeoutException) {
181
- Log.e(name, "Timed out waiting for local association to close", e)
209
+ Log.e(TAG, "Timed out waiting for local association to close", e)
182
210
  cleanup()
183
211
  promise.reject("Failed to end session", e)
184
212
  } catch (e: Throwable) {
185
- Log.e(name, "Failed to end session", e)
213
+ Log.e(TAG, "Failed to end session", e)
186
214
  cleanup()
187
215
  promise.reject("Failed to end session", e)
188
216
  }
189
217
  }
190
- }
191
- ?: throw NullPointerException("Tried to end a session without an active session")
218
+ } ?: throw NullPointerException("Tried to end a session without an active session")
192
219
  }
193
220
 
194
221
  private fun cleanup() {
@@ -247,6 +247,14 @@ function signInFallback(signInPayload, authorizationResult, protocolRequestHandl
247
247
  });
248
248
  }
249
249
 
250
+ reactNative.AppRegistry.registerHeadlessTask('SolanaMobileWalletAdapterSessionBackgroundTask', () => {
251
+ return () => __awaiter(void 0, void 0, void 0, function* () {
252
+ // This is a no-op task that is used to keep the app alive while the session is active.
253
+ // The actual session management is handled in the native module.
254
+ // This is necessary for the React Native Android implementation to work correctly.
255
+ // The task is started before startActivityResult and stopped when the activity result callback is triggered
256
+ });
257
+ });
250
258
  const LINKING_ERROR = `The package 'solana-mobile-wallet-adapter-protocol' doesn't seem to be linked. Make sure: \n\n` +
251
259
  '- You rebuilt the app after installing the package\n' +
252
260
  '- If you are using Lerna workspaces\n' +
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@solana-mobile/mobile-wallet-adapter-protocol",
3
3
  "description": "An implementation of the Solana Mobile Mobile Wallet Adapter protocol. Use this to open a session with a mobile wallet app, and to issue API calls to it.",
4
- "version": "2.2.2",
4
+ "version": "2.2.3",
5
5
  "author": "Steven Luscher <steven.luscher@solanamobile.com>",
6
6
  "repository": {
7
7
  "type": "git",
package/.gitignore DELETED
@@ -1,2 +0,0 @@
1
- lib/
2
- android/build
@@ -1,14 +0,0 @@
1
- # OSX
2
- #
3
- .DS_Store
4
-
5
- # Android/IJ
6
- #
7
- .classpath
8
- .cxx
9
- .gradle
10
- .idea
11
- .project
12
- .settings
13
- local.properties
14
- android.iml
@@ -1 +0,0 @@
1
- export { encode, fromUint8Array, toUint8Array } from 'js-base64';
@@ -1,91 +0,0 @@
1
- import { Platform } from 'react-native';
2
-
3
- import NativeSolanaMobileWalletAdapter from '../../codegenSpec/NativeSolanaMobileWalletAdapter.js';
4
- import createMobileWalletProxy from '../../createMobileWalletProxy.js';
5
- import { SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterProtocolError } from '../../errors.js';
6
- import { MobileWallet, SessionProperties, WalletAssociationConfig } from '../../types.js';
7
-
8
- type ReactNativeError = Error & { code?: string; userInfo?: Record<string, unknown> };
9
-
10
- const LINKING_ERROR =
11
- `The package 'solana-mobile-wallet-adapter-protocol' doesn't seem to be linked. Make sure: \n\n` +
12
- '- You rebuilt the app after installing the package\n' +
13
- '- If you are using Lerna workspaces\n' +
14
- ' - You have added `@solana-mobile/mobile-wallet-adapter-protocol` as an explicit dependency, and\n' +
15
- ' - You have added `@solana-mobile/mobile-wallet-adapter-protocol` to the `nohoist` section of your package.json\n' +
16
- '- You are not using Expo managed workflow\n';
17
-
18
- const SolanaMobileWalletAdapter =
19
- Platform.OS === 'android' && NativeSolanaMobileWalletAdapter
20
- ? NativeSolanaMobileWalletAdapter
21
- : (new Proxy(
22
- {},
23
- {
24
- get() {
25
- throw new Error(
26
- Platform.OS !== 'android'
27
- ? 'The package `solana-mobile-wallet-adapter-protocol` is only compatible with React Native Android'
28
- : LINKING_ERROR,
29
- );
30
- },
31
- },
32
- ) as typeof NativeSolanaMobileWalletAdapter);
33
-
34
- function getErrorMessage(e: ReactNativeError): string {
35
- switch (e.code) {
36
- case 'ERROR_WALLET_NOT_FOUND':
37
- return 'Found no installed wallet that supports the mobile wallet protocol.';
38
- default:
39
- return e.message;
40
- }
41
- }
42
-
43
- function handleError(e: any): never {
44
- if (e instanceof Error) {
45
- const reactNativeError: ReactNativeError = e;
46
- switch (reactNativeError.code) {
47
- case undefined:
48
- throw e;
49
- case 'JSON_RPC_ERROR': {
50
- const details = reactNativeError.userInfo as Readonly<{ jsonRpcErrorCode: number }>;
51
- throw new SolanaMobileWalletAdapterProtocolError(
52
- 0 /* jsonRpcMessageId */,
53
- details.jsonRpcErrorCode,
54
- e.message,
55
- );
56
- }
57
- default:
58
- throw new SolanaMobileWalletAdapterError<any>(
59
- reactNativeError.code,
60
- getErrorMessage(reactNativeError),
61
- reactNativeError.userInfo,
62
- );
63
- }
64
- }
65
- throw e;
66
- }
67
-
68
- export async function transact<TReturn>(
69
- callback: (wallet: MobileWallet) => TReturn,
70
- config?: WalletAssociationConfig,
71
- ): Promise<TReturn> {
72
- let didSuccessfullyConnect = false;
73
- try {
74
- const sessionProperties: SessionProperties = await SolanaMobileWalletAdapter.startSession(config);
75
- didSuccessfullyConnect = true;
76
- const wallet = createMobileWalletProxy(sessionProperties.protocol_version, async (method, params) => {
77
- try {
78
- return SolanaMobileWalletAdapter.invoke(method, params);
79
- } catch (e) {
80
- return handleError(e);
81
- }
82
- });
83
- return await callback(wallet);
84
- } catch (e) {
85
- return handleError(e);
86
- } finally {
87
- if (didSuccessfullyConnect) {
88
- await SolanaMobileWalletAdapter.endSession();
89
- }
90
- }
91
- }
@@ -1,10 +0,0 @@
1
- // https://stackoverflow.com/a/9458996/802047
2
- export default function arrayBufferToBase64String(buffer: ArrayBuffer) {
3
- let binary = '';
4
- const bytes = new Uint8Array(buffer);
5
- const len = bytes.byteLength;
6
- for (let ii = 0; ii < len; ii++) {
7
- binary += String.fromCharCode(bytes[ii]);
8
- }
9
- return window.btoa(binary);
10
- }
@@ -1,19 +0,0 @@
1
- import { SolanaMobileWalletAdapterError, SolanaMobileWalletAdapterErrorCode } from './errors.js';
2
-
3
- declare const tag: unique symbol;
4
- export type AssociationPort = number & { readonly [tag]: 'AssociationPort' };
5
-
6
- export function getRandomAssociationPort(): AssociationPort {
7
- return assertAssociationPort(49152 + Math.floor(Math.random() * (65535 - 49152 + 1)));
8
- }
9
-
10
- export function assertAssociationPort(port: number): AssociationPort {
11
- if (port < 49152 || port > 65535) {
12
- throw new SolanaMobileWalletAdapterError(
13
- SolanaMobileWalletAdapterErrorCode.ERROR_ASSOCIATION_PORT_OUT_OF_RANGE,
14
- `Association port number must be between 49152 and 65535. ${port} given.`,
15
- { port },
16
- );
17
- }
18
- return port as AssociationPort;
19
- }
@@ -1,22 +0,0 @@
1
- export function encode(input: string): string {
2
- return window.btoa(input);
3
- }
4
-
5
- export function fromUint8Array(byteArray: Uint8Array, urlsafe?: boolean): string {
6
- const base64 = window.btoa(String.fromCharCode.call(null, ...byteArray));
7
- if (urlsafe) {
8
- return base64
9
- .replace(/\+/g, '-')
10
- .replace(/\//g, '_')
11
- .replace(/=+$/, '');
12
- } else return base64;
13
- }
14
-
15
- export function toUint8Array(base64EncodedByteArray: string): Uint8Array {
16
- return new Uint8Array(
17
- window
18
- .atob(base64EncodedByteArray)
19
- .split('')
20
- .map((c) => c.charCodeAt(0)),
21
- );
22
- }
@@ -1,12 +0,0 @@
1
- export default async function createHelloReq(ecdhPublicKey: CryptoKey, associationKeypairPrivateKey: CryptoKey) {
2
- const publicKeyBuffer = await crypto.subtle.exportKey('raw', ecdhPublicKey);
3
- const signatureBuffer = await crypto.subtle.sign(
4
- { hash: 'SHA-256', name: 'ECDSA' },
5
- associationKeypairPrivateKey,
6
- publicKeyBuffer,
7
- );
8
- const response = new Uint8Array(publicKeyBuffer.byteLength + signatureBuffer.byteLength);
9
- response.set(new Uint8Array(publicKeyBuffer), 0);
10
- response.set(new Uint8Array(signatureBuffer), publicKeyBuffer.byteLength);
11
- return response;
12
- }