@solana-mobile/mobile-wallet-adapter-protocol 1.0.0 → 1.0.1

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.
Files changed (36) hide show
  1. package/README.md +69 -62
  2. package/android/.gradle/4.4.1/fileChanges/last-build.bin +0 -0
  3. package/android/.gradle/4.4.1/fileHashes/fileHashes.bin +0 -0
  4. package/android/.gradle/4.4.1/fileHashes/fileHashes.lock +0 -0
  5. package/android/.gradle/7.1/dependencies-accessors/dependencies-accessors.lock +0 -0
  6. package/android/.gradle/7.1/dependencies-accessors/gc.properties +0 -0
  7. package/android/.gradle/7.1/fileChanges/last-build.bin +0 -0
  8. package/android/.gradle/7.1/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/7.1/gc.properties +0 -0
  10. package/android/.gradle/7.5/checksums/checksums.lock +0 -0
  11. package/android/.gradle/7.5/dependencies-accessors/dependencies-accessors.lock +0 -0
  12. package/android/.gradle/7.5/dependencies-accessors/gc.properties +0 -0
  13. package/android/.gradle/7.5/fileChanges/last-build.bin +0 -0
  14. package/android/.gradle/7.5/fileHashes/fileHashes.lock +0 -0
  15. package/android/.gradle/7.5/gc.properties +0 -0
  16. package/android/.gradle/7.5.1/checksums/checksums.lock +0 -0
  17. package/android/.gradle/7.5.1/checksums/sha1-checksums.bin +0 -0
  18. package/android/.gradle/7.5.1/dependencies-accessors/dependencies-accessors.lock +0 -0
  19. package/android/.gradle/7.5.1/dependencies-accessors/gc.properties +0 -0
  20. package/android/.gradle/7.5.1/fileChanges/last-build.bin +0 -0
  21. package/android/.gradle/7.5.1/fileHashes/fileHashes.lock +0 -0
  22. package/android/.gradle/7.5.1/gc.properties +0 -0
  23. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  24. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  25. package/android/.gradle/checksums/checksums.lock +0 -0
  26. package/android/.gradle/checksums/md5-checksums.bin +0 -0
  27. package/android/.gradle/checksums/sha1-checksums.bin +0 -0
  28. package/android/.gradle/vcs-1/gc.properties +0 -0
  29. package/android/build.gradle +146 -146
  30. package/android/gradle/wrapper/gradle-wrapper.properties +5 -5
  31. package/android/src/main/java/com/solanamobile/mobilewalletadapter/reactnative/SolanaMobileWalletAdapterModule.kt +176 -139
  32. package/lib/types/index.browser.d.ts +1 -0
  33. package/lib/types/index.d.ts +1 -0
  34. package/lib/types/index.native.d.ts +1 -0
  35. package/package.json +1 -1
  36. package/src/types.ts +111 -111
package/README.md CHANGED
@@ -1,63 +1,70 @@
1
- # `@solana-mobile/mobile-wallet-adapter-protocol`
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_messages`) as per the spec.
4
-
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
-
7
- ## Quick start
8
-
9
- Use this API to start a session:
10
-
11
- ```typescript
12
- import {transact} from '@solana-mobile/mobile-wallet-adapter-protocol';
13
-
14
- await transact(async (wallet) => {
15
- /* ... */
16
- });
17
- ```
18
-
19
- The callback you provide will be called once a session has been established with a wallet. It will recieve the `MobileWallet` API as an argument. You can call protocol-specified methods using this function. Whatever you return from this callback will be returned by `transact`.
20
-
21
- ```typescript
22
- const signedPayloads = await transact(async (wallet) => {
23
- const {signed_payloads} = await wallet.signMessages({
24
- auth_token,
25
- payloads: [/* ... */],
26
- });
27
- return signed_payloads;
28
- });
29
- ```
30
-
31
- The wallet session will stay active until your callback returns. Typically, wallets will redirect back to your app once the session ends.
32
-
33
- ## Exception handling
34
-
35
- You can catch exceptions at any level. See `errors.ts` for a list of exceptions that might be thrown.
36
-
37
- ```typescript
38
- try {
39
- await transact(async (wallet) => {
40
- try {
41
- await wallet.signTransactions(/* ... */);
42
- } catch (e) {
43
- if (
44
- e instanceof SolanaMobileWalletAdapterProtocolError &&
45
- e.code === SolanaMobileWalletAdapterProtocolErrorCode.ERROR_REAUTHORIZE
46
- ) {
47
- console.error('The auth token has gone stale');
48
- await wallet.reauthorize({auth_token});
49
- // Retry...
50
- }
51
- throw e;
52
- }
53
- });
54
- } catch(e) {
55
- if (
56
- e instanceof SolanaMobileWalletAdapterError &&
57
- e.code === SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND
58
- ) {
59
- /* ... */
60
- }
61
- throw e;
62
- }
1
+ # `@solana-mobile/mobile-wallet-adapter-protocol`
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_messages`) as per the spec.
4
+
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
+
7
+ ## Learn how to use this API on our [documentation website](https://docs.solanamobile.com/):
8
+ - React Native
9
+ - [Quickstart Setup](https://docs.solanamobile.com/react-native/quickstart)
10
+ - [dApp Integration Guide](https://docs.solanamobile.com/react-native/mwa_integration_rn)
11
+ - [Hello World Tutorial](https://docs.solanamobile.com/getting-started/hello_world_tutorial)
12
+ - [Sample App Reference](https://docs.solanamobile.com/sample-apps/sample_app_overview)
13
+
14
+ ## Quick start
15
+
16
+ Use this API to start a session:
17
+
18
+ ```typescript
19
+ import {transact} from '@solana-mobile/mobile-wallet-adapter-protocol';
20
+
21
+ await transact(async (wallet) => {
22
+ /* ... */
23
+ });
24
+ ```
25
+
26
+ The callback you provide will be called once a session has been established with a wallet. It will recieve the `MobileWallet` API as an argument. You can call protocol-specified methods using this function. Whatever you return from this callback will be returned by `transact`.
27
+
28
+ ```typescript
29
+ const signedPayloads = await transact(async (wallet) => {
30
+ const {signed_payloads} = await wallet.signMessages({
31
+ auth_token,
32
+ payloads: [/* ... */],
33
+ });
34
+ return signed_payloads;
35
+ });
36
+ ```
37
+
38
+ The wallet session will stay active until your callback returns. Typically, wallets will redirect back to your app once the session ends.
39
+
40
+ ## Exception handling
41
+
42
+ You can catch exceptions at any level. See `errors.ts` for a list of exceptions that might be thrown.
43
+
44
+ ```typescript
45
+ try {
46
+ await transact(async (wallet) => {
47
+ try {
48
+ await wallet.signTransactions(/* ... */);
49
+ } catch (e) {
50
+ if (
51
+ e instanceof SolanaMobileWalletAdapterProtocolError &&
52
+ e.code === SolanaMobileWalletAdapterProtocolErrorCode.ERROR_REAUTHORIZE
53
+ ) {
54
+ console.error('The auth token has gone stale');
55
+ await wallet.reauthorize({auth_token, identity});
56
+ // Retry...
57
+ }
58
+ throw e;
59
+ }
60
+ });
61
+ } catch(e) {
62
+ if (
63
+ e instanceof SolanaMobileWalletAdapterError &&
64
+ e.code === SolanaMobileWalletAdapterErrorCode.ERROR_WALLET_NOT_FOUND
65
+ ) {
66
+ /* ... */
67
+ }
68
+ throw e;
69
+ }
63
70
  ```
File without changes
File without changes
File without changes
@@ -0,0 +1,2 @@
1
+ #Wed Mar 08 13:54:55 MST 2023
2
+ gradle.version=7.5
File without changes
@@ -1,146 +1,146 @@
1
- buildscript {
2
- // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
- def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['SolanaMobileWalletAdapterModule_kotlinVersion']
4
-
5
- repositories {
6
- google()
7
- mavenCentral()
8
- }
9
-
10
- dependencies {
11
- classpath 'com.android.tools.build:gradle:3.5.4'
12
- // noinspection DifferentKotlinGradleVersion
13
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
- }
15
- }
16
-
17
- def isNewArchitectureEnabled() {
18
- return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
- }
20
-
21
- apply plugin: 'com.android.library'
22
- apply plugin: 'kotlin-android'
23
-
24
- if (isNewArchitectureEnabled()) {
25
- apply plugin: 'com.facebook.react'
26
- }
27
-
28
- def getExtOrDefault(name) {
29
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['SolanaMobileWalletAdapterModule_' + name]
30
- }
31
-
32
- def getExtOrIntegerDefault(name) {
33
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['SolanaMobileWalletAdapterModule_' + name]).toInteger()
34
- }
35
-
36
- android {
37
- compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
38
-
39
- defaultConfig {
40
- minSdkVersion getExtOrIntegerDefault('minSdkVersion')
41
- targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
42
- buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
43
- }
44
- buildTypes {
45
- release {
46
- minifyEnabled false
47
- }
48
- }
49
-
50
- lintOptions {
51
- disable 'GradleCompatible'
52
- }
53
-
54
- compileOptions {
55
- sourceCompatibility JavaVersion.VERSION_1_8
56
- targetCompatibility JavaVersion.VERSION_1_8
57
- }
58
- }
59
-
60
- repositories {
61
- mavenCentral()
62
- google()
63
-
64
- def found = false
65
- def defaultDir = null
66
- def androidSourcesName = 'React Native sources'
67
-
68
- if (rootProject.ext.has('reactNativeAndroidRoot')) {
69
- defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
70
- } else {
71
- defaultDir = new File(
72
- projectDir,
73
- '/../../../node_modules/react-native/android'
74
- )
75
- }
76
-
77
- if (defaultDir.exists()) {
78
- maven {
79
- url defaultDir.toString()
80
- name androidSourcesName
81
- }
82
-
83
- logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
84
- found = true
85
- } else {
86
- def parentDir = rootProject.projectDir
87
-
88
- 1.upto(5, {
89
- if (found) return true
90
- parentDir = parentDir.parentFile
91
-
92
- def androidSourcesDir = new File(
93
- parentDir,
94
- 'node_modules/react-native'
95
- )
96
-
97
- def androidPrebuiltBinaryDir = new File(
98
- parentDir,
99
- 'node_modules/react-native/android'
100
- )
101
-
102
- if (androidPrebuiltBinaryDir.exists()) {
103
- maven {
104
- url androidPrebuiltBinaryDir.toString()
105
- name androidSourcesName
106
- }
107
-
108
- logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
109
- found = true
110
- } else if (androidSourcesDir.exists()) {
111
- maven {
112
- url androidSourcesDir.toString()
113
- name androidSourcesName
114
- }
115
-
116
- logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
117
- found = true
118
- }
119
- })
120
- }
121
-
122
- if (!found) {
123
- throw new GradleException(
124
- "${project.name}: unable to locate React Native android sources. " +
125
- "Ensure you have you installed React Native as a dependency in your project and try again."
126
- )
127
- }
128
- }
129
-
130
- def kotlin_version = getExtOrDefault('kotlinVersion')
131
-
132
- dependencies {
133
- //noinspection GradleDynamicVersion
134
- implementation "com.facebook.react:react-native:+" // From node_modules
135
- implementation "com.solanamobile:mobile-wallet-adapter-clientlib:0.9.0"
136
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
137
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2"
138
- }
139
-
140
- if (isNewArchitectureEnabled()) {
141
- react {
142
- jsRootDir = file("../src/")
143
- libraryName = "SolanaMobileWalletAdapterModule"
144
- codegenJavaPackageName = "com.solanamobile.mobilewalletadapter.reactnative"
145
- }
146
- }
1
+ buildscript {
2
+ // Buildscript is evaluated before everything else so we can't use getExtOrDefault
3
+ def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['SolanaMobileWalletAdapterModule_kotlinVersion']
4
+
5
+ repositories {
6
+ google()
7
+ mavenCentral()
8
+ }
9
+
10
+ dependencies {
11
+ classpath 'com.android.tools.build:gradle:7.4.2'
12
+ // noinspection DifferentKotlinGradleVersion
13
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14
+ }
15
+ }
16
+
17
+ def isNewArchitectureEnabled() {
18
+ return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
19
+ }
20
+
21
+ apply plugin: 'com.android.library'
22
+ apply plugin: 'kotlin-android'
23
+
24
+ if (isNewArchitectureEnabled()) {
25
+ apply plugin: 'com.facebook.react'
26
+ }
27
+
28
+ def getExtOrDefault(name) {
29
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['SolanaMobileWalletAdapterModule_' + name]
30
+ }
31
+
32
+ def getExtOrIntegerDefault(name) {
33
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['SolanaMobileWalletAdapterModule_' + name]).toInteger()
34
+ }
35
+
36
+ android {
37
+ compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
38
+
39
+ defaultConfig {
40
+ minSdkVersion getExtOrIntegerDefault('minSdkVersion')
41
+ targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
42
+ buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
43
+ }
44
+ buildTypes {
45
+ release {
46
+ minifyEnabled false
47
+ }
48
+ }
49
+
50
+ lintOptions {
51
+ disable 'GradleCompatible'
52
+ }
53
+
54
+ compileOptions {
55
+ sourceCompatibility JavaVersion.VERSION_1_8
56
+ targetCompatibility JavaVersion.VERSION_1_8
57
+ }
58
+ }
59
+
60
+ repositories {
61
+ mavenCentral()
62
+ google()
63
+
64
+ def found = false
65
+ def defaultDir = null
66
+ def androidSourcesName = 'React Native sources'
67
+
68
+ if (rootProject.ext.has('reactNativeAndroidRoot')) {
69
+ defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
70
+ } else {
71
+ defaultDir = new File(
72
+ projectDir,
73
+ '/../../../node_modules/react-native/android'
74
+ )
75
+ }
76
+
77
+ if (defaultDir.exists()) {
78
+ maven {
79
+ url defaultDir.toString()
80
+ name androidSourcesName
81
+ }
82
+
83
+ logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
84
+ found = true
85
+ } else {
86
+ def parentDir = rootProject.projectDir
87
+
88
+ 1.upto(5, {
89
+ if (found) return true
90
+ parentDir = parentDir.parentFile
91
+
92
+ def androidSourcesDir = new File(
93
+ parentDir,
94
+ 'node_modules/react-native'
95
+ )
96
+
97
+ def androidPrebuiltBinaryDir = new File(
98
+ parentDir,
99
+ 'node_modules/react-native/android'
100
+ )
101
+
102
+ if (androidPrebuiltBinaryDir.exists()) {
103
+ maven {
104
+ url androidPrebuiltBinaryDir.toString()
105
+ name androidSourcesName
106
+ }
107
+
108
+ logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
109
+ found = true
110
+ } else if (androidSourcesDir.exists()) {
111
+ maven {
112
+ url androidSourcesDir.toString()
113
+ name androidSourcesName
114
+ }
115
+
116
+ logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
117
+ found = true
118
+ }
119
+ })
120
+ }
121
+
122
+ if (!found) {
123
+ throw new GradleException(
124
+ "${project.name}: unable to locate React Native android sources. " +
125
+ "Ensure you have you installed React Native as a dependency in your project and try again."
126
+ )
127
+ }
128
+ }
129
+
130
+ def kotlin_version = getExtOrDefault('kotlinVersion')
131
+
132
+ dependencies {
133
+ //noinspection GradleDynamicVersion
134
+ implementation "com.facebook.react:react-native:+" // From node_modules
135
+ implementation "com.solanamobile:mobile-wallet-adapter-clientlib:1.1.0"
136
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
137
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1"
138
+ }
139
+
140
+ if (isNewArchitectureEnabled()) {
141
+ react {
142
+ jsRootDir = file("../src/")
143
+ libraryName = "SolanaMobileWalletAdapterModule"
144
+ codegenJavaPackageName = "com.solanamobile.mobilewalletadapter.reactnative"
145
+ }
146
+ }
@@ -1,5 +1,5 @@
1
- distributionBase=GRADLE_USER_HOME
2
- distributionPath=wrapper/dists
3
- distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
4
- zipStoreBase=GRADLE_USER_HOME
5
- zipStorePath=wrapper/dists
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
4
+ zipStoreBase=GRADLE_USER_HOME
5
+ zipStorePath=wrapper/dists
@@ -1,139 +1,176 @@
1
- package com.solanamobile.mobilewalletadapter.reactnative
2
-
3
- import android.content.ActivityNotFoundException
4
- import android.net.Uri
5
- import android.util.Log
6
- import com.facebook.react.bridge.*
7
- import com.solana.mobilewalletadapter.clientlib.protocol.JsonRpc20Client
8
- import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient
9
- import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
10
- import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario
11
- import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertJsonToMap
12
- import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertMapToJson
13
- import kotlinx.coroutines.*
14
- import kotlinx.coroutines.sync.Mutex
15
- import org.json.JSONObject
16
- import java.util.concurrent.ExecutionException
17
- import java.util.concurrent.TimeUnit
18
- import java.util.concurrent.TimeoutException
19
-
20
- class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
21
- ReactContextBaseJavaModule(reactContext), CoroutineScope {
22
-
23
- data class SessionState(
24
- val client: MobileWalletAdapterClient,
25
- val localAssociation: LocalAssociationScenario,
26
- )
27
-
28
- override val coroutineContext =
29
- Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()
30
-
31
- companion object {
32
- private const val ASSOCIATION_TIMEOUT_MS = 10000
33
- private const val CLIENT_TIMEOUT_MS = 90000
34
-
35
- // Used to ensure that you can't start more than one session at a time.
36
- private val mutex: Mutex = Mutex()
37
- private var sessionState: SessionState? = null
38
- }
39
-
40
- override fun getName(): String {
41
- return "SolanaMobileWalletAdapter"
42
- }
43
-
44
- @ReactMethod
45
- fun startSession(config: ReadableMap?, promise: Promise) = launch {
46
- mutex.lock()
47
- Log.d(name, "startSession with config $config")
48
- try {
49
- val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
50
- val localAssociation = LocalAssociationScenario(
51
- CLIENT_TIMEOUT_MS,
52
- )
53
- val intent = LocalAssociationIntentCreator.createAssociationIntent(
54
- uriPrefix,
55
- localAssociation.port,
56
- localAssociation.session
57
- )
58
- currentActivity?.startActivityForResult(intent, 0)
59
- ?: throw NullPointerException("Could not find a current activity from which to launch a local association")
60
- val client =
61
- localAssociation.start().get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
62
- sessionState = SessionState(client, localAssociation)
63
- promise.resolve(true)
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) {
69
- Log.e(name, "Timed out waiting for local association to be ready", e)
70
- cleanup()
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)
80
- } catch (e: Throwable) {
81
- Log.e(name, "Failed to start session", e)
82
- cleanup()
83
- promise.reject(e)
84
- }
85
- }
86
-
87
- @ReactMethod
88
- fun invoke(method: String, params: ReadableMap, promise: Promise) = sessionState?.let {
89
- Log.d(name, "invoke `$method` with params $params")
90
- try {
91
- val result = it.client.methodCall(
92
- method,
93
- convertMapToJson(params),
94
- CLIENT_TIMEOUT_MS
95
- ).get() as JSONObject
96
- promise.resolve(convertJsonToMap(result))
97
- } catch (e: ExecutionException) {
98
- val cause = e.cause
99
- if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
100
- val userInfo = Arguments.createMap()
101
- userInfo.putInt("jsonRpcErrorCode", cause.code)
102
- promise.reject("JSON_RPC_ERROR", cause, userInfo)
103
- } else {
104
- throw e
105
- }
106
- } catch (e: Throwable) {
107
- Log.e(name, "Failed to invoke `$method` with params $params", e)
108
- promise.reject(e)
109
- }
110
- } ?: throw NullPointerException("Tried to invoke `$method` without an active session")
111
-
112
- @ReactMethod
113
- fun endSession(promise: Promise) = sessionState?.let {
114
- launch {
115
- Log.d(name, "endSession")
116
- try {
117
- it.localAssociation.close()
118
- .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
119
- cleanup()
120
- promise.resolve(true)
121
- } catch (e: TimeoutException) {
122
- Log.e(name, "Timed out waiting for local association to close", e)
123
- cleanup()
124
- promise.reject("Failed to end session", e)
125
- } catch (e: Throwable) {
126
- Log.e(name, "Failed to end session", e)
127
- cleanup()
128
- promise.reject("Failed to end session", e)
129
- }
130
- }
131
- } ?: throw NullPointerException("Tried to end a session without an active session")
132
-
133
- private fun cleanup() {
134
- sessionState = null
135
- if (mutex.isLocked) {
136
- mutex.unlock()
137
- }
138
- }
139
- }
1
+ package com.solanamobile.mobilewalletadapter.reactnative
2
+
3
+ import android.app.Activity
4
+ import android.content.ActivityNotFoundException
5
+ import android.content.Intent
6
+ import android.net.Uri
7
+ import android.util.Log
8
+ import com.facebook.react.bridge.*
9
+ import com.solana.mobilewalletadapter.clientlib.protocol.JsonRpc20Client
10
+ import com.solana.mobilewalletadapter.clientlib.protocol.MobileWalletAdapterClient
11
+ import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationIntentCreator
12
+ import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenario
13
+ import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertJsonToMap
14
+ import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertMapToJson
15
+ import kotlinx.coroutines.*
16
+ import kotlinx.coroutines.sync.Mutex
17
+ import org.json.JSONObject
18
+ import java.util.concurrent.ExecutionException
19
+ import java.util.concurrent.TimeUnit
20
+ import java.util.concurrent.TimeoutException
21
+
22
+ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
23
+ ReactContextBaseJavaModule(reactContext), CoroutineScope {
24
+
25
+ data class SessionState(
26
+ val client: MobileWalletAdapterClient,
27
+ val localAssociation: LocalAssociationScenario,
28
+ )
29
+
30
+ override val coroutineContext =
31
+ Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()
32
+
33
+ companion object {
34
+ private const val ASSOCIATION_TIMEOUT_MS = 10000
35
+ private const val CLIENT_TIMEOUT_MS = 90000
36
+ private const val REQUEST_LOCAL_ASSOCIATION = 0
37
+
38
+ // Used to ensure that you can't start more than one session at a time.
39
+ private val mutex: Mutex = Mutex()
40
+ private var sessionState: SessionState? = null
41
+ }
42
+
43
+ private val mActivityEventCallbacks = mutableMapOf<Int, (Int, Intent?) -> (Unit)>()
44
+ private val mActivityEventListener: ActivityEventListener =
45
+ object : BaseActivityEventListener() {
46
+ override fun onActivityResult(
47
+ activity: Activity?,
48
+ requestCode: Int,
49
+ resultCode: Int,
50
+ data: Intent?
51
+ ) {
52
+ mActivityEventCallbacks[requestCode]?.let { callback ->
53
+ callback(resultCode, data)
54
+ mActivityEventCallbacks.remove(requestCode)
55
+ }
56
+ }
57
+ }
58
+
59
+ init {
60
+ reactContext.addActivityEventListener(mActivityEventListener)
61
+ }
62
+
63
+ override fun getName(): String {
64
+ return "SolanaMobileWalletAdapter"
65
+ }
66
+
67
+ @ReactMethod
68
+ fun startSession(config: ReadableMap?, promise: Promise) = launch {
69
+ mutex.lock()
70
+ Log.d(name, "startSession with config $config")
71
+ try {
72
+ val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
73
+ val localAssociation = LocalAssociationScenario(
74
+ CLIENT_TIMEOUT_MS,
75
+ )
76
+ val intent = LocalAssociationIntentCreator.createAssociationIntent(
77
+ uriPrefix,
78
+ localAssociation.port,
79
+ localAssociation.session
80
+ )
81
+ launchIntent(intent, REQUEST_LOCAL_ASSOCIATION) { resultCode, _ ->
82
+ if (resultCode == Activity.RESULT_CANCELED) {
83
+ Log.d(name, "Local association cancelled by user, ending session")
84
+ promise.reject("Session not established: Local association cancelled by user",
85
+ LocalAssociationScenario.ConnectionFailedException("Local association cancelled by user"))
86
+ localAssociation.close()
87
+ }
88
+ }
89
+ val client =
90
+ localAssociation.start().get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
91
+ sessionState = SessionState(client, localAssociation)
92
+ promise.resolve(true)
93
+ } catch (e: ActivityNotFoundException) {
94
+ Log.e(name, "Found no installed wallet that supports the mobile wallet protocol", e)
95
+ cleanup()
96
+ promise.reject("ERROR_WALLET_NOT_FOUND", e)
97
+ } catch (e: TimeoutException) {
98
+ Log.e(name, "Timed out waiting for local association to be ready", e)
99
+ cleanup()
100
+ promise.reject("Timed out waiting for local association to be ready", e)
101
+ } catch (e: InterruptedException) {
102
+ Log.w(name, "Interrupted while waiting for local association to be ready", e)
103
+ cleanup()
104
+ promise.reject(e)
105
+ } catch (e: ExecutionException) {
106
+ Log.e(name, "Failed establishing local association with wallet", e.cause)
107
+ cleanup()
108
+ promise.reject(e)
109
+ } catch (e: Throwable) {
110
+ Log.e(name, "Failed to start session", e)
111
+ cleanup()
112
+ promise.reject(e)
113
+ }
114
+ }
115
+
116
+ @ReactMethod
117
+ fun invoke(method: String, params: ReadableMap, promise: Promise) = sessionState?.let {
118
+ Log.d(name, "invoke `$method` with params $params")
119
+ try {
120
+ val result = it.client.methodCall(
121
+ method,
122
+ convertMapToJson(params),
123
+ CLIENT_TIMEOUT_MS
124
+ ).get() as JSONObject
125
+ promise.resolve(convertJsonToMap(result))
126
+ } catch (e: ExecutionException) {
127
+ val cause = e.cause
128
+ if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
129
+ val userInfo = Arguments.createMap()
130
+ userInfo.putInt("jsonRpcErrorCode", cause.code)
131
+ promise.reject("JSON_RPC_ERROR", cause, userInfo)
132
+ } else {
133
+ throw e
134
+ }
135
+ } catch (e: Throwable) {
136
+ Log.e(name, "Failed to invoke `$method` with params $params", e)
137
+ promise.reject(e)
138
+ }
139
+ } ?: throw NullPointerException("Tried to invoke `$method` without an active session")
140
+
141
+ @ReactMethod
142
+ fun endSession(promise: Promise) = sessionState?.let {
143
+ launch {
144
+ Log.d(name, "endSession")
145
+ try {
146
+ it.localAssociation.close()
147
+ .get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
148
+ cleanup()
149
+ promise.resolve(true)
150
+ } catch (e: TimeoutException) {
151
+ Log.e(name, "Timed out waiting for local association to close", e)
152
+ cleanup()
153
+ promise.reject("Failed to end session", e)
154
+ } catch (e: Throwable) {
155
+ Log.e(name, "Failed to end session", e)
156
+ cleanup()
157
+ promise.reject("Failed to end session", e)
158
+ }
159
+ }
160
+ } ?: throw NullPointerException("Tried to end a session without an active session")
161
+
162
+ private fun launchIntent(intent: Intent, requestCode: Int, callback: (Int, Intent?) -> (Unit)) {
163
+ mActivityEventCallbacks[requestCode] = callback
164
+ currentActivity?.startActivityForResult(intent, REQUEST_LOCAL_ASSOCIATION) ?: run {
165
+ mActivityEventCallbacks.remove(requestCode)
166
+ throw NullPointerException("Could not find a current activity from which to launch a local association")
167
+ }
168
+ }
169
+
170
+ private fun cleanup() {
171
+ sessionState = null
172
+ if (mutex.isLocked) {
173
+ mutex.unlock()
174
+ }
175
+ }
176
+ }
@@ -143,6 +143,7 @@ interface GetCapabilitiesAPI {
143
143
  interface ReauthorizeAPI {
144
144
  reauthorize(params: {
145
145
  auth_token: AuthToken;
146
+ identity: AppIdentity;
146
147
  }): Promise<AuthorizationResult>;
147
148
  }
148
149
  interface SignMessagesAPI {
@@ -143,6 +143,7 @@ interface GetCapabilitiesAPI {
143
143
  interface ReauthorizeAPI {
144
144
  reauthorize(params: {
145
145
  auth_token: AuthToken;
146
+ identity: AppIdentity;
146
147
  }): Promise<AuthorizationResult>;
147
148
  }
148
149
  interface SignMessagesAPI {
@@ -143,6 +143,7 @@ interface GetCapabilitiesAPI {
143
143
  interface ReauthorizeAPI {
144
144
  reauthorize(params: {
145
145
  auth_token: AuthToken;
146
+ identity: AppIdentity;
146
147
  }): Promise<AuthorizationResult>;
147
148
  }
148
149
  interface SignMessagesAPI {
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": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "author": "Steven Luscher <steven.luscher@solanamobile.com>",
6
6
  "repository": "https://github.com/solana-mobile/mobile-wallet-adapter",
7
7
  "license": "Apache-2.0",
package/src/types.ts CHANGED
@@ -1,111 +1,111 @@
1
- import type { TransactionVersion } from '@solana/web3.js';
2
-
3
- export type Account = Readonly<{
4
- address: Base64EncodedAddress;
5
- label?: string;
6
- }>;
7
-
8
- /**
9
- * Properties that wallets may present to users when an app
10
- * asks for authorization to execute privileged methods (see
11
- * {@link PrivilegedMethods}).
12
- */
13
- export type AppIdentity = Readonly<{
14
- uri?: string;
15
- icon?: string;
16
- name?: string;
17
- }>;
18
-
19
- /**
20
- * An ephemeral elliptic-curve keypair on the P-256 curve.
21
- * This public key is used to create the association token.
22
- * The private key is used during session establishment.
23
- */
24
- export type AssociationKeypair = CryptoKeyPair;
25
-
26
- /**
27
- * The context returned from a wallet after having authorized a given
28
- * account for use with a given application. You can cache this and
29
- * use it later to invoke privileged methods.
30
- */
31
- export type AuthorizationResult = Readonly<{
32
- accounts: Account[];
33
- auth_token: AuthToken;
34
- wallet_uri_base: string;
35
- }>;
36
-
37
- export type AuthToken = string;
38
-
39
- export type Base64EncodedAddress = string;
40
-
41
- type Base64EncodedSignature = string;
42
-
43
- type Base64EncodedMessage = string;
44
-
45
- type Base64EncodedSignedMessage = string;
46
-
47
- type Base64EncodedSignedTransaction = string;
48
-
49
- export type Base64EncodedTransaction = string;
50
-
51
- export type Cluster = 'devnet' | 'testnet' | 'mainnet-beta';
52
-
53
- export type Finality = 'confirmed' | 'finalized' | 'processed';
54
-
55
- export type WalletAssociationConfig = Readonly<{
56
- baseUri?: string;
57
- }>;
58
-
59
- export interface AuthorizeAPI {
60
- authorize(params: { cluster: Cluster; identity: AppIdentity }): Promise<AuthorizationResult>;
61
- }
62
- export interface CloneAuthorizationAPI {
63
- cloneAuthorization(params: { auth_token: AuthToken }): Promise<Readonly<{ auth_token: AuthToken }>>;
64
- }
65
- export interface DeauthorizeAPI {
66
- deauthorize(params: { auth_token: AuthToken }): Promise<Readonly<Record<string, never>>>;
67
- }
68
-
69
- export interface GetCapabilitiesAPI {
70
- getCapabilities(): Promise<
71
- Readonly<{
72
- supports_clone_authorization: boolean;
73
- supports_sign_and_send_transactions: boolean;
74
- max_transactions_per_request: boolean;
75
- max_messages_per_request: boolean;
76
- supported_transaction_versions: ReadonlyArray<TransactionVersion>;
77
- }>
78
- >;
79
- }
80
- export interface ReauthorizeAPI {
81
- reauthorize(params: { auth_token: AuthToken }): Promise<AuthorizationResult>;
82
- }
83
- export interface SignMessagesAPI {
84
- signMessages(params: {
85
- addresses: Base64EncodedAddress[];
86
- payloads: Base64EncodedMessage[];
87
- }): Promise<Readonly<{ signed_payloads: Base64EncodedSignedMessage[] }>>;
88
- }
89
- export interface SignTransactionsAPI {
90
- signTransactions(params: {
91
- payloads: Base64EncodedTransaction[];
92
- }): Promise<Readonly<{ signed_payloads: Base64EncodedSignedTransaction[] }>>;
93
- }
94
- export interface SignAndSendTransactionsAPI {
95
- signAndSendTransactions(params: {
96
- options?: Readonly<{
97
- min_context_slot?: number;
98
- }>;
99
- payloads: Base64EncodedTransaction[];
100
- }): Promise<Readonly<{ signatures: Base64EncodedSignature[] }>>;
101
- }
102
-
103
- export interface MobileWallet
104
- extends AuthorizeAPI,
105
- CloneAuthorizationAPI,
106
- DeauthorizeAPI,
107
- GetCapabilitiesAPI,
108
- ReauthorizeAPI,
109
- SignMessagesAPI,
110
- SignTransactionsAPI,
111
- SignAndSendTransactionsAPI {}
1
+ import type { TransactionVersion } from '@solana/web3.js';
2
+
3
+ export type Account = Readonly<{
4
+ address: Base64EncodedAddress;
5
+ label?: string;
6
+ }>;
7
+
8
+ /**
9
+ * Properties that wallets may present to users when an app
10
+ * asks for authorization to execute privileged methods (see
11
+ * {@link PrivilegedMethods}).
12
+ */
13
+ export type AppIdentity = Readonly<{
14
+ uri?: string;
15
+ icon?: string;
16
+ name?: string;
17
+ }>;
18
+
19
+ /**
20
+ * An ephemeral elliptic-curve keypair on the P-256 curve.
21
+ * This public key is used to create the association token.
22
+ * The private key is used during session establishment.
23
+ */
24
+ export type AssociationKeypair = CryptoKeyPair;
25
+
26
+ /**
27
+ * The context returned from a wallet after having authorized a given
28
+ * account for use with a given application. You can cache this and
29
+ * use it later to invoke privileged methods.
30
+ */
31
+ export type AuthorizationResult = Readonly<{
32
+ accounts: Account[];
33
+ auth_token: AuthToken;
34
+ wallet_uri_base: string;
35
+ }>;
36
+
37
+ export type AuthToken = string;
38
+
39
+ export type Base64EncodedAddress = string;
40
+
41
+ type Base64EncodedSignature = string;
42
+
43
+ type Base64EncodedMessage = string;
44
+
45
+ type Base64EncodedSignedMessage = string;
46
+
47
+ type Base64EncodedSignedTransaction = string;
48
+
49
+ export type Base64EncodedTransaction = string;
50
+
51
+ export type Cluster = 'devnet' | 'testnet' | 'mainnet-beta';
52
+
53
+ export type Finality = 'confirmed' | 'finalized' | 'processed';
54
+
55
+ export type WalletAssociationConfig = Readonly<{
56
+ baseUri?: string;
57
+ }>;
58
+
59
+ export interface AuthorizeAPI {
60
+ authorize(params: { cluster: Cluster; identity: AppIdentity }): Promise<AuthorizationResult>;
61
+ }
62
+ export interface CloneAuthorizationAPI {
63
+ cloneAuthorization(params: { auth_token: AuthToken }): Promise<Readonly<{ auth_token: AuthToken }>>;
64
+ }
65
+ export interface DeauthorizeAPI {
66
+ deauthorize(params: { auth_token: AuthToken }): Promise<Readonly<Record<string, never>>>;
67
+ }
68
+
69
+ export interface GetCapabilitiesAPI {
70
+ getCapabilities(): Promise<
71
+ Readonly<{
72
+ supports_clone_authorization: boolean;
73
+ supports_sign_and_send_transactions: boolean;
74
+ max_transactions_per_request: boolean;
75
+ max_messages_per_request: boolean;
76
+ supported_transaction_versions: ReadonlyArray<TransactionVersion>;
77
+ }>
78
+ >;
79
+ }
80
+ export interface ReauthorizeAPI {
81
+ reauthorize(params: { auth_token: AuthToken; identity: AppIdentity }): Promise<AuthorizationResult>;
82
+ }
83
+ export interface SignMessagesAPI {
84
+ signMessages(params: {
85
+ addresses: Base64EncodedAddress[];
86
+ payloads: Base64EncodedMessage[];
87
+ }): Promise<Readonly<{ signed_payloads: Base64EncodedSignedMessage[] }>>;
88
+ }
89
+ export interface SignTransactionsAPI {
90
+ signTransactions(params: {
91
+ payloads: Base64EncodedTransaction[];
92
+ }): Promise<Readonly<{ signed_payloads: Base64EncodedSignedTransaction[] }>>;
93
+ }
94
+ export interface SignAndSendTransactionsAPI {
95
+ signAndSendTransactions(params: {
96
+ options?: Readonly<{
97
+ min_context_slot?: number;
98
+ }>;
99
+ payloads: Base64EncodedTransaction[];
100
+ }): Promise<Readonly<{ signatures: Base64EncodedSignature[] }>>;
101
+ }
102
+
103
+ export interface MobileWallet
104
+ extends AuthorizeAPI,
105
+ CloneAuthorizationAPI,
106
+ DeauthorizeAPI,
107
+ GetCapabilitiesAPI,
108
+ ReauthorizeAPI,
109
+ SignMessagesAPI,
110
+ SignTransactionsAPI,
111
+ SignAndSendTransactionsAPI {}