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 @@ -