@solana-mobile/mobile-wallet-adapter-walletlib 1.0.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.
Files changed (48) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +93 -0
  3. package/android/build.gradle +150 -0
  4. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  5. package/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/gradlew +185 -0
  8. package/android/gradlew.bat +89 -0
  9. package/android/src/main/AndroidManifest.xml +23 -0
  10. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/Extensions.kt +99 -0
  11. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/JsonSerializationUtils.kt +83 -0
  12. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/MobileWalletAdapterBottomSheetActivity.kt +55 -0
  13. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/MobileWalletAdapterWalletLibReactNativePackage.kt +16 -0
  14. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/SolanaMobileWalletAdapterWalletLibModule.kt +401 -0
  15. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/model/MobileWalletAdapterConfig.kt +60 -0
  16. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/model/MobileWalletAdapterRequest.kt +83 -0
  17. package/android/src/main/java/com/solanamobile/mobilewalletadapterwalletlib/reactnative/model/MobileWalletAdapterResponse.kt +46 -0
  18. package/android/src/main/res/drawable/background_bottom_sheet_dialog.xml +11 -0
  19. package/android/src/main/res/values/styles.xml +9 -0
  20. package/lib/commonjs/index.js +39 -0
  21. package/lib/commonjs/index.js.map +1 -0
  22. package/lib/commonjs/mwaSessionEvents.js +24 -0
  23. package/lib/commonjs/mwaSessionEvents.js.map +1 -0
  24. package/lib/commonjs/resolve.js +48 -0
  25. package/lib/commonjs/resolve.js.map +1 -0
  26. package/lib/commonjs/useMobileWalletAdapterSession.js +50 -0
  27. package/lib/commonjs/useMobileWalletAdapterSession.js.map +1 -0
  28. package/lib/module/index.js +4 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/mwaSessionEvents.js +17 -0
  31. package/lib/module/mwaSessionEvents.js.map +1 -0
  32. package/lib/module/resolve.js +46 -0
  33. package/lib/module/resolve.js.map +1 -0
  34. package/lib/module/useMobileWalletAdapterSession.js +44 -0
  35. package/lib/module/useMobileWalletAdapterSession.js.map +1 -0
  36. package/lib/typescript/index.d.ts +4 -0
  37. package/lib/typescript/index.d.ts.map +1 -0
  38. package/lib/typescript/mwaSessionEvents.d.ts +48 -0
  39. package/lib/typescript/mwaSessionEvents.d.ts.map +1 -0
  40. package/lib/typescript/resolve.d.ts +94 -0
  41. package/lib/typescript/resolve.d.ts.map +1 -0
  42. package/lib/typescript/useMobileWalletAdapterSession.d.ts +12 -0
  43. package/lib/typescript/useMobileWalletAdapterSession.d.ts.map +1 -0
  44. package/package.json +69 -0
  45. package/src/index.ts +3 -0
  46. package/src/mwaSessionEvents.ts +75 -0
  47. package/src/resolve.ts +165 -0
  48. package/src/useMobileWalletAdapterSession.ts +83 -0
@@ -0,0 +1,83 @@
1
+ package com.solanamobile.mobilewalletadapterwalletlib.reactnative
2
+
3
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.AuthorizeDappResponse
4
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.MobileWalletAdapterFailureResponse
5
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.MobileWalletAdapterRequest
6
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.MobileWalletAdapterResponse
7
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.SignedAndSentTransactions
8
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.SignedPayloads
9
+ import kotlinx.serialization.DeserializationStrategy
10
+ import kotlinx.serialization.KSerializer
11
+ import kotlinx.serialization.builtins.ByteArraySerializer
12
+ import kotlinx.serialization.builtins.ListSerializer
13
+ import kotlinx.serialization.builtins.MapSerializer
14
+ import kotlinx.serialization.builtins.serializer
15
+ import kotlinx.serialization.descriptors.SerialDescriptor
16
+ import kotlinx.serialization.encoding.Decoder
17
+ import kotlinx.serialization.encoding.Encoder
18
+ import kotlinx.serialization.json.JsonContentPolymorphicSerializer
19
+ import kotlinx.serialization.json.JsonElement
20
+ import kotlinx.serialization.json.JsonObject
21
+ import kotlinx.serialization.json.JsonTransformingSerializer
22
+
23
+ internal open class TypeTransformingSerializer<T: Any>(serializer: KSerializer<T>) : JsonTransformingSerializer<T>(serializer) {
24
+ override fun transformSerialize(element: JsonElement): JsonElement =
25
+ if ((element as? JsonObject)?.containsKey("type") == true)
26
+ JsonObject(element.toMutableMap().apply {
27
+ this["__type"] = this.remove("type")!!
28
+ })
29
+ else element
30
+
31
+ override fun transformDeserialize(element: JsonElement): JsonElement =
32
+ if ((element as? JsonObject)?.containsKey("__type") == true)
33
+ JsonObject(element.toMutableMap().apply {
34
+ this["type"] = this.remove("__type")!!
35
+ })
36
+ else element
37
+ }
38
+
39
+ internal object FailReasonTransformingSerializer
40
+ : JsonTransformingSerializer<MobileWalletAdapterFailureResponse>(MobileWalletAdapterFailureResponse.serializer()) {
41
+ override fun transformDeserialize(element: JsonElement): JsonElement =
42
+ if ((element as? JsonObject)?.containsKey("failReason") == true)
43
+ JsonObject(element.toMutableMap().apply {
44
+ this["type"] = this.remove("failReason")!!
45
+ })
46
+ else element
47
+ }
48
+
49
+ internal object MobileWalletAdapterResponseSerializer : JsonContentPolymorphicSerializer<MobileWalletAdapterResponse>(MobileWalletAdapterResponse::class) {
50
+ override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out MobileWalletAdapterResponse> =
51
+ if ((element as? JsonObject)?.containsKey("failReason") == true) FailReasonTransformingSerializer
52
+ else if ((element as? JsonObject)?.containsKey("publicKey") == true) AuthorizeDappResponse.serializer()
53
+ else if ((element as? JsonObject)?.containsKey("signedPayloads") == true) SignedPayloads.serializer()
54
+ else if ((element as? JsonObject)?.containsKey("signedTransactions") == true) SignedAndSentTransactions.serializer()
55
+ else MobileWalletAdapterResponse.serializer()
56
+ }
57
+
58
+ internal object MobileWalletAdapterRequestSerializer : TypeTransformingSerializer<MobileWalletAdapterRequest>(MobileWalletAdapterRequest.serializer())
59
+
60
+ internal object ByteArrayAsMapSerializer : KSerializer<ByteArray> {
61
+ override val descriptor: SerialDescriptor = ByteArraySerializer().descriptor
62
+
63
+ override fun deserialize(decoder: Decoder): ByteArray =
64
+ try {
65
+ decoder.decodeSerializableValue(MapSerializer(String.serializer(), Int.serializer())).values.map { it.toByte() }.toByteArray()
66
+ } catch (e: Exception) {
67
+ decoder.decodeSerializableValue(ByteArraySerializer())
68
+ }
69
+
70
+ override fun serialize(encoder: Encoder, value: ByteArray) {
71
+ TODO("Not yet implemented")
72
+ }
73
+ }
74
+
75
+ object ByteArrayCollectionAsMapCollectionSerializer : KSerializer<List<ByteArray>> {
76
+ override val descriptor: SerialDescriptor = ListSerializer(ByteArraySerializer()).descriptor
77
+
78
+ override fun deserialize(decoder: Decoder): List<ByteArray> =
79
+ decoder.decodeSerializableValue(ListSerializer(ByteArrayAsMapSerializer))
80
+
81
+ override fun serialize(encoder: Encoder, value: List<ByteArray>) =
82
+ encoder.encodeSerializableValue(ListSerializer(ByteArrayAsMapSerializer), value)
83
+ }
@@ -0,0 +1,55 @@
1
+ package com.solanamobile.mobilewalletadapterwalletlib.reactnative
2
+
3
+ import android.os.Bundle
4
+ import android.view.Gravity
5
+ import android.view.WindowManager
6
+
7
+ import com.facebook.react.ReactActivity
8
+ import com.facebook.react.ReactActivityDelegate
9
+ import com.facebook.react.ReactRootView
10
+
11
+ class MobileWalletAdapterBottomSheetActivity : ReactActivity() {
12
+
13
+ /**
14
+ * Returns the name of the main component registered from JavaScript. This is used to schedule
15
+ * rendering of the component.
16
+ */
17
+ override protected fun getMainComponentName() = "MobileWalletAdapterEntrypoint"
18
+
19
+ override fun onCreate(savedInstanceState: Bundle?) {
20
+
21
+ val windowLayoutParams = window.attributes
22
+
23
+ windowLayoutParams.gravity = Gravity.BOTTOM
24
+ windowLayoutParams.flags =
25
+ windowLayoutParams.flags and WindowManager.LayoutParams.FLAG_DIM_BEHIND
26
+
27
+ window.attributes = windowLayoutParams
28
+ super.onCreate(null)
29
+ }
30
+
31
+ /**
32
+ * Returns the instance of the [ReactActivityDelegate]. There the RootView is created and
33
+ * you can specify the renderer you wish to use - the new renderer (Fabric) or the old renderer
34
+ * (Paper).
35
+ */
36
+ override protected fun createReactActivityDelegate(): ReactActivityDelegate {
37
+ return MainActivityDelegate(this, mainComponentName)
38
+ }
39
+
40
+ class MainActivityDelegate(val activity: ReactActivity?, mainComponentName: String?) :
41
+ ReactActivityDelegate(activity, mainComponentName) {
42
+
43
+ override protected fun createRootView(): ReactRootView {
44
+ val reactRootView = ReactRootView(getContext())
45
+ // If you opted-in for the New Architecture, we enable the Fabric Renderer.
46
+ reactRootView.setIsFabric(false)
47
+
48
+ return reactRootView
49
+ }
50
+
51
+ // If you opted-in for the New Architecture, we enable Concurrent Root (i.e. React 18).
52
+ // More on this on https://reactjs.org/blog/2022/03/29/react-v18.html
53
+ override protected fun isConcurrentRootEnabled() = false
54
+ }
55
+ }
@@ -0,0 +1,16 @@
1
+ package com.solanamobile.mobilewalletadapterwalletlib.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class MobileWalletAdapterWalletLibReactNativePackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
10
+ return listOf(SolanaMobileWalletAdapterWalletLibModule(reactContext))
11
+ }
12
+
13
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
14
+ return emptyList()
15
+ }
16
+ }
@@ -0,0 +1,401 @@
1
+ package com.solanamobile.mobilewalletadapterwalletlib.reactnative
2
+
3
+ import android.net.Uri
4
+ import android.util.Log
5
+ import com.facebook.react.bridge.*
6
+ import com.facebook.react.modules.core.DeviceEventManagerModule
7
+ import com.solana.mobilewalletadapter.common.util.NotifyingCompletableFuture
8
+ import com.solana.mobilewalletadapter.walletlib.association.AssociationUri
9
+ import com.solana.mobilewalletadapter.walletlib.association.LocalAssociationUri
10
+ import com.solana.mobilewalletadapter.walletlib.authorization.AuthIssuerConfig
11
+ import com.solana.mobilewalletadapter.walletlib.protocol.MobileWalletAdapterConfig
12
+ import com.solana.mobilewalletadapter.walletlib.scenario.*
13
+ import com.solana.mobilewalletadapter.common.ProtocolContract
14
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.BuildConfig
15
+ import com.solanamobile.mobilewalletadapterwalletlib.reactnative.model.*
16
+ import kotlinx.coroutines.*
17
+ import kotlinx.serialization.json.Json
18
+ import kotlinx.serialization.json.JsonObject
19
+ import kotlinx.serialization.json.JsonPrimitive
20
+ import kotlinx.serialization.json.jsonObject
21
+ import java.util.UUID
22
+
23
+ class SolanaMobileWalletAdapterWalletLibModule(val reactContext: ReactApplicationContext) :
24
+ ReactContextBaseJavaModule(reactContext), CoroutineScope {
25
+
26
+ private val json = Json { ignoreUnknownKeys = true }
27
+
28
+ // Sets the name of the module in React, accessible at ReactNative.NativeModules.SolanaMobileWalletAdapterWalletLib
29
+ override fun getName() = "SolanaMobileWalletAdapterWalletLib"
30
+
31
+ override val coroutineContext =
32
+ Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterWalletLibModuleScope") + SupervisorJob()
33
+
34
+ // Session events that notify about the lifecycle of the Scenario session. We are choosing
35
+ // to go with the naming convention Session rather than Scenario for readability.
36
+ sealed interface MobileWalletAdapterSessionEvent {
37
+ val type: String
38
+ object None : MobileWalletAdapterSessionEvent {
39
+ override val type: String = ""
40
+ }
41
+ object SessionTerminated : MobileWalletAdapterSessionEvent {
42
+ override val type: String = "SESSION_TERMINATED"
43
+ }
44
+ object ScenarioReady : MobileWalletAdapterSessionEvent {
45
+ override val type: String = "SESSION_READY"
46
+ }
47
+ object ScenarioServingClients : MobileWalletAdapterSessionEvent {
48
+ override val type: String = "SESSION_SERVING_CLIENTS"
49
+ }
50
+ object ScenarioServingComplete : MobileWalletAdapterSessionEvent {
51
+ override val type: String = "SESSION_SERVING_COMPLETE"
52
+ }
53
+ object ScenarioComplete : MobileWalletAdapterSessionEvent {
54
+ override val type: String = "SESSION_COMPLETE"
55
+ }
56
+ class ScenarioError(val message: String? = null) : MobileWalletAdapterSessionEvent {
57
+ override val type: String = "SESSION_ERROR"
58
+ }
59
+ object ScenarioTeardownComplete : MobileWalletAdapterSessionEvent {
60
+ override val type: String = "SESSION_TEARDOWN_COMPLETE"
61
+ }
62
+ object LowPowerNoConnection : MobileWalletAdapterSessionEvent {
63
+ override val type: String = "LOW_POWER_NO_CONNECTION"
64
+ }
65
+ }
66
+
67
+ // Service requests that come from the dApp for Authorization, Signing, Sending, hence "RemoteRequest".
68
+ sealed class MobileWalletAdapterRemoteRequest(open val request: ScenarioRequest,
69
+ val id: String = UUID.randomUUID().toString()) {
70
+ data class AuthorizeDapp(override val request: AuthorizeRequest) : MobileWalletAdapterRemoteRequest(request)
71
+ data class ReauthorizeDapp(override val request: ReauthorizeRequest) : MobileWalletAdapterRemoteRequest(request)
72
+
73
+ sealed class SignPayloads(override val request: SignPayloadsRequest) : MobileWalletAdapterRemoteRequest(request)
74
+ data class SignTransactions(override val request: SignTransactionsRequest) : SignPayloads(request)
75
+ data class SignMessages(override val request: SignMessagesRequest) : SignPayloads(request)
76
+ data class SignAndSendTransactions(
77
+ override val request: SignAndSendTransactionsRequest,
78
+ val endpointUri: Uri,
79
+ ) : MobileWalletAdapterRemoteRequest(request)
80
+ }
81
+
82
+ // currently we only allow a single scenario to exist at a time
83
+ private var scenarioId: String? = null
84
+ private var scenarioUri: Uri? = null
85
+ private var scenario: Scenario? = null
86
+ set(value) {
87
+ value?.let { scenarioId = UUID.randomUUID().toString() } ?: run {
88
+ scenarioId = null
89
+ scenario?.close()
90
+ }
91
+ pendingRequests.clear()
92
+ field = value
93
+ }
94
+
95
+ // very basic/naive implememtion of request cache.
96
+ // we could replace this with an abstraction that has expiration, persistance, etc.
97
+ private val pendingRequests = mutableMapOf<String, MobileWalletAdapterRemoteRequest>()
98
+
99
+ private fun clusterToRpcUri(cluster: String?): Uri {
100
+ return when (cluster) {
101
+ ProtocolContract.CLUSTER_MAINNET_BETA ->
102
+ Uri.parse("https://api.mainnet-beta.solana.com")
103
+ ProtocolContract.CLUSTER_DEVNET ->
104
+ Uri.parse("https://api.devnet.solana.com")
105
+ else ->
106
+ Uri.parse("https://api.testnet.solana.com")
107
+ }
108
+ }
109
+
110
+ @ReactMethod
111
+ fun createScenario(
112
+ walletName: String,
113
+ uriStr: String,
114
+ config: String,
115
+ ) = launch {
116
+ val uri = Uri.parse(uriStr)
117
+
118
+ // TODO: this is dirty, need some stateful object/data to know what state we are in.
119
+ // also, should we suport multiple simulatneous scenario?
120
+ if (uri == scenarioUri && scenario != null) {
121
+ Log.w(TAG, "Session already created for uri: $uri")
122
+ return@launch
123
+ }
124
+
125
+ val associationUri = AssociationUri.parse(uri)
126
+ if (associationUri == null) {
127
+ Log.e(TAG, "Unsupported association URI: $uri")
128
+ return@launch
129
+ } else if (associationUri !is LocalAssociationUri) {
130
+ Log.e(TAG, "Current implementation of fakewallet does not support remote clients")
131
+ return@launch
132
+ }
133
+
134
+ scenarioUri = uri
135
+
136
+ val kotlinConfig = json.decodeFromString(MobileWalletAdapterConfigSerializer, config)
137
+
138
+ // created a scenario, told it to start (kicks off some threads in the background)
139
+ // we've kept a reference to it in the global state of this module (scenario)
140
+ // this won't be garbage collected and will just run, sit & wait for an incoming connection
141
+ scenario = associationUri.createScenario(
142
+ reactContext,
143
+ kotlinConfig,
144
+ AuthIssuerConfig(walletName),
145
+ MobileWalletAdapterScenarioCallbacks()
146
+ ).also { it.start() }
147
+
148
+ Log.d(TAG, "scenario created: $walletName")
149
+ }
150
+
151
+ /* Generic Request functions */
152
+ @ReactMethod
153
+ fun cancelRequest(sessionId: String, requestId: String) {
154
+ Log.d(TAG, "Cancelled request $requestId");
155
+ (pendingRequests.remove(requestId) as? ScenarioRequest)?.let { scenarioRequest ->
156
+ scenarioRequest.cancel();
157
+ }
158
+ }
159
+
160
+ // Apparently we cant have overlaod methods like this becuase React is completly idiotic
161
+ // @ReactMethod
162
+ // fun resolve(request: ReadableMap, response: ReadableMap) {
163
+ // resolve(request.toJson().toString(), response.toJson().toString())
164
+ // }
165
+
166
+ @ReactMethod
167
+ fun resolve(requestJson: String, responseJson: String) = launch {
168
+ val completedRequest = json.decodeFromString(MobileWalletAdapterRequestSerializer, requestJson)
169
+ val response = json.decodeFromString(MobileWalletAdapterResponseSerializer, responseJson)
170
+ val pendingRequest = pendingRequests[completedRequest.requestId]
171
+
172
+ if (completedRequest.sessionId != scenarioId) {
173
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioError(
174
+ "Invalid session (${completedRequest.sessionId}). This session does not exist/is no longer active."
175
+ ))
176
+ return@launch
177
+ }
178
+
179
+ fun completeWithInvalidResponse() {
180
+ pendingRequest?.request?.completeWithInternalError(Exception("Invalid Response For Request: response = $responseJson"))
181
+ }
182
+
183
+ when (completedRequest) {
184
+ is AuthorizeDapp -> when (response) {
185
+ is MobileWalletAdapterFailureResponse -> {
186
+ when (response) {
187
+ is UserDeclinedResponse ->
188
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.AuthorizeDapp)?.request?.completeWithDecline()
189
+ else -> completeWithInvalidResponse()
190
+ }
191
+ }
192
+ is AuthorizeDappResponse ->
193
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.AuthorizeDapp)
194
+ ?.request?.completeWithAuthorize(
195
+ response.publicKey,
196
+ response.accountLabel,
197
+ null, //Uri.parse(response.walletUriBase),
198
+ null //response.authorizationScope
199
+ )
200
+ else -> completeWithInvalidResponse()
201
+ }
202
+ is ReauthorizeDapp -> when (response) {
203
+ is MobileWalletAdapterFailureResponse -> {
204
+ when (response) {
205
+ is UserDeclinedResponse ->
206
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.ReauthorizeDapp)?.request?.completeWithDecline()
207
+ else -> completeWithInvalidResponse()
208
+ }
209
+ }
210
+ is AuthorizeDappResponse ->
211
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.ReauthorizeDapp)?.request?.completeWithReauthorize()
212
+ else -> completeWithInvalidResponse()
213
+ }
214
+ is SignAndSendTransactions -> when (response) {
215
+ is MobileWalletAdapterFailureResponse -> {
216
+ when (response) {
217
+ is UserDeclinedResponse ->
218
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignAndSendTransactions)?.request?.completeWithDecline()
219
+ is TooManyPayloadsResponse ->
220
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignAndSendTransactions)?.request?.completeWithTooManyPayloads()
221
+ is AuthorizationNotValidResponse ->
222
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignAndSendTransactions)?.request?.completeWithAuthorizationNotValid()
223
+ is InvalidSignaturesResponse ->
224
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignAndSendTransactions)?.request?.completeWithInvalidSignatures(response.valid)
225
+ }
226
+ }
227
+ is SignedAndSentTransactions ->
228
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignAndSendTransactions)?.request?.completeWithSignatures(response.signedTransactions.toTypedArray())
229
+ else -> completeWithInvalidResponse()
230
+ }
231
+ is SignPayloads -> when (response) {
232
+ is MobileWalletAdapterFailureResponse -> {
233
+ when (response) {
234
+ is UserDeclinedResponse ->
235
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignPayloads)?.request?.completeWithDecline()
236
+ is TooManyPayloadsResponse ->
237
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignPayloads)?.request?.completeWithTooManyPayloads()
238
+ is AuthorizationNotValidResponse ->
239
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignPayloads)?.request?.completeWithAuthorizationNotValid()
240
+ is InvalidSignaturesResponse ->
241
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignPayloads)?.request?.completeWithInvalidPayloads(response.valid)
242
+ }
243
+ }
244
+ is SignedPayloads ->
245
+ (pendingRequest as? MobileWalletAdapterRemoteRequest.SignPayloads)?.request?.completeWithSignedPayloads(response.signedPayloads.toTypedArray())
246
+ else -> completeWithInvalidResponse()
247
+ }
248
+ }
249
+ }
250
+
251
+ private fun checkSessionId(sessionId: String, doIfValid: (() -> Unit)) =
252
+ if (sessionId == scenarioId) doIfValid()
253
+ else sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioError(
254
+ "Invalid session ($sessionId). This session does not exist/is no longer active."
255
+ ))
256
+
257
+ private fun sendSessionEventToReact(sessionEvent: MobileWalletAdapterSessionEvent) {
258
+ val eventInfo = when(sessionEvent) {
259
+ is MobileWalletAdapterSessionEvent.None -> null
260
+ is MobileWalletAdapterSessionEvent.ScenarioError -> Arguments.createMap().apply {
261
+ putString("__type", sessionEvent.type)
262
+ putString("error", sessionEvent.message)
263
+ }
264
+ else -> Arguments.createMap().apply {
265
+ putString("__type", sessionEvent.type)
266
+ }
267
+ }
268
+
269
+ eventInfo?.putString("sessionId", scenarioId)
270
+
271
+ eventInfo?.let { sendEvent(reactContext,
272
+ Companion.MOBILE_WALLET_ADAPTER_SERVICE_REQUEST_BRIDGE_NAME, it) }
273
+ }
274
+
275
+ private fun sendWalletServiceRequestToReact(request: MobileWalletAdapterRemoteRequest) {
276
+ val surrogate = when(request) {
277
+ is MobileWalletAdapterRemoteRequest.AuthorizeDapp -> AuthorizeDapp(
278
+ scenarioId!!, request.request.cluster, request.request.identityName,
279
+ request.request.identityUri.toString(), request.request.iconRelativeUri.toString()
280
+ )
281
+ is MobileWalletAdapterRemoteRequest.ReauthorizeDapp -> ReauthorizeDapp(
282
+ scenarioId!!, request.request.cluster, request.request.identityName,
283
+ request.request.identityUri.toString(), request.request.iconRelativeUri.toString(),
284
+ request.request.authorizationScope
285
+ )
286
+ is MobileWalletAdapterRemoteRequest.SignMessages -> SignMessages(
287
+ scenarioId!!, request.request.cluster, request.request.identityName,
288
+ request.request.identityUri.toString(), request.request.iconRelativeUri.toString(),
289
+ request.request.authorizationScope, request.request.payloads.toList()
290
+ )
291
+ is MobileWalletAdapterRemoteRequest.SignTransactions -> SignTransactions(
292
+ scenarioId!!, request.request.cluster, request.request.identityName,
293
+ request.request.identityUri.toString(), request.request.iconRelativeUri.toString(),
294
+ request.request.authorizationScope, request.request.payloads.toList()
295
+ )
296
+ is MobileWalletAdapterRemoteRequest.SignAndSendTransactions -> SignAndSendTransactions(
297
+ scenarioId!!, request.request.cluster, request.request.identityName,
298
+ request.request.identityUri.toString(), request.request.iconRelativeUri.toString(),
299
+ request.request.authorizationScope, request.request.payloads.toList()
300
+ )
301
+ }
302
+
303
+ // this is dirty, the requestId needs to line up so have to manually overwrite here
304
+ // should we change javascript side to accept json?
305
+ val eventInfo =
306
+ JsonObject(json.encodeToJsonElement(MobileWalletAdapterRequestSerializer, surrogate)
307
+ .jsonObject.toMutableMap().apply {
308
+ put("requestId", JsonPrimitive(request.id))
309
+ }).toReadableMap()
310
+
311
+ sendEvent(reactContext, Companion.MOBILE_WALLET_ADAPTER_SERVICE_REQUEST_BRIDGE_NAME, eventInfo)
312
+ }
313
+
314
+ private fun sendEvent(reactContext: ReactContext, eventName: String, params: ReadableMap? = null) {
315
+ reactContext
316
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
317
+ .emit(eventName, params)
318
+ }
319
+
320
+ private inner class MobileWalletAdapterScenarioCallbacks : LocalScenario.Callbacks {
321
+ /* Session Events */
322
+ override fun onScenarioReady() {
323
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioReady)
324
+ }
325
+
326
+ override fun onScenarioServingClients() {
327
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioServingClients)
328
+ }
329
+
330
+ override fun onScenarioServingComplete() {
331
+ launch(Dispatchers.Main) {
332
+ scenario = null
333
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioServingComplete)
334
+ }
335
+ }
336
+
337
+ override fun onScenarioComplete() {
338
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioComplete)
339
+ }
340
+
341
+ override fun onScenarioError() {
342
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioError())
343
+ }
344
+
345
+ override fun onScenarioTeardownComplete() {
346
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.ScenarioTeardownComplete)
347
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.SessionTerminated)
348
+ }
349
+
350
+ override fun onLowPowerAndNoConnection() {
351
+ sendSessionEventToReact(MobileWalletAdapterSessionEvent.LowPowerNoConnection)
352
+ }
353
+
354
+ /* Remote Requests */
355
+ override fun onAuthorizeRequest(request: AuthorizeRequest) {
356
+ val request = MobileWalletAdapterRemoteRequest.AuthorizeDapp(request)
357
+ pendingRequests.put(request.id, request)
358
+ sendWalletServiceRequestToReact(request)
359
+ }
360
+
361
+ override fun onReauthorizeRequest(request: ReauthorizeRequest) {
362
+ Log.i(TAG, "Reauthorization request: auto completing, DO NOT DO THIS IN PRODUCTION")
363
+ // TODO: Implement client trust use case
364
+ request.completeWithReauthorize()
365
+ }
366
+
367
+ override fun onSignTransactionsRequest(request: SignTransactionsRequest) {
368
+ val request = MobileWalletAdapterRemoteRequest.SignTransactions(request)
369
+ pendingRequests.put(request.id, request)
370
+ sendWalletServiceRequestToReact(request)
371
+ }
372
+
373
+ override fun onSignMessagesRequest(request: SignMessagesRequest) {
374
+ val request = MobileWalletAdapterRemoteRequest.SignMessages(request)
375
+ pendingRequests.put(request.id, request)
376
+ sendWalletServiceRequestToReact(request)
377
+ }
378
+
379
+ override fun onSignAndSendTransactionsRequest(request: SignAndSendTransactionsRequest) {
380
+ val endpointUri = clusterToRpcUri(request.cluster)
381
+ val request = MobileWalletAdapterRemoteRequest.SignAndSendTransactions(request, endpointUri)
382
+ pendingRequests.put(request.id, request)
383
+ sendWalletServiceRequestToReact(request)
384
+ }
385
+
386
+ private fun verifyPrivilegedMethodSource(request: VerifiableIdentityRequest): Boolean {
387
+ // TODO: Implement client trust use case
388
+ return true
389
+ }
390
+
391
+ override fun onDeauthorizedEvent(event: DeauthorizedEvent) {
392
+ event.complete()
393
+ }
394
+ }
395
+
396
+ companion object {
397
+ private val TAG = SolanaMobileWalletAdapterWalletLibModule::class.simpleName
398
+ const val MOBILE_WALLET_ADAPTER_SERVICE_REQUEST_BRIDGE_NAME = "MobileWalletAdapterServiceRequestBridge"
399
+ const val MOBILE_WALLET_ADAPTER_SESSION_EVENT_BRIDGE_NAME = "MobileWalletAdapterSessionEventBridge"
400
+ }
401
+ }
@@ -0,0 +1,60 @@
1
+ package com.solanamobile.mobilewalletadapterwalletlib.reactnative.model
2
+
3
+ import com.solana.mobilewalletadapter.walletlib.protocol.MobileWalletAdapterConfig
4
+ import kotlinx.serialization.DeserializationStrategy
5
+ import kotlinx.serialization.KSerializer
6
+ import kotlinx.serialization.Serializable
7
+ import kotlinx.serialization.builtins.ListSerializer
8
+ import kotlinx.serialization.builtins.serializer
9
+ import kotlinx.serialization.descriptors.PrimitiveKind
10
+ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
11
+ import kotlinx.serialization.descriptors.SerialDescriptor
12
+ import kotlinx.serialization.encoding.Decoder
13
+ import kotlinx.serialization.encoding.Encoder
14
+ import kotlinx.serialization.json.JsonContentPolymorphicSerializer
15
+ import kotlinx.serialization.json.JsonElement
16
+ import kotlinx.serialization.json.JsonPrimitive
17
+
18
+ @Serializable
19
+ data class MobileWalletAdapterConfigSurrogate(
20
+ val supportsSignAndSendTransactions: Boolean,
21
+ val maxTransactionsPerSigningRequest: Int,
22
+ val maxMessagesPerSigningRequest: Int,
23
+ val supportedTransactionVersions: List<@Serializable(with = TransactionVersionSerializer::class) Any>,
24
+ val noConnectionWarningTimeoutMs: Long,
25
+ )
26
+
27
+ object TransactionVersionSerializer : JsonContentPolymorphicSerializer<Any>(Any::class) {
28
+ override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Any> =
29
+ if ((element as? JsonPrimitive)?.content?.toIntOrNull() != null) Int.serializer()
30
+ else if ((element as? JsonPrimitive)?.content == "legacy") String.serializer()
31
+ else throw IllegalArgumentException("supportedTransactionVersions must be either the string \"legacy\" or a non-negative integer")
32
+ }
33
+
34
+ object MobileWalletAdapterConfigSerializer : KSerializer<MobileWalletAdapterConfig> {
35
+ private val delegateSerializer = MobileWalletAdapterConfigSurrogate.serializer()
36
+ override val descriptor: SerialDescriptor = delegateSerializer.descriptor
37
+
38
+ override fun deserialize(decoder: Decoder): MobileWalletAdapterConfig {
39
+ val surrogate = decoder.decodeSerializableValue(delegateSerializer)
40
+ return MobileWalletAdapterConfig(
41
+ surrogate.supportsSignAndSendTransactions,
42
+ surrogate.maxTransactionsPerSigningRequest,
43
+ surrogate.maxMessagesPerSigningRequest,
44
+ surrogate.supportedTransactionVersions.toTypedArray(),
45
+ surrogate.noConnectionWarningTimeoutMs
46
+ )
47
+ }
48
+
49
+ override fun serialize(encoder: Encoder, value: MobileWalletAdapterConfig) {
50
+ encoder.encodeSerializableValue(delegateSerializer,
51
+ MobileWalletAdapterConfigSurrogate(
52
+ value.supportsSignAndSendTransactions,
53
+ value.maxTransactionsPerSigningRequest,
54
+ value.maxMessagesPerSigningRequest,
55
+ value.supportedTransactionVersions.toList(),
56
+ value.noConnectionWarningTimeoutMs
57
+ )
58
+ )
59
+ }
60
+ }