diff --git a/app/build.gradle b/app/build.gradle index 251ffa5c62..621e4953db 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,7 +79,8 @@ dependencies { implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:develop-65' + implementation 'com.tangem:blockchain:develop-66' +// implementation 'com.tangem:blockchain:0.0.1' implementation 'com.tangem.tangem-sdk-kotlin:core:develop-139' implementation 'com.tangem.tangem-sdk-kotlin:android:develop-139' diff --git a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt b/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt new file mode 100644 index 0000000000..d78a72ead5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt @@ -0,0 +1,16 @@ +package com.tangem.tap.common.extensions.compose + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.core.graphics.alpha +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red + +/** +[REDACTED_AUTHOR] + */ +fun Color.argb(): Int { + val argb = this.toArgb() + return android.graphics.Color.argb(argb.alpha, argb.red, argb.green, argb.blue) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 55fc7f2154..326babc29c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -3,7 +3,6 @@ package com.tangem.tap.domain import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.common.services.Result -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.stripZeroPlainString @@ -42,20 +41,31 @@ class TapWalletManager { by lazy { WalletManagerFactory(blockchainSdkConfig) } suspend fun loadWalletData(walletManager: WalletManager) { - handleUpdateWalletResult(walletManager.safeUpdate(), walletManager) - } - - suspend fun updateWallet(walletManager: WalletManager) { val result = walletManager.safeUpdate() withContext(Dispatchers.Main) { when (result) { - is Result.Success -> - store.dispatch(WalletAction.UpdateWallet.Success(result.data)) - is Result.Failure -> - store.dispatch(WalletAction.UpdateWallet.Failure(result.error.localizedMessage)) + is Result.Success -> { + checkForRentWarning(walletManager) + store.dispatch(WalletAction.LoadWallet.Success(result.data)) + } + is Result.Failure -> { + when (result.error) { + is TapError.WalletManagerUpdate.NoAccountError -> { + store.dispatch(WalletAction.LoadWallet.NoAccount( + walletManager.wallet, + (result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage + )) + } + else -> { + store.dispatch(WalletAction.LoadWallet.Failure( + walletManager.wallet, + result.error.localizedMessage + )) + } + } + } } } - } suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) { @@ -65,11 +75,6 @@ class TapWalletManager { loadFiatRate(fiatCurrency, currencies) } - suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currency: Currency) { - val currencies = listOf(currency) - loadFiatRate(fiatCurrency, currencies) - } - suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List) { val results = mutableListOf?>>() currencies.forEach { @@ -79,7 +84,7 @@ class TapWalletManager { } suspend fun onCardScanned(data: ScanResponse) { - store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data.card) + store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data) updateConfigManager(data) withContext(Dispatchers.Main) { @@ -224,36 +229,6 @@ class TapWalletManager { } } - private suspend fun handleUpdateWalletResult(result: Result, walletManager: WalletManager) { - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> { - checkForRentWarning(walletManager) - store.dispatch(WalletAction.LoadWallet.Success(result.data)) - } - is Result.Failure -> { - val error = (result.error as? TapError) ?: TapError.UnknownError - when (error) { - TapError.NoInternetConnection -> { - store.dispatch(WalletAction.LoadData.Failure(error)) - } - is TapError.WalletManagerUpdate.NoAccountError -> { - store.dispatch(WalletAction.LoadWallet - .NoAccount(walletManager.wallet, error.customMessage)) - } - is TapError.WalletManagerUpdate.InternalError -> { - store.dispatch(WalletAction.LoadWallet - .Failure(walletManager.wallet, error.customMessage)) - } - else -> { - store.dispatchDebugErrorNotification(error) - } - } - } - } - } - } - private fun checkForRentWarning(walletManager: WalletManager) { val rentProvider = walletManager as? RentProvider ?: return diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt index 89130c4a16..6443b95987 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.TangemSdkError import com.tangem.operations.CommandResponse import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask @@ -22,6 +23,11 @@ class CreateWalletsTask( private val createdWalletsResponses = mutableListOf() override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + if (curves.isEmpty()){ + callback(CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())) + return + } + val curve = curves[createdWalletsResponses.size] createWallet(curve, session, callback) } diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index 34f6cc4807..e3d88e1b99 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -7,12 +7,10 @@ import android.os.Build import com.tangem.Log import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.* -import com.tangem.common.card.Card import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.domain.TapWorkarounds -import com.tangem.tap.features.feedback.EmailData.Companion.appendBlankLine -import com.tangem.tap.features.feedback.EmailData.Companion.appendDelimiter +import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.wallet.R import timber.log.Timber import java.io.File @@ -86,16 +84,19 @@ class FeedbackManager( class TangemLogCollector : TangemSdkLogger { private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS") private val logs = mutableListOf() + private val mutex = Object() override fun log(message: () -> String, level: Log.Level) { val time = dateFormatter.format(Date()) - logs.add("$time: ${message()}\n") + synchronized(mutex) { + logs.add("$time: ${message()}\n") + } } - fun getLogs(): List = logs.toList() + fun getLogs(): List = synchronized(mutex) { logs.toList() } fun clearLogs() { - logs.clear() + synchronized(mutex) { logs.clear() } } } @@ -105,8 +106,6 @@ class AdditionalEmailInfo { var address: String = "", var explorerLink: String = "", var host: String = "", - // var outputsCount: String = "" - // var transactionHex: String = "" ) var appVersion: String = "" @@ -115,6 +114,7 @@ class AdditionalEmailInfo { var cardId: String = "" var cardFirmwareVersion: String = "" var cardIssuer: String = "" + var cardBlockchain: String = "" // wallets internal val walletsInfo = mutableListOf() @@ -141,12 +141,13 @@ class AdditionalEmailInfo { } } - fun setCardInfo(card: Card) { - cardId = card.cardId - cardFirmwareVersion = card.firmwareVersion.stringValue - cardIssuer = card.issuer.name - signedHashesCount = card.wallets - .joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" } + fun setCardInfo(data: ScanResponse) { + cardId = data.card.cardId + cardBlockchain = data.walletData?.blockchain ?: "" + cardFirmwareVersion = data.card.firmwareVersion.stringValue + cardIssuer = data.card.issuer.name + signedHashesCount = data.card.wallets + .joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" } } fun setWalletsInfo(walletManagers: List) { @@ -217,33 +218,23 @@ interface EmailData { fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String { return StringBuilder().apply { append(context.getString(mainMessageResId)) - append("\n\n\n\n") + appendLine(3) append(context.getString(getDataCollectionMessageResId())) + appendLine() append(createOptionalMessage(infoHolder)) }.toString() } - - companion object { - internal fun StringBuilder.appendDelimiter() = append("----------\n") - internal fun StringBuilder.appendBlankLine() = append("\n") - } } class RateCanBeBetterEmail : EmailData { override val subjectResId: Int = R.string.feedback_subject_rate_negative override val mainMessageResId: Int = R.string.feedback_preface_rate_negative - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { - val walletInfo = infoHolder.walletsInfo[0] - return StringBuilder().apply { - appendKeyValue("Card ID", infoHolder.cardId) - appendKeyValue("Blockchain", walletInfo.blockchain.fullName) - appendBlankLine() - appendKeyValue("Phone model", infoHolder.phoneModel) - appendKeyValue("OS version", infoHolder.osVersion) - appendKeyValue("App version", infoHolder.appVersion) - }.toString() - } + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) + .appendCardInfo() + .appendLine() + .appendPhoneInfo() + .build() } class ScanFailsEmail : EmailData { @@ -251,51 +242,31 @@ class ScanFailsEmail : EmailData { override val subjectResId: Int = R.string.feedback_subject_scan_failed override val mainMessageResId: Int = R.string.feedback_preface_scan_failed - override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String { - return StringBuilder().apply { - append(context.getString(mainMessageResId)) - append("\n\n\n\n") - append(createOptionalMessage(infoHolder)) - }.toString() - } + override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String = StringBuilder().apply { + append(context.getString(mainMessageResId)) + appendLine(4) + append(createOptionalMessage(infoHolder)) + }.toString() - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { - return StringBuilder().apply { - appendBlankLine() - appendKeyValue("Phone model", infoHolder.phoneModel) - appendKeyValue("OS version", infoHolder.osVersion) - appendKeyValue("App version", infoHolder.appVersion) - }.toString() - } + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) + .appendPhoneInfo() + .build() } -class SendTransactionFailedEmail(private val error: String) : EmailData { +class SendTransactionFailedEmail( + val error: String +) : EmailData { + override val subjectResId: Int = R.string.feedback_subject_tx_failed override val mainMessageResId: Int = R.string.feedback_preface_tx_failed - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { - val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo() - return StringBuilder().apply { - appendKeyValue("Card ID", infoHolder.cardId) - appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) - appendKeyValue("Signed hashes", infoHolder.signedHashesCount) - appendDelimiter() - appendKeyValue("Blockchain", walletInfo.blockchain.fullName) - appendKeyValue("Host", walletInfo.host) - appendKeyValue("Token", infoHolder.token) - appendKeyValue("Error", error) - appendDelimiter() - appendKeyValue("Source address", walletInfo.address) - appendKeyValue("Destination address", infoHolder.destinationAddress) - appendKeyValue("Amount", infoHolder.amount) - appendKeyValue("Fee", infoHolder.fee) - appendBlankLine() - appendKeyValue("Phone model", infoHolder.phoneModel) - appendKeyValue("OS version", infoHolder.osVersion) - appendKeyValue("App version", infoHolder.appVersion) -// appendKeyValue("Transaction HEX", infoHolder.transactionHex) - }.toString() - } + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) + .appendCardInfo() + .appendDelimiter() + .appendTxFailedBlockchainInfo(error) + .appendLine() + .appendPhoneInfo() + .build() } class FeedbackEmail : EmailData { @@ -316,37 +287,88 @@ class FeedbackEmail : EmailData { isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer) } - override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String { - val builder = StringBuilder() + override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder) + .appendCardInfo() + .appendWalletsInfo() + .appendLine() + .appendPhoneInfo() + .build() +} + + +class EmailDataBuilder( + private val infoHolder: AdditionalEmailInfo +) { + val builder = StringBuilder() + + fun appendDelimiter(): EmailDataBuilder { + builder.appendDelimiter() + return this + } + + fun appendLine(count: Int = 1): EmailDataBuilder { + builder.appendLine(count) + return this + } + + fun appendCardInfo(): EmailDataBuilder { builder.appendKeyValue("Card ID", infoHolder.cardId) builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion) + builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain) builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount) + return this + } + fun appendWalletsInfo(): EmailDataBuilder { infoHolder.walletsInfo.forEach { builder.appendDelimiter() builder.appendKeyValue("Blockchain", it.blockchain.fullName) builder.appendKeyValue("Host", it.host) builder.appendKeyValue("Wallet address", it.address) builder.appendKeyValue("Explorer link", it.explorerLink) - } - builder.appendBlankLine() - infoHolder.tokens.forEach { tokens -> - builder.appendDelimiter() - builder.appendKeyValue("Blockchain", tokens.key.fullName) - builder.appendKeyValue("Tokens", tokens.value.map { "${it.name} - ${it.symbol}" }.toString()) + infoHolder.tokens[it.blockchain]?.let { tokens -> + builder.append("Tokens:") + appendLine() + tokens.forEach { token -> + builder.appendKeyValue("Name", token.name) + builder.appendKeyValue("Contract address", token.contractAddress) + } + } } + return this + } + + fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder { + val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo() + builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName) + builder.appendKeyValue("Host", walletInfo.host) + builder.appendKeyValue("Token", infoHolder.token) + builder.appendKeyValue("Error", error) builder.appendDelimiter() - builder.appendBlankLine() -// appendKeyValue("Outputs count", infoHolder.outputsCount) + builder.appendKeyValue("Source address", walletInfo.address) + builder.appendKeyValue("Destination address", infoHolder.destinationAddress) + builder.appendKeyValue("Amount", infoHolder.amount) + builder.appendKeyValue("Fee", infoHolder.fee) + return this + } + + fun appendPhoneInfo(): EmailDataBuilder { builder.appendKeyValue("Phone model", infoHolder.phoneModel) builder.appendKeyValue("OS version", infoHolder.osVersion) builder.appendKeyValue("App version", infoHolder.appVersion) - - return builder.toString() + return this } + + fun build(): String = builder.toString() } private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder { return if (value.isNotBlank()) this.append("$key: $value\n") else this +} + +private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") + +private fun StringBuilder.appendLine(count: Int = 1): StringBuilder { + return append(List(count) { "\n" }.joinToString(separator = "")) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt index b12be36802..8d2577fc01 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt @@ -1,5 +1,11 @@ package com.tangem.tap.features.home.compose +import android.content.Context +import android.graphics.Typeface +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.TextView import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material.Text @@ -14,6 +20,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import com.tangem.tangem_sdk_new.extensions.dpToPx +import com.tangem.tap.common.extensions.compose.argb import com.tangem.wallet.R @Composable @@ -22,8 +31,9 @@ fun StoriesGeneralContent( subtitleText: String, imageSource: Int?, isDarkBackground: Boolean, + subtitleTextId: Int? = null, imageComposable: (() -> Unit)? = null, - ) { +) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, @@ -45,15 +55,7 @@ fun StoriesGeneralContent( Spacer(modifier = Modifier.size(16.dp)) - Text( - text = subtitleText, - fontSize = 20.sp, - fontWeight = FontWeight.Normal, - modifier = Modifier - .padding(start = 40.dp, end = 40.dp), - color = Color(0xFFA6AAAD), - textAlign = TextAlign.Center - ) + SubtitleText(subtitleText, subtitleTextId) Spacer(modifier = Modifier.size(25.dp)) @@ -61,7 +63,7 @@ fun StoriesGeneralContent( Image( painter = painterResource(id = imageSource), contentDescription = null, - contentScale = if(isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, + contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth, modifier = Modifier.fillMaxWidth() ) } @@ -69,6 +71,34 @@ fun StoriesGeneralContent( } } +@Composable +fun SubtitleText(subtitleText: String, subtitleTextId: Int?) { + val color = Color(0xFFA6AAAD) + + if (subtitleTextId == null) { + Text( + text = subtitleText, + fontSize = 20.sp, + fontWeight = FontWeight.Normal, + modifier = Modifier.padding(start = 40.dp, end = 40.dp), + color = color, + textAlign = TextAlign.Center + ) + } else { + HtmlText(subtitleTextId) { context -> + val padding = context.dpToPx(40f).toInt() + TextView(context).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + setPadding(padding, 0, padding, 0) + textAlignment = View.TEXT_ALIGNMENT_CENTER + typeface = Typeface.DEFAULT + setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f) + setTextColor(color.argb()) + } + } + } +} + @Composable fun StoriesRevolutionaryWallet() { StoriesGeneralContent( @@ -85,6 +115,7 @@ fun StoriesUltraSecureBackup() { titleText = stringResource(id = R.string.story_backup_title), subtitleText = stringResource(id = R.string.story_backup_description), imageSource = R.drawable.floating_cards, + subtitleTextId = R.string.story_backup_description, isDarkBackground = false ) } @@ -117,4 +148,13 @@ fun StoriesWalletForEveryone() { imageSource = R.drawable.wallet_for_everyone, isDarkBackground = true ) +} + +@Composable +fun HtmlText( + stringResId: Int, + modifier: Modifier = Modifier, + factory: (Context) -> TextView +) { + AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index f103fc3817..74aadc12bb 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -202,11 +202,11 @@ private fun sendTransaction( } scope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) { - dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain)) + dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain)) } delay(10000) withContext(Dispatchers.Main) { - dispatch(WalletAction.UpdateWallet(walletManager.wallet.blockchain)) + dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain)) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 57a03d4313..d7c9c56390 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -74,14 +74,8 @@ sealed class WalletAction : Action { class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings() } - data class UpdateWallet(val blockchain: Blockchain? = null) : WalletAction() { - object ScheduleUpdatingWallet : WalletAction() - data class Success(val wallet: Wallet) : WalletAction() - data class Failure(val errorMessage: String? = null) : WalletAction() - } - data class LoadFiatRate( - val wallet: Wallet? = null, val currency: Currency? = null, + val wallet: Wallet? = null, val currencyList: List? = null, ) : WalletAction() { data class Success(val fiatRate: Pair) : WalletAction() object Failure : WalletAction() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 92491013ad..7a5db9c286 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.extensions.isAboveZero +import com.tangem.common.extensions.isZero import com.tangem.tap.common.entities.Button import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.StateDialog @@ -32,11 +33,11 @@ data class WalletState( val hashesCountVerified: Boolean? = null, val walletDialog: StateDialog? = null, val mainWarningsList: List = mutableListOf(), - val wallets: List = emptyList(), + val walletsData: List = emptyList(), val walletManagers: List = emptyList(), val isMultiwalletAllowed: Boolean = false, val cardCurrency: CryptoCurrencyName? = null, - val selectedWallet: Currency? = null, + val selectedCurrency: Currency? = null, val primaryBlockchain: Blockchain? = null, val primaryToken: Token? = null, val isTestnet: Boolean = false, @@ -51,7 +52,7 @@ data class WalletState( val isTangemTwins: Boolean get() = store.state.globalState.scanResponse?.isTangemTwins() == true - val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null + val primaryWallet = if (walletsData.isNotEmpty()) walletsData[0] else null val shouldShowDetails: Boolean = primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard && @@ -61,7 +62,7 @@ data class WalletState( get() = walletManagers.map { it.wallet.blockchain } val currencies: List - get() = wallets.mapNotNull { it.currency } + get() = walletsData.mapNotNull { it.currency } fun getWalletManager(token: Token?): WalletManager? { if (token == null) return null @@ -79,21 +80,21 @@ data class WalletState( fun getWalletData(currency: Currency?): WalletData? { if (currency == null) return null - return wallets.find { it.currency == currency } + return walletsData.find { it.currency == currency } } fun getWalletData(blockchain: Blockchain?): WalletData? { if (blockchain == null) return null - return wallets.find { (it.currency as? Currency.Blockchain)?.blockchain == blockchain } + return walletsData.find { (it.currency as? Currency.Blockchain)?.blockchain == blockchain } } fun getWalletData(token: Token?): WalletData? { if (token == null) return null - return wallets.find { (it.currency as? Currency.Token)?.token == token } + return walletsData.find { (it.currency as? Currency.Token)?.token == token } } fun getSelectedWalletData(): WalletData? { - return wallets.find { it.currency == selectedWallet } + return walletsData.find { it.currency == selectedCurrency } } fun canBeRemoved(walletData: WalletData?): Boolean { @@ -132,9 +133,9 @@ data class WalletState( } fun replaceWalletInWallets(walletData: WalletData?): List { - if (walletData == null) return wallets + if (walletData == null) return walletsData var changed = false - val updatedWallets = wallets.map { + val updatedWallets = walletsData.map { if (it.currency == walletData.currency) { changed = true walletData @@ -142,12 +143,12 @@ data class WalletState( it } } - return if (changed) updatedWallets else wallets + walletData + return if (changed) updatedWallets else walletsData + walletData } fun replaceSomeWallets(newWallets: List): List { val remainingWallets: MutableList = newWallets.toMutableList() - val updatedWallets = wallets.map { wallet -> + val updatedWallets = walletsData.map { wallet -> val newWallet = newWallets .firstOrNull { wallet.currency == it.currency } if (newWallet == null) { @@ -248,17 +249,26 @@ data class WalletData( val currencyData: BalanceWidgetData = BalanceWidgetData(), val updatingWallet: Boolean = false, val tradeCryptoState: TradeCryptoState = TradeCryptoState(), - val allowToSend: Boolean = true, val fiatRateString: String? = null, val fiatRate: BigDecimal? = null, val mainButton: WalletMainButton = WalletMainButton.SendButton(false), val currency: Currency, - val walletRent: WalletRent? = null, + val warningRent: WalletRent? = null, ) { fun shouldShowMultipleAddress(): Boolean { val listOfAddresses = walletAddresses?.list ?: return false return listOfAddresses.size > 1 } + + fun shouldShowCoinAmountWarning(): Boolean = when (currency) { + is Currency.Blockchain -> false + is Currency.Token -> blockchainAmountIsEmpty() && !tokenAmountIsEmpty() + } + + fun shouldEnableTokenSendButton(): Boolean = !blockchainAmountIsEmpty() || !tokenAmountIsEmpty() + + private fun blockchainAmountIsEmpty(): Boolean = currencyData.blockchainAmount?.isZero() ?: false + private fun tokenAmountIsEmpty(): Boolean = currencyData.amount?.isZero() == true } data class WalletRent( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 19197ad893..ade8ab52d7 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen @@ -47,7 +46,7 @@ class MultiWalletMiddleware { globalState.scanResponse?.card?.cardId?.let { currenciesRepository.saveAddedToken(it, action.token) } - addToken(action.token, walletState, globalState) + addTokens(listOf(action.token), walletState, globalState) } is WalletAction.MultiWallet.AddTokens -> { addTokens(action.tokens, walletState, globalState) @@ -62,7 +61,9 @@ class MultiWalletMiddleware { } } } - store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain))) + store.dispatch(WalletAction.LoadFiatRate( + currencyList = listOf(Currency.Blockchain(action.blockchain))) + ) store.dispatch(WalletAction.LoadWallet(action.blockchain) ) } @@ -172,80 +173,39 @@ class MultiWalletMiddleware { } } - private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) { + private fun addTokens(tokens: List, walletState: WalletState?, globalState: GlobalState?) { val scanResponse = globalState?.scanResponse ?: return + val wmFactory = globalState.tapWalletManager.walletManagerFactory - val walletManager = walletState?.getWalletManager(token) - ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( - scanResponse, - token.blockchain - )?.also { walletManager -> - store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) - store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) - } - - store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Token(token))) - - scope.launch { - when (val result = walletManager?.addToken(token)) { - is Result.Success -> { - store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, token)) + val groupedTokens = tokens.groupBy { it.blockchain } + val walletManagers = groupedTokens.mapNotNull { entry -> + val blockchain = entry.key + val tokensList = entry.value + val walletManager = walletState?.getWalletManager(blockchain) + ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchain)?.also { + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it)) + store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain)) } - is Result.Failure -> { - when (val result = walletManager.safeUpdate()) { - is com.tangem.common.services.Result.Success -> { - val tokenAmount = result.data.getTokenAmount(token) ?: return@launch - store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, token)) - } - is com.tangem.common.services.Result.Failure -> { - } + store.dispatch(WalletAction.LoadFiatRate(currencyList = tokensList.map { Currency.Token(it) })) + walletManager?.apply { addTokens(tokensList) } + } + scope.launch { + walletManagers.forEach { walletManager -> + when (val result = walletManager.safeUpdate()) { + is com.tangem.common.services.Result.Success -> { + val wallet = result.data + wallet.getTokens() + .filter { tokens.contains(it) } + .mapNotNull { token -> wallet.getTokenAmount(token)?.let { Pair(token, it) } } + .forEach { + withContext(Dispatchers.Main) { + store.dispatch(WalletAction.MultiWallet.TokenLoaded(it.second, it.first)) + } + } } + is com.tangem.common.services.Result.Failure -> {} } } } } - - private fun addTokens( - tokens: List, - walletState: WalletState?, - globalState: GlobalState?, - ) { - val scanResponse = globalState?.scanResponse ?: return - - val tokensWithManagers = tokens.map { token -> - val walletManager = walletState?.getWalletManager(token) - ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( - scanResponse, - token.blockchain - )?.also { walletManager -> - store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) - store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) - } - store.dispatch( - WalletAction.LoadFiatRate(currency = Currency.Token(token)) - ) - TokenWithManager(token, walletManager) - } - scope.launch { - tokensWithManagers.forEach { - when (val result = it.walletManager?.addToken(it.token)) { - is Result.Success -> { - store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, it.token)) - } - is Result.Failure -> { - when (val result = it.walletManager.safeUpdate()) { - is com.tangem.common.services.Result.Success -> { - val tokenAmount = result.data.getTokenAmount(it.token) ?: return@launch - store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, it.token)) - } - is com.tangem.common.services.Result.Failure -> { - } - } - } - } - } - } - } - - private data class TokenWithManager(val token: Token, val walletManager: WalletManager?) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 379008a18b..b009981dfb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -24,7 +24,10 @@ import com.tangem.tap.domain.extensions.toSendableAmounts import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.send.redux.PrepareSendScreen -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.Currency +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.network.NetworkStateChanged import com.tangem.tap.scope import com.tangem.tap.store @@ -63,9 +66,9 @@ class WalletMiddleware { is WalletAction.LoadWallet -> { scope.launch { if (action.blockchain == null) { - walletState.walletManagers.map { walletManager -> - async { globalState.tapWalletManager.loadWalletData(walletManager) } - }.awaitAll() + walletState.walletManagers.map { walletManager -> + async { globalState.tapWalletManager.loadWalletData(walletManager) } + }.awaitAll() } else { val walletManager = walletState.getWalletManager(action.blockchain) walletManager?.let { globalState.tapWalletManager.loadWalletData(it) } @@ -84,23 +87,19 @@ class WalletMiddleware { warningsMiddleware.tryToShowAppRatingWarning(action.wallet) } is WalletAction.LoadFiatRate -> { + val tapWalletManager = globalState.tapWalletManager + val fiatAppCurrency = globalState.appCurrency scope.launch { when { action.wallet != null -> { - globalState.tapWalletManager.loadFiatRate( - globalState.appCurrency, action.wallet - ) + tapWalletManager.loadFiatRate(fiatAppCurrency, action.wallet) } - action.currency != null -> { - globalState.tapWalletManager.loadFiatRate( - globalState.appCurrency, action.currency - ) + action.currencyList != null -> { + tapWalletManager.loadFiatRate(fiatAppCurrency, action.currencyList) } else -> { - globalState.tapWalletManager.loadFiatRate( - fiatCurrency = globalState.appCurrency, - currencies = walletState.wallets.mapNotNull { it.currency } - ) + val currencyList = walletState.walletsData.map { it.currency } + tapWalletManager.loadFiatRate(fiatAppCurrency, currencyList) } } } @@ -127,22 +126,6 @@ class WalletMiddleware { } } } - is WalletAction.UpdateWallet -> { - if (action.blockchain != null) { - scope.launch { - val walletManager = walletState.getWalletManager(action.blockchain) - walletManager?.let { globalState.tapWalletManager.updateWallet(it) } - } - } else { - scope.launch { - if (walletState.state == ProgressState.Done) { - walletState.walletManagers.map { walletManager -> - globalState.tapWalletManager.updateWallet(walletManager) - } - } - } - } - } is WalletAction.Scan -> { store.dispatch(HomeAction.ShouldScanCardOnResume(true)) store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) @@ -170,7 +153,7 @@ class WalletMiddleware { is WalletAction.LoadData -> { scope.launch { val scanNoteResponse = globalState.scanResponse ?: return@launch - if (!walletState.wallets.isEmpty()) { + if (walletState.walletsData.isNotEmpty()) { globalState.tapWalletManager.reloadData(scanNoteResponse) } else { globalState.tapWalletManager.loadData(scanNoteResponse) @@ -203,7 +186,7 @@ class WalletMiddleware { } } is WalletAction.ShowDialog.QrCode -> { - val selectedWalletData = walletState.getWalletData(walletState.selectedWallet) ?: return + val selectedWalletData = walletState.getWalletData(walletState.selectedCurrency) ?: return val selectedAddressData = selectedWalletData.walletAddresses?.selectedAddress ?: return val currency = selectedWalletData.currency @@ -230,12 +213,12 @@ class WalletMiddleware { when (currency) { is Currency.Blockchain -> { val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency } - ?: return WalletAction.Send.ChooseCurrency(amounts) + ?: return WalletAction.Send.ChooseCurrency(amounts) PrepareSendScreen(amountToSend, selectedWalletData.fiatRate, walletManager) } is Currency.Token -> { val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol } - ?: return WalletAction.Send.ChooseCurrency(amounts) + ?: return WalletAction.Send.ChooseCurrency(amounts) prepareSendActionForToken(amountToSend, state, selectedWalletData, wallet, walletManager) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index e0f1fec69b..e64d5a82cf 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -21,7 +21,7 @@ class MultiWalletReducer { state.addWalletManagers(action.walletManagers) } is WalletAction.MultiWallet.AddBlockchains -> { - val wallets = action.blockchains.map { blockchain -> + val walletsData = action.blockchains.map { blockchain -> val wallet = state.getWalletManager(blockchain)?.wallet val cardToken = if (!state.isMultiwalletAllowed) { wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) } @@ -29,41 +29,41 @@ class MultiWalletReducer { null } WalletData( - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - blockchain.fullName, - currencySymbol = blockchain.currency, - token = cardToken - ), - walletAddresses = createAddressList(wallet), - mainButton = WalletMainButton.SendButton(false), - currency = Currency.Blockchain(blockchain), + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + blockchain.fullName, + currencySymbol = blockchain.currency, + token = cardToken + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + currency = Currency.Blockchain(blockchain), ) } - val selectedWallet = if (!state.isMultiwalletAllowed) { - wallets[0].currency + val selectedCurrency = if (!state.isMultiwalletAllowed) { + walletsData[0].currency } else { - state.selectedWallet + state.selectedCurrency } state.copy( - wallets = wallets, - selectedWallet = selectedWallet + walletsData = walletsData, + selectedCurrency = selectedCurrency ) } is WalletAction.MultiWallet.AddBlockchain -> { val wallet = state.getWalletManager(action.blockchain)?.wallet val walletData = WalletData( - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - action.blockchain.fullName, - currencySymbol = action.blockchain.currency, - ), - walletAddresses = createAddressList(wallet), - mainButton = WalletMainButton.SendButton(false), - currency = Currency.Blockchain(action.blockchain), + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + action.blockchain.fullName, + currencySymbol = action.blockchain.currency, + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + currency = Currency.Blockchain(action.blockchain), ) - val newState = state.copy(wallets = state.replaceWalletInWallets(walletData)) + val newState = state.copy(walletsData = state.replaceWalletInWallets(walletData)) if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) { OnWalletLoadedReducer().reduce(wallet, newState) } else { @@ -72,21 +72,21 @@ class MultiWalletReducer { } is WalletAction.MultiWallet.AddTokens -> { val wallets = action.tokens.mapNotNull { token -> token.toWallet(state) } - state.copy(wallets = state.replaceSomeWallets(wallets)) + state.copy(walletsData = state.replaceSomeWallets(wallets)) } is WalletAction.MultiWallet.AddToken -> { val walletData = action.token.toWallet(state) ?: return state - state.copy(wallets = state.replaceWalletInWallets(walletData)) + state.copy(walletsData = state.replaceWalletInWallets(walletData)) } is WalletAction.MultiWallet.TokenLoaded -> { val pendingTransactions = state.getWalletManager(action.token) - ?.wallet?.let { wallet -> - wallet.recentTransactions.toPendingTransactions(wallet.address) - } ?: emptyList() + ?.wallet?.let { wallet -> + wallet.recentTransactions.toPendingTransactions(wallet.address) + } ?: emptyList() val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty() val tokenPendingTransactions = pendingTransactions - .filter { it.currency == action.amount.currencySymbol } + .filter { it.currency == action.amount.currencySymbol } val tokenBalanceStatus = when { tokenPendingTransactions.isNotEmpty() -> BalanceStatus.TransactionInProgress pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress @@ -94,42 +94,43 @@ class MultiWalletReducer { } val tokenWalletData = state.getWalletData(action.token) val newTokenWalletData = tokenWalletData?.copy( - currencyData = tokenWalletData.currencyData.copy( - status = tokenBalanceStatus, - amount = action.amount.value?.toFormattedCurrencyString( - action.amount.decimals, action.amount.currencySymbol - ), - fiatAmountFormatted = tokenWalletData.fiatRate?.let { - action.amount.value - ?.toFiatString(it, store.state.globalState.appCurrency) - } + currencyData = tokenWalletData.currencyData.copy( + status = tokenBalanceStatus, + amount = action.amount.value, + amountFormatted = action.amount.value?.toFormattedCurrencyString( + action.amount.decimals, action.amount.currencySymbol ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled), - currency = Currency.Token(action.token) + fiatAmountFormatted = tokenWalletData.fiatRate?.let { + action.amount.value + ?.toFiatString(it, store.state.globalState.appCurrency) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled), + currency = Currency.Token(action.token) ) val wallets = state.replaceWalletInWallets(newTokenWalletData) - state.copy(wallets = wallets) + state.copy(walletsData = wallets) } is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) is WalletAction.MultiWallet.SelectWallet -> - state.copy(selectedWallet = action.walletData?.currency) + state.copy(selectedCurrency = action.walletData?.currency) is WalletAction.MultiWallet.RemoveWallet -> { - val wallets = state.wallets.filterNot { + val wallets = state.walletsData.filterNot { it.currency == action.walletData.currency } if (action.walletData.currency is Currency.Blockchain) { state.copy( - wallets = wallets, + walletsData = wallets, walletManagers = state.walletManagers.filterNot { it.wallet.blockchain == action.walletData.currency.blockchain } ) } else { - state.copy(wallets = wallets) + state.copy(walletsData = wallets) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index c28bea2b5b..caf41a9386 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -36,11 +36,11 @@ class OnWalletLoadedReducer { return walletState } val formattedAmount = coinAmountValue?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency) + wallet.blockchain.decimals(), + wallet.blockchain.currency) val pendingTransactions = wallet.recentTransactions - .toPendingTransactions(wallet.address) + .toPendingTransactions(wallet.address) val coinSendButton = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty() val balanceStatus = if (pendingTransactions.isNotEmpty()) { @@ -52,17 +52,19 @@ class OnWalletLoadedReducer { val fiatAmount = walletData?.fiatRate?.let { coinAmountValue?.toFiatValue(it) } val newWalletData = walletData?.copy( - currencyData = walletData.currencyData.copy( - status = balanceStatus, currency = wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - amount = formattedAmount, - fiatAmount = fiatAmount, - fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencySymbol) - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(coinSendButton), - currency = Currency.Blockchain(wallet.blockchain), - tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData) + currencyData = walletData.currencyData.copy( + status = balanceStatus, currency = wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + blockchainAmount = coinAmountValue, + amount = coinAmountValue, + amountFormatted = formattedAmount, + fiatAmount = fiatAmount, + fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencySymbol) + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(coinSendButton), + currency = Currency.Blockchain(wallet.blockchain), + tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData), ) val tokens = wallet.getTokens().mapNotNull { token -> @@ -73,20 +75,23 @@ class OnWalletLoadedReducer { pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress else -> BalanceStatus.VerifiedOnline } - val tokenAmountValue = wallet.getTokenAmount(token)?.value - val tokenFiatAmount = tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate)} + val tokenAmountValue = wallet.getTokenAmount(token)?.value + val tokenFiatAmount = tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate) } - val tokenSendButton = tokenAmountValue?.isZero() == false && tokenPendingTransactions.isEmpty() + val tokenSendButton = newWalletData?.shouldEnableTokenSendButton() == true + && tokenPendingTransactions.isEmpty() tokenWalletData?.copy( - currencyData = tokenWalletData.currencyData.copy( - status = tokenBalanceStatus, - amount = tokenAmountValue?.toFormattedCurrencyString(token.decimals, token.symbol), - fiatAmount = tokenFiatAmount, - fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrencySymbol) - ), - pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(tokenSendButton), - tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData) + currencyData = tokenWalletData.currencyData.copy( + status = tokenBalanceStatus, + blockchainAmount = coinAmountValue, + amount = tokenAmountValue, + amountFormatted = tokenAmountValue?.toFormattedCurrencyString(token.decimals, token.symbol), + fiatAmount = tokenFiatAmount, + fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrencySymbol) + ), + pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(tokenSendButton), + tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData), ) } val newWallets = (tokens + newWalletData).mapNotNull { it } @@ -98,7 +103,7 @@ class OnWalletLoadedReducer { ProgressState.Done } return walletState.copy( - state = state, wallets = wallets, error = null + state = state, walletsData = wallets, error = null ) } @@ -128,15 +133,13 @@ class OnWalletLoadedReducer { } val amount = wallet.amounts[AmountType.Coin]?.value val formattedAmount = amount?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency) + wallet.blockchain.decimals(), + wallet.blockchain.currency) val fiatRate = walletState.primaryWallet?.fiatRate val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN) val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } - val pendingTransactions = wallet.recentTransactions - .toPendingTransactions(wallet.address) - + val pendingTransactions = wallet.recentTransactions.toPendingTransactions(wallet.address) val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() val balanceStatus = if (pendingTransactions.isNotEmpty()) { BalanceStatus.TransactionInProgress @@ -144,21 +147,23 @@ class OnWalletLoadedReducer { BalanceStatus.VerifiedOnline } val walletData = walletState.primaryWallet?.copy( - currencyData = BalanceWidgetData( - balanceStatus, wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - formattedAmount, - token = tokenData, - fiatAmountFormatted = fiatAmount, - fiatAmount = fiatAmountRaw - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled), - tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet) + currencyData = BalanceWidgetData( + balanceStatus, wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + token = tokenData, + blockchainAmount = amount, + amount = amount, + amountFormatted = formattedAmount, + fiatAmountFormatted = fiatAmount, + fiatAmount = fiatAmountRaw + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled), + tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet), ) val wallets = walletData?.let { listOf(walletData) } ?: emptyList() return walletState.copy( - state = ProgressState.Done, wallets = wallets, error = null + state = ProgressState.Done, walletsData = wallets, error = null ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index af75afcb1b..ec2ca28027 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -46,7 +46,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { is WalletAction.EmptyWallet -> { newState = newState.copy( state = ProgressState.Done, - wallets = listOf( + walletsData = listOf( WalletData( currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), mainButton = WalletMainButton.CreateWalletButton(true), @@ -58,7 +58,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { is WalletAction.LoadData.Failure -> { when (action.error) { is TapError.NoInternetConnection -> { - val wallets = newState.wallets + val wallets = newState.walletsData .map { it.copy( currencyData = it.currencyData.copy( @@ -69,13 +69,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState { newState = newState.copy( state = ProgressState.Error, error = ErrorType.NoInternetConnection, - wallets = wallets + walletsData = wallets ) } is TapError.UnknownBlockchain -> { newState = newState.copy( state = ProgressState.Done, - wallets = listOf( + walletsData = listOf( WalletData( currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), currency = Currency.Blockchain(Blockchain.Unknown) @@ -94,7 +94,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } is WalletAction.LoadWallet -> { if (action.blockchain == null) { - val wallets = newState.wallets.map { wallet -> + val wallets = newState.walletsData.map { wallet -> wallet.copy( currencyData = wallet.currencyData.copy( @@ -108,14 +108,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy( state = ProgressState.Loading, - wallets = wallets, + walletsData = wallets, ) } else { val walletManager = newState.getWalletManager(action.blockchain) ?: return newState val blockchain = walletManager.wallet.blockchain val currencies = listOf(Currency.Blockchain(blockchain)) + walletManager.cardTokens.map { Currency.Token(it) } - val newWallets = newState.wallets.filter { currencies.contains(it.currency) } + val newWallets = newState.walletsData.filter { currencies.contains(it.currency) } .map { wallet -> wallet.copy( currencyData = wallet.currencyData.copy( @@ -128,14 +128,10 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) } val wallets = newState.replaceSomeWallets(newWallets) - newState = newState.copy(wallets = newState.updateTradeCryptoState(exchangeManager, wallets)) + newState = newState.copy(walletsData = newState.updateTradeCryptoState(exchangeManager, wallets)) } } - is WalletAction.LoadWallet.Success -> newState = - onWalletLoadedReducer.reduce(action.wallet, newState) - is WalletAction.UpdateWallet.Success -> { - newState = onWalletLoadedReducer.reduce(action.wallet, newState) - } + is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState) is WalletAction.LoadWallet.NoAccount -> { val walletData = newState.getWalletData(action.wallet.blockchain)?.copy( currencyData = BalanceWidgetData( @@ -153,7 +149,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy( state = progressState, - wallets = newState.updateTradeCryptoState(exchangeManager, wallets) + walletsData = newState.updateTradeCryptoState(exchangeManager, wallets) ) } is WalletAction.LoadWallet.Failure -> { @@ -188,7 +184,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } newState = newState.copy( state = progressState, - wallets = newState.updateTradeCryptoState(exchangeManager, wallets) + walletsData = newState.updateTradeCryptoState(exchangeManager, wallets) ) } is WalletAction.SetArtworkId -> { @@ -228,7 +224,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState { is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) is WalletAction.TradeCryptoAction -> return newState is WalletAction.ChangeSelectedAddress -> { - val selectedWalletData = newState.getWalletData(newState.selectedWallet) + val selectedWalletData = newState.getWalletData(newState.selectedCurrency) val walletAddresses = newState.getWalletData(selectedWalletData?.currency)?.walletAddresses @@ -243,16 +239,16 @@ private fun internalReduce(action: Action, state: AppState): WalletState { ) ) ) - newState = newState.copy(wallets = wallets) + newState = newState.copy(walletsData = wallets) } is WalletAction.SetWalletRent -> { var walletData = newState.getWalletData(action.blockchain) if (walletData == null) { newState } else { - walletData = walletData.copy(walletRent = WalletRent(action.minRent, action.rentExempt)) + walletData = walletData.copy(warningRent = WalletRent(action.minRent, action.rentExempt)) newState = newState.copy( - wallets = newState.replaceSomeWallets(listOf(walletData)) + walletsData = newState.replaceSomeWallets(listOf(walletData)) ) } } @@ -343,7 +339,7 @@ private fun setMultiWalletFiatRate( ), fiatRate = rate, fiatRateString = rateFormatted ) - return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + return state.copy(walletsData = state.replaceWalletInWallets(newWalletData)) } private fun setSingeWalletFiatRate( @@ -361,7 +357,7 @@ private fun setSingeWalletFiatRate( fiatRate = rate, fiatRateString = rateFormatted ) - return state.copy(wallets = listOf(walletData)) + return state.copy(walletsData = listOf(walletData)) } else if (currency is Currency.Token && currency.token == token) { val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) val tokenData = state.primaryWallet?.currencyData?.token?.copy( @@ -376,7 +372,7 @@ private fun setSingeWalletFiatRate( ) ) val wallets = walletData?.let { listOf(walletData) } ?: emptyList() - return state.copy(wallets = wallets) + return state.copy(walletsData = wallets) } return state } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 42527e8528..6f666e8601 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -22,7 +22,9 @@ data class BalanceWidgetData( val status: BalanceStatus? = null, val currency: String? = null, val currencySymbol: String? = null, - val amount: String? = null, + val blockchainAmount: BigDecimal? = BigDecimal.ZERO, + val amount: BigDecimal? = null, + val amountFormatted: String? = null, val fiatAmountFormatted: String? = null, val fiatAmount: BigDecimal? = null, val token: TokenData? = null, @@ -149,7 +151,7 @@ class BalanceWidget( tvCurrency.text = data.token?.tokenSymbol tvBaseCurrency.text = data.currency tvAmount.text = if (showAmount) data.token?.amount else "" - tvBaseAmount.text = if (showAmount) data.amount else "" + tvBaseAmount.text = if (showAmount) data.amountFormatted else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = data.token?.fiatAmount @@ -160,7 +162,7 @@ class BalanceWidget( with(binding.lBalance) { groupBaseCurrency.hide() tvCurrency.text = data.currency - tvAmount.text = if (showAmount) data.amount else "" + tvAmount.text = if (showAmount) data.amountFormatted else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = data.fiatAmountFormatted diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 57d4b733de..6342b8f813 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -94,7 +94,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), override fun newState(state: WalletState) { if (activity == null || view == null) return - if (state.selectedWallet == null) return + if (state.selectedCurrency == null) return val selectedWallet = state.getSelectedWalletData() ?: return @@ -107,8 +107,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), handleDialogs(state.walletDialog) handleCurrencyIcon(selectedWallet) - handleWalletRent(selectedWallet.walletRent) - + handleWalletRent(selectedWallet.warningRent) + handleNotEnoughFundsOnMainCurrency(selectedWallet) binding.srlWalletDetails.setOnRefreshListener { if (selectedWallet.currencyData.status != BalanceStatus.Loading) { @@ -153,14 +153,27 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), private fun handleWalletRent(rent: WalletRent?) = with(binding) { val rent = rent.guard { - lRentWarning.root.hide() + lWarning.root.hide() return } val warningMessage = requireContext().getString( R.string.solana_rent_warning, rent.minRentValue, rent.rentExemptValue ) - lRentWarning.tvRentWarningMessage.text = warningMessage - lRentWarning.root.show() + lWarning.tvWarningMessage.text = warningMessage + lWarning.root.show() + } + + private fun handleNotEnoughFundsOnMainCurrency(selectedWalletData: WalletData) = with(binding) { + if (selectedWalletData.shouldShowCoinAmountWarning()) { + val blockchainName = selectedWalletData.currency.blockchain.fullName + val warningMessage = requireContext().getString( + R.string.token_details_send_blocked_fee_format, blockchainName, blockchainName + ) + lWarning.tvWarningMessage.text = warningMessage + lWarning.root.show() + } else { + lWarning.root.hide() + } } private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) { @@ -234,7 +247,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = data.amount + lBalance.tvAmount.text = data.amountFormatted lBalance.tvFiatAmount.text = data.fiatAmountFormatted lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading) } @@ -244,7 +257,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = data.amount + lBalance.tvAmount.text = data.amountFormatted lBalance.tvFiatAmount.text = data.fiatAmountFormatted when (data.status) { BalanceStatus.VerifiedOnline, BalanceStatus.SameCurrencyTransactionInProgress -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 9f6a876537..7aeb852988 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -63,7 +63,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber hideWarning() BalanceStatus.Loading -> { hideWarning() - if (wallet.currencyData.amount == null) { + if (wallet.currencyData.amountFormatted == null) { tvExchangeRate.text = root.getString(R.string.wallet_balance_loading) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 7b2e9ba9ec..fc1233af3a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -94,11 +94,11 @@ class MultiWalletView : WalletView { val fragment = fragment ?: return val binding = binding ?: return - walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken) + walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken) binding.btnAddToken.setOnClickListener { store.dispatch(TokensAction.LoadCurrencies) - store.dispatch(TokensAction.SetAddedCurrencies(state.wallets)) + store.dispatch(TokensAction.SetAddedCurrencies(state.walletsData)) store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml index 0f6ef2b42d..a2d946a3ca 100644 --- a/app/src/main/res/layout/fragment_wallet_details.xml +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -99,8 +99,8 @@ app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" /> diff --git a/app/src/main/res/layout/layout_wallet_details_rent.xml b/app/src/main/res/layout/layout_wallet_details_warning.xml similarity index 91% rename from app/src/main/res/layout/layout_wallet_details_rent.xml rename to app/src/main/res/layout/layout_wallet_details_warning.xml index 1f44d10ea7..43eeed159a 100644 --- a/app/src/main/res/layout/layout_wallet_details_rent.xml +++ b/app/src/main/res/layout/layout_wallet_details_warning.xml @@ -13,7 +13,7 @@ android:layout_height="wrap_content"> diff --git a/app/src/main/res/values-de/strings_final.xml b/app/src/main/res/values-de/strings_final.xml index fa5d2dc4b3..4a431cc050 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -5,7 +5,7 @@ 2 cards Shipping Free - I have a promo code… + I have a promo code... Total Other payment methods Buy now @@ -23,16 +23,17 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to 3 physical cards to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - Order card + Order Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode + Not enough funds for fee on your %s wallet to send a transaction. Top up your %s wallet first. \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings_final.xml b/app/src/main/res/values-fr/strings_final.xml index fa5d2dc4b3..4a431cc050 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -5,7 +5,7 @@ 2 cards Shipping Free - I have a promo code… + I have a promo code... Total Other payment methods Buy now @@ -23,16 +23,17 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to 3 physical cards to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - Order card + Order Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode + Not enough funds for fee on your %s wallet to send a transaction. Top up your %s wallet first. \ No newline at end of file diff --git a/app/src/main/res/values-it/strings_final.xml b/app/src/main/res/values-it/strings_final.xml index fa5d2dc4b3..4a431cc050 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -5,7 +5,7 @@ 2 cards Shipping Free - I have a promo code… + I have a promo code... Total Other payment methods Buy now @@ -23,16 +23,17 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to 3 physical cards to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - Order card + Order Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode + Not enough funds for fee on your %s wallet to send a transaction. Top up your %s wallet first. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index 288761b97c..89595fb93d 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -23,7 +23,7 @@ Революционный аппаратный кошелек Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Все ключи в безопасности - До трех карт с одним кошельком + До трех карт с одним кошельком Тысячи криптовалют Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте Поддержка DeFi @@ -35,4 +35,5 @@ Поиск токенов Приложение работает в демонстрационном режиме. Средства на всех счетах ненастоящие. Эта функция недоступна в демонстрационном режиме + Недостаточно средств для комиссии на вашем %s кошельке для отправки транзакции. Сначала пополните свой %s кошелек. \ No newline at end of file diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index fa5d2dc4b3..4a431cc050 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -5,7 +5,7 @@ 2 cards Shipping Free - I have a promo code… + I have a promo code... Total Other payment methods Buy now @@ -23,16 +23,17 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to 3 physical cards to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible Exchange, buy NFT’s, make loans and deposits in more than 100 different decentralized services The Wallet for Everyone Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - Order card + Order Search tokens Search tokens You are currently running in Demo mode. All funds are not real. This feature is disabled in Demo mode + Not enough funds for fee on your %s wallet to send a transaction. Top up your %s wallet first. \ No newline at end of file