Skip to content

Commit

Permalink
[WIP] New Architecture: mobile-wallet-adapter-protocol (#1037)
Browse files Browse the repository at this point in the history
* New Architecture: mobile-wallet-adapter-protocol

* attempt to fix gradle wrapper

* revert comment change (used to force git change)

---------

Co-authored-by: funkatronics <funkatronicsmail@gmail.com>
  • Loading branch information
Michaelsulistio and Funkatronics authored Nov 26, 2024
1 parent 3c122da commit 91a34c3
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 133 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true

# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
Expand Down
12 changes: 12 additions & 0 deletions js/packages/mobile-wallet-adapter-protocol/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"src/newarch"
]
} else {
java.srcDirs += ["src/oldarch"]
}
}
}
}

repositories {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,26 @@ import com.solana.mobilewalletadapter.clientlib.scenario.LocalAssociationScenari
import com.solana.mobilewalletadapter.common.protocol.SessionProperties.ProtocolVersion
import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertJsonToMap
import com.solanamobile.mobilewalletadapter.reactnative.JSONSerializationUtils.convertMapToJson
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import org.json.JSONObject
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import org.json.JSONObject

class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext), CoroutineScope {
SolanaMobileWalletAdapterSpec(reactContext), CoroutineScope {

data class SessionState(
val client: MobileWalletAdapterClient,
val localAssociation: LocalAssociationScenario,
val client: MobileWalletAdapterClient,
val localAssociation: LocalAssociationScenario,
)

override val coroutineContext =
Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()
Dispatchers.IO + CoroutineName("SolanaMobileWalletAdapterModuleScope") + SupervisorJob()

companion object {
const val NAME = "SolanaMobileWalletAdapter"
private const val ASSOCIATION_TIMEOUT_MS = 10000
private const val CLIENT_TIMEOUT_MS = 90000
private const val REQUEST_LOCAL_ASSOCIATION = 0
Expand All @@ -43,130 +44,152 @@ class SolanaMobileWalletAdapterModule(reactContext: ReactApplicationContext) :
}

private val mActivityEventListener: ActivityEventListener =
object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity?,
requestCode: Int,
resultCode: Int,
data: Intent?
) {
if (requestCode == REQUEST_LOCAL_ASSOCIATION)
associationResultCallback?.invoke(resultCode)
object : BaseActivityEventListener() {
override fun onActivityResult(
activity: Activity?,
requestCode: Int,
resultCode: Int,
data: Intent?
) {
if (requestCode == REQUEST_LOCAL_ASSOCIATION)
associationResultCallback?.invoke(resultCode)
}
}
}

init {
reactContext.addActivityEventListener(mActivityEventListener)
}

override fun getName(): String {
return "SolanaMobileWalletAdapter"
return NAME
}

@ReactMethod
fun startSession(config: ReadableMap?, promise: Promise) = launch {
mutex.lock()
Log.d(name, "startSession with config $config")
try {
val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
val localAssociation = LocalAssociationScenario(
CLIENT_TIMEOUT_MS,
)
val intent = LocalAssociationIntentCreator.createAssociationIntent(
uriPrefix,
localAssociation.port,
localAssociation.session
)
associationResultCallback = { resultCode ->
if (resultCode == Activity.RESULT_CANCELED) {
Log.d(name, "Local association cancelled by user, ending session")
promise.reject("Session not established: Local association cancelled by user",
LocalAssociationScenario.ConnectionFailedException("Local association cancelled by user"))
localAssociation.close()
override fun startSession(config: ReadableMap?, promise: Promise): Unit {
launch {
mutex.lock()
Log.d(name, "startSession with config $config")
try {
val uriPrefix = config?.getString("baseUri")?.let { Uri.parse(it) }
val localAssociation =
LocalAssociationScenario(
CLIENT_TIMEOUT_MS,
)
val intent =
LocalAssociationIntentCreator.createAssociationIntent(
uriPrefix,
localAssociation.port,
localAssociation.session
)
associationResultCallback = { resultCode ->
if (resultCode == Activity.RESULT_CANCELED) {
Log.d(name, "Local association cancelled by user, ending session")
promise.reject(
"Session not established: Local association cancelled by user",
LocalAssociationScenario.ConnectionFailedException(
"Local association cancelled by user"
)
)
localAssociation.close()
}
}
currentActivity?.startActivityForResult(intent, REQUEST_LOCAL_ASSOCIATION)
?: throw NullPointerException(
"Could not find a current activity from which to launch a local association"
)
val client =
localAssociation
.start()
.get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
sessionState = SessionState(client, localAssociation)
val sessionPropertiesMap: WritableMap = WritableNativeMap()
sessionPropertiesMap.putString(
"protocol_version",
when (localAssociation.session.sessionProperties.protocolVersion) {
ProtocolVersion.LEGACY -> "legacy"
ProtocolVersion.V1 -> "v1"
}
)
promise.resolve(sessionPropertiesMap)
} catch (e: ActivityNotFoundException) {
Log.e(name, "Found no installed wallet that supports the mobile wallet protocol", e)
cleanup()
promise.reject("ERROR_WALLET_NOT_FOUND", e)
} catch (e: TimeoutException) {
Log.e(name, "Timed out waiting for local association to be ready", e)
cleanup()
promise.reject("Timed out waiting for local association to be ready", e)
} catch (e: InterruptedException) {
Log.w(name, "Interrupted while waiting for local association to be ready", e)
cleanup()
promise.reject(e)
} catch (e: ExecutionException) {
Log.e(name, "Failed establishing local association with wallet", e.cause)
cleanup()
promise.reject(e)
} catch (e: Throwable) {
Log.e(name, "Failed to start session", e)
cleanup()
promise.reject(e)
}
currentActivity?.startActivityForResult(intent, REQUEST_LOCAL_ASSOCIATION)
?: throw NullPointerException("Could not find a current activity from which to launch a local association")
val client =
localAssociation.start().get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
sessionState = SessionState(client, localAssociation)
val sessionPropertiesMap: WritableMap = WritableNativeMap()
sessionPropertiesMap.putString("protocol_version",
when (localAssociation.session.sessionProperties.protocolVersion) {
ProtocolVersion.LEGACY -> "legacy"
ProtocolVersion.V1 -> "v1"
})
promise.resolve(sessionPropertiesMap)
} catch (e: ActivityNotFoundException) {
Log.e(name, "Found no installed wallet that supports the mobile wallet protocol", e)
cleanup()
promise.reject("ERROR_WALLET_NOT_FOUND", e)
} catch (e: TimeoutException) {
Log.e(name, "Timed out waiting for local association to be ready", e)
cleanup()
promise.reject("Timed out waiting for local association to be ready", e)
} catch (e: InterruptedException) {
Log.w(name, "Interrupted while waiting for local association to be ready", e)
cleanup()
promise.reject(e)
} catch (e: ExecutionException) {
Log.e(name, "Failed establishing local association with wallet", e.cause)
cleanup()
promise.reject(e)
} catch (e: Throwable) {
Log.e(name, "Failed to start session", e)
cleanup()
promise.reject(e)
}
}

@ReactMethod
fun invoke(method: String, params: ReadableMap, promise: Promise) = sessionState?.let {
Log.d(name, "invoke `$method` with params $params")
try {
val result = it.client.methodCall(
method,
convertMapToJson(params),
CLIENT_TIMEOUT_MS
).get() as JSONObject
promise.resolve(convertJsonToMap(result))
} catch (e: ExecutionException) {
val cause = e.cause
if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
val userInfo = Arguments.createMap()
userInfo.putInt("jsonRpcErrorCode", cause.code)
promise.reject("JSON_RPC_ERROR", cause, userInfo)
} else if (cause is TimeoutException) {
promise.reject("Timed out waiting for response", e)
} else {
throw e
override fun invoke(method: String, params: ReadableMap?, promise: Promise): Unit =
sessionState?.let {
Log.d(name, "invoke `$method` with params $params")
try {
val result =
it.client
.methodCall(method, convertMapToJson(params), CLIENT_TIMEOUT_MS)
.get() as
JSONObject
promise.resolve(convertJsonToMap(result))
} catch (e: ExecutionException) {
val cause = e.cause
if (cause is JsonRpc20Client.JsonRpc20RemoteException) {
val userInfo = Arguments.createMap()
userInfo.putInt("jsonRpcErrorCode", cause.code)
promise.reject("JSON_RPC_ERROR", cause, userInfo)
} else if (cause is TimeoutException) {
promise.reject("Timed out waiting for response", e)
} else {
throw e
}
} catch (e: Throwable) {
Log.e(name, "Failed to invoke `$method` with params $params", e)
promise.reject(e)
}
}
} catch (e: Throwable) {
Log.e(name, "Failed to invoke `$method` with params $params", e)
promise.reject(e)
}
} ?: throw NullPointerException("Tried to invoke `$method` without an active session")
?: throw NullPointerException(
"Tried to invoke `$method` without an active session"
)

@ReactMethod
fun endSession(promise: Promise) = sessionState?.let {
launch {
Log.d(name, "endSession")
try {
it.localAssociation.close()
.get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
cleanup()
promise.resolve(true)
} catch (e: TimeoutException) {
Log.e(name, "Timed out waiting for local association to close", e)
cleanup()
promise.reject("Failed to end session", e)
} catch (e: Throwable) {
Log.e(name, "Failed to end session", e)
cleanup()
promise.reject("Failed to end session", e)
override fun endSession(promise: Promise): Unit {
sessionState?.let {
launch {
Log.d(name, "endSession")
try {
it.localAssociation
.close()
.get(ASSOCIATION_TIMEOUT_MS.toLong(), TimeUnit.MILLISECONDS)
cleanup()
promise.resolve(true)
} catch (e: TimeoutException) {
Log.e(name, "Timed out waiting for local association to close", e)
cleanup()
promise.reject("Failed to end session", e)
} catch (e: Throwable) {
Log.e(name, "Failed to end session", e)
cleanup()
promise.reject("Failed to end session", e)
}
}
}
} ?: throw NullPointerException("Tried to end a session without an active session")
?: throw NullPointerException("Tried to end a session without an active session")
}

private fun cleanup() {
sessionState = null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
package com.solanamobile.mobilewalletadapter.reactnative

import com.facebook.react.ReactPackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import java.util.HashMap

class SolanaMobileWalletAdapterPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(SolanaMobileWalletAdapterModule(reactContext))
class SolanaMobileWalletAdapterModulePackage : TurboReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return if (name == SolanaMobileWalletAdapterModule.NAME) {
SolanaMobileWalletAdapterModule(reactContext)
} else {
null
}
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
return ReactModuleInfoProvider {
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
moduleInfos[SolanaMobileWalletAdapterModule.NAME] =
ReactModuleInfo(
SolanaMobileWalletAdapterModule.NAME,
SolanaMobileWalletAdapterModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
moduleInfos
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.solanamobile.mobilewalletadapter.reactnative

import com.facebook.react.bridge.ReactApplicationContext

abstract class SolanaMobileWalletAdapterSpec
internal constructor(context: ReactApplicationContext) :
NativeSolanaMobileWalletAdapterSpec(context) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.solanamobile.mobilewalletadapter.reactnative

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReadableMap

abstract class SolanaMobileWalletAdapterSpec
internal constructor(context: ReactApplicationContext) : ReactContextBaseJavaModule(context) {

abstract fun startSession(config: ReadableMap?, promise: Promise)

abstract fun invoke(method: String, params: ReadableMap?, promise: Promise)

abstract fun endSession(promise: Promise)
}
8 changes: 8 additions & 0 deletions js/packages/mobile-wallet-adapter-protocol/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,13 @@
"peerDependencies": {
"@solana/web3.js": "^1.58.0",
"react-native": ">0.69"
},
"codegenConfig": {
"name": "SolanaMobileWalletAdapter",
"type": "all",
"jsSrcsDir": "./src/codegenSpec",
"android": {
"javaPackageName": "com.solanamobile.mobilewalletadapter.reactnative"
}
}
}
Loading

0 comments on commit 91a34c3

Please sign in to comment.