Skip to content

Commit c260ec3

Browse files
authored
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
1 parent 74d57e1 commit c260ec3

19 files changed

+726
-167
lines changed

example/build.gradle

+13-3
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ plugins {
1313

1414
android {
1515
namespace 'org.xmtp.android.example'
16-
compileSdk 33
16+
compileSdk 34
1717

1818
defaultConfig {
1919
applicationId "org.xmtp.android.example"
2020
minSdk 27
21-
targetSdk 33
21+
targetSdk 34
2222
versionCode 1
2323
versionName "1.0"
2424

2525
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
2626
vectorDrawables {
2727
useSupportLibrary true
2828
}
29+
buildConfigField("String", "PROJECT_ID", "\"Your Application ID from https://cloud.walletconnect.com/\"")
2930
}
3031

3132
buildTypes {
@@ -63,9 +64,18 @@ dependencies {
6364
implementation 'androidx.activity:activity-ktx:1.6.1'
6465
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
6566
implementation 'androidx.recyclerview:recyclerview:1.3.0'
66-
implementation 'dev.pinkroom:walletconnectkit:0.3.2'
6767
implementation 'org.web3j:crypto:5.0.0'
6868

69+
// WalletConnect V2: core library + WalletConnectModal
70+
implementation(platform("com.walletconnect:android-bom:1.19.1"))
71+
implementation("com.walletconnect:android-core")
72+
implementation("com.walletconnect:walletconnect-modal")
73+
74+
//Navigation Component
75+
def nav_version = "2.7.5"
76+
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
77+
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
78+
6979
testImplementation 'junit:junit:4.13.2'
7080
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
7181
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

example/src/main/AndroidManifest.xml

+25-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@
66
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
77

88
<queries>
9+
<package android:name="io.metamask"/>
10+
<package android:name="com.wallet.crypto.trustapp"/>
11+
<package android:name="io.gnosis.safe"/>
12+
<package android:name="me.rainbow"/>
13+
<package android:name="im.token.app"/>
14+
<package android:name="io.zerion.android"/>
15+
<package android:name="com.spot.spot"/>
16+
<package android:name="fi.steakwallet.app"/>
17+
<package android:name="vip.mytokenpocket"/>
18+
<package android:name="com.frontierwallet"/>
19+
<package android:name="com.bitkeep.wallet"/>
20+
<package android:name="im.argent.contractwalletclient"/>
21+
<package android:name="com.walletconnect.sample.wallet"/>
922
<intent>
1023
<action android:name="android.intent.action.VIEW" />
1124
<data android:scheme="wc" />
@@ -21,10 +34,12 @@
2134
android:fullBackupContent="@xml/backup_rules"
2235
android:icon="@mipmap/ic_launcher"
2336
android:label="@string/app_name"
37+
android:name=".ExampleApp"
2438
android:roundIcon="@mipmap/ic_launcher_round"
2539
android:supportsRtl="true"
2640
android:theme="@style/Theme.XMTPAndroid"
27-
tools:targetApi="31">
41+
tools:targetApi="31"
42+
tools:replace="dataExtractionRules">
2843
<activity
2944
android:name=".MainActivity"
3045
android:exported="true"
@@ -36,7 +51,15 @@
3651
</activity>
3752
<activity
3853
android:name=".connect.ConnectWalletActivity"
39-
android:exported="false" />
54+
android:exported="true" >
55+
<intent-filter>
56+
<action android:name="android.intent.action.VIEW" />
57+
<category android:name="android.intent.category.DEFAULT" />
58+
<data
59+
android:scheme="xmtp-example-wc"
60+
android:host="request" />
61+
</intent-filter>
62+
</activity>
4063
<activity
4164
android:name=".conversation.ConversationDetailActivity"
4265
android:exported="false" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.xmtp.android.example
2+
3+
import android.app.Application
4+
import com.walletconnect.android.Core
5+
import com.walletconnect.android.CoreClient
6+
import com.walletconnect.android.relay.ConnectionType
7+
import com.walletconnect.wcmodal.client.Modal
8+
import com.walletconnect.wcmodal.client.WalletConnectModal
9+
import timber.log.Timber
10+
11+
const val BASE_LOG_TAG = "WC2"
12+
class ExampleApp: Application() {
13+
14+
override fun onCreate() {
15+
super.onCreate()
16+
val connectionType = ConnectionType.AUTOMATIC
17+
val relayUrl = "relay.walletconnect.com"
18+
val serverUrl = "wss://$relayUrl?projectId=${BuildConfig.PROJECT_ID}"
19+
val appMetaData = Core.Model.AppMetaData(
20+
name = "XMTP Example",
21+
description = "Example app using the xmtp-android SDK",
22+
url = "https://xmtp.org",
23+
icons = listOf("https://avatars.githubusercontent.com/u/82580170?s=48&v=4"),
24+
redirect = "xmtp-example-wc://request"
25+
)
26+
27+
CoreClient.initialize(
28+
metaData = appMetaData,
29+
relayServerUrl = serverUrl,
30+
connectionType = connectionType,
31+
application = this,
32+
onError = {
33+
Timber.tag("$BASE_LOG_TAG CoreClient").d(it.toString())
34+
}
35+
)
36+
37+
WalletConnectModal.initialize(
38+
init = Modal.Params.Init(core = CoreClient),
39+
onSuccess = {
40+
Timber.tag("$BASE_LOG_TAG initialize").d("initialize successfully")
41+
},
42+
onError = { error ->
43+
Timber.tag("$BASE_LOG_TAG initialize").d(error.toString())
44+
}
45+
)
46+
}
47+
}

example/src/main/java/org/xmtp/android/example/account/WalletConnectAccount.kt

-37
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.xmtp.android.example.account
2+
3+
import android.net.Uri
4+
import com.walletconnect.wcmodal.client.Modal
5+
import kotlinx.coroutines.flow.first
6+
import org.web3j.crypto.Keys
7+
import org.xmtp.android.example.connect.getPersonalSignBody
8+
import org.xmtp.android.example.extension.requestMethod
9+
import org.xmtp.android.library.SigningKey
10+
import org.xmtp.android.library.messages.SignatureBuilder
11+
import org.xmtp.proto.message.contents.SignatureOuterClass
12+
13+
14+
data class WalletConnectV2Account(
15+
val session: Modal.Model.ApprovedSession,
16+
val chain: String,
17+
private val sendSessionRequestDeepLink: (Uri) -> Unit
18+
) :
19+
SigningKey {
20+
override val address: String
21+
get() = Keys.toChecksumAddress(
22+
session.namespaces.getValue(chain).accounts[0].substringAfterLast(
23+
":"
24+
)
25+
)
26+
27+
override suspend fun sign(data: ByteArray): SignatureOuterClass.Signature? {
28+
return sign(String(data))
29+
}
30+
31+
override suspend fun sign(message: String): SignatureOuterClass.Signature? {
32+
val (parentChain, chainId, account) = session.namespaces.getValue(chain).accounts[0].split(":")
33+
val requestParams = session.namespaces.getValue(chain).methods.find { method ->
34+
method == "personal_sign"
35+
}?.let { method ->
36+
Modal.Params.Request(
37+
sessionTopic = session.topic,
38+
method = method,
39+
params = getPersonalSignBody(message, account),
40+
chainId = "$parentChain:$chainId"
41+
)
42+
}
43+
runCatching {
44+
requestMethod(requestParams!!, sendSessionRequestDeepLink).first().getOrThrow()
45+
}
46+
.onSuccess {
47+
return SignatureBuilder.buildFromSignatureData(it)
48+
}
49+
.onFailure {}
50+
51+
return null
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.xmtp.android.example.connect
2+
3+
4+
data class ChainSelectionUi(
5+
val chainName: String,
6+
val chainNamespace: String,
7+
val chainReference: String,
8+
val icon: Int,
9+
val methods: List<String>,
10+
val events: List<String>,
11+
var isSelected: Boolean = false,
12+
) {
13+
val chainId = "${chainNamespace}:${chainReference}"
14+
}
15+
16+
fun Chains.toChainUiState() = ChainSelectionUi(chainName, chainNamespace, chainReference, icon, methods, events)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package org.xmtp.android.example.connect
2+
3+
import androidx.annotation.DrawableRes
4+
import org.xmtp.android.example.R
5+
6+
7+
fun getPersonalSignBody(message:String, account: String): String {
8+
val msg = message.encodeToByteArray()
9+
.joinToString(separator = "", prefix = "0x") { eachByte -> "%02x".format(eachByte) }
10+
return "[\"$msg\", \"$account\"]"
11+
}
12+
enum class Chains(
13+
val chainName: String,
14+
val chainNamespace: String,
15+
val chainReference: String,
16+
@DrawableRes val icon: Int,
17+
val methods: List<String>,
18+
val events: List<String>,
19+
) {
20+
ETHEREUM_MAIN(
21+
chainName = "Ethereum",
22+
chainNamespace = Info.Eth.chain,
23+
chainReference = "1",
24+
icon = R.drawable.ic_ethereum,
25+
methods = Info.Eth.defaultMethods,
26+
events = Info.Eth.defaultEvents,
27+
)
28+
}
29+
30+
sealed class Info {
31+
abstract val chain: String
32+
abstract val defaultEvents: List<String>
33+
abstract val defaultMethods: List<String>
34+
35+
object Eth : Info() {
36+
override val chain = "eip155"
37+
override val defaultEvents: List<String> = listOf("chainChanged", "accountsChanged")
38+
override val defaultMethods: List<String> = listOf(
39+
"eth_sendTransaction",
40+
"personal_sign",
41+
"eth_sign",
42+
"eth_signTypedData"
43+
)
44+
}
45+
46+
}

example/src/main/java/org/xmtp/android/example/connect/ConnectWalletActivity.kt

-67
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ import org.xmtp.android.example.databinding.ActivityConnectWalletBinding
2020

2121
class ConnectWalletActivity : AppCompatActivity() {
2222

23-
companion object {
24-
private const val WC_URI_SCHEME = "wc://wc?uri="
25-
}
26-
27-
private val viewModel: ConnectWalletViewModel by viewModels()
2823
private lateinit var binding: ActivityConnectWalletBinding
2924

3025
override fun onCreate(savedInstanceState: Bundle?) {
@@ -33,72 +28,10 @@ class ConnectWalletActivity : AppCompatActivity() {
3328
binding = ActivityConnectWalletBinding.inflate(layoutInflater)
3429
setContentView(binding.root)
3530

36-
lifecycleScope.launch {
37-
repeatOnLifecycle(Lifecycle.State.STARTED) {
38-
viewModel.uiState.collect(::ensureUiState)
39-
}
40-
}
4131

42-
binding.generateButton.setOnClickListener {
43-
viewModel.generateWallet()
44-
}
4532

46-
val isConnectWalletAvailable = isConnectAvailable()
47-
binding.connectButton.isEnabled = isConnectWalletAvailable
48-
binding.connectError.isVisible = !isConnectWalletAvailable
49-
binding.connectButton.setOnClickListener {
50-
binding.connectButton.start(viewModel.walletConnectKit, ::onConnected, ::onDisconnected)
51-
}
52-
}
5333

54-
private fun onConnected(address: String) {
55-
viewModel.connectWallet()
5634
}
5735

58-
private fun onDisconnected() {
59-
// No-op currently.
60-
}
6136

62-
private fun isConnectAvailable(): Boolean {
63-
val wcIntent = Intent(Intent.ACTION_VIEW).apply {
64-
data = Uri.parse(WC_URI_SCHEME)
65-
}
66-
return wcIntent.resolveActivity(packageManager) != null
67-
}
68-
69-
private fun ensureUiState(uiState: ConnectWalletViewModel.ConnectUiState) {
70-
when (uiState) {
71-
is ConnectWalletViewModel.ConnectUiState.Error -> showError(uiState.message)
72-
ConnectWalletViewModel.ConnectUiState.Loading -> showLoading()
73-
is ConnectWalletViewModel.ConnectUiState.Success -> signIn(
74-
uiState.address,
75-
uiState.encodedKeyData
76-
)
77-
ConnectWalletViewModel.ConnectUiState.Unknown -> Unit
78-
}
79-
}
80-
81-
private fun signIn(address: String, encodedKey: String) {
82-
val accountManager = AccountManager.get(this)
83-
Account(address, resources.getString(R.string.account_type)).also { account ->
84-
accountManager.addAccountExplicitly(account, encodedKey, null)
85-
}
86-
startActivity(Intent(this, MainActivity::class.java))
87-
finish()
88-
}
89-
90-
private fun showError(message: String) {
91-
binding.progress.visibility = View.GONE
92-
binding.generateButton.visibility = View.VISIBLE
93-
binding.connectButton.visibility = View.VISIBLE
94-
binding.connectError.isVisible = !isConnectAvailable()
95-
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
96-
}
97-
98-
private fun showLoading() {
99-
binding.progress.visibility = View.VISIBLE
100-
binding.generateButton.visibility = View.GONE
101-
binding.connectButton.visibility = View.GONE
102-
binding.connectError.visibility = View.GONE
103-
}
10437
}

0 commit comments

Comments
 (0)