From c260ec3e6ddeda1aeed2c7828722915c55636276 Mon Sep 17 00:00:00 2001
From: Babak <10467399+Babak-gh@users.noreply.github.com>
Date: Tue, 14 Nov 2023 21:45:43 -0800
Subject: [PATCH] Wallet connect v2 (#134)
* add WalletConnect v2 core and WalletConnectModal libraries
* add Navigation Component in ConnectWalletActivity because of WalletConnectModal integration
* remove a unnecessary line in ConnectWalletFragment from prev commit
* Create application class and basic Wallet Connect V2 and WalletConnectModal Configurations
* create Chains.kt enum for WC v2 integration and make a default Ethereum_Main chain in it
* add ic_ethereum.xml file as chain icon
* create DappDelegate.kt for receiving asynchronously updates sent from the Wallet
* create WalletConnectV2Account.kt to make xmtp SDK with WC v2
* add connect with WalletConnectModal in ConnectWalletFragment
* remove walletConnectKit library and replace the wallet connect button with outlined material button
* replacing suspendCoroutine with callbackFlow for fixing double resume problem
---
example/build.gradle | 16 +-
example/src/main/AndroidManifest.xml | 27 +++-
.../org/xmtp/android/example/ExampleApp.kt | 47 ++++++
.../example/account/WalletConnectAccount.kt | 37 -----
.../example/account/WalletConnectV2Account.kt | 53 +++++++
.../example/connect/ChainSelectionUi.kt | 16 ++
.../xmtp/android/example/connect/Chains.kt | 46 ++++++
.../example/connect/ConnectWalletActivity.kt | 67 ---------
.../example/connect/ConnectWalletFragment.kt | 140 ++++++++++++++++++
.../example/connect/ConnectWalletViewModel.kt | 92 ++++++++++--
.../android/example/connect/DappDelegate.kt | 89 +++++++++++
.../extension/WalletConnectRequestHelper.kt | 86 +++++++++++
example/src/main/res/drawable/ic_ethereum.xml | 27 ++++
.../res/drawable/ic_wallet_connect_logo.xml | 9 ++
.../res/layout/activity_connect_wallet.xml | 51 ++-----
.../res/layout/fragment_connect_wallet.xml | 63 ++++++++
example/src/main/res/navigation/nav_graph.xml | 24 +++
example/src/main/res/values/strings.xml | 1 +
example/src/main/res/values/themes.xml | 2 +-
19 files changed, 726 insertions(+), 167 deletions(-)
create mode 100644 example/src/main/java/org/xmtp/android/example/ExampleApp.kt
delete mode 100644 example/src/main/java/org/xmtp/android/example/account/WalletConnectAccount.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/account/WalletConnectV2Account.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/connect/ChainSelectionUi.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/connect/Chains.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/connect/DappDelegate.kt
create mode 100644 example/src/main/java/org/xmtp/android/example/extension/WalletConnectRequestHelper.kt
create mode 100644 example/src/main/res/drawable/ic_ethereum.xml
create mode 100644 example/src/main/res/drawable/ic_wallet_connect_logo.xml
create mode 100644 example/src/main/res/layout/fragment_connect_wallet.xml
create mode 100644 example/src/main/res/navigation/nav_graph.xml
diff --git a/example/build.gradle b/example/build.gradle
index 16c70bdf9..5bfe01bbc 100644
--- a/example/build.gradle
+++ b/example/build.gradle
@@ -13,12 +13,12 @@ plugins {
android {
namespace 'org.xmtp.android.example'
- compileSdk 33
+ compileSdk 34
defaultConfig {
applicationId "org.xmtp.android.example"
minSdk 27
- targetSdk 33
+ targetSdk 34
versionCode 1
versionName "1.0"
@@ -26,6 +26,7 @@ android {
vectorDrawables {
useSupportLibrary true
}
+ buildConfigField("String", "PROJECT_ID", "\"Your Application ID from https://cloud.walletconnect.com/\"")
}
buildTypes {
@@ -63,9 +64,18 @@ dependencies {
implementation 'androidx.activity:activity-ktx:1.6.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
- implementation 'dev.pinkroom:walletconnectkit:0.3.2'
implementation 'org.web3j:crypto:5.0.0'
+ // WalletConnect V2: core library + WalletConnectModal
+ implementation(platform("com.walletconnect:android-bom:1.19.1"))
+ implementation("com.walletconnect:android-core")
+ implementation("com.walletconnect:walletconnect-modal")
+
+ //Navigation Component
+ def nav_version = "2.7.5"
+ implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
+ implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
+
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml
index 0297194b5..d684e55a2 100644
--- a/example/src/main/AndroidManifest.xml
+++ b/example/src/main/AndroidManifest.xml
@@ -6,6 +6,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -21,10 +34,12 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:name=".ExampleApp"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.XMTPAndroid"
- tools:targetApi="31">
+ tools:targetApi="31"
+ tools:replace="dataExtractionRules">
+ android:exported="true" >
+
+
+
+
+
+
diff --git a/example/src/main/java/org/xmtp/android/example/ExampleApp.kt b/example/src/main/java/org/xmtp/android/example/ExampleApp.kt
new file mode 100644
index 000000000..961945b07
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/ExampleApp.kt
@@ -0,0 +1,47 @@
+package org.xmtp.android.example
+
+import android.app.Application
+import com.walletconnect.android.Core
+import com.walletconnect.android.CoreClient
+import com.walletconnect.android.relay.ConnectionType
+import com.walletconnect.wcmodal.client.Modal
+import com.walletconnect.wcmodal.client.WalletConnectModal
+import timber.log.Timber
+
+const val BASE_LOG_TAG = "WC2"
+class ExampleApp: Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ val connectionType = ConnectionType.AUTOMATIC
+ val relayUrl = "relay.walletconnect.com"
+ val serverUrl = "wss://$relayUrl?projectId=${BuildConfig.PROJECT_ID}"
+ val appMetaData = Core.Model.AppMetaData(
+ name = "XMTP Example",
+ description = "Example app using the xmtp-android SDK",
+ url = "https://xmtp.org",
+ icons = listOf("https://avatars.githubusercontent.com/u/82580170?s=48&v=4"),
+ redirect = "xmtp-example-wc://request"
+ )
+
+ CoreClient.initialize(
+ metaData = appMetaData,
+ relayServerUrl = serverUrl,
+ connectionType = connectionType,
+ application = this,
+ onError = {
+ Timber.tag("$BASE_LOG_TAG CoreClient").d(it.toString())
+ }
+ )
+
+ WalletConnectModal.initialize(
+ init = Modal.Params.Init(core = CoreClient),
+ onSuccess = {
+ Timber.tag("$BASE_LOG_TAG initialize").d("initialize successfully")
+ },
+ onError = { error ->
+ Timber.tag("$BASE_LOG_TAG initialize").d(error.toString())
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/example/src/main/java/org/xmtp/android/example/account/WalletConnectAccount.kt b/example/src/main/java/org/xmtp/android/example/account/WalletConnectAccount.kt
deleted file mode 100644
index 237d80ed9..000000000
--- a/example/src/main/java/org/xmtp/android/example/account/WalletConnectAccount.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.xmtp.android.example.account
-
-import dev.pinkroom.walletconnectkit.WalletConnectKit
-import org.web3j.crypto.Keys
-import org.web3j.utils.Numeric
-import org.xmtp.android.library.SigningKey
-import org.xmtp.android.library.messages.SignatureBuilder
-import org.xmtp.proto.message.contents.SignatureOuterClass
-
-data class WalletConnectAccount(private val wcKit: WalletConnectKit) : SigningKey {
- override val address: String
- get() = Keys.toChecksumAddress(wcKit.address.orEmpty())
-
- override suspend fun sign(data: ByteArray): SignatureOuterClass.Signature? {
- return sign(String(data))
- }
-
- override suspend fun sign(message: String): SignatureOuterClass.Signature? {
- runCatching { wcKit.personalSign(message) }
- .onSuccess {
- var result = it.result as String
- if (result.startsWith("0x") && result.length == 132) {
- result = result.drop(2)
- }
-
- val resultData = Numeric.hexStringToByteArray(result)
-
- // Ensure we have a valid recovery byte
- resultData[resultData.size - 1] =
- (1 - resultData[resultData.size - 1] % 2).toByte()
-
- return SignatureBuilder.buildFromSignatureData(resultData)
- }
- .onFailure {}
- return null
- }
-}
\ No newline at end of file
diff --git a/example/src/main/java/org/xmtp/android/example/account/WalletConnectV2Account.kt b/example/src/main/java/org/xmtp/android/example/account/WalletConnectV2Account.kt
new file mode 100644
index 000000000..3225e839e
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/account/WalletConnectV2Account.kt
@@ -0,0 +1,53 @@
+package org.xmtp.android.example.account
+
+import android.net.Uri
+import com.walletconnect.wcmodal.client.Modal
+import kotlinx.coroutines.flow.first
+import org.web3j.crypto.Keys
+import org.xmtp.android.example.connect.getPersonalSignBody
+import org.xmtp.android.example.extension.requestMethod
+import org.xmtp.android.library.SigningKey
+import org.xmtp.android.library.messages.SignatureBuilder
+import org.xmtp.proto.message.contents.SignatureOuterClass
+
+
+data class WalletConnectV2Account(
+ val session: Modal.Model.ApprovedSession,
+ val chain: String,
+ private val sendSessionRequestDeepLink: (Uri) -> Unit
+) :
+ SigningKey {
+ override val address: String
+ get() = Keys.toChecksumAddress(
+ session.namespaces.getValue(chain).accounts[0].substringAfterLast(
+ ":"
+ )
+ )
+
+ override suspend fun sign(data: ByteArray): SignatureOuterClass.Signature? {
+ return sign(String(data))
+ }
+
+ override suspend fun sign(message: String): SignatureOuterClass.Signature? {
+ val (parentChain, chainId, account) = session.namespaces.getValue(chain).accounts[0].split(":")
+ val requestParams = session.namespaces.getValue(chain).methods.find { method ->
+ method == "personal_sign"
+ }?.let { method ->
+ Modal.Params.Request(
+ sessionTopic = session.topic,
+ method = method,
+ params = getPersonalSignBody(message, account),
+ chainId = "$parentChain:$chainId"
+ )
+ }
+ runCatching {
+ requestMethod(requestParams!!, sendSessionRequestDeepLink).first().getOrThrow()
+ }
+ .onSuccess {
+ return SignatureBuilder.buildFromSignatureData(it)
+ }
+ .onFailure {}
+
+ return null
+ }
+}
diff --git a/example/src/main/java/org/xmtp/android/example/connect/ChainSelectionUi.kt b/example/src/main/java/org/xmtp/android/example/connect/ChainSelectionUi.kt
new file mode 100644
index 000000000..d930b5391
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/connect/ChainSelectionUi.kt
@@ -0,0 +1,16 @@
+package org.xmtp.android.example.connect
+
+
+data class ChainSelectionUi(
+ val chainName: String,
+ val chainNamespace: String,
+ val chainReference: String,
+ val icon: Int,
+ val methods: List,
+ val events: List,
+ var isSelected: Boolean = false,
+) {
+ val chainId = "${chainNamespace}:${chainReference}"
+}
+
+fun Chains.toChainUiState() = ChainSelectionUi(chainName, chainNamespace, chainReference, icon, methods, events)
diff --git a/example/src/main/java/org/xmtp/android/example/connect/Chains.kt b/example/src/main/java/org/xmtp/android/example/connect/Chains.kt
new file mode 100644
index 000000000..e25d10d76
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/connect/Chains.kt
@@ -0,0 +1,46 @@
+package org.xmtp.android.example.connect
+
+import androidx.annotation.DrawableRes
+import org.xmtp.android.example.R
+
+
+fun getPersonalSignBody(message:String, account: String): String {
+ val msg = message.encodeToByteArray()
+ .joinToString(separator = "", prefix = "0x") { eachByte -> "%02x".format(eachByte) }
+ return "[\"$msg\", \"$account\"]"
+}
+enum class Chains(
+ val chainName: String,
+ val chainNamespace: String,
+ val chainReference: String,
+ @DrawableRes val icon: Int,
+ val methods: List,
+ val events: List,
+) {
+ ETHEREUM_MAIN(
+ chainName = "Ethereum",
+ chainNamespace = Info.Eth.chain,
+ chainReference = "1",
+ icon = R.drawable.ic_ethereum,
+ methods = Info.Eth.defaultMethods,
+ events = Info.Eth.defaultEvents,
+ )
+}
+
+sealed class Info {
+ abstract val chain: String
+ abstract val defaultEvents: List
+ abstract val defaultMethods: List
+
+ object Eth : Info() {
+ override val chain = "eip155"
+ override val defaultEvents: List = listOf("chainChanged", "accountsChanged")
+ override val defaultMethods: List = listOf(
+ "eth_sendTransaction",
+ "personal_sign",
+ "eth_sign",
+ "eth_signTypedData"
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletActivity.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletActivity.kt
index 827e55bc8..be6cde3ce 100644
--- a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletActivity.kt
+++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletActivity.kt
@@ -20,11 +20,6 @@ import org.xmtp.android.example.databinding.ActivityConnectWalletBinding
class ConnectWalletActivity : AppCompatActivity() {
- companion object {
- private const val WC_URI_SCHEME = "wc://wc?uri="
- }
-
- private val viewModel: ConnectWalletViewModel by viewModels()
private lateinit var binding: ActivityConnectWalletBinding
override fun onCreate(savedInstanceState: Bundle?) {
@@ -33,72 +28,10 @@ class ConnectWalletActivity : AppCompatActivity() {
binding = ActivityConnectWalletBinding.inflate(layoutInflater)
setContentView(binding.root)
- lifecycleScope.launch {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
- viewModel.uiState.collect(::ensureUiState)
- }
- }
- binding.generateButton.setOnClickListener {
- viewModel.generateWallet()
- }
- val isConnectWalletAvailable = isConnectAvailable()
- binding.connectButton.isEnabled = isConnectWalletAvailable
- binding.connectError.isVisible = !isConnectWalletAvailable
- binding.connectButton.setOnClickListener {
- binding.connectButton.start(viewModel.walletConnectKit, ::onConnected, ::onDisconnected)
- }
- }
- private fun onConnected(address: String) {
- viewModel.connectWallet()
}
- private fun onDisconnected() {
- // No-op currently.
- }
- private fun isConnectAvailable(): Boolean {
- val wcIntent = Intent(Intent.ACTION_VIEW).apply {
- data = Uri.parse(WC_URI_SCHEME)
- }
- return wcIntent.resolveActivity(packageManager) != null
- }
-
- private fun ensureUiState(uiState: ConnectWalletViewModel.ConnectUiState) {
- when (uiState) {
- is ConnectWalletViewModel.ConnectUiState.Error -> showError(uiState.message)
- ConnectWalletViewModel.ConnectUiState.Loading -> showLoading()
- is ConnectWalletViewModel.ConnectUiState.Success -> signIn(
- uiState.address,
- uiState.encodedKeyData
- )
- ConnectWalletViewModel.ConnectUiState.Unknown -> Unit
- }
- }
-
- private fun signIn(address: String, encodedKey: String) {
- val accountManager = AccountManager.get(this)
- Account(address, resources.getString(R.string.account_type)).also { account ->
- accountManager.addAccountExplicitly(account, encodedKey, null)
- }
- startActivity(Intent(this, MainActivity::class.java))
- finish()
- }
-
- private fun showError(message: String) {
- binding.progress.visibility = View.GONE
- binding.generateButton.visibility = View.VISIBLE
- binding.connectButton.visibility = View.VISIBLE
- binding.connectError.isVisible = !isConnectAvailable()
- Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
- }
-
- private fun showLoading() {
- binding.progress.visibility = View.VISIBLE
- binding.generateButton.visibility = View.GONE
- binding.connectButton.visibility = View.GONE
- binding.connectError.visibility = View.GONE
- }
}
diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt
new file mode 100644
index 000000000..711736eae
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletFragment.kt
@@ -0,0 +1,140 @@
+package org.xmtp.android.example.connect
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.fragment.findNavController
+import com.walletconnect.wcmodal.client.WalletConnectModal
+import com.walletconnect.wcmodal.ui.openWalletConnectModal
+import kotlinx.coroutines.launch
+import org.xmtp.android.example.MainActivity
+import org.xmtp.android.example.R
+import org.xmtp.android.example.databinding.FragmentConnectWalletBinding
+import timber.log.Timber
+
+
+class ConnectWalletFragment : Fragment() {
+
+
+ private val viewModel: ConnectWalletViewModel by viewModels()
+
+ private var _binding: FragmentConnectWalletBinding? = null
+ private val binding get() = _binding!!
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ _binding = FragmentConnectWalletBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.uiState.collect(::ensureUiState)
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.showWalletState.collect(::showWalletState)
+ }
+ }
+
+ binding.generateButton.setOnClickListener {
+ viewModel.generateWallet()
+ }
+
+ val isConnectWalletAvailable = isConnectAvailable()
+ binding.connectButton.isEnabled = isConnectWalletAvailable
+ binding.connectError.isVisible = !isConnectWalletAvailable
+ binding.connectButton.setOnClickListener {
+ WalletConnectModal.setSessionParams(viewModel.getSessionParams())
+ findNavController().openWalletConnectModal(id = R.id.action_to_bottomSheet)
+ }
+
+ }
+
+ private fun isConnectAvailable(): Boolean {
+ val wcIntent = Intent(Intent.ACTION_VIEW).apply {
+ data = Uri.parse(WC_URI_SCHEME)
+ }
+ return wcIntent.resolveActivity(requireActivity().packageManager) != null
+ }
+
+ private fun ensureUiState(uiState: ConnectWalletViewModel.ConnectUiState) {
+ when (uiState) {
+ is ConnectWalletViewModel.ConnectUiState.Error -> showError(uiState.message)
+ ConnectWalletViewModel.ConnectUiState.Loading -> showLoading()
+ is ConnectWalletViewModel.ConnectUiState.Success -> signIn(
+ uiState.address,
+ uiState.encodedKeyData
+ )
+
+ ConnectWalletViewModel.ConnectUiState.Unknown -> Unit
+ }
+ }
+
+ private fun showWalletState(walletState: ConnectWalletViewModel.ShowWalletForSigningState) {
+ if (walletState.showWallet) {
+ try {
+ val intent = Intent(Intent.ACTION_VIEW, walletState.uri)
+ intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ requireActivity().startActivity(intent)
+ viewModel.clearShowWalletState()
+ } catch (e: Exception) {
+ Timber.tag(FRAGMENT_LOG_TAG).e("Activity not found: $e")
+ }
+ }
+ }
+
+ private fun signIn(address: String, encodedKey: String) {
+ val accountManager = AccountManager.get(requireContext())
+ Account(address, resources.getString(R.string.account_type)).also { account ->
+ accountManager.addAccountExplicitly(account, encodedKey, null)
+ }
+ requireActivity().startActivity(Intent(requireActivity(), MainActivity::class.java))
+ requireActivity().finish()
+ }
+
+ private fun showError(message: String) {
+ binding.progress.visibility = View.GONE
+ binding.generateButton.visibility = View.VISIBLE
+ binding.connectButton.visibility = View.VISIBLE
+ binding.connectError.isVisible = !isConnectAvailable()
+ Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
+ }
+
+ private fun showLoading() {
+ binding.progress.visibility = View.VISIBLE
+ binding.generateButton.visibility = View.GONE
+ binding.connectButton.visibility = View.GONE
+ binding.connectError.visibility = View.GONE
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ companion object {
+ private const val WC_URI_SCHEME = "wc://wc?uri="
+ private const val FRAGMENT_LOG_TAG = "ConnectWalletFragment"
+ }
+
+}
\ No newline at end of file
diff --git a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt
index ef4a10580..5255f0d21 100644
--- a/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt
+++ b/example/src/main/java/org/xmtp/android/example/connect/ConnectWalletViewModel.kt
@@ -1,35 +1,82 @@
package org.xmtp.android.example.connect
-import android.app.Application
+import android.net.Uri
import androidx.annotation.UiThread
-import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import dev.pinkroom.walletconnectkit.WalletConnectKit
-import dev.pinkroom.walletconnectkit.WalletConnectKitConfig
+import com.walletconnect.wcmodal.client.Modal
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.xmtp.android.example.ClientManager
-import org.xmtp.android.example.account.WalletConnectAccount
+import org.xmtp.android.example.account.WalletConnectV2Account
import org.xmtp.android.library.Client
import org.xmtp.android.library.XMTPException
import org.xmtp.android.library.messages.PrivateKeyBuilder
import org.xmtp.android.library.messages.PrivateKeyBundleV1Builder
-class ConnectWalletViewModel(application: Application) : AndroidViewModel(application) {
+class ConnectWalletViewModel : ViewModel() {
+
+ private val chains: List =
+ Chains.values().map { it.toChainUiState() }
+
+ private val _showWalletState = MutableStateFlow(ShowWalletForSigningState(showWallet = false))
+ val showWalletState: StateFlow
+ get() = _showWalletState.asStateFlow()
private val _uiState = MutableStateFlow(ConnectUiState.Unknown)
val uiState: StateFlow = _uiState
- private val walletConnectKitConfig = WalletConnectKitConfig(
- context = application,
- bridgeUrl = "https://safe-walletconnect.safe.global/",
- appUrl = "https://xmtp.org",
- appName = "XMTP Example",
- appDescription = "Example app using the xmtp-android SDK"
+ init {
+ DappDelegate.wcEventModels
+ .filterNotNull()
+ .onEach { walletEvent ->
+ when (walletEvent) {
+ is Modal.Model.ApprovedSession -> {
+ connectWallet(walletEvent)
+ }
+
+ else -> Unit
+ }
+
+ }.launchIn(viewModelScope)
+ }
+
+ fun getSessionParams() = Modal.Params.SessionParams(
+ requiredNamespaces = getNamespaces(),
+ optionalNamespaces = getOptionalNamespaces()
)
- val walletConnectKit = WalletConnectKit.Builder(walletConnectKitConfig).build()
+
+ private fun getNamespaces(): Map {
+ val namespaces: Map =
+ chains
+ .groupBy { it.chainNamespace }
+ .map { (key: String, selectedChains: List) ->
+ key to Modal.Model.Namespace.Proposal(
+ chains = selectedChains.map { it.chainId },
+ methods = selectedChains.flatMap { it.methods }.distinct(),
+ events = selectedChains.flatMap { it.events }.distinct()
+ )
+ }.toMap()
+
+
+ return namespaces.toMutableMap()
+ }
+
+ private fun getOptionalNamespaces() = chains
+ .groupBy { it.chainId }
+ .map { (key: String, selectedChains: List) ->
+ key to Modal.Model.Namespace.Proposal(
+ methods = selectedChains.flatMap { it.methods }.distinct(),
+ events = selectedChains.flatMap { it.events }.distinct()
+ )
+ }.toMap()
@UiThread
fun generateWallet() {
@@ -49,11 +96,18 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic
}
@UiThread
- fun connectWallet() {
+ fun connectWallet(approvedSession: Modal.Model.ApprovedSession) {
viewModelScope.launch(Dispatchers.IO) {
_uiState.value = ConnectUiState.Loading
try {
- val wallet = WalletConnectAccount(walletConnectKit)
+ val wallet = WalletConnectV2Account(
+ approvedSession,
+ Chains.ETHEREUM_MAIN.chainNamespace
+ ) { uri ->
+ _showWalletState.update {
+ it.copy(showWallet = true, uri = uri)
+ }
+ }
val client = Client().create(wallet, ClientManager.CLIENT_OPTIONS)
_uiState.value = ConnectUiState.Success(
wallet.address,
@@ -65,10 +119,18 @@ class ConnectWalletViewModel(application: Application) : AndroidViewModel(applic
}
}
+ fun clearShowWalletState() {
+ _showWalletState.update {
+ it.copy(showWallet = false)
+ }
+ }
+
sealed class ConnectUiState {
object Unknown : ConnectUiState()
object Loading : ConnectUiState()
data class Success(val address: String, val encodedKeyData: String) : ConnectUiState()
data class Error(val message: String) : ConnectUiState()
}
+
+ data class ShowWalletForSigningState(val showWallet: Boolean, val uri: Uri? = null)
}
diff --git a/example/src/main/java/org/xmtp/android/example/connect/DappDelegate.kt b/example/src/main/java/org/xmtp/android/example/connect/DappDelegate.kt
new file mode 100644
index 000000000..8f10f65dd
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/connect/DappDelegate.kt
@@ -0,0 +1,89 @@
+package org.xmtp.android.example.connect
+
+import com.walletconnect.wcmodal.client.Modal
+import com.walletconnect.wcmodal.client.WalletConnectModal
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+object DappDelegate : WalletConnectModal.ModalDelegate {
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ private val _wcEventModels: MutableSharedFlow = MutableSharedFlow()
+ val wcEventModels: SharedFlow = _wcEventModels.asSharedFlow()
+
+ var selectedSessionTopic: String? = null
+ private set
+
+ init {
+ WalletConnectModal.setDelegate(this)
+ }
+
+ override fun onSessionApproved(approvedSession: Modal.Model.ApprovedSession) {
+ selectedSessionTopic = approvedSession.topic
+
+ scope.launch {
+ _wcEventModels.emit(approvedSession)
+ }
+ }
+
+ override fun onSessionRejected(rejectedSession: Modal.Model.RejectedSession) {
+ scope.launch {
+ _wcEventModels.emit(rejectedSession)
+ }
+ }
+
+ override fun onSessionUpdate(updatedSession: Modal.Model.UpdatedSession) {
+ scope.launch {
+ _wcEventModels.emit(updatedSession)
+ }
+ }
+
+ override fun onSessionEvent(sessionEvent: Modal.Model.SessionEvent) {
+ scope.launch {
+ _wcEventModels.emit(sessionEvent)
+ }
+ }
+
+ override fun onSessionDelete(deletedSession: Modal.Model.DeletedSession) {
+ deselectAccountDetails()
+
+ scope.launch {
+ _wcEventModels.emit(deletedSession)
+ }
+ }
+
+ override fun onSessionExtend(session: Modal.Model.Session) {
+ scope.launch {
+ _wcEventModels.emit(session)
+ }
+ }
+
+ override fun onSessionRequestResponse(response: Modal.Model.SessionRequestResponse) {
+ scope.launch {
+ _wcEventModels.emit(response)
+ }
+ }
+
+ fun deselectAccountDetails() {
+ selectedSessionTopic = null
+ }
+
+ override fun onConnectionStateChange(state: Modal.Model.ConnectionState) {
+ Timber.d("WalletConnect", "onConnectionStateChange($state)")
+ scope.launch {
+ _wcEventModels.emit(state)
+ }
+ }
+
+ override fun onError(error: Modal.Model.Error) {
+ Timber.d("WalletConnect", error.throwable.stackTraceToString())
+ scope.launch {
+ _wcEventModels.emit(error)
+ }
+ }
+}
diff --git a/example/src/main/java/org/xmtp/android/example/extension/WalletConnectRequestHelper.kt b/example/src/main/java/org/xmtp/android/example/extension/WalletConnectRequestHelper.kt
new file mode 100644
index 000000000..b6f21b0a6
--- /dev/null
+++ b/example/src/main/java/org/xmtp/android/example/extension/WalletConnectRequestHelper.kt
@@ -0,0 +1,86 @@
+package org.xmtp.android.example.extension
+
+import android.net.Uri
+import androidx.core.net.toUri
+import com.walletconnect.wcmodal.client.Modal
+import com.walletconnect.wcmodal.client.WalletConnectModal
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
+import org.web3j.utils.Numeric
+import org.xmtp.android.example.connect.DappDelegate
+import timber.log.Timber
+
+suspend fun requestMethod(
+ requestParams: Modal.Params.Request,
+ sendSessionRequestDeepLink: (Uri) -> Unit
+): Flow> {
+ val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ return withContext(Dispatchers.IO) {
+ callbackFlow {
+ WalletConnectModal.request(
+ request = requestParams,
+ onSuccess = { sentRequest ->
+ WalletConnectModal.getActiveSessionByTopic(requestParams.sessionTopic)?.redirect?.toUri()
+ ?.let { deepLinkUri ->
+ sendSessionRequestDeepLink(deepLinkUri)
+ }
+ onResponse(scope, this, sentRequest)
+
+ },
+ onError = { Timber.e(it.throwable) }
+ )
+ awaitClose { }
+ }
+
+ }
+
+}
+
+private fun onResponse(
+ scope: CoroutineScope,
+ continuation: ProducerScope>,
+ sentRequest: Modal.Model.SentRequest
+) {
+ DappDelegate.wcEventModels
+ .filterNotNull()
+ .onEach { event ->
+ when (event) {
+ is Modal.Model.SessionRequestResponse -> {
+ if (event.topic == sentRequest.sessionTopic && event.result.id == sentRequest.requestId) {
+ when (val res = event.result) {
+ is Modal.Model.JsonRpcResponse.JsonRpcResult -> {
+ var result = res.result
+ if (result.startsWith("0x") && result.length == 132) {
+ result = result.drop(2)
+ }
+
+ val resultData = Numeric.hexStringToByteArray(result)
+
+ // Ensure we have a valid recovery byte
+ resultData[resultData.size - 1] =
+ (1 - resultData[resultData.size - 1] % 2).toByte()
+
+ continuation.trySendBlocking(Result.success(resultData))
+ }
+ is Modal.Model.JsonRpcResponse.JsonRpcError -> {
+ continuation.trySendBlocking(Result.failure(Throwable(res.message)))
+ }
+ }
+ } else continuation.trySendBlocking(Result.failure(Throwable("The result id is different from the request id!")))
+ }
+
+ else -> {}
+ }
+ }.launchIn(scope)
+
+}
\ No newline at end of file
diff --git a/example/src/main/res/drawable/ic_ethereum.xml b/example/src/main/res/drawable/ic_ethereum.xml
new file mode 100644
index 000000000..21a0773b7
--- /dev/null
+++ b/example/src/main/res/drawable/ic_ethereum.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/drawable/ic_wallet_connect_logo.xml b/example/src/main/res/drawable/ic_wallet_connect_logo.xml
new file mode 100644
index 000000000..4fdaa6193
--- /dev/null
+++ b/example/src/main/res/drawable/ic_wallet_connect_logo.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/example/src/main/res/layout/activity_connect_wallet.xml b/example/src/main/res/layout/activity_connect_wallet.xml
index 3ca88eda2..8b4cdd196 100644
--- a/example/src/main/res/layout/activity_connect_wallet.xml
+++ b/example/src/main/res/layout/activity_connect_wallet.xml
@@ -6,51 +6,18 @@
android:layout_height="match_parent"
tools:context=".connect.ConnectWalletActivity">
-
-
-
+ app:defaultNavHost="true"
+ app:navGraph="@navigation/nav_graph" />
-
-
diff --git a/example/src/main/res/layout/fragment_connect_wallet.xml b/example/src/main/res/layout/fragment_connect_wallet.xml
new file mode 100644
index 000000000..8d93ca824
--- /dev/null
+++ b/example/src/main/res/layout/fragment_connect_wallet.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/src/main/res/navigation/nav_graph.xml b/example/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 000000000..9a2af81f8
--- /dev/null
+++ b/example/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml
index 4ebe177b3..55d3b7730 100644
--- a/example/src/main/res/values/strings.xml
+++ b/example/src/main/res/values/strings.xml
@@ -3,6 +3,7 @@
org.xmtp.example.android
Something went wrong
+ WalletConnect
Generate wallet
Connect wallet
No wallet apps installed
diff --git a/example/src/main/res/values/themes.xml b/example/src/main/res/values/themes.xml
index b02e56c74..88ae2efc5 100644
--- a/example/src/main/res/values/themes.xml
+++ b/example/src/main/res/values/themes.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file