From b38eb5fb54f2b8ce77fba0a4d409e307db41a69b Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 5 Nov 2025 09:14:26 +0800 Subject: [PATCH 1/9] Check destination before check both destination and tag --- .../main/java/one/mixin/android/api/service/AccountService.kt | 1 + .../main/java/one/mixin/android/repository/AccountRepository.kt | 2 +- .../java/one/mixin/android/ui/address/page/LabelInputPage.kt | 2 +- .../java/one/mixin/android/ui/address/page/MemoInputPage.kt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/one/mixin/android/api/service/AccountService.kt b/app/src/main/java/one/mixin/android/api/service/AccountService.kt index abe9294b3e..ff1465866d 100644 --- a/app/src/main/java/one/mixin/android/api/service/AccountService.kt +++ b/app/src/main/java/one/mixin/android/api/service/AccountService.kt @@ -239,6 +239,7 @@ interface AccountService { @Query("asset") assetId: String, @Query("chain") chain: String, @Query("destination") destination: String, + @Query("insecureSkipTagCheck") insecureSkipTagCheck: Boolean? = null, @Query("tag") tag: String?, ): MixinResponse diff --git a/app/src/main/java/one/mixin/android/repository/AccountRepository.kt b/app/src/main/java/one/mixin/android/repository/AccountRepository.kt index 44b9bb014b..f4d790de78 100644 --- a/app/src/main/java/one/mixin/android/repository/AccountRepository.kt +++ b/app/src/main/java/one/mixin/android/repository/AccountRepository.kt @@ -438,7 +438,7 @@ class AccountRepository destination: String, tag: String?, ) = - accountService.validateExternalAddress(assetId, chain, destination, tag) + accountService.validateExternalAddress(assetId, chain, destination, if (tag.isNullOrBlank()) true else null, tag) suspend fun refreshSticker(id: String): Sticker? { val sticker = stickerDao.findStickerById(id) diff --git a/app/src/main/java/one/mixin/android/ui/address/page/LabelInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/LabelInputPage.kt index 20560b79d7..0ed9a6d69a 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/LabelInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/LabelInputPage.kt @@ -92,7 +92,7 @@ fun LabelInputPage( Box( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(horizontal = 16.dp) ) { Column(modifier = Modifier.imePadding()) { TokenInfoHeader(token = token, web3Token = web3Token) diff --git a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt index 7510e07561..1cf3f10bd3 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt @@ -97,7 +97,7 @@ fun MemoInputPage( Box( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(horizontal = 16.dp) ) { Column(modifier = Modifier.imePadding()) { TokenInfoHeader(token = token, web3Token = web3Token) From 3c11ddd086e0b24034fa229e49270be88dd51fa1 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 12 Nov 2025 12:09:47 +0800 Subject: [PATCH 2/9] Pre-check address --- .../address/TransferDestinationInputFragment.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index 8fd54ed84e..054d42b03c 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -337,7 +337,16 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres val memoEnabled = token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE if (memoEnabled) { - navController.navigate("${TransferDestination.SendMemo.name}?address=${address}") + token?.let { t -> // Only privacy wallet + validateAndNavigateToInput( + assetId = t.assetId, + chainId = t.chainId, + destination = address, + asset = t, + ) { + navController.navigate("${TransferDestination.SendMemo.name}?address=${address}") + } + } } else if (web3Token != null) { lifecycleScope.launch { web3Token?.let { token -> @@ -364,7 +373,6 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres chainId = t.chainId, destination = address, asset = t, - toAccount = true ) } } @@ -593,7 +601,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres web3Token: Web3TokenItem? = null, chainToken: Web3TokenItem? = null, asset: TokenItem? = null, - toAccount: Boolean? = null, + callback: (()-> Unit)? = null ) { requireView().hideKeyboard() val dialog = indeterminateProgressDialog(message = R.string.Please_wait_a_bit).apply { @@ -608,6 +616,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres if (response.isSuccess) { errorInfo = null when { + callback != null -> { + callback.invoke() + } asset != null && destination.isNotEmpty() && tag != null -> { navigateToInputFragmentWithBundle(Bundle().apply { putParcelable(InputFragment.ARGS_TOKEN, asset) From 73e85d7125a9ca30047b58cecf9979bf00aba3da Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 12 Nov 2025 15:50:17 +0800 Subject: [PATCH 3/9] Update address check --- .../TransferDestinationInputFragment.kt | 39 ++++++++++++------- .../ui/address/page/AddressInputPage.kt | 11 ++++++ .../android/ui/address/page/MemoInputPage.kt | 2 +- 3 files changed, 38 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index 054d42b03c..b9401fe8f9 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -338,6 +338,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE if (memoEnabled) { token?.let { t -> // Only privacy wallet + errorInfo = null validateAndNavigateToInput( assetId = t.assetId, chainId = t.chainId, @@ -368,6 +369,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } } else { token?.let { t -> + errorInfo = null validateAndNavigateToInput( assetId = t.assetId, chainId = t.chainId, @@ -435,11 +437,23 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres token = token, web3Token = web3Token, contentText = scannedAddress, + errorInfo = errorInfo, onNext = { address -> - if (token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE) - navController.navigate("${TransferDestination.Memo.name}?address=$address") - else - navController.navigate("${TransferDestination.Label.name}?address=$address") + errorInfo = null + requireView().hideKeyboard() + validateAndNavigateToInput( + assetId = token?.assetId ?: web3Token?.assetId ?: "", + chainId = token?.chainId ?: web3Token?.chainId ?: "", + destination = address, + asset = token, + web3Token = web3Token + ) { + if (token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE) { + navController.navigate("${TransferDestination.Memo.name}?address=$address") + } else { + navController.navigate("${TransferDestination.Label.name}?address=$address") + } + } }, onScan = { startQrScan(ScanType.ADDRESS) }, pop = { navController.popBackStack() } @@ -460,15 +474,14 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres onNext = { memo -> errorInfo = null requireView().hideKeyboard() - token?.let { t -> - validateAndNavigateToInput( - assetId = t.assetId, - chainId = t.chainId, - destination = address, - tag = memo, - asset = t - ) - } + validateAndNavigateToInput( + assetId = token?.assetId ?: web3Token?.assetId ?: "", + chainId = token?.chainId ?: web3Token?.chainId ?: "", + destination = address, + asset = token, + web3Token = web3Token, + tag = memo + ) }, onScan = { startQrScan(ScanType.MEMO) }, pop = { navController.popBackStack() } diff --git a/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt index 5a3a12dac6..70412e8af3 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color @@ -59,6 +60,7 @@ fun AddressInputPage( onNext: (String) -> Unit, pop: () -> Unit, onScan: (() -> Unit)? = null, + errorInfo: String? = null ) { var address by remember(contentText) { mutableStateOf(contentText) } val focusRequester = remember { FocusRequester() } @@ -181,6 +183,15 @@ fun AddressInputPage( Spacer(modifier = Modifier.weight(1f)) + Text( + text = errorInfo ?: "", + color = MixinAppTheme.colors.red, + modifier = Modifier + .padding(vertical = 8.dp) + .align(Alignment.CenterHorizontally) + .alpha(if (errorInfo.isNullOrBlank()) 0f else 1f) + ) + Spacer(modifier = Modifier.height(8.dp)) Button( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt index 1cf3f10bd3..72aaae72bb 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt @@ -206,7 +206,7 @@ fun MemoInputPage( .align(Alignment.CenterHorizontally) .alpha(if (errorInfo.isNullOrBlank()) 0f else 1f) ) - + Spacer(modifier = Modifier.height(8.dp)) Button( modifier = Modifier .fillMaxWidth() From 231552ae4f1d7e6605d9979362cce48fd62919f9 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 13 Nov 2025 10:50:14 +0800 Subject: [PATCH 4/9] Display error --- .../android/ui/address/TransferDestinationInputFragment.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index b9401fe8f9..ab0095e5f5 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import one.mixin.android.R +import one.mixin.android.api.ServerErrorException import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.databinding.FragmentAddressInputBinding import one.mixin.android.db.web3.vo.Web3TokenItem @@ -60,6 +61,7 @@ import one.mixin.android.ui.wallet.TransactionsFragment.Companion.ARGS_ASSET import one.mixin.android.ui.wallet.TransferContactBottomSheetDialogFragment import one.mixin.android.ui.wallet.WalletListBottomSheetDialogFragment import one.mixin.android.ui.wallet.transfer.TransferBottomSheetDialogFragment +import one.mixin.android.util.ErrorHandler import one.mixin.android.util.decodeICAP import one.mixin.android.util.getMixinErrorStringByCode import one.mixin.android.util.isIcapAddress @@ -659,7 +661,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } } } catch (e: Exception) { - errorInfo = e.message ?: getString(R.string.Unknown) + errorInfo = ErrorHandler.getErrorMessage(e) } finally { dialog.dismiss() } From fc75df1c24ccc67e457e0da29016b6fbf78f98bc Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 13 Nov 2025 11:05:57 +0800 Subject: [PATCH 5/9] Button loading --- .../TransferDestinationInputFragment.kt | 12 ++++---- .../ui/address/page/AddressInputPage.kt | 29 ++++++++++++------- .../android/ui/address/page/MemoInputPage.kt | 21 ++++++++++---- .../page/TransferDestinationInputPage.kt | 24 ++++++++++----- 4 files changed, 57 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index ab0095e5f5..6235d63927 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -126,6 +126,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres private var scannedLabel by mutableStateOf("") private var scannedTransferDest by mutableStateOf("") private var errorInfo by mutableStateOf(null) + private var isLoading by mutableStateOf(false) enum class ScanType { ADDRESS, MEMO, LABEL, TRANSFER_DEST } @@ -208,6 +209,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres web3Token = web3Token, name = if (wallet?.isWatch() == true || wallet?.isImported() == true) wallet?.name else null, addressShown = addressShown, + isLoading = isLoading, pop = { requireActivity().onBackPressedDispatcher.onBackPressed() }, @@ -440,6 +442,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres web3Token = web3Token, contentText = scannedAddress, errorInfo = errorInfo, + isLoading = isLoading, onNext = { address -> errorInfo = null requireView().hideKeyboard() @@ -473,6 +476,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres address = address, contentText = scannedMemo, errorInfo = errorInfo, + isLoading = isLoading, onNext = { memo -> errorInfo = null requireView().hideKeyboard() @@ -619,12 +623,8 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres callback: (()-> Unit)? = null ) { requireView().hideKeyboard() - val dialog = indeterminateProgressDialog(message = R.string.Please_wait_a_bit).apply { - setCancelable(false) - } - lifecycleScope.launch { - dialog.show() + isLoading = true try { if (assetId.isNotEmpty() && destination.isNotEmpty()) { val response = viewModel.validateExternalAddress(assetId, chainId, destination, tag) @@ -663,7 +663,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } catch (e: Exception) { errorInfo = ErrorHandler.getErrorMessage(e) } finally { - dialog.dismiss() + isLoading = false } } } diff --git a/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt index 70412e8af3..7f7f22af07 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/AddressInputPage.kt @@ -10,7 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.CircularProgressIndicator import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button @@ -60,7 +62,8 @@ fun AddressInputPage( onNext: (String) -> Unit, pop: () -> Unit, onScan: (() -> Unit)? = null, - errorInfo: String? = null + errorInfo: String? = null, + isLoading: Boolean = false ) { var address by remember(contentText) { mutableStateOf(contentText) } val focusRequester = remember { FocusRequester() } @@ -199,11 +202,9 @@ fun AddressInputPage( onClick = { onNext.invoke(address) }, - enabled = address.isBlank().not(), + enabled = address.isBlank().not() && !isLoading, colors = ButtonDefaults.outlinedButtonColors( - backgroundColor = if (address.isNullOrBlank() - .not() - ) MixinAppTheme.colors.accent else MixinAppTheme.colors.backgroundGrayLight, + backgroundColor = if (address.isBlank().not()) MixinAppTheme.colors.accent else MixinAppTheme.colors.backgroundGrayLight, ), shape = RoundedCornerShape(32.dp), elevation = ButtonDefaults.elevation( @@ -213,11 +214,19 @@ fun AddressInputPage( focusedElevation = 0.dp, ), ) { - Text( - text = stringResource(R.string.Next), - color = if (address.isBlank() - ) MixinAppTheme.colors.textAssist else Color.White, - ) + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + text = stringResource(R.string.Next), + color = if (address.isBlank() + ) MixinAppTheme.colors.textAssist else Color.White, + ) + } } Spacer(modifier = Modifier.height(20.dp)) } diff --git a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt index 72aaae72bb..5f21362e32 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/MemoInputPage.kt @@ -11,7 +11,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material.CircularProgressIndicator import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.Button @@ -62,6 +64,7 @@ fun MemoInputPage( address: String, contentText: String = "", errorInfo: String? = null, + isLoading: Boolean = false, onNext: (String?) -> Unit, pop: () -> Unit, onScan: (() -> Unit)? = null, @@ -214,7 +217,7 @@ fun MemoInputPage( onClick = { onNext.invoke(memo) }, - enabled = isValidMemo, + enabled = isValidMemo && !isLoading, colors = ButtonDefaults.outlinedButtonColors( backgroundColor = if (isValidMemo) MixinAppTheme.colors.accent else MixinAppTheme.colors.backgroundGrayLight, ), @@ -226,10 +229,18 @@ fun MemoInputPage( focusedElevation = 0.dp, ), ) { - Text( - text = stringResource(R.string.Next), - color = if (isValidMemo) Color.White else MixinAppTheme.colors.textAssist, - ) + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + text = stringResource(R.string.Next), + color = if (isValidMemo) Color.White else MixinAppTheme.colors.textAssist, + ) + } } Spacer(modifier = Modifier.height(20.dp)) } diff --git a/app/src/main/java/one/mixin/android/ui/address/page/TransferDestinationInputPage.kt b/app/src/main/java/one/mixin/android/ui/address/page/TransferDestinationInputPage.kt index 454377c2f5..9ee7a65005 100644 --- a/app/src/main/java/one/mixin/android/ui/address/page/TransferDestinationInputPage.kt +++ b/app/src/main/java/one/mixin/android/ui/address/page/TransferDestinationInputPage.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.CircularProgressIndicator import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions @@ -82,6 +83,7 @@ fun TransferDestinationInputPage( web3Token: Web3TokenItem?, name: String?, addressShown: Boolean, + isLoading: Boolean = false, pop: (() -> Unit)?, onScan: (() -> Unit)? = null, contentText: String = "", @@ -382,11 +384,9 @@ fun TransferDestinationInputPage( onClick = { onSend.invoke(text) }, - enabled = text.isBlank().not(), + enabled = text.isBlank().not() && !isLoading, colors = ButtonDefaults.outlinedButtonColors( - backgroundColor = if (text.isBlank() - .not() - ) MixinAppTheme.colors.accent else MixinAppTheme.colors.backgroundGrayLight, + backgroundColor = if (text.isBlank().not()) MixinAppTheme.colors.accent else MixinAppTheme.colors.backgroundGrayLight, ), shape = RoundedCornerShape(32.dp), elevation = ButtonDefaults.elevation( @@ -396,10 +396,18 @@ fun TransferDestinationInputPage( focusedElevation = 0.dp, ), ) { - Text( - text = stringResource(R.string.Send), - color = if (text.isBlank()) MixinAppTheme.colors.textAssist else Color.White, - ) + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + text = stringResource(R.string.Send), + color = if (text.isBlank()) MixinAppTheme.colors.textAssist else Color.White, + ) + } } } } From 0bb896b614a621c82770415f6d3c9e3dc3e09855 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Fri, 14 Nov 2025 11:49:48 +0800 Subject: [PATCH 6/9] Check before input label --- .../TransferDestinationInputFragment.kt | 131 +++++++++++------- 1 file changed, 79 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index 6235d63927..abac728651 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -223,7 +223,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres token?.let { t -> TransferContactBottomSheetDialogFragment.newInstance() .apply { - onUserClick = { user-> + onUserClick = { user -> navigateToInputFragmentWithBundle( Bundle().apply { putParcelable(InputFragment.ARGS_TO_USER, user) @@ -272,59 +272,60 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres toWallet = { fromWalletId -> requireView().hideKeyboard() WalletListBottomSheetDialogFragment.newInstance(fromWalletId, web3Token?.chainId ?: token!!.chainId).apply { - setOnWalletClickListener { destinationWallet -> - this@TransferDestinationInputFragment.lifecycleScope.launch(CoroutineExceptionHandler { _, error -> - Timber.e(error) - }) { - when { - web3Token != null -> { - val tokenToSend = web3Token!! - val fromAddress = web3ViewModel.getAddressesByChainId(fromWalletId!!, tokenToSend.chainId) - val toAddress = if(destinationWallet == null) { - try { - val depositEntry = web3ViewModel.findAndSyncDepositEntry(tokenToSend) - depositEntry?.destination - } catch (e: Exception) { - null - } - } else { - web3ViewModel.getAddressesByChainId(destinationWallet.id, tokenToSend.chainId)?.destination - } - if (fromAddress == null || fromAddress.destination.isBlank() || toAddress.isNullOrBlank()) { - toast(R.string.Alert_Not_Support) - } else { - (chainToken ?: web3ViewModel.web3TokenItemById(tokenToSend.walletId, tokenToSend.chainId))?.let { chain -> - navigateToInputFragmentWithBundle( - Bundle().apply { - putString(InputFragment.ARGS_FROM_ADDRESS, fromAddress.destination) - putString(InputFragment.ARGS_TO_ADDRESS, toAddress) - putParcelable(InputFragment.ARGS_WEB3_TOKEN, tokenToSend) - putParcelable(InputFragment.ARGS_WEB3_CHAIN_TOKEN, chain) - putParcelable(ARGS_WALLET, wallet) - }) - } + setOnWalletClickListener { destinationWallet -> + this@TransferDestinationInputFragment.lifecycleScope.launch(CoroutineExceptionHandler { _, error -> + Timber.e(error) + }) { + when { + web3Token != null -> { + val tokenToSend = web3Token!! + val fromAddress = web3ViewModel.getAddressesByChainId(fromWalletId!!, tokenToSend.chainId) + val toAddress = if (destinationWallet == null) { + try { + val depositEntry = web3ViewModel.findAndSyncDepositEntry(tokenToSend) + depositEntry?.destination + } catch (e: Exception) { + null } + } else { + web3ViewModel.getAddressesByChainId(destinationWallet.id, tokenToSend.chainId)?.destination } - - token != null -> { - val toAddress = withContext(Dispatchers.IO) { - web3ViewModel.getAddressesByChainId(destinationWallet!!.id, token!!.chainId) - } - if (toAddress != null) { + if (fromAddress == null || fromAddress.destination.isBlank() || toAddress.isNullOrBlank()) { + toast(R.string.Alert_Not_Support) + } else { + (chainToken ?: web3ViewModel.web3TokenItemById(tokenToSend.walletId, tokenToSend.chainId))?.let { chain -> navigateToInputFragmentWithBundle( Bundle().apply { - putParcelable(InputFragment.ARGS_TOKEN, token) - putString(InputFragment.ARGS_TO_ADDRESS, toAddress.destination) + putString(InputFragment.ARGS_FROM_ADDRESS, fromAddress.destination) + putString(InputFragment.ARGS_TO_ADDRESS, toAddress) + putParcelable(InputFragment.ARGS_WEB3_TOKEN, tokenToSend) + putParcelable(InputFragment.ARGS_WEB3_CHAIN_TOKEN, chain) + putParcelable(ARGS_WALLET, wallet) }) - } else { - toast(R.string.Alert_Not_Support) } } - else -> {} } + + token != null -> { + val toAddress = withContext(Dispatchers.IO) { + web3ViewModel.getAddressesByChainId(destinationWallet!!.id, token!!.chainId) + } + if (toAddress != null) { + navigateToInputFragmentWithBundle( + Bundle().apply { + putParcelable(InputFragment.ARGS_TOKEN, token) + putString(InputFragment.ARGS_TO_ADDRESS, toAddress.destination) + }) + } else { + toast(R.string.Alert_Not_Support) + } + } + + else -> {} } } - }.show(parentFragmentManager, WalletListBottomSheetDialogFragment.TAG) + } + }.show(parentFragmentManager, WalletListBottomSheetDialogFragment.TAG) }, toAddAddress = { @@ -359,7 +360,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres if (fromAddress.isNullOrBlank()) { toast(R.string.Alert_Not_Support) } else { - val chain = chainToken ?: web3ViewModel.web3TokenItemById(token.walletId, token.chainId) ?:return@launch + val chain = chainToken ?: web3ViewModel.web3TokenItemById(token.walletId, token.chainId) ?: return@launch validateAndNavigateToInput( assetId = token.assetId, chainId = token.chainId, @@ -373,7 +374,6 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } } else { token?.let { t -> - errorInfo = null validateAndNavigateToInput( assetId = t.assetId, chainId = t.chainId, @@ -453,6 +453,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres asset = token, web3Token = web3Token ) { + errorInfo = null if (token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE) { navController.navigate("${TransferDestination.Memo.name}?address=$address") } else { @@ -461,7 +462,10 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } }, onScan = { startQrScan(ScanType.ADDRESS) }, - pop = { navController.popBackStack() } + pop = { + errorInfo = null + navController.popBackStack() + } ) } @@ -490,7 +494,10 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres ) }, onScan = { startQrScan(ScanType.MEMO) }, - pop = { navController.popBackStack() } + pop = { + errorInfo = null + navController.popBackStack() + } ) } @@ -505,10 +512,23 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres address = address, contentText = scannedMemo, onNext = { memo -> - navController.navigate("${TransferDestination.Label.name}?address=${address}&memo=${memo}") + errorInfo = null + validateAndNavigateToInput( + assetId = token?.assetId ?: web3Token?.assetId ?: "", + chainId = token?.chainId ?: web3Token?.chainId ?: "", + destination = address, + asset = token, + web3Token = web3Token, + tag = memo + ) { + navController.navigate("${TransferDestination.Label.name}?address=${address}&memo=${memo}") + } }, onScan = { startQrScan(ScanType.MEMO) }, - pop = { navController.popBackStack() } + pop = { + errorInfo = null + navController.popBackStack() + } ) } @@ -532,6 +552,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres contentText = scannedLabel, onScan = { startQrScan(ScanType.LABEL) }, onComplete = { label -> + errorInfo = null if (token == null && web3Token != null) { lifecycleScope.launch { val t = web3ViewModel.syncAsset(web3Token!!.assetId) ?: return@launch @@ -543,7 +564,10 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres toast(R.string.Data_error) } }, - pop = { navController.popBackStack() } + pop = { + errorInfo = null + navController.popBackStack() + } ) } } @@ -620,7 +644,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres web3Token: Web3TokenItem? = null, chainToken: Web3TokenItem? = null, asset: TokenItem? = null, - callback: (()-> Unit)? = null + callback: (() -> Unit)? = null, ) { requireView().hideKeyboard() lifecycleScope.launch { @@ -634,6 +658,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres callback != null -> { callback.invoke() } + asset != null && destination.isNotEmpty() && tag != null -> { navigateToInputFragmentWithBundle(Bundle().apply { putParcelable(InputFragment.ARGS_TOKEN, asset) @@ -641,12 +666,14 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres putString(InputFragment.ARGS_TO_ADDRESS_TAG, tag) }) } + asset != null && destination.isNotEmpty() -> { navigateToInputFragmentWithBundle(Bundle().apply { putParcelable(InputFragment.ARGS_TOKEN, asset) putString(InputFragment.ARGS_TO_ADDRESS, destination) }) } + fromAddress != null && destination.isNotEmpty() && web3Token != null && chainToken != null -> { navigateToInputFragmentWithBundle(Bundle().apply { putString(InputFragment.ARGS_FROM_ADDRESS, fromAddress) From a49f9fad2ba63169f917aeec7849efe0718e7a68 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 17 Nov 2025 13:48:40 +0800 Subject: [PATCH 7/9] Parse external url --- .../mixin/android/extension/UrlExtension.kt | 2 + .../android/repository/Web3Repository.kt | 34 +++++- .../TransferDestinationInputFragment.kt | 105 +++++++++++++++++- .../android/ui/home/web3/Web3ViewModel.kt | 77 +++++-------- 4 files changed, 162 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt index d7bc1630ae..a21910eaf8 100644 --- a/app/src/main/java/one/mixin/android/extension/UrlExtension.kt +++ b/app/src/main/java/one/mixin/android/extension/UrlExtension.kt @@ -301,6 +301,8 @@ fun String.isValidStartParam(): Boolean { fun String.isExternalTransferUrl() = externalTransferAssetIdMap.keys.any { startsWith("$it:", ignoreCase = true) } +fun String.isEthereumOrSolURLString() = startsWith("ethereum:", true) || startsWith("solana:", true) + fun String.isLightningUrl() = startsWith("lnbc", true) || startsWith("lnurl", true) || startsWith("lightning:", true) private fun String.isUserScheme() = diff --git a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt index 07fadb9196..3a6921817e 100644 --- a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt +++ b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt @@ -21,6 +21,7 @@ import one.mixin.android.db.web3.Web3WalletDao import one.mixin.android.db.web3.updateWithLocalKeyInfo import one.mixin.android.db.web3.vo.Web3Address import one.mixin.android.db.web3.vo.Web3Chain +import one.mixin.android.db.web3.vo.Web3Token import one.mixin.android.db.web3.vo.Web3TokenItem import one.mixin.android.db.web3.vo.Web3TokensExtra import one.mixin.android.db.web3.vo.Web3TransactionItem @@ -51,8 +52,37 @@ constructor( suspend fun web3TokenItemByAddress(address: String) = web3TokenDao.web3TokenItemByAddress(address) suspend fun web3TokenItemById(walletId: String, assetId: String) = web3TokenDao.web3TokenItemById(walletId, assetId) - - suspend fun findWeb3TokenItemsByIds(walletId: String, assetIds: List) = web3TokenDao.findWeb3TokenItemsByIds(walletId, assetIds) + + suspend fun findAndRefreshWeb3TokenItem(walletId: String, assetId: String): Web3TokenItem? { + val localToken = web3TokenDao.web3TokenItemById(walletId, assetId) + if (localToken != null) { + return localToken + } + + return try { + val token = tokenRepository.findOrSyncAsset(assetId) ?: return null + val w = token.toWeb3TokenItem(walletId) + web3TokenDao.insert( + Web3Token( + walletId = walletId, + assetId = token.assetId, + chainId = token.chainId, + assetKey = token.assetKey ?: "", + name = token.name, + symbol = token.symbol, + iconUrl = token.chainIconUrl?:"", + priceUsd = token.priceUsd, + precision = token.precision, + balance = "0", + changeUsd = "0" + ) + ) + w + } catch (e: Exception) { + Timber.e(e) + null + } + } fun web3TokensExcludeHidden(walletId: String) = web3TokenDao.web3TokenItemsExcludeHidden(walletId) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index 8fd54ed84e..cda614b9cc 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -33,11 +33,13 @@ import one.mixin.android.compose.theme.MixinAppTheme import one.mixin.android.databinding.FragmentAddressInputBinding import one.mixin.android.db.web3.vo.Web3TokenItem import one.mixin.android.db.web3.vo.Web3Wallet +import one.mixin.android.db.web3.vo.buildTransaction import one.mixin.android.db.web3.vo.isImported import one.mixin.android.db.web3.vo.isWatch import one.mixin.android.extension.getParcelableCompat import one.mixin.android.extension.hideKeyboard import one.mixin.android.extension.indeterminateProgressDialog +import one.mixin.android.extension.isEthereumOrSolURLString import one.mixin.android.extension.isExternalTransferUrl import one.mixin.android.extension.isLightningUrl import one.mixin.android.extension.openPermissionSetting @@ -45,6 +47,8 @@ import one.mixin.android.extension.toast import one.mixin.android.job.MixinJobManager import one.mixin.android.job.RefreshAddressJob import one.mixin.android.job.SyncOutputJob +import one.mixin.android.pay.ExternalTransfer +import one.mixin.android.pay.parseExternalTransferUri import one.mixin.android.ui.address.FetchUserAddressFragment.Companion.ARGS_TO_USER import one.mixin.android.ui.address.page.AddressInputPage import one.mixin.android.ui.address.page.LabelInputPage @@ -54,6 +58,7 @@ import one.mixin.android.ui.common.BaseFragment import one.mixin.android.ui.common.biometric.AddressManageBiometricItem import one.mixin.android.ui.conversation.link.LinkBottomSheetDialogFragment import one.mixin.android.ui.home.web3.Web3ViewModel +import one.mixin.android.ui.home.web3.showGasCheckAndBrowserBottomSheetDialogFragment import one.mixin.android.ui.qr.CaptureActivity import one.mixin.android.ui.wallet.InputFragment import one.mixin.android.ui.wallet.TransactionsFragment.Companion.ARGS_ASSET @@ -68,6 +73,8 @@ import one.mixin.android.util.viewBinding import one.mixin.android.vo.Address import one.mixin.android.vo.WithdrawalMemoPossibility import one.mixin.android.vo.safe.TokenItem +import one.mixin.android.web3.Rpc +import one.mixin.android.web3.js.Web3Signer import timber.log.Timber import javax.inject.Inject @@ -81,6 +88,84 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres const val ARGS_WALLET = "args_wallet" } + private suspend fun handleWeb3ExternalTransfer(url: String) { + Timber.d("[$TAG] handleWeb3ExternalTransfer url=%s", url) + val ext = parseExternalForWeb3(url) ?: run { + Timber.e("[$TAG] handleWeb3ExternalTransfer parseExternalForWeb3 returned null, url=%s", url) + toast(R.string.Data_error) + return + } + Timber.d("[$TAG] handleWeb3ExternalTransfer parsed assetId=%s destination=%s amount=%s", ext.assetId, ext.destination, ext.amount) + val t = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, ext.assetId) + if (t == null) { + Timber.e("[$TAG] handleWeb3ExternalTransfer web3 token not found for assetId=%s", ext.assetId) + toast(R.string.Data_error) + return + } + val c = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, t.chainId) + if (c == null) { + Timber.e("[$TAG] handleWeb3ExternalTransfer web3 chain token not found for chainId=%s", t.chainId) + toast(R.string.Data_error) + return + } + val from = web3ViewModel.getAddressesByChainId(walletId = Web3Signer.currentWalletId, c.chainId)?.destination + if (from == null) { + Timber.e("[$TAG] handleWeb3ExternalTransfer from address not found for chainId=%s", c.chainId) + toast(R.string.Data_error) + return + } + val to = ext.destination + val amount = ext.amount + if (amount.isNullOrBlank() || amount == "0") { + navigateToInputFragmentWithBundle( + Bundle().apply { + putString(InputFragment.ARGS_FROM_ADDRESS, from) + putString(InputFragment.ARGS_TO_ADDRESS, to) + putParcelable(InputFragment.ARGS_WEB3_TOKEN, t) + putParcelable(InputFragment.ARGS_WEB3_CHAIN_TOKEN, c) + putParcelable(ARGS_WALLET, wallet) + } + ) + } else { + val transaction = t.buildTransaction(rpc, from, to, amount) + showGasCheckAndBrowserBottomSheetDialogFragment( + requireActivity(), + transaction, + amount = amount, + token = t, + chainToken = c, + toAddress = to, + onTxhash = { _, _ -> }, + onDismiss = { _ -> } + ) + } + } + + private suspend fun parseExternalForWeb3(url: String): ExternalTransfer? { + return parseExternalTransferUri( + url, + validateAddress = { assetId, chainId, destination -> + web3ViewModel.validateExternalAddress(assetId, chainId, destination, null).data + }, + getFee = { assetId, destination -> + web3ViewModel.getFees(assetId, destination).data + }, + findAssetIdByAssetKey = { assetKey -> + web3ViewModel.findAssetIdByAssetKey(assetKey) + }, + getAssetPrecisionById = { assetId -> + web3ViewModel.getAssetPrecisionById(assetId).data + }, + balanceCheck = { _, _, _, _ -> + // do nothing + }, + parseLighting = { ln -> + val r = web3ViewModel.paySuspend(one.mixin.android.api.request.TransferRequest(assetId = one.mixin.android.Constants.ChainId.LIGHTNING_NETWORK_CHAIN_ID, rawPaymentUrl = ln)) + r.data + } + ) + } + private val token: TokenItem? by lazy { requireArguments().getParcelableCompat( ARGS_ASSET, @@ -148,6 +233,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres @Inject lateinit var jobManager: MixinJobManager + @Inject + lateinit var rpc: Rpc + enum class TransferDestination { Initial, Address, @@ -327,12 +415,17 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres navController.navigate(TransferDestination.Address.name) }, onSend = { address -> + Timber.d("[$TAG] onSend address=%s token=%s web3Token=%s", address, token, web3Token) errorInfo = null if (token != null && (address.isExternalTransferUrl() || address.isLightningUrl())) { LinkBottomSheetDialogFragment.newInstance(address).show( parentFragmentManager, LinkBottomSheetDialogFragment.TAG ) + } else if (web3Token != null && address.isEthereumOrSolURLString()) { + lifecycleScope.launch { + handleWeb3ExternalTransfer(address) + } } else { val memoEnabled = token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSITIVE || token?.withdrawalMemoPossibility == WithdrawalMemoPossibility.POSSIBLE @@ -340,6 +433,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres navController.navigate("${TransferDestination.SendMemo.name}?address=${address}") } else if (web3Token != null) { lifecycleScope.launch { + Timber.d("[$TAG] onSend web3 normal address=%s", address) web3Token?.let { token -> val fromAddress = web3ViewModel.getAddressesByChainId(token.walletId, token.chainId)?.destination if (fromAddress.isNullOrBlank()) { @@ -359,6 +453,7 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } } else { token?.let { t -> + Timber.d("[$TAG] onSend normal token address=%s", address) validateAndNavigateToInput( assetId = t.assetId, chainId = t.chainId, @@ -539,8 +634,8 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres private fun handleScanResult(data: Intent?, isAddr: Boolean = true) { if (data == null) return - - data.getStringExtra(CaptureActivity.Companion.ARGS_FOR_SCAN_RESULT)?.let { result -> + data.getStringExtra(CaptureActivity.ARGS_FOR_SCAN_RESULT)?.let { result -> + Timber.d("[$TAG] handleScanResult result=%s currentScanType=%s", result, currentScanType) if (token != null && (result.isLightningUrl() || result.isExternalTransferUrl())) { LinkBottomSheetDialogFragment.newInstance(result).show( parentFragmentManager, @@ -548,6 +643,12 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres ) return@let } + if (web3Token != null && result.isEthereumOrSolURLString()) { + lifecycleScope.launch { + handleWeb3ExternalTransfer(result) + } + return@let + } when (currentScanType) { ScanType.ADDRESS -> { scannedAddress = if (isIcapAddress(result)) { diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt index 8264f7a0f5..2792eaa1bb 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/Web3ViewModel.kt @@ -19,6 +19,7 @@ import one.mixin.android.R import one.mixin.android.api.MixinResponse import one.mixin.android.api.handleMixinResponse import one.mixin.android.api.request.AccountUpdateRequest +import one.mixin.android.api.request.TransferRequest import one.mixin.android.api.request.web3.EstimateFeeRequest import one.mixin.android.api.response.PaymentStatus import one.mixin.android.api.response.web3.StakeAccount @@ -43,6 +44,7 @@ import one.mixin.android.ui.common.biometric.NftBiometricItem import one.mixin.android.ui.common.biometric.maxUtxoCount import one.mixin.android.ui.home.inscription.component.OwnerState import one.mixin.android.ui.oldwallet.AssetRepository +import one.mixin.android.vo.AssetPrecision import one.mixin.android.util.GsonHelper import one.mixin.android.util.mlkit.firstUrl import one.mixin.android.vo.Account @@ -98,6 +100,11 @@ internal constructor( web3Repository.web3TokenItemById(walletId, assetId) } + + suspend fun findAndRefreshWeb3TokenItem(walletId: String, assetId: String) = withContext(Dispatchers.IO) { + web3Repository.findAndRefreshWeb3TokenItem(walletId, assetId) + } + fun getTokenPriceUsdFlow(assetId: String): Flow = flow { val item = tokenRepository.findAssetItemById(assetId)?.priceUsd emit(item) @@ -131,20 +138,6 @@ internal constructor( } } - fun getLatestActiveSignSessions(): List { - val v2List = - WalletConnectV2.getListOfActiveSessions().mapIndexed { index, wcSession -> - ConnectionUI( - index = index, - icon = wcSession.metaData?.icons?.firstOrNull(), - name = wcSession.metaData!!.name.takeIf { it.isNotBlank() } ?: "Dapp", - uri = wcSession.metaData!!.url.takeIf { it.isNotBlank() } ?: "Not provided", - data = wcSession.topic, - ) - } - return v2List - } - fun dapps(chainId: String): List { val gson = GsonHelper.customGson val dapps = MixinApplication.get().defaultSharedPreferences.getString("dapp_$chainId", null) @@ -163,24 +156,6 @@ internal constructor( } } - private fun updateTokens(chain: String, tokens: List) { - val tokenMap = if (chain == ChainType.ethereum.name) evmTokenMap else solanaTokenMap - val newTokenIds = tokens.map { "${it.chainId}${it.assetKey}" }.toSet() - - val missingTokenIds = tokenMap.keys - newTokenIds - missingTokenIds.forEach { tokenId -> - val token = tokenMap[tokenId] - if (token != null) { - tokenMap[tokenId] = token.copy(balance = "0") - } - } - - tokens.forEach { token -> - val tokenId = "${token.chainId}${token.assetKey}" - tokenMap[tokenId] = token - } - } - suspend fun fetchSessionsSuspend(ids: List) = userRepository.fetchSessionsSuspend(ids) suspend fun findBotPublicKey( @@ -197,19 +172,32 @@ internal constructor( tokenRepository.findAndCheckDepositEntry(token.chainId, token.assetId).first } - suspend fun web3TokenItems(chainIds: List) = tokenRepository.web3TokenItems(chainIds) - suspend fun getFees( id: String, destination: String, ) = tokenRepository.getFees(id, destination) + suspend fun validateExternalAddress( + assetId: String, + chain: String, + destination: String, + tag: String?, + ) = accountRepository.validateExternalAddress(assetId, chain, destination, tag) + + suspend fun findAssetIdByAssetKey(assetKey: String): String? = + tokenRepository.findAssetIdByAssetKey(assetKey) + + suspend fun getAssetPrecisionById(assetId: String): MixinResponse = + tokenRepository.getAssetPrecisionById(assetId) + + suspend fun paySuspend(request: TransferRequest) = + withContext(Dispatchers.IO) { + tokenRepository.paySuspend(request) + } + suspend fun findTokenItems(ids: List): List = tokenRepository.findTokenItems(ids) - suspend fun findWeb3TokenItems(walletId: String): List = - tokenRepository.findWeb3TokenItems(walletId) - suspend fun findTokensExtra(assetId: String) = withContext(Dispatchers.IO) { tokenRepository.findTokensExtra(assetId) @@ -319,7 +307,6 @@ internal constructor( } } - suspend fun getWeb3Tx(txhash: String) = assetRepository.getWeb3Tx(txhash) suspend fun isBlockhashValid(blockhash: String): Boolean = withContext(Dispatchers.IO) { @@ -425,18 +412,6 @@ internal constructor( return web3Repository.getAddressesByChainId(walletId, chainId) } - suspend fun getClassicWalletId(): String? = web3Repository.getClassicWalletId() - - suspend fun getTransactionsById(traceId: String) = tokenRepository.getTransactionsById(traceId) - - suspend fun findTokensByIds(walletId: String, assetIds: List): List = withContext(Dispatchers.IO) { - return@withContext web3Repository.findWeb3TokenItemsByIds(walletId, assetIds) - } - - suspend fun getRawTransactionByHashAndChain(hash: String, chainId: String) = tokenRepository.getRawTransactionByHashAndChain(hash, chainId) - - suspend fun getWalletName(walletId: String): String? = web3Repository.findWalletById(walletId)?.name - suspend fun findWalletById(walletId: String) = web3Repository.findWalletById(walletId) suspend fun getAddresses(walletId: String) = web3Repository.getAddresses(walletId) @@ -454,8 +429,6 @@ internal constructor( suspend fun getPendingTransactions(walletId: String) = tokenRepository.getPendingTransactions(walletId) - suspend fun getPendingRawTransactions(walletId: String, chainId: String) = tokenRepository.getPendingRawTransactions(walletId, chainId) - fun getPendingTransactionCount(walletId: String): LiveData = tokenRepository.getPendingTransactionCount(walletId) suspend fun transaction(hash: String, chainId: String) = tokenRepository.transaction(hash, chainId) From 1eaddd4c57074b0a1aa219e374f7b0776d6d2134 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 17 Nov 2025 14:48:51 +0800 Subject: [PATCH 8/9] Check balance --- .../android/repository/Web3Repository.kt | 2 +- .../TransferDestinationInputFragment.kt | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt index 3a6921817e..961062882a 100644 --- a/app/src/main/java/one/mixin/android/repository/Web3Repository.kt +++ b/app/src/main/java/one/mixin/android/repository/Web3Repository.kt @@ -70,7 +70,7 @@ constructor( assetKey = token.assetKey ?: "", name = token.name, symbol = token.symbol, - iconUrl = token.chainIconUrl?:"", + iconUrl = token.chainIconUrl ?: "", priceUsd = token.priceUsd, precision = token.precision, balance = "0", diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index cda614b9cc..970872778f 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -76,6 +76,7 @@ import one.mixin.android.vo.safe.TokenItem import one.mixin.android.web3.Rpc import one.mixin.android.web3.js.Web3Signer import timber.log.Timber +import java.math.BigDecimal import javax.inject.Inject @AndroidEntryPoint @@ -90,7 +91,15 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres private suspend fun handleWeb3ExternalTransfer(url: String) { Timber.d("[$TAG] handleWeb3ExternalTransfer url=%s", url) - val ext = parseExternalForWeb3(url) ?: run { + val (ext, insufficientSymbol) = parseExternalForWeb3(url) + if (insufficientSymbol != null) { + withContext(Dispatchers.Main) { + val message: String = getString(R.string.insufficient_balance_symbol, insufficientSymbol) + toast(message) + } + return + } + if (ext == null) { Timber.e("[$TAG] handleWeb3ExternalTransfer parseExternalForWeb3 returned null, url=%s", url) toast(R.string.Data_error) return @@ -141,8 +150,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } } - private suspend fun parseExternalForWeb3(url: String): ExternalTransfer? { - return parseExternalTransferUri( + private suspend fun parseExternalForWeb3(url: String): Pair { + var insufficientSymbol:String? = null + val result = parseExternalTransferUri( url, validateAddress = { assetId, chainId, destination -> web3ViewModel.validateExternalAddress(assetId, chainId, destination, null).data @@ -156,14 +166,26 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres getAssetPrecisionById = { assetId -> web3ViewModel.getAssetPrecisionById(assetId).data }, - balanceCheck = { _, _, _, _ -> - // do nothing + balanceCheck = { assetId, amount, feeAssetId, feeAmount -> + if (feeAssetId != null && feeAmount != null) { + val feeExtra = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, feeAssetId) + val feeBalance = feeExtra?.balance?.toBigDecimalOrNull() ?: BigDecimal.ZERO + if (feeBalance < feeAmount) { + insufficientSymbol = feeExtra?.symbol + } + } + val extra = web3ViewModel.findAndRefreshWeb3TokenItem(Web3Signer.currentWalletId, assetId) + val balance = extra?.balance?.toBigDecimalOrNull() ?: BigDecimal.ZERO + if (balance < amount) { + insufficientSymbol = extra?.symbol + } }, parseLighting = { ln -> val r = web3ViewModel.paySuspend(one.mixin.android.api.request.TransferRequest(assetId = one.mixin.android.Constants.ChainId.LIGHTNING_NETWORK_CHAIN_ID, rawPaymentUrl = ln)) r.data } ) + return Pair(result, insufficientSymbol) } private val token: TokenItem? by lazy { From f3dd602026a3dfaf993c6a572a58784e5aed1332 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Mon, 17 Nov 2025 15:20:44 +0800 Subject: [PATCH 9/9] Loading during parsing --- .../android/ui/address/TransferDestinationInputFragment.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt index c7a40c30d7..b55f2ef88f 100644 --- a/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/address/TransferDestinationInputFragment.kt @@ -451,7 +451,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres ) } else if (web3Token != null && address.isEthereumOrSolURLString()) { lifecycleScope.launch { + isLoading = true handleWeb3ExternalTransfer(address) + isLoading = false } } else { val memoEnabled = @@ -718,7 +720,9 @@ class TransferDestinationInputFragment() : BaseFragment(R.layout.fragment_addres } if (web3Token != null && result.isEthereumOrSolURLString()) { lifecycleScope.launch { + isLoading = true handleWeb3ExternalTransfer(result) + isLoading = false } return@let }