diff --git a/app/src/main/java/one/mixin/android/compose/theme/Theme.kt b/app/src/main/java/one/mixin/android/compose/theme/Theme.kt index fd6f02b6e4..c3b5857ac1 100644 --- a/app/src/main/java/one/mixin/android/compose/theme/Theme.kt +++ b/app/src/main/java/one/mixin/android/compose/theme/Theme.kt @@ -16,12 +16,20 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.colorspace.ColorSpaces import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode import one.mixin.android.MixinApplication import one.mixin.android.extension.isNightMode import one.mixin.android.extension.isScreenWideColorGamut import one.mixin.android.util.isCurrChinese -val isP3Supported = MixinApplication.appContext.isScreenWideColorGamut() +val isP3Supported by lazy { + if (isP3Enabled) { + MixinApplication.appContext.isScreenWideColorGamut() + } else { + false + } +} +private var isP3Enabled = true class AppColors( val primary: Color, @@ -151,9 +159,10 @@ private val LocalColors = compositionLocalOf { LightColorPalette } @Composable fun MixinAppTheme( - darkTheme: Boolean = MixinApplication.get().isNightMode(), + darkTheme: Boolean = if (LocalInspectionMode.current) false else MixinApplication.get().isNightMode(), content: @Composable () -> Unit, ) { + isP3Enabled = !LocalInspectionMode.current val colors = if (darkTheme) { DarkColorPalette diff --git a/app/src/main/java/one/mixin/android/tip/wc/internal/TipGas.kt b/app/src/main/java/one/mixin/android/tip/wc/internal/TipGas.kt index 1314ed1ee7..865b98ad88 100644 --- a/app/src/main/java/one/mixin/android/tip/wc/internal/TipGas.kt +++ b/app/src/main/java/one/mixin/android/tip/wc/internal/TipGas.kt @@ -1,5 +1,7 @@ package one.mixin.android.tip.wc.internal +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import one.mixin.android.Constants.DEFAULT_GAS_LIMIT_FOR_NONFUNGIBLE_TOKENS import one.mixin.android.api.request.web3.EstimateFeeResponse import org.web3j.protocol.core.methods.response.EthEstimateGas @@ -9,12 +11,13 @@ import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode +@Parcelize data class TipGas( val assetId: String, val gasLimit: BigInteger, val maxFeePerGas: BigInteger, val maxPriorityFeePerGas: BigInteger, -) { +) : Parcelable { fun selectMaxFeePerGas(maxFeePerGas: BigInteger): BigInteger { return this.maxFeePerGas.max(maxFeePerGas) } diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserPage.kt index 7c9da046f1..aad545567f 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserPage.kt @@ -326,16 +326,17 @@ fun BrowserPage( Box(modifier = Modifier.height(10.dp)) } val fee = tipGas?.displayValue(transaction?.maxFeePerGas) ?: solanaFee?.stripTrailingZeros() ?: BigDecimal.ZERO + val feeSymbol = asset?.symbol ?: chain.symbol if (fee == BigDecimal.ZERO) { FeeInfo( - amount = "$fee", + amount = "$fee $feeSymbol", fee = fee.multiply(asset.priceUSD()), isFree = isFeeWaived, onFreeClick = onFreeClick, ) } else { FeeInfo( - amount = "$fee ${asset?.symbol ?: ""}", + amount = "$fee $feeSymbol", fee = fee.multiply(asset.priceUSD()), gasPrice = tipGas?.displayGas(transaction?.maxFeePerGas)?.toPlainString(), isFree = isFeeWaived, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt index fbb1b92372..37fa057c62 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/BrowserWalletBottomSheetDialogFragment.kt @@ -91,6 +91,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag const val ARGS_TO_ADDRESS = "args_to_address" const val ARGS_TO_USER = "args_to_user" const val ARGS_IS_FEE_FREE = "args_is_fee_free" + const val ARGS_TIP_GAS = "args_tip_gas" fun newInstance( jsSignMessage: JsSignMessage, @@ -102,6 +103,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag toAddress: String? = null, toUser: User? = null, isFeeWaived: Boolean = false, + tipGas: TipGas? = null, ) = BrowserWalletBottomSheetDialogFragment().withArgs { putParcelable(ARGS_MESSAGE, jsSignMessage) putString( @@ -117,6 +119,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag toAddress?.let { putString(ARGS_TO_ADDRESS, it) } toUser?.let { putParcelable(ARGS_TO_USER, it) } putBoolean(ARGS_IS_FEE_FREE, isFeeWaived) + tipGas?.let { putParcelable(ARGS_TIP_GAS, it) } } } @@ -161,6 +164,7 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag super.onViewCreated(view, savedInstanceState) token = requireArguments().getParcelableCompat(ARGS_TOKEN, Web3TokenItem::class.java) amount = requireArguments().getString(ARGS_AMOUNT) + tipGas = requireArguments().getParcelableCompat(ARGS_TIP_GAS, TipGas::class.java) refreshEstimatedGasAndAsset(currentChain) } @@ -273,7 +277,8 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag .onEach { asset = viewModel.refreshAsset(assetId) try { - tipGas = withContext(Dispatchers.IO) { + if (tipGas == null) { + tipGas = withContext(Dispatchers.IO) { val r = runCatching { viewModel.estimateFee( EstimateFeeRequest( @@ -291,8 +296,9 @@ class BrowserWalletBottomSheetDialogFragment : MixinComposeBottomSheetDialogFrag ErrorHandler.handleMixinError(r?.errorCode ?: 0, r?.errorDescription ?: "") return@withContext null } - buildTipGas(chain.chainId, r.data!!) - } ?: return@onEach + buildTipGas(chain.chainId, r.data!!) + } ?: return@onEach + } insufficientGas = checkGas(token, chainToken, tipGas, transaction.value, transaction.maxFeePerGas) if (insufficientGas) { handleException(IllegalArgumentException(requireContext().getString(R.string.insufficient_gas, chainToken?.symbol ?: currentChain.symbol))) @@ -553,8 +559,9 @@ fun showBrowserBottomSheetDialogFragment( onTxhash: ((String, String) -> Unit)? = null, toUser: User? = null, isFeeWaived: Boolean = false, + tipGas: TipGas? = null, ) { - val wcBottomSheet = BrowserWalletBottomSheetDialogFragment.newInstance(signMessage, currentUrl, currentTitle, amount, token, chainToken, toAddress, toUser, isFeeWaived) + val wcBottomSheet = BrowserWalletBottomSheetDialogFragment.newInstance(signMessage, currentUrl, currentTitle, amount, token, chainToken, toAddress, toUser, isFeeWaived, tipGas) onDismiss?.let { wcBottomSheet.setOnDismiss(onDismiss) } 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 64ac28863c..f82cb02796 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 @@ -39,6 +39,7 @@ import one.mixin.android.tip.wc.SortOrder import one.mixin.android.tip.wc.WalletConnect import one.mixin.android.tip.wc.WalletConnectV2 import one.mixin.android.tip.wc.internal.Chain +import one.mixin.android.tip.wc.internal.TipGas import one.mixin.android.tip.wc.internal.buildTipGas import one.mixin.android.ui.common.biometric.NftBiometricItem import one.mixin.android.ui.common.biometric.maxUtxoCount @@ -320,6 +321,40 @@ internal constructor( } } + suspend fun calcFeeWithTipGas( + token: Web3TokenItem, + transaction: JsSignMessage, + fromAddress: String, + ): Pair { + val chain = token.getChainFromName() + if (chain == Chain.Solana) { + val tx = VersionedTransactionCompat.from(transaction.data ?: "") + val fee = tx.calcFee(fromAddress) + return Pair(fee, null) + } else { + val r = withContext(Dispatchers.IO) { + runCatching { + web3Repository.estimateFee( + EstimateFeeRequest( + token.chainId, + null, + transaction.data ?: transaction.wcEthereumTransaction?.data, + fromAddress, + transaction.wcEthereumTransaction?.to, + transaction.wcEthereumTransaction?.value, + ) + ) + }.getOrNull() + } + if (r?.isSuccess != true) return Pair(BigDecimal.ZERO, null) + return withContext(Dispatchers.IO) { + val tipGas = buildTipGas(chain.chainId, r.data!!) + val fee = tipGas.displayValue(transaction.wcEthereumTransaction?.maxFeePerGas) ?: BigDecimal.ZERO + Pair(fee, tipGas) + } + } + } + suspend fun getWeb3Tx(txhash: String) = assetRepository.getWeb3Tx(txhash) suspend fun isBlockhashValid(blockhash: String): Boolean = diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/components/Review.kt b/app/src/main/java/one/mixin/android/ui/home/web3/components/Review.kt index 7ea6cbed84..c1a6c17296 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/components/Review.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/components/Review.kt @@ -204,9 +204,10 @@ fun ParsedTxPreview( if (parsedTx == null) { BalanceChangeHead() CircularProgressIndicator( - modifier = Modifier.size(32.dp), + modifier = Modifier.size(48.dp), color = MixinAppTheme.colors.accent, ) + Box(modifier = Modifier.height(14.dp)) } else if (parsedTx.instructions?.isEmpty() == true) { BalanceChangeHead() Row( @@ -521,6 +522,13 @@ private fun SingleBalanceChangeItem( maxLines = 1, fontSize = 12.sp, ) + } else { + Text( + text = " ", //holder + color = MixinAppTheme.colors.textAssist, + maxLines = 1, + fontSize = 12.sp, + ) } } @@ -558,6 +566,13 @@ private fun BalanceChangeItem( maxLines = 1, fontSize = 12.sp, ) + } else { + Text( + text = " ", //holder + color = MixinAppTheme.colors.textAssist, + maxLines = 1, + fontSize = 12.sp, + ) } } } diff --git a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt index c587c1955b..e353307b2c 100644 --- a/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt +++ b/app/src/main/java/one/mixin/android/ui/tip/wc/sessionrequest/SessionRequestPage.kt @@ -326,16 +326,17 @@ fun SessionRequestPage( } Box(modifier = Modifier.height(20.dp)) + val feeSymbol = asset?.symbol ?: chain.symbol if (fee == BigDecimal.ZERO) { FeeInfo( - amount = "$fee", + amount = "$fee $feeSymbol", fee = fee.multiply(asset.priceUSD()), isFree = isFeeWaived, onFreeClick = onFreeClick, ) } else { FeeInfo( - amount = "$fee ${asset?.symbol}", + amount = "$fee $feeSymbol", fee = fee.multiply(asset.priceUSD()), gasPrice = tipGas?.displayGas( if (sessionRequestUI.data is WCEthereumTransaction) { diff --git a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt index f27706a991..4c7b229425 100644 --- a/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/wallet/InputFragment.kt @@ -43,6 +43,7 @@ import one.mixin.android.extension.viewDestroyed import one.mixin.android.job.MixinJobManager import one.mixin.android.job.SyncOutputJob import one.mixin.android.session.Session +import one.mixin.android.tip.wc.internal.TipGas import one.mixin.android.ui.address.ReceiveSelectionBottom.OnReceiveSelectionClicker import one.mixin.android.ui.address.TransferDestinationInputFragment import one.mixin.android.ui.common.BaseFragment @@ -521,6 +522,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC toUser = user, chainToken = chainToken, isFeeWaived = isFeeWaived, + tipGas = tipGas, onTxhash = { _, serializedTx -> }, onDismiss = { isDone-> @@ -1034,6 +1036,7 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC } private var gas: BigDecimal? = null + private var tipGas: TipGas? = null private suspend fun refreshFee(t: TokenItem) { val toAddress = toAddress?: return @@ -1220,7 +1223,9 @@ class InputFragment : BaseFragment(R.layout.fragment_input), OnReceiveSelectionC delay(3000) refreshGas(t) } else if (isAdded) { - gas = web3ViewModel.calcFee(t, transaction, fromAddress) + val feeResult = web3ViewModel.calcFeeWithTipGas(t, transaction, fromAddress) + gas = feeResult.first + tipGas = feeResult.second if (chainToken?.assetId == t.assetId) { val balance = runCatching { tokenBalance.toBigDecimalOrNull()?.subtract(gas ?: BigDecimal.ZERO)