From 358387d1fe2e2645e80d89c600cd562e8684f0fe Mon Sep 17 00:00:00 2001 From: Rui <102453770+ruixhuang@users.noreply.github.com> Date: Wed, 27 Nov 2024 10:58:43 -0800 Subject: [PATCH] Add WalletConnect Modal support (#22) * Add WalletConnect Modal support * Clean up --- app/build.gradle | 24 +- .../dydx/carteraExample/MainActivity.kt | 40 +- .../dydx/carteraExample/WalletList.kt | 60 ++- .../carteraExample/WalletListViewModel.kt | 67 ++- .../WalletProvidersConfigUtil.kt | 24 +- cartera/build.gradle | 17 +- .../exchange/dydx/cartera/CarteraConfig.kt | 27 +- .../exchange/dydx/cartera/CarteraProvider.kt | 56 +- .../dydx/cartera/entities/ModelExtensions.kt | 28 +- .../walletprovider/WalletProviderProtocols.kt | 22 +- .../providers/MagicLinkProvider.kt | 5 + .../providers/WalletConnectModalProvider.kt | 499 ++++++++++++++++++ .../providers/WalletConnectV1Provider.kt | 5 + .../providers/WalletConnectV2Provider.kt | 93 ++-- .../providers/WalletSegueProvider.kt | 5 + gradle.properties | 2 +- 16 files changed, 855 insertions(+), 119 deletions(-) create mode 100644 cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectModalProvider.kt diff --git a/app/build.gradle b/app/build.gradle index d21f46d..ef8f9a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ plugins { android { namespace 'exchange.dydx.carteraexample' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "exchange.dydx.carteraexample" @@ -47,24 +47,30 @@ android { dependencies { implementation project(path: ':cartera') - implementation 'androidx.core:core-ktx:1.12.0' - implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0') - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2022.10.00') + implementation 'androidx.core:core-ktx:1.15.0' + implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24') + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.7' + implementation 'androidx.activity:activity-compose:1.9.3' + implementation platform('androidx.compose:compose-bom:2024.11.00') + implementation 'com.google.accompanist:accompanist-navigation-material:0.34.0' implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-graphics' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.material:material' + implementation 'androidx.navigation:navigation-runtime-ktx:2.8.4' + implementation 'androidx.navigation:navigation-compose:2.8.4' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00') + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + androidTestImplementation platform('androidx.compose:compose-bom:2024.11.00') androidTestImplementation 'androidx.compose.ui:ui-test-junit4' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' implementation 'com.github.kenglxn.QRGen:android:3.0.1' + implementation platform('com.walletconnect:android-bom:1.35.2') + implementation("com.walletconnect:android-core") + implementation("com.walletconnect:walletconnect-modal") } \ No newline at end of file diff --git a/app/src/main/java/exchange/dydx/carteraExample/MainActivity.kt b/app/src/main/java/exchange/dydx/carteraExample/MainActivity.kt index 567260e..a09d6d4 100644 --- a/app/src/main/java/exchange/dydx/carteraExample/MainActivity.kt +++ b/app/src/main/java/exchange/dydx/carteraExample/MainActivity.kt @@ -5,13 +5,26 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.material.ModalBottomSheetValue +import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.google.accompanist.navigation.material.BottomSheetNavigator +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.google.accompanist.navigation.material.ModalBottomSheetLayout +import com.walletconnect.wcmodal.ui.walletConnectModalGraph import exchange.dydx.cartera.CarteraConfig +import exchange.dydx.cartera.WalletConnectionType +import exchange.dydx.cartera.walletprovider.providers.WalletConnectModalProvider class MainActivity : ComponentActivity() { + @OptIn(ExperimentalMaterialNavigationApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -29,7 +42,32 @@ class MainActivity : ComponentActivity() { setContent { MyApp { - WalletList.Content() + val sheetState = rememberModalBottomSheetState( + initialValue = ModalBottomSheetValue.Hidden, + skipHalfExpanded = true, + ) + val bottomSheetNavigator = BottomSheetNavigator(sheetState) + val navController = rememberNavController(bottomSheetNavigator) + + LaunchedEffect(Unit) { + // Need to set the nav controller for the WalletConnectModalProvider + // so that it can open the modal when needed + val modal = CarteraConfig.shared?.getProvider(WalletConnectionType.WalletConnectModal) as? WalletConnectModalProvider + modal?.nav = navController + } + + ModalBottomSheetLayout(bottomSheetNavigator = bottomSheetNavigator) { + NavHost( + navController = navController, + startDestination = "walletList", + ) { + composable("walletList") { + WalletList.Content() + } + + walletConnectModalGraph(navController) + } + } } } } diff --git a/app/src/main/java/exchange/dydx/carteraExample/WalletList.kt b/app/src/main/java/exchange/dydx/carteraExample/WalletList.kt index 1361455..3822218 100644 --- a/app/src/main/java/exchange/dydx/carteraExample/WalletList.kt +++ b/app/src/main/java/exchange/dydx/carteraExample/WalletList.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.MaterialTheme import androidx.compose.material.ModalBottomSheetLayout import androidx.compose.material.ModalBottomSheetState @@ -55,7 +54,7 @@ import net.glxn.qrgen.android.QRCode object WalletList { enum class WalletAction { - Connect, SignMessage, SignTypedData, SignTransaction + Connect, SignMessage, SignTypedData, SignTransaction, Disconnect } enum class QrCodeAction { @@ -65,17 +64,18 @@ object WalletList { data class WalletListState( val wallets: List = listOf(), var selectedWallet: Wallet? = null, - val walletAction: ((WalletAction, Wallet?, Boolean) -> Unit)? = null, - val debugQRCodeAction: ((QrCodeAction, Boolean) -> Unit)? = null + val walletAction: ((WalletAction, Wallet?, Boolean, Boolean) -> Unit)? = null, + val debugQRCodeAction: ((QrCodeAction, Boolean) -> Unit)? = null, + val wcModalAction: (() -> Unit)? = null ) { var showingQrCodeState: Boolean by mutableStateOf(false) var deeplinkUri: String? by mutableStateOf(null) var showBottomSheet: Boolean by mutableStateOf(false) var useTestnet: Boolean by mutableStateOf(true) + var useWcModal: Boolean by mutableStateOf(false) } @SuppressLint("CoroutineCreationDuringComposition") - @OptIn(ExperimentalMaterialApi::class) @Composable fun Content() { val context = LocalContext.current @@ -91,11 +91,11 @@ object WalletList { ModalBottomSheetLayout( sheetContent = { - walletActionSheetView(viewState = state.value) + WalletActionSheetView(viewState = state.value) }, sheetState = bottomSheetState, ) { - walletListContent( + WalletListContent( viewState = state.value, coroutineScope = coroutineScope, bottomSheetState = bottomSheetState, @@ -119,9 +119,8 @@ object WalletList { } } - @OptIn(ExperimentalMaterialApi::class) @Composable - fun walletListContent( + fun WalletListContent( viewState: WalletList.WalletListState, coroutineScope: CoroutineScope, bottomSheetState: ModalBottomSheetState @@ -137,6 +136,7 @@ object WalletList { .clickable { if (wallet.installed(context)) { viewState.selectedWallet = wallet + viewState.useWcModal = false coroutineScope.launch { bottomSheetState.show() } @@ -172,6 +172,29 @@ object WalletList { item { Spacer(modifier = Modifier.height(24.dp)) } + item { + Divider() + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .clickable { + viewState.wcModalAction?.invoke() + }, + ) { + Text( + "WalletConnect Modal", + modifier = Modifier + .padding(16.dp) + .weight(1f, false), + textAlign = TextAlign.Start, + ) + } + Divider() + } + + item { Spacer(modifier = Modifier.height(24.dp)) } + item { Divider() Row( @@ -227,7 +250,7 @@ object WalletList { } @Composable - private fun walletActionSheetView(viewState: WalletListState) { + private fun WalletActionSheetView(viewState: WalletListState) { val buttonModifier = Modifier .padding(all = 15.dp) .fillMaxWidth() @@ -244,6 +267,7 @@ object WalletList { WalletAction.Connect, viewState.selectedWallet, viewState.useTestnet, + viewState.useWcModal, ) }, modifier = buttonModifier, @@ -256,6 +280,7 @@ object WalletList { WalletAction.SignMessage, viewState.selectedWallet, viewState.useTestnet, + viewState.useWcModal, ) }, modifier = buttonModifier, @@ -268,6 +293,7 @@ object WalletList { WalletAction.SignTypedData, viewState.selectedWallet, viewState.useTestnet, + viewState.useWcModal, ) }, modifier = buttonModifier, @@ -280,12 +306,26 @@ object WalletList { WalletAction.SignTransaction, viewState.selectedWallet, viewState.useTestnet, + viewState.useWcModal, ) }, modifier = buttonModifier, ) { Text("Send Transaction", style = buttonTextStyle) } + TextButton( + onClick = { + viewState.walletAction?.invoke( + WalletAction.Disconnect, + viewState.selectedWallet, + viewState.useTestnet, + viewState.useWcModal, + ) + }, + modifier = buttonModifier, + ) { + Text("Disconnect", style = buttonTextStyle) + } Spacer(modifier = Modifier.height(20.dp)) TextButton( onClick = { diff --git a/app/src/main/java/exchange/dydx/carteraExample/WalletListViewModel.kt b/app/src/main/java/exchange/dydx/carteraExample/WalletListViewModel.kt index 24f18cb..87b4ca0 100644 --- a/app/src/main/java/exchange/dydx/carteraExample/WalletListViewModel.kt +++ b/app/src/main/java/exchange/dydx/carteraExample/WalletListViewModel.kt @@ -39,23 +39,27 @@ class WalletListViewModel( viewModelScope.launch { viewState.value = WalletList.WalletListState( wallets = CarteraConfig.shared?.wallets ?: listOf(), - walletAction = { action: WalletList.WalletAction, wallet: Wallet?, useTestnet: Boolean -> + walletAction = { action: WalletList.WalletAction, wallet: Wallet?, useTestnet: Boolean, useModal: Boolean -> val chainId: String = if (useTestnet) CarteraConstants.testnetChainId else "1" when (action) { WalletList.WalletAction.Connect -> { - testConnect(wallet, chainId) + testConnect(wallet, chainId, useModal) } WalletList.WalletAction.SignMessage -> { - testSignMessage(wallet, chainId) + testSignMessage(wallet, chainId, useModal) } WalletList.WalletAction.SignTypedData -> { - testSignTypedData(wallet, chainId) + testSignTypedData(wallet, chainId, useModal) } WalletList.WalletAction.SignTransaction -> { - testSendTransaction(wallet, chainId) + testSendTransaction(wallet, chainId, useModal) + } + + WalletList.WalletAction.Disconnect -> { + provider.disconnect() } } }, @@ -71,15 +75,31 @@ class WalletListViewModel( } } }, + wcModalAction = { + testWcModal() + }, ) } } + private fun testWcModal() { + viewState.value.showingQrCodeState = false + viewState.value.showBottomSheet = false + viewModelScope.launch { + viewState.value.showBottomSheet = true + viewState.value.selectedWallet = null + + viewState.value.useWcModal = true + } + } + private fun testQRCodeV2(chainId: String) { viewState.value.showingQrCodeState = true viewState.value.showBottomSheet = false viewState.value.selectedWallet = null + viewState.value.useWcModal = false + provider.startDebugLink(chainId = chainId) { _, error -> viewState.value.showingQrCodeState = false if (error != null) { @@ -90,8 +110,8 @@ class WalletListViewModel( } } - private fun testConnect(wallet: Wallet?, chainId: String) { - val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context) + private fun testConnect(wallet: Wallet?, chainId: String, useModal: Boolean) { + val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal) provider.connect(request) { info, error -> if (error != null) { toastWalletError(error) @@ -101,15 +121,21 @@ class WalletListViewModel( } } - private fun testSignMessage(wallet: Wallet?, chainId: String) { - val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context) + private fun testSignMessage(wallet: Wallet?, chainId: String, useModal: Boolean) { + val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal) provider.signMessage( request = request, message = "Test Message", connected = { info -> Log.d(tag(this@WalletListViewModel), "Connected to: ${info?.peerName ?: info?.address}") }, + status = { requireAppSwitching -> + Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching") + toastMessage("Please switch to the wallet app") + }, completion = { signature, error -> + // delay for 1 second, according to app switching + Thread.sleep(1000) if (error != null) { toastWalletError(error) } else { @@ -119,18 +145,24 @@ class WalletListViewModel( ) } - private fun testSignTypedData(wallet: Wallet?, chainId: String) { + private fun testSignTypedData(wallet: Wallet?, chainId: String, useModal: Boolean) { val dydxSign = EIP712DomainTypedDataProvider(name = "dYdX", chainId = chainId.toInt()) dydxSign.message = message(action = "Sample Action", chainId = chainId.toInt()) - val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context) + val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal) provider.sign( request = request, typedDataProvider = dydxSign, connected = { info -> toastMessage("Connected to: ${info?.peerName ?: info?.address}") }, + status = { requireAppSwitching -> + Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching") + toastMessage("Please switch to the wallet app") + }, completion = { signature, error -> + // delay for 1 second + Thread.sleep(1000) if (error != null) { toastWalletError(error) } else { @@ -140,8 +172,8 @@ class WalletListViewModel( ) } - private fun testSendTransaction(wallet: Wallet?, chainId: String) { - val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context) + private fun testSendTransaction(wallet: Wallet?, chainId: String, useModal: Boolean) { + val request = WalletRequest(wallet = wallet, address = null, chainId = chainId, context = context, useModal = useModal) provider.connect(request) { info, error -> if (error != null) { toastWalletError(error) @@ -165,7 +197,14 @@ class WalletListViewModel( connected = { info -> toastMessage("Connected to: ${info?.peerName ?: info?.address}") }, + status = { requireAppSwitching -> + Log.d(tag(this@WalletListViewModel), "Require app switching: $requireAppSwitching") + toastMessage("Please switch to the wallet app") + }, + completion = { txHash, error -> + // delay for 1 second + Thread.sleep(1000) if (error != null) { toastWalletError(error) } else { @@ -189,7 +228,7 @@ class WalletListViewModel( } private fun toastWalletError(error: WalletError) { - Toast.makeText(context, "$error.title: $error.message", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "${error.title}: ${error.message}", Toast.LENGTH_SHORT).show() } private fun message(action: String, chainId: Int): WalletTypedData { diff --git a/app/src/main/java/exchange/dydx/carteraExample/WalletProvidersConfigUtil.kt b/app/src/main/java/exchange/dydx/carteraExample/WalletProvidersConfigUtil.kt index f7fa48a..cf6227a 100644 --- a/app/src/main/java/exchange/dydx/carteraExample/WalletProvidersConfigUtil.kt +++ b/app/src/main/java/exchange/dydx/carteraExample/WalletProvidersConfigUtil.kt @@ -8,24 +8,24 @@ import exchange.dydx.cartera.WalletSegueConfig object WalletProvidersConfigUtil { fun getWalletProvidersConfig(): WalletProvidersConfig { val walletConnectV1Config = WalletConnectV1Config( - "dYdX", - "dYdX Trading App", - "https://media.dydx.exchange/logos/dydx-x.png", - "dydx:", - "https://trade.dydx.exchange/", - "wss://api.stage.dydx.exchange/wc/", + clientName = "dYdX", + clientDescription = "dYdX Trading App", + iconUrl = "https://media.dydx.exchange/logos/dydx-x.png", + scheme = "dydx:", + clientUrl = "https://trade.dydx.exchange/", + bridgeUrl = "wss://api.stage.dydx.exchange/wc/", ) val walletConnectV2Config = WalletConnectV2Config( - "156a34507d8e657347be0ecd294659bb", - "dYdX", - "dYdX Trading App", - "https://trade.dydx.exchange/", - listOf("https://media.dydx.exchange/logos/dydx-x.png"), + projectId = "156a34507d8e657347be0ecd294659bb", + clientName = "dYdX", + clientDescription = "dYdX Trading App", + clientUrl = "https://trade.dydx.exchange/", + iconUrls = listOf("https://media.dydx.exchange/logos/dydx-x.png"), ) val walletSegueConfig = WalletSegueConfig( - "https://trade.stage.dydx.exchange/walletsegueCarteraExample", + callbackUrl = "https://trade.stage.dydx.exchange/walletsegueCarteraExample", ) return WalletProvidersConfig( diff --git a/cartera/build.gradle b/cartera/build.gradle index 90897e8..7521f19 100644 --- a/cartera/build.gradle +++ b/cartera/build.gradle @@ -47,12 +47,12 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.core:core-ktx:1.15.0' + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' // implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' implementation 'com.google.code.gson:gson:2.10.1' @@ -61,15 +61,16 @@ dependencies { // https://github.com/WalletConnect/WalletConnectKotlinV2 // - implementation platform('com.walletconnect:android-bom:1.31.0') - implementation('com.walletconnect:android-core') + implementation platform('com.walletconnect:android-bom:1.35.2') + implementation("com.walletconnect:android-core") + implementation("com.walletconnect:walletconnect-modal") implementation 'com.walletconnect:sign' //implementation 'com.walletconnect:push' // // https://docs.cloud.coinbase.com/wallet-sdk/docs/android-install // - implementation 'com.coinbase:coinbase-wallet-sdk:1.0.4' + implementation 'com.coinbase:coinbase-wallet-sdk:1.1.1' // // https://github.com/WalletConnect/kotlin-walletconnect-lib diff --git a/cartera/src/main/java/exchange/dydx/cartera/CarteraConfig.kt b/cartera/src/main/java/exchange/dydx/cartera/CarteraConfig.kt index b55fff1..ab8b809 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/CarteraConfig.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/CarteraConfig.kt @@ -5,12 +5,14 @@ import android.content.Context import android.content.Intent import android.net.Uri import androidx.activity.result.ActivityResultLauncher +import androidx.navigation.NavHostController import com.google.gson.Gson import com.google.gson.reflect.TypeToken import exchange.dydx.cartera.entities.Wallet import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol import exchange.dydx.cartera.walletprovider.WalletUserConsentProtocol import exchange.dydx.cartera.walletprovider.providers.MagicLinkProvider +import exchange.dydx.cartera.walletprovider.providers.WalletConnectModalProvider import exchange.dydx.cartera.walletprovider.providers.WalletConnectV1Provider import exchange.dydx.cartera.walletprovider.providers.WalletConnectV2Provider import exchange.dydx.cartera.walletprovider.providers.WalletSegueProvider @@ -19,6 +21,7 @@ import java.lang.reflect.Type sealed class WalletConnectionType(val rawValue: String) { object WalletConnect : WalletConnectionType("walletConnect") object WalletConnectV2 : WalletConnectionType("walletConnectV2") + object WalletConnectModal : WalletConnectionType("walletConnectModal") object WalletSegue : WalletConnectionType("walletSegue") object MagicLink : WalletConnectionType("magicLink") class Custom(val value: String) : WalletConnectionType(value) @@ -29,6 +32,7 @@ sealed class WalletConnectionType(val rawValue: String) { return when (rawValue) { WalletConnect.rawValue -> WalletConnect WalletConnectV2.rawValue -> WalletConnectV2 + WalletConnectModal.rawValue -> WalletConnectModal WalletSegue.rawValue -> WalletSegue MagicLink.rawValue -> MagicLink else -> Custom(rawValue) @@ -70,6 +74,12 @@ class CarteraConfig( application, ), ) + + registration[WalletConnectionType.WalletConnectModal] = RegistrationConfig( + provider = WalletConnectModalProvider( + application = application, + ), + ) } if (walletProvidersConfig.walletSegue != null) { registration[WalletConnectionType.WalletSegue] = RegistrationConfig( @@ -85,21 +95,26 @@ class CarteraConfig( ) } - fun updateConfig(walletProvidersConfig: WalletProvidersConfig) { + fun updateConfig(walletProvidersConfig: WalletProvidersConfig, nav: NavHostController) { if (walletProvidersConfig.walletConnectV2 != null) { registration[WalletConnectionType.WalletConnectV2] = RegistrationConfig( provider = WalletConnectV2Provider( - walletProvidersConfig.walletConnectV2, - application, + walletConnectV2Config = walletProvidersConfig.walletConnectV2, + application = application, + ), + ) + registration[WalletConnectionType.WalletConnectModal] = RegistrationConfig( + provider = WalletConnectModalProvider( + application = application, ), ) } if (walletProvidersConfig.walletSegue != null) { registration[WalletConnectionType.WalletSegue] = RegistrationConfig( provider = WalletSegueProvider( - walletProvidersConfig.walletSegue, - application, - launcher, + walletSegueConfig = walletProvidersConfig.walletSegue, + application = application, + launcher = launcher, ), ) } diff --git a/cartera/src/main/java/exchange/dydx/cartera/CarteraProvider.kt b/cartera/src/main/java/exchange/dydx/cartera/CarteraProvider.kt index 2b81be0..0e4e1f5 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/CarteraProvider.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/CarteraProvider.kt @@ -9,6 +9,7 @@ import exchange.dydx.cartera.walletprovider.WalletConnectedCompletion import exchange.dydx.cartera.walletprovider.WalletError import exchange.dydx.cartera.walletprovider.WalletOperationCompletion import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletStatusDelegate import exchange.dydx.cartera.walletprovider.WalletStatusProtocol @@ -52,21 +53,38 @@ class CarteraProvider(private val context: Context) : WalletOperationProviderPro currentRequestHandler?.disconnect() } - override fun signMessage(request: WalletRequest, message: String, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) { + override fun signMessage( + request: WalletRequest, + message: String, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { updateCurrentHandler(request) - currentRequestHandler?.signMessage(request, message, connected, completion) + currentRequestHandler?.signMessage(request, message, connected, status, completion) } - override fun sign(request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) { + override fun sign( + request: WalletRequest, + typedDataProvider: WalletTypedDataProviderProtocol?, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { updateCurrentHandler(request) - currentRequestHandler?.sign(request, typedDataProvider, connected, completion) + currentRequestHandler?.sign(request, typedDataProvider, connected, status, completion) } - override fun send(request: WalletTransactionRequest, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) { + override fun send( + request: WalletTransactionRequest, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { updateCurrentHandler(request.walletRequest) - userConsentDelegate?.showTransactionConsent(request) { status -> - when (status) { - WalletUserConsentStatus.CONSENTED -> currentRequestHandler?.send(request, connected, completion) + userConsentDelegate?.showTransactionConsent(request) { consentStatus -> + when (consentStatus) { + WalletUserConsentStatus.CONSENTED -> currentRequestHandler?.send(request, connected, status, completion) WalletUserConsentStatus.REJECTED -> { val error = WalletError(CarteraErrorCode.USER_CANCELED, "User canceled") completion(null, error) @@ -75,22 +93,32 @@ class CarteraProvider(private val context: Context) : WalletOperationProviderPro } } - override fun addChain(request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) { + override fun addChain( + request: WalletRequest, + chain: EthereumAddChainRequest, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { updateCurrentHandler(request) // Disregard chainId, since we don't want to check for chainId match here. val addChainRequest = WalletRequest(request.wallet, null, null, context) - currentRequestHandler?.addChain(addChainRequest, chain, connected, completion) + currentRequestHandler?.addChain(addChainRequest, chain, connected, status, completion) } // Private private fun updateCurrentHandler(request: WalletRequest) { - val newHandler = request.wallet?.config?.connectionType(context)?.let { - CarteraConfig.shared?.getProvider(it) - } ?: run { - debugQrCodeProvider + val provider = if (request.useModal) { + CarteraConfig.shared?.getProvider(WalletConnectionType.WalletConnectModal) + } else { + request.wallet?.config?.connectionType(context)?.let { + CarteraConfig.shared?.getProvider(it) + } } + val newHandler = provider ?: debugQrCodeProvider + if (newHandler !== currentRequestHandler) { currentRequestHandler?.disconnect() currentRequestHandler?.walletStatusDelegate = null diff --git a/cartera/src/main/java/exchange/dydx/cartera/entities/ModelExtensions.kt b/cartera/src/main/java/exchange/dydx/cartera/entities/ModelExtensions.kt index 2d576a1..eaf1f91 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/entities/ModelExtensions.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/entities/ModelExtensions.kt @@ -7,6 +7,11 @@ import android.net.Uri import androidx.core.content.ContextCompat.startActivity import exchange.dydx.cartera.Utils import exchange.dydx.cartera.WalletConnectionType +import exchange.dydx.cartera.toHexString +import exchange.dydx.cartera.walletprovider.EthereumTransactionRequest +import okhttp3.internal.toHexString +import org.json.JSONException +import org.json.JSONObject fun Wallet.installed(context: Context): Boolean { config?.androidPackage?.let { androidPackage -> @@ -42,7 +47,7 @@ val WalletConfig.iosEnabled: Boolean } fun WalletConfig.connectionType(context: Context): WalletConnectionType { - connections?.firstOrNull()?.type?.let { type -> + connections.firstOrNull()?.type?.let { type -> return WalletConnectionType.fromRawValue(type) } return WalletConnectionType.Unknown @@ -51,3 +56,24 @@ fun WalletConfig.connectionType(context: Context): WalletConnectionType { fun WalletConfig.connections(type: WalletConnectionType): WalletConnections? { return connections?.firstOrNull { it.type == type.rawValue } } + +fun EthereumTransactionRequest.toJsonRequest(): String? { + val request: MutableMap = mutableMapOf() + + request["from"] = fromAddress + request["to"] = toAddress ?: "0x" + request["gas"] = gasLimit?.toHexString() + request["gasPrice"] = gasPriceInWei?.toHexString() + request["value"] = weiValue.toHexString() + request["data"] = data + request["nonce"] = nonce?.let { + "0x" + it.toHexString() + } + val filtered = request.filterValues { it != null } + + return try { + JSONObject(filtered).toString() + } catch (e: JSONException) { + null + } +} diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/WalletProviderProtocols.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/WalletProviderProtocols.kt index 586b180..655b576 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/WalletProviderProtocols.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/WalletProviderProtocols.kt @@ -9,8 +9,17 @@ data class WalletRequest( val wallet: Wallet? = null, val address: String? = null, val chainId: String? = null, - val context: Context -) + val context: Context, + val useModal: Boolean = false, +) { + val ethChain: String? + get() { + if (chainId == null) { + return null + } + return "eip155:$chainId" + } +} data class WalletTransactionRequest( val walletRequest: WalletRequest, @@ -35,16 +44,17 @@ data class EthereumAddChainRequest( ) typealias WalletConnectedCompletion = (info: WalletInfo?) -> Unit +typealias WalletOperationStatus = (requireAppSwitching: Boolean) -> Unit typealias WalletOperationCompletion = (signed: String?, error: WalletError?) -> Unit typealias WalletConnectCompletion = (info: WalletInfo?, error: WalletError?) -> Unit interface WalletOperationProtocol { fun connect(request: WalletRequest, completion: WalletConnectCompletion) fun disconnect() - fun signMessage(request: WalletRequest, message: String, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) - fun sign(request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) - fun send(request: WalletTransactionRequest, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) - fun addChain(request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, completion: WalletOperationCompletion) + fun signMessage(request: WalletRequest, message: String, connected: WalletConnectedCompletion?, status: WalletOperationStatus?, completion: WalletOperationCompletion) + fun sign(request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, status: WalletOperationStatus?, completion: WalletOperationCompletion) + fun send(request: WalletTransactionRequest, connected: WalletConnectedCompletion?, status: WalletOperationStatus?, completion: WalletOperationCompletion) + fun addChain(request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, status: WalletOperationStatus?, completion: WalletOperationCompletion) } interface WalletUserConsentOperationProtocol : WalletOperationProtocol { diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/MagicLinkProvider.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/MagicLinkProvider.kt index 91245fb..082ac62 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/MagicLinkProvider.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/MagicLinkProvider.kt @@ -6,6 +6,7 @@ import exchange.dydx.cartera.walletprovider.WalletConnectCompletion import exchange.dydx.cartera.walletprovider.WalletConnectedCompletion import exchange.dydx.cartera.walletprovider.WalletOperationCompletion import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletStatusDelegate import exchange.dydx.cartera.walletprovider.WalletStatusImp @@ -35,6 +36,7 @@ class MagicLinkProvider : WalletOperationProviderProtocol { request: WalletRequest, message: String, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -44,6 +46,7 @@ class MagicLinkProvider : WalletOperationProviderProtocol { request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -52,6 +55,7 @@ class MagicLinkProvider : WalletOperationProviderProtocol { override fun send( request: WalletTransactionRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -61,6 +65,7 @@ class MagicLinkProvider : WalletOperationProviderProtocol { request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectModalProvider.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectModalProvider.kt new file mode 100644 index 0000000..ae0b6e8 --- /dev/null +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectModalProvider.kt @@ -0,0 +1,499 @@ +package exchange.dydx.cartera.walletprovider.providers + +import android.app.Application +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import androidx.navigation.NavHostController +import com.walletconnect.android.CoreClient +import com.walletconnect.wcmodal.client.Modal +import com.walletconnect.wcmodal.client.WalletConnectModal +import com.walletconnect.wcmodal.ui.openWalletConnectModal +import exchange.dydx.cartera.CarteraErrorCode +import exchange.dydx.cartera.entities.Wallet +import exchange.dydx.cartera.entities.toJsonRequest +import exchange.dydx.cartera.tag +import exchange.dydx.cartera.typeddata.WalletTypedDataProviderProtocol +import exchange.dydx.cartera.typeddata.typedDataAsString +import exchange.dydx.cartera.walletprovider.EthereumAddChainRequest +import exchange.dydx.cartera.walletprovider.WalletConnectCompletion +import exchange.dydx.cartera.walletprovider.WalletConnectedCompletion +import exchange.dydx.cartera.walletprovider.WalletError +import exchange.dydx.cartera.walletprovider.WalletInfo +import exchange.dydx.cartera.walletprovider.WalletOperationCompletion +import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus +import exchange.dydx.cartera.walletprovider.WalletRequest +import exchange.dydx.cartera.walletprovider.WalletState +import exchange.dydx.cartera.walletprovider.WalletStatusDelegate +import exchange.dydx.cartera.walletprovider.WalletStatusImp +import exchange.dydx.cartera.walletprovider.WalletStatusProtocol +import exchange.dydx.cartera.walletprovider.WalletTransactionRequest +import exchange.dydx.cartera.walletprovider.WalletUserConsentProtocol +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import timber.log.Timber + +class WalletConnectModalProvider( + private val application: Application +) : WalletOperationProviderProtocol, WalletConnectModal.ModalDelegate { + + private var _walletStatus = WalletStatusImp() + set(value) { + field = value + walletStatusDelegate?.statusChanged(value) + } + override val walletStatus: WalletStatusProtocol + get() = _walletStatus + + override var walletStatusDelegate: WalletStatusDelegate? = null + override var userConsentDelegate: WalletUserConsentProtocol? = null + + private var requestingWallet: WalletRequest? = null + private var currentSession: Modal.Model.ApprovedSession? = null + + private val connectCompletions: MutableList = mutableListOf() + private val operationCompletions: MutableMap = mutableMapOf() + + // expiry must be between current timestamp + MIN_INTERVAL and current timestamp + MAX_INTERVAL (MIN_INTERVAL: 300, MAX_INTERVAL: 604800) + private val requestExpiry: Long + get() = (System.currentTimeMillis() / 1000) + 400 + + private val ethNamespace = "eip155" // only support eth for now + + var nav: NavHostController? = null + + init { + WalletConnectModal.initialize( + init = Modal.Params.Init(CoreClient), + onSuccess = { + // Callback will be called if initialization is successful + Timber.tag(tag(this)).d("WalletConnectModal initialized.") + }, + onError = { error -> + // Error will be thrown if there's an issue during initialization + Timber.tag(tag(this)) + .e(error.throwable.stackTraceToString()) + }, + ) + } + + override fun connect(request: WalletRequest, completion: WalletConnectCompletion) { + if (_walletStatus.state == WalletState.CONNECTED_TO_WALLET) { + completion(walletStatus.connectedWallet, null) + } else { + requestingWallet = request + + val chain: String = if (request.chainId != null) { + "$ethNamespace:${request.chainId}" + } else { + "$ethNamespace:1" + } + val chains: List = listOf(chain) + val methods: List = listOf( + "personal_sign", + "eth_sendTransaction", + "eth_signTypedData", + // "wallet_addEthereumChain", + // "eth_sign" + ) + val events: List = listOf( + "accountsChanged", + "chainChanged", + ) + val namespaces = mapOf( + ethNamespace to Modal.Model.Namespace.Proposal( + chains = chains, + methods = methods, + events = events, + ), + ) + + val sessionParams = Modal.Params.SessionParams( + requiredNamespaces = namespaces, + optionalNamespaces = null, + properties = null, + ) + + WalletConnectModal.setSessionParams(sessionParams) + + WalletConnectModal.setDelegate(this) + + connectCompletions.add(completion) + + nav?.openWalletConnectModal() + } + } + + override fun disconnect() { + val currentSession = this.currentSession + if (currentSession != null) { + WalletConnectModal.disconnect(Modal.Params.Disconnect(currentSession.topic), onSuccess = { + Timber.tag(tag(this)).d("Disconnected from session: ${currentSession!!.topic}") + }, onError = { + Timber.tag(tag(this)).e(it.throwable.stackTraceToString()) + }) + this.currentSession = null + } + + _walletStatus.state = WalletState.IDLE + _walletStatus.connectedWallet = null + _walletStatus.connectionDeeplink = null + walletStatusDelegate?.statusChanged(_walletStatus) + + connectCompletions.clear() + operationCompletions.clear() + } + + override fun signMessage( + request: WalletRequest, + message: String, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + fun requestParams(): Modal.Params.Request? { + val sessionTopic = currentSession?.topic + val account = _walletStatus.connectedWallet?.address + val chainId = currentSession?.namespaces?.get(ethNamespace)?.chains?.firstOrNull() + return if (sessionTopic != null && account != null && chainId != null) { + Modal.Params.Request( + sessionTopic = sessionTopic, + method = "personal_sign", + params = "[\"${message}\", \"${account}\"]", + chainId = chainId, + expiry = requestExpiry, + ) + } else { + null + } + } + + connectAndMakeRequest(request, { requestParams() }, connected, status, completion) + } + + override fun sign( + request: WalletRequest, + typedDataProvider: WalletTypedDataProviderProtocol?, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + fun requestParams(): Modal.Params.Request? { + val sessionTopic = currentSession?.topic + val account = _walletStatus.connectedWallet?.address + val chainId = currentSession?.namespaces?.get(ethNamespace)?.chains?.firstOrNull() + val message = typedDataProvider?.typedDataAsString?.replace("\"", "\\\"") + + return if (sessionTopic != null && account != null && chainId != null && message != null) { + Modal.Params.Request( + sessionTopic = sessionTopic, + method = "eth_signTypedData", + params = "[\"${account}\", \"${message}\"]", + chainId = chainId, + expiry = requestExpiry, + ) + } else { + null + } + } + + connectAndMakeRequest(request, { requestParams() }, connected, status, completion) + } + + override fun send( + request: WalletTransactionRequest, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + fun requestParams(): Modal.Params.Request? { + val sessionTopic = currentSession?.topic + val account = _walletStatus.connectedWallet?.address + val chainId = currentSession?.namespaces?.get(ethNamespace)?.chains?.firstOrNull() + val message = request.ethereum?.toJsonRequest() + + return if (sessionTopic != null && account != null && chainId != null && message != null) { + Modal.Params.Request( + sessionTopic = sessionTopic, + method = "eth_sendTransaction", + params = "[$message]", + chainId = chainId, + expiry = requestExpiry, + ) + } else { + null + } + } + + connectAndMakeRequest(request.walletRequest, { requestParams() }, connected, status, completion) + } + + override fun addChain( + request: WalletRequest, + chain: EthereumAddChainRequest, + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + TODO("Not yet implemented") + } + + private fun connectAndMakeRequest( + request: WalletRequest, + requestParams: (() -> Modal.Params.Request?), + connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + connect(request) { info, error -> + if (error != null) { + completion(null, error) + } else if (currentSession != null) { + if (connected != null) { + connected(info) + } + + Thread.sleep(1000) + val params = requestParams() + if (params != null) { + reallyMakeRequest(params, status) { result, requestError -> + CoroutineScope(Dispatchers.Main).launch { + completion(result, requestError) + } + } + } else { + CoroutineScope(Dispatchers.Main).launch { + completion(null, WalletError(CarteraErrorCode.INVALID_SESSION)) + } + } + } else { + CoroutineScope(Dispatchers.Main).launch { + completion(null, WalletError(CarteraErrorCode.INVALID_SESSION)) + } + } + } + } + + private fun reallyMakeRequest( + requestParams: Modal.Params.Request, + status: WalletOperationStatus?, + completion: WalletOperationCompletion + ) { + WalletConnectModal.request( + request = requestParams, + onSuccess = { sendRequest -> + /* callback that letting you know that you have successful request */ + Timber.d("Wallet request made.") + operationCompletions[sendRequest.sessionTopic] = completion + }, + onError = { error -> + /* callback that letting you know that you have error */ + Timber.e(error.throwable.stackTraceToString()) + completion( + null, + WalletError( + CarteraErrorCode.CONNECTION_FAILED, + "WalletConnectModal.request error", + error.throwable.stackTraceToString(), + ), + ) + }, + ) + + openPeerDeeplink(status) + } + + // MARK: WalletConnectModal.ModalDelegate + + override fun onConnectionStateChange(state: Modal.Model.ConnectionState) { + Timber.d("Connection state changed: $state") + } + + override fun onError(error: Modal.Model.Error) { + Timber.e("WalletConnectModal error: $error") + } + + override fun onProposalExpired(proposal: Modal.Model.ExpiredProposal) { + Timber.d("Proposal expired: $proposal") + } + + override fun onRequestExpired(request: Modal.Model.ExpiredRequest) { + Timber.d("Request expired: $request") + } + + override fun onSessionApproved(approvedSession: Modal.Model.ApprovedSession) { + Timber.d("Session approved: $approvedSession") + + CoroutineScope(Dispatchers.Main).launch { + val approvedSsssion = approvedSession.namespaces[ethNamespace] + val approvedChain = approvedSsssion?.chains?.firstOrNull() + if (requestingWallet?.ethChain != null && + approvedChain != requestingWallet?.ethChain + ) { + for (connectCompletion in connectCompletions) { + connectCompletion.invoke( + null, + WalletError( + code = CarteraErrorCode.WALLET_MISMATCH, + message = CarteraErrorCode.WALLET_MISMATCH.message, + ), + ) + } + connectCompletions.clear() + return@launch + } + + currentSession = approvedSession + + _walletStatus.state = WalletState.CONNECTED_TO_WALLET + _walletStatus.connectedWallet = + fromApprovedSession(approvedSession, requestingWallet?.wallet) + _walletStatus.connectionDeeplink = approvedSession.metaData?.appLink ?: approvedSession.metaData?.redirect + + for (connectCompletion in connectCompletions) { + connectCompletion.invoke( + _walletStatus.connectedWallet, + null, + ) + } + connectCompletions.clear() + + walletStatusDelegate?.statusChanged(_walletStatus) + } + } + + override fun onSessionDelete(deletedSession: Modal.Model.DeletedSession) { + Timber.d("Session deleted: $deletedSession") + + when (deletedSession) { + is Modal.Model.DeletedSession.Success -> { + if (currentSession?.topic == deletedSession.topic) { + currentSession = null + + _walletStatus.state = WalletState.IDLE + _walletStatus.connectedWallet = null + _walletStatus.connectionDeeplink = null + + walletStatusDelegate?.statusChanged(_walletStatus) + } + } + is Modal.Model.DeletedSession.Error -> { + Timber.e("Session delete error: ${deletedSession.error}") + } + } + } + + override fun onSessionEvent(sessionEvent: Modal.Model.SessionEvent) { + Timber.d("Session event: $sessionEvent") + } + + override fun onSessionExtend(session: Modal.Model.Session) { + Timber.d("Session extended: $session") + } + + override fun onSessionRejected(rejectedSession: Modal.Model.RejectedSession) { + Timber.d("Session rejected: $rejectedSession") + + CoroutineScope(Dispatchers.Main).launch { + currentSession = null + + _walletStatus.state = WalletState.IDLE + _walletStatus.connectedWallet = null + + for (connectCompletion in connectCompletions) { + connectCompletion.invoke( + null, + WalletError( + code = CarteraErrorCode.REFUSED_BY_WALLET, + message = rejectedSession.reason, + ), + ) + } + connectCompletions.clear() + + walletStatusDelegate?.statusChanged(_walletStatus) + } + } + + override fun onSessionRequestResponse(response: Modal.Model.SessionRequestResponse) { + Timber.d("Session request response: $response") + + CoroutineScope(Dispatchers.Main).launch { + val completion = operationCompletions[response.topic] + if (completion != null) { + when (response.result) { + is Modal.Model.JsonRpcResponse.JsonRpcResult -> { + val result = + response.result as Modal.Model.JsonRpcResponse.JsonRpcResult + completion.invoke( + result.result, + null, + ) + } + + is Modal.Model.JsonRpcResponse.JsonRpcError -> { + val error = + response.result as Modal.Model.JsonRpcResponse.JsonRpcError + completion.invoke( + null, + WalletError( + code = CarteraErrorCode.UNEXPECTED_RESPONSE, + message = error.message, + ), + ) + } + } + + operationCompletions.remove(response.topic) + } + } + } + + override fun onSessionUpdate(updatedSession: Modal.Model.UpdatedSession) { + Timber.d("Session updated: $updatedSession") + } + + private fun fromApprovedSession(session: Modal.Model.ApprovedSession, wallet: Wallet?): WalletInfo { + val account = session.accounts.firstOrNull() + var address: String? = null + var chainId: String? = null + if (account != null) { + val comps = account.split(":") + if (comps.size == 3) { + address = comps[2] + chainId = comps[1] + } + } + return WalletInfo( + address = address, + chainId = chainId, + wallet = wallet, + peerName = session.metaData?.name, + peerImageUrl = session.metaData?.icons?.firstOrNull(), + ) + } + + private fun openPeerDeeplink(status: WalletOperationStatus?) { + if (currentSession == null) { + Timber.d("Current session is null") + return + } + + val deeplinkPairingUri = currentSession?.metaData?.appLink ?: currentSession?.metaData?.redirect + if (deeplinkPairingUri != null) { + try { + val uri = Uri.parse(deeplinkPairingUri) + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + application.startActivity(intent) + } catch (exception: ActivityNotFoundException) { + Timber.e(exception) + } + } else { + Timber.d("Invalid deeplink uri") + status?.invoke(true) // tell the client user needs to manually switch to the wallet + } + } +} diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV1Provider.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV1Provider.kt index 15a3ca3..648ccee 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV1Provider.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV1Provider.kt @@ -6,6 +6,7 @@ import exchange.dydx.cartera.walletprovider.WalletConnectCompletion import exchange.dydx.cartera.walletprovider.WalletConnectedCompletion import exchange.dydx.cartera.walletprovider.WalletOperationCompletion import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletStatusDelegate import exchange.dydx.cartera.walletprovider.WalletStatusImp @@ -36,6 +37,7 @@ class WalletConnectV1Provider : WalletOperationProviderProtocol { request: WalletRequest, message: String, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -45,6 +47,7 @@ class WalletConnectV1Provider : WalletOperationProviderProtocol { request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -53,6 +56,7 @@ class WalletConnectV1Provider : WalletOperationProviderProtocol { override fun send( request: WalletTransactionRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -62,6 +66,7 @@ class WalletConnectV1Provider : WalletOperationProviderProtocol { request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV2Provider.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV2Provider.kt index 2a1e69a..f55ef90 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV2Provider.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletConnectV2Provider.kt @@ -14,18 +14,18 @@ import exchange.dydx.cartera.CarteraErrorCode import exchange.dydx.cartera.WalletConnectV2Config import exchange.dydx.cartera.WalletConnectionType import exchange.dydx.cartera.entities.Wallet +import exchange.dydx.cartera.entities.toJsonRequest import exchange.dydx.cartera.tag -import exchange.dydx.cartera.toHexString import exchange.dydx.cartera.typeddata.WalletTypedDataProviderProtocol import exchange.dydx.cartera.typeddata.typedDataAsString import exchange.dydx.cartera.walletprovider.EthereumAddChainRequest -import exchange.dydx.cartera.walletprovider.EthereumTransactionRequest import exchange.dydx.cartera.walletprovider.WalletConnectCompletion import exchange.dydx.cartera.walletprovider.WalletConnectedCompletion import exchange.dydx.cartera.walletprovider.WalletError import exchange.dydx.cartera.walletprovider.WalletInfo import exchange.dydx.cartera.walletprovider.WalletOperationCompletion import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletState import exchange.dydx.cartera.walletprovider.WalletStatusDelegate @@ -37,9 +37,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch -import okhttp3.internal.toHexString -import org.json.JSONException -import org.json.JSONObject import java.util.concurrent.TimeUnit class WalletConnectV2Provider( @@ -51,7 +48,7 @@ class WalletConnectV2Provider( field = value walletStatusDelegate?.statusChanged(value) } - override val walletStatus: WalletStatusProtocol? + override val walletStatus: WalletStatusProtocol get() = _walletStatus override var walletStatusDelegate: WalletStatusDelegate? = null @@ -69,6 +66,41 @@ class WalletConnectV2Provider( private val requestExpiry: Long get() = (System.currentTimeMillis() / 1000) + 400 + private val nilDelegate = object : SignClient.DappDelegate { + override fun onConnectionStateChange(state: Sign.Model.ConnectionState) { + } + + override fun onError(error: Sign.Model.Error) { + } + + override fun onProposalExpired(proposal: Sign.Model.ExpiredProposal) { + } + + override fun onRequestExpired(request: Sign.Model.ExpiredRequest) { + } + + override fun onSessionApproved(approvedSession: Sign.Model.ApprovedSession) { + } + + override fun onSessionDelete(deletedSession: Sign.Model.DeletedSession) { + } + + override fun onSessionEvent(sessionEvent: Sign.Model.SessionEvent) { + } + + override fun onSessionExtend(session: Sign.Model.Session) { + } + + override fun onSessionRejected(rejectedSession: Sign.Model.RejectedSession) { + } + + override fun onSessionRequestResponse(response: Sign.Model.SessionRequestResponse) { + } + + override fun onSessionUpdate(updatedSession: Sign.Model.UpdatedSession) { + } + } + private val dappDelegate = object : SignClient.DappDelegate { override fun onSessionApproved(approvedSession: Sign.Model.ApprovedSession) { // Triggered when Dapp receives the session approval from wallet @@ -244,16 +276,17 @@ class WalletConnectV2Provider( SignClient.initialize(init) { error -> Log.e(tag(this@WalletConnectV2Provider), error.throwable.stackTraceToString()) } - - SignClient.setDappDelegate(dappDelegate) } } override fun connect(request: WalletRequest, completion: WalletConnectCompletion) { if (_walletStatus.state == WalletState.CONNECTED_TO_WALLET) { - completion(walletStatus?.connectedWallet, null) + completion(walletStatus.connectedWallet, null) } else { requestingWallet = request + + SignClient.setDappDelegate(dappDelegate) + CoroutineScope(IO).launch { doConnect(request = request) { pairing, error -> CoroutineScope(Dispatchers.Main).launch { @@ -297,20 +330,24 @@ class WalletConnectV2Provider( Log.e(tag(this@WalletConnectV2Provider), error.throwable.stackTraceToString()) } currentPairing = null - _walletStatus.state = WalletState.IDLE - _walletStatus.connectedWallet = null - _walletStatus.connectionDeeplink = null - walletStatusDelegate?.statusChanged(_walletStatus) - - connectCompletions.clear() - operationCompletions.clear() } + + _walletStatus.state = WalletState.IDLE + _walletStatus.connectedWallet = null + _walletStatus.connectionDeeplink = null + walletStatusDelegate?.statusChanged(_walletStatus) + + connectCompletions.clear() + operationCompletions.clear() + + SignClient.setDappDelegate(nilDelegate) } override fun signMessage( request: WalletRequest, message: String, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { fun requestParams(): Sign.Params.Request? { @@ -338,6 +375,7 @@ class WalletConnectV2Provider( request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { fun requestParams(): Sign.Params.Request? { @@ -366,6 +404,7 @@ class WalletConnectV2Provider( override fun send( request: WalletTransactionRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { fun requestParams(): Sign.Params.Request? { @@ -394,6 +433,7 @@ class WalletConnectV2Provider( request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") @@ -600,24 +640,3 @@ private fun Sign.Model.ApprovedSession.account(): String? { null } } - -private fun EthereumTransactionRequest.toJsonRequest(): String? { - var request: MutableMap = mutableMapOf() - - request["from"] = fromAddress - request["to"] = toAddress ?: "0x" - request["gas"] = gasLimit?.toHexString() - request["gasPrice"] = gasPriceInWei?.toHexString() - request["value"] = weiValue.toHexString() - request["data"] = data - request["nonce"] = nonce?.let { - "0x" + it.toHexString() - } - val filtered = request.filterValues { it != null } - - return try { - JSONObject(filtered).toString() - } catch (e: JSONException) { - null - } -} diff --git a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletSegueProvider.kt b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletSegueProvider.kt index 1134208..12a156f 100644 --- a/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletSegueProvider.kt +++ b/cartera/src/main/java/exchange/dydx/cartera/walletprovider/providers/WalletSegueProvider.kt @@ -25,6 +25,7 @@ import exchange.dydx.cartera.walletprovider.WalletError import exchange.dydx.cartera.walletprovider.WalletInfo import exchange.dydx.cartera.walletprovider.WalletOperationCompletion import exchange.dydx.cartera.walletprovider.WalletOperationProviderProtocol +import exchange.dydx.cartera.walletprovider.WalletOperationStatus import exchange.dydx.cartera.walletprovider.WalletRequest import exchange.dydx.cartera.walletprovider.WalletState import exchange.dydx.cartera.walletprovider.WalletStatusDelegate @@ -145,6 +146,7 @@ class WalletSegueProvider( request: WalletRequest, message: String, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { val signAction = Web3JsonRPC.PersonalSign(address = "", message = message).action() @@ -155,6 +157,7 @@ class WalletSegueProvider( request: WalletRequest, typedDataProvider: WalletTypedDataProviderProtocol?, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { typedDataProvider?.typedDataAsString?.let { typedData -> @@ -167,6 +170,7 @@ class WalletSegueProvider( override fun send( request: WalletTransactionRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { if (request.ethereum != null) { @@ -200,6 +204,7 @@ class WalletSegueProvider( request: WalletRequest, chain: EthereumAddChainRequest, connected: WalletConnectedCompletion?, + status: WalletOperationStatus?, completion: WalletOperationCompletion ) { TODO("Not yet implemented") diff --git a/gradle.properties b/gradle.properties index 0fa1676..934020e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,6 +26,6 @@ android.nonTransitiveRClass=true LIBRARY_GROUP=dydxprotocol LIBRARY_ARTIFACT_ID=cartera-android -LIBRARY_VERSION_NAME=0.1.15 +LIBRARY_VERSION_NAME=0.1.16 android.enableR8.fullMode = false \ No newline at end of file