Skip to content

Commit

Permalink
Merge pull request #147 from MetaMask/handle-rn-read-only-rpc
Browse files Browse the repository at this point in the history
fix:  concurrency handling and read-only calls improvements
  • Loading branch information
elefantel authored Sep 11, 2024
2 parents 6f8fcc5 + abd9748 commit 7e2da21
Show file tree
Hide file tree
Showing 9 changed files with 44 additions and 41 deletions.
5 changes: 2 additions & 3 deletions metamask-android-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ android {
targetSdk 33

ext.versionCode = 1
ext.versionName = "0.6.4"
ext.versionName = "0.6.5"

testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
consumerProguardFiles 'consumer-rules.pro'
Expand Down Expand Up @@ -53,7 +53,6 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"

testImplementation 'junit:junit:4.13.2'
Expand All @@ -68,7 +67,7 @@ dependencies {

ext {
PUBLISH_GROUP_ID = 'io.metamask.androidsdk'
PUBLISH_VERSION = '0.6.4'
PUBLISH_VERSION = '0.6.5'
PUBLISH_ARTIFACT_ID = 'metamask-android-sdk'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.metamask.androidsdk

import org.json.JSONObject

open class ReadOnlyRPCProvider(private val infuraAPIKey: String?, readonlyRPCMap: Map<String, String>?, private val logger: Logger = DefaultLogger) {
open class ReadOnlyRPCProvider(private val infuraAPIKey: String?, private val readonlyRPCMap: Map<String, String>?, private val logger: Logger = DefaultLogger) {
val rpcUrls: Map<String, String> = when {
readonlyRPCMap != null && infuraAPIKey != null -> {
// Merge infuraReadonlyRPCMap with readonlyRPCMap, overriding infura's keys if they are present in readonlyRPCMap
Expand All @@ -16,14 +16,16 @@ open class ReadOnlyRPCProvider(private val infuraAPIKey: String?, readonlyRPCMap
}

fun supportsChain(chainId: String): Boolean {
return !rpcUrls[chainId].isNullOrEmpty()
val apiKey = infuraAPIKey ?: ""
val readonlyMap = readonlyRPCMap ?: mapOf()
return !rpcUrls[chainId].isNullOrEmpty() && (readonlyMap.containsKey(chainId) || apiKey.isNotEmpty())
}

fun infuraReadonlyRPCMap(infuraAPIKey: String) : Map<String, String> {
return mapOf(
// ###### Ethereum ######
// Mainnet
//"0x1" to "https://mainnet.infura.io/v3/${infuraAPIKey}",
"0x1" to "https://mainnet.infura.io/v3/${infuraAPIKey}",

// Sepolia 11155111
"0x2a" to "https://sepolia.infura.io/v3/${infuraAPIKey}",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.metamask.androidsdk

object SDKInfo {
const val VERSION = "0.6.4"
const val VERSION = "0.6.5"
const val PLATFORM = "android"
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ class EthereumTests {
private fun findRequestIdForAccountRequest(method: EthereumMethod): String {
return communicationClient.submittedRequests.entries.find {
it.value.request.method == method.value
}?.key ?: throw IllegalStateException("No account request found")
}?.key ?: ""
}

private fun prepareCommunicationClient() {
Expand Down
13 changes: 13 additions & 0 deletions nativesdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ The MetaMask Android SDK Native Module is a Kotlin [Android Native Module](https

This module handles encrypted communication between the dapp and MetaMask and then relays the messages over to the Android SDK communication layer implemented in React Native in the wallet. The wallet calls the Native Module via NativeModules - an API that enables react-native code to call native Kotlin primitives.

## Compiling
To compile the nativesdk as a `.aar` file, you need to modify the project's `settings.gradle` file to have the nativesdk as a standalone module to include to the project:
```
include ':app'
include ':nativesdk'
include ':metamask-android-sdk'
```
Then run
```
./gradlew assembleDebug
```
The output file will be located in `nativesdk/build/outputs/aar`. This nativesdk library is then embedded in the MetaMask mobile in the path `android/libs/nativesdk.aar`. It becomes the server-side component of the AIDL communication mechanism described below in the Architecture section.

## Architecture
The client SDK communicates with the server SDK (Android Native Module) via IPC implemented using AIDL.

Expand Down
3 changes: 1 addition & 2 deletions nativesdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,12 @@ android {
dependencies {

compileOnly(files("libs/ecies.aar"))
implementation("com.facebook.react:react-native:+")
implementation("com.facebook.react:react-android:0.73.3")
implementation("androidx.core:core-ktx:1.7.0")
implementation ("com.squareup.okhttp3:okhttp:4.9.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,6 @@ class CommunicationClient(reactContext: ReactApplicationContext) : ReactContextB
connectionStatusManager.onMetaMaskBroadcastUnregistered()
}

override fun invalidate() {
super.invalidate()
Logger.log("CommunicationClient:: invalidate - app terminated")

if (!didPerformTearDown) {
destroyActiveConnections()
}
}

override fun onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy()
Logger.log("CommunicationClient:: onCatalystInstanceDestroy - app terminating")
Expand Down Expand Up @@ -142,13 +133,14 @@ class CommunicationClient(reactContext: ReactApplicationContext) : ReactContextB

Logger.log("CommunicationClient:: Binding native module")
val intent = Intent(reactAppContext, MessageService::class.java)
return reactAppContext.bindService(
val success = reactAppContext.bindService(
intent,
serviceConnection,
Context.BIND_IMPORTANT)

// Metamask ready, start sending messages
connectionStatusManager.onMetaMaskReady()
return success
}

@ReactMethod
Expand Down Expand Up @@ -184,7 +176,7 @@ class CommunicationClient(reactContext: ReactApplicationContext) : ReactContextB

if (type == "ready") {
val dataJson = json.optJSONObject("data")
val id = dataJson.optString("id")
val id = dataJson?.optString("id")
val sessionId = SessionManager.getInstance().sessionId

if (id != sessionId) {
Expand Down
4 changes: 1 addition & 3 deletions nativesdk/src/main/java/io/metamask/nativesdk/KeyExchange.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ class KeyExchange {

fun resetKeys() {
privateKey = crypto.generatePrivateKey()
privateKey?.let {
publicKey = crypto.publicKey(it)
}
publicKey = crypto.publicKey(privateKey)
setIsKeysExchanged(false)
theirPublicKey = null
}
Expand Down
34 changes: 17 additions & 17 deletions nativesdk/src/main/java/io/metamask/nativesdk/MessageService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -162,11 +162,17 @@ class MessageService : Service(), MetaMaskConnectionStatusCallback {
}
}

private fun sendMessage(message: String) {
private fun sendMessage(key: String = MESSAGE, message: String) {
val bundle = Bundle().apply {
putString(MESSAGE, message)
putString(key, message)
}
try {
synchronized(this@MessageService) {
dappMessageServiceCallback?.onMessageReceived(bundle)
}
} catch (e: Exception) {
Logger.log("MessageService:: Error sending message to dapp: ${e.message}")
}
dappMessageServiceCallback?.onMessageReceived(bundle)
}

private fun handleKeyExchange(message: String) {
Expand Down Expand Up @@ -199,7 +205,7 @@ class MessageService : Service(), MetaMaskConnectionStatusCallback {

Logger.log("MessageService:: $keysExchangedMessage")
val payload = keyExchange.encrypt(keysExchangedMessage)
sendMessage(payload)
sendMessage(message=payload)
}
}

Expand Down Expand Up @@ -248,18 +254,17 @@ class MessageService : Service(), MetaMaskConnectionStatusCallback {
private fun resumeQueuedJobs() {
Logger.log("MessageService:: Resuming jobs")

while (jobQueue.isNotEmpty()) {
val job = jobQueue.removeFirstOrNull()
job?.invoke()
synchronized(this) {
while (jobQueue.isNotEmpty()) {
val job = jobQueue.removeFirstOrNull()
job?.invoke()
}
}
}

private fun sendKeyExchangeMessage(message: String) {
Logger.log("MessageService:: Sending key exchange: $message")
val bundle = Bundle().apply {
putString(KEY_EXCHANGE, message)
}
dappMessageServiceCallback?.onMessageReceived(bundle)
sendMessage(key=KEY_EXCHANGE, message=message)
}

private fun handleMessage(message: String, id: String) {
Expand Down Expand Up @@ -329,12 +334,7 @@ class MessageService : Service(), MetaMaskConnectionStatusCallback {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == "local.service") {
val message = intent.getStringExtra(EventType.MESSAGE.value) as String

val bundle = Bundle().apply {
putString(EventType.MESSAGE.value, message)
}

dappMessageServiceCallback?.onMessageReceived(bundle)
sendMessage(message=message)
}
}
}
Expand Down

0 comments on commit 7e2da21

Please sign in to comment.