Updated on 2026-08-14

This commit is contained in:
Tangem 2022-10-03 14:54:06 +04:00
commit 8998fbb75c
135 changed files with 2039 additions and 1613 deletions

View file

@ -29,7 +29,6 @@
<codeStyleSettings language="Groovy">
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
@ -174,9 +173,8 @@
<option name="RIGHT_MARGIN" value="120" />
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
<option name="LINE_COMMENT_ADD_SPACE_ON_REFORMAT" value="true" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="0" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="0" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="EXTENDS_LIST_WRAP" value="5" />

View file

@ -7,7 +7,6 @@
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.USE_BIOMETRICT" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
@ -131,18 +130,6 @@
<activity android:name="zendesk.messaging.MessagingActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.guide.HelpCenterActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.guide.ViewArticleActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.request.RequestActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="zendesk.support.requestlist.RequestListActivity"
android:theme="@style/ZendeskTheme" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View file

@ -430,6 +430,27 @@
"networkId": "ethereum-pow-iou/test"
}
]
},
{
"id" : "polkadot",
"symbol" : "DOT",
"name" : "Polkadot",
"networks" : [
{
"networkId" : "polkadot/test"
}
]
},
{
"id": "optimistic-ethereum",
"name": "Optimistic Ethereum",
"symbol": "ETH",
"active": true,
"networks": [
{
"networkId": "optimistic-ethereum/test"
}
]
}
],

View file

@ -25,7 +25,6 @@ import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
@ -66,7 +65,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
backupService = BackupService.init(tangemSdk, this)
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this)
@ -85,6 +83,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
}
private fun systemActions() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val windowInsetsController = WindowInsetsControllerCompat(window, binding.root)

View file

@ -1,17 +1,19 @@
package com.tangem.tap
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.appsflyer.AppsFlyerLib
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
@ -21,12 +23,11 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.config.Config
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
@ -37,117 +38,118 @@ import timber.log.Timber
val store = Store(
reducer = ::appReducer,
middleware = AppState.getMiddleware(),
state = AppState()
state = AppState(),
)
val logConfig = LogConfig()
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
class TapApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
DomainLayer.init()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
this.minimumFetchIntervalInSeconds = 60
})
} else {
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {
this.minimumFetchIntervalInSeconds = 3600
})
}
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
currenciesRepository = CurrenciesRepository(
this, store.state.domainNetworks.tangemTechService
)
walletConnectRepository = WalletConnectRepository(this)
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
initFeedbackManager()
loadConfigs()
BlockchainSdkRetrofitBuilder.enableNetworkLogging = BuildConfig.DEBUG
DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this)
initAppsFlyer()
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.defaultMoshi())
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
BlockchainSdkRetrofitBuilder.enableNetworkLogging = LogConfig.network.blockchainSdkNetwork
userTokensRepository = UserTokensRepository.init(
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
)
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(context = this)
return createCoilImageLoader(
context = this,
logEnabled = LogConfig.imageLoader
)
}
private fun loadConfigs() {
val moshi = MoshiConverter.defaultMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load { config ->
private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
val configManager = ConfigManager()
configManager.load(loader) { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
shopService = TangemShopService(
application = this,
shopifyShop = config.shopify!!
)
store.state.globalState.feedbackManager?.initChat(
context = this,
zendeskConfig = config.zendesk!!
)
onComplete(config)
}
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}
private fun initFeedbackManager() {
val infoHolder = AdditionalFeedbackInfo()
infoHolder.setAppVersion(this)
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
)
val logWriter = TangemLogCollector(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(),
)
Log.addLogger(logWriter)
store.dispatch(
GlobalAction.SetFeedbackManager(
FeedbackManager(
infoHolder = infoHolder,
logCollector = logWriter,
preferencesStorage = preferencesStorage,
),
),
)
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAppsFlyer(this, config)
initFeedbackManager(this, preferencesStorage, config)
}
private fun initAppsFlyer() {
val devKey = store.state.globalState.configManager?.config?.appsFlyerDevKey ?: return
AppsFlyerLib.getInstance().init(devKey, null, this)
AppsFlyerLib.getInstance().start(this)
val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(this)
private fun initAppsFlyer(context: Context, config: Config) {
AppsFlyerLib.getInstance().init(config.appsFlyerDevKey, null, context)
AppsFlyerLib.getInstance().start(context)
val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(context)
store.dispatch(GlobalAction.SetAnanlyticHandlers(analyticsHandler))
}
}
data class LogConfig(
val coil: Boolean = BuildConfig.DEBUG,
val storeAction: Boolean = BuildConfig.DEBUG,
val zendesk: Boolean = BuildConfig.DEBUG,
)
private fun initFeedbackManager(context: Context, preferencesStorage: PreferencesStorage, config: Config) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
"x.y.z"
}
}
fun initTangemLogCollector(): TangemLogCollector {
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
Log.Level.Tlv,
Log.Level.Nfc,
Log.Level.Command,
Log.Level.Session,
Log.Level.View,
Log.Level.Network,
Log.Level.Error,
)
return TangemLogCollector(logLevels, LogFormat.StairsFormatter())
}
val additionalFeedbackInfo = initAdditionalFeedbackInfo(context)
val tangemLogCollector = initTangemLogCollector()
Log.addLogger(tangemLogCollector)
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
preferencesStorage = preferencesStorage,
logEnabled = LogConfig.zendesk,
)
feedbackManager.initChat(
context = context,
zendeskConfig = config.zendesk!!,
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}
private fun initWarningMessagesManager() {
store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager()))
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common
import android.content.Context
import com.tangem.tap.common.extensions.readAssetAsString
/**
[REDACTED_AUTHOR]
*/
interface AssetReader {
fun readAssetAsString(name: String): String
}
class AndroidAssetReader(
private val context: Context,
) : AssetReader {
override fun readAssetAsString(name: String): String {
return context.readAssetAsString(name)
}
}

View file

@ -113,7 +113,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
data = state.dialog.data,
session = state.dialog.session,
sessionId = state.dialog.sessionId,
cardId = state.dialog.cardId,
dAppName = state.dialog.dAppName,
context = context,
)

View file

@ -0,0 +1,20 @@
package com.tangem.tap.common
import android.content.Context
import com.tangem.tap.common.extensions.readFile
import com.tangem.tap.common.extensions.rewriteFile
interface FileReader {
fun readFile(fileName: String): String
fun rewriteFile(content: String, fileName: String)
}
class AndroidFileReader(private val context: Context) : FileReader {
override fun readFile(fileName: String): String {
return context.readFile(fileName)
}
override fun rewriteFile(content: String, fileName: String) {
context.rewriteFile(content, fileName)
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics
import android.app.Application
import android.content.Context
import com.appsflyer.AFInAppEventParameterName
import com.appsflyer.AFInAppEventType
import com.appsflyer.AppsFlyerLib
@ -8,7 +8,7 @@ import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() {
class AppsFlyerAnalyticsHandler(val context: Context): AnalyticsHandler() {
override fun triggerEvent(
event: AnalyticsEvent,
@ -16,8 +16,7 @@ class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() {
blockchain: String?,
params: Map<String, String>
) {
AppsFlyerLib.getInstance().logEvent(context,
event.event, prepareParams(card, blockchain))
AppsFlyerLib.getInstance().logEvent(context, event.event, prepareParams(card, blockchain))
}
override fun logCardSdkError(

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.analytics
import android.app.Application
import android.content.Context
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -42,7 +42,7 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
}
companion object {
fun createDefaultAnalyticHandlers(context: Application): GlobalAnalyticsHandler {
fun createDefaultAnalyticHandlers(context: Context): GlobalAnalyticsHandler {
return GlobalAnalyticsHandler(
listOf(
FirebaseAnalyticsHandler,

View file

@ -30,6 +30,10 @@ fun Blockchain.getRoundIconRes(): Int {
Blockchain.Gnosis -> R.drawable.ic_gnosis_round
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_round
Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_round
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_round
Blockchain.Kusama -> R.drawable.ic_kusama_round
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism
Blockchain.Dash -> R.drawable.ic_dash
else -> R.drawable.ic_tangem_logo
}
}
@ -59,6 +63,10 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Gnosis -> R.drawable.ic_gnosis_no_color
Blockchain.EthereumPow, Blockchain.EthereumPowTestnet -> R.drawable.ic_ethereumpow_no_color
Blockchain.EthereumFair -> R.drawable.ic_ethereumfair_no_color
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.drawable.ic_polkadot_no_color
Blockchain.Kusama -> R.drawable.ic_kusama_no_color
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color
Blockchain.Dash -> R.drawable.ic_dash_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -1,7 +1,12 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.*
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
@ -21,7 +26,6 @@ import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
import com.tangem.common.extensions.VoidCallback
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(requireContext(), drawableResId)
}
@ -53,6 +57,10 @@ fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
fun View.getQuantityString(@StringRes id: Int, quantity: Int): String {
return context.resources.getQuantityString(id, quantity, quantity)
}
fun View.getResourceName(): String {
return try {
resources.getResourceEntryName(id)
@ -89,15 +97,13 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
} else {
this.show(invokeBeforeStateChanged)
}
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics,
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
@ -107,7 +113,6 @@ fun Context.pixelsToDp(pixels: Int): Int {
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
fun Context.pixelsToDp(pixels: Float): Float =
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
@ -118,14 +123,14 @@ fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginBottomDp: Int = 8,
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginBottomDp),
)
this.layoutParams = params
}
@ -144,7 +149,8 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
}
fun View.hideKeyboard() {
val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
val inputMethodManager =
context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager
inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0)
}
@ -182,7 +188,7 @@ fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
finally: VoidCallback? = null,
) {
try {
this.startActivity(intent, options)
@ -200,7 +206,7 @@ fun View.getString(resId: Int, vararg formatArgs: Any?): String {
fun View.animateVisibility(
show: Boolean,
durationMillis: Long = SHORT_ANIMATION_DURATION,
hiddenVisibility: Int = View.GONE
hiddenVisibility: Int = View.GONE,
) {
if (show) {
this.animate()

View file

@ -1,7 +1,5 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
@ -9,16 +7,19 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.common.card.CardWallet
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
var derivationPath: String = "",
var outputsCount: String? = null,
var host: String = "",
var addresses: String = "",
var explorerLink: String = "",
)
var appVersion: String = ""
@ -45,37 +46,19 @@ class AdditionalFeedbackInfo {
var fee: String = ""
var token: String = ""
fun setAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
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}" }
signedHashesCount = formatSignedHashes(data.card.wallets)
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(
EmailWalletInfo(
blockchain = manager.wallet.blockchain,
address = getAddress(manager.wallet),
explorerLink = getExploreUri(manager.wallet),
host = manager.currentHost,
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
)
)
walletsInfo.add(createEmailWalletInfo(manager))
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
@ -83,45 +66,55 @@ class AdditionalFeedbackInfo {
}
fun updateOnSendError(
wallet: Wallet,
host: String,
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
) {
onSendErrorWalletInfo = EmailWalletInfo(
blockchain = wallet.blockchain,
address = getAddress(wallet),
explorerLink = getExploreUri(wallet),
host = host,
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
)
onSendErrorWalletInfo = createEmailWalletInfo(walletManager)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
private fun createEmailWalletInfo(walletManager: WalletManager): EmailWalletInfo {
return EmailWalletInfo(
blockchain = walletManager.wallet.blockchain,
derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath ?: "",
outputsCount = walletManager.outputsCount?.toString(),
host = walletManager.currentHost,
addresses = formatAddresses(walletManager.wallet),
explorerLink = formatExploreUrls(walletManager.wallet),
)
}
private fun formatSignedHashes(wallets: List<CardWallet>): String {
return wallets.joinToString("\n") { "Signed hashes: ${it.curve.curve} - ${it.totalSignedHashes}" }
}
private fun formatAddresses(wallet: Wallet): String {
return wallet.formatAddressWith("Multiple address:") {
"${it.name} - ${it.value}"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
private fun formatExploreUrls(wallet: Wallet): String {
return wallet.formatAddressWith("Multiple explorers links:") {
"${it.name} - ${wallet.getExploreUrl(it.value)}"
}
}
private fun Wallet.formatAddressWith(with: String, mapAddress: (Address) -> String): String {
return if (addresses.size == 1) {
getExploreUrl(address)
} else {
addresses.map { mapAddress(it) }.toMutableList()
.apply { add(0, with) }
.joinToString("\n")
}
}
private val Address.name: String
get() = type.javaClass.simpleName
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.common.feedback
import com.tangem.tap.common.extensions.breakLine
class FeedbackDataBuilder(
private val infoHolder: AdditionalFeedbackInfo
private val infoHolder: AdditionalFeedbackInfo,
) {
val builder = StringBuilder()
@ -21,7 +21,7 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
builder.appendKeyValue("", infoHolder.signedHashesCount)
return this
}
@ -29,20 +29,22 @@ class FeedbackDataBuilder(
infoHolder.walletsInfo.forEach {
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Explorer link", it.explorerLink)
builder.appendKeyValue("Outputs count", it.outputsCount)
infoHolder.tokens[it.blockchain]?.let { tokens ->
builder.append("Tokens:")
breakLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.addresses)
builder.appendKeyValue("Explorer link", it.explorerLink)
}
return this
}
@ -55,7 +57,7 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)
builder.appendDelimiter()
builder.appendKeyValue("Source address", walletInfo.address)
builder.appendKeyValue("Source address", walletInfo.addresses)
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
builder.appendKeyValue("Amount", infoHolder.amount)
builder.appendKeyValue("Fee", infoHolder.fee)
@ -72,8 +74,10 @@ class FeedbackDataBuilder(
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.appendKeyValue(key: String, value: String?): StringBuilder = when {
value.isNullOrBlank() -> this
key.isBlank() -> this.append("$value\n")
else -> this.append("$key: $value\n")
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")

View file

@ -7,7 +7,6 @@ import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.logConfig
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
@ -31,6 +30,7 @@ class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val preferencesStorage: PreferencesStorage,
private val logEnabled: Boolean = false,
) {
fun initChat(
context: Context,
@ -43,7 +43,7 @@ class FeedbackManager(
)
// Zendesk logs
Logger.setLoggable(logConfig.zendesk)
Logger.setLoggable(logEnabled)
}
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {

View file

@ -4,7 +4,6 @@ import android.content.Context
import android.util.Log
import coil.ImageLoader
import coil.util.Logger
import com.tangem.tap.logConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
@ -12,11 +11,12 @@ import timber.log.Timber
private const val COIL_LOG_TAG = "COIL"
fun createCoilImageLoader(
context: Context
context: Context,
logEnabled: Boolean = false,
): ImageLoader {
return ImageLoader.Builder(context)
.apply {
if (!logConfig.coil) return@apply
if (!logEnabled) return@apply
logger(CoilTimberLogger())
okHttpClient {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.logConfig
import com.tangem.domain.common.LogConfig
import org.rekotlin.Middleware
import timber.log.Timber
@ -10,7 +10,9 @@ import timber.log.Timber
val logMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
if (logConfig.storeAction) Timber.d("Dispatch action: $action")
if (LogConfig.storeAction) {
Timber.d("Dispatch action: $action")
}
nextDispatch(action)
}
}

View file

@ -5,13 +5,13 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.services.Result
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -24,6 +24,7 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -96,7 +97,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.ExchangeManager.Init -> {
val config = appState()?.globalState?.configManager?.config
val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config
ifNotNull(
config?.mercuryoWidgetId,
config?.mercuryoSecret,
@ -108,8 +110,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
apiVersion = MercuryoApi.API_VERSION,
mercuryoWidgetId = mercuryoWidgetId,
secret = mercuryoSecret,
logEnabled = LogConfig.network.mercuryoService,
)
val sellService = MoonPayService(
apiKey = moonPayKey,
secretKey = moonPaySecretKey,
logEnabled = LogConfig.network.moonPayService,
)
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
val cardProvider = { store.state.globalState.scanResponse?.card }
val exchangeManager = CurrencyExchangeManager(
@ -134,7 +141,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandlers,
currenciesRepository,
userTokensRepository,
action.additionalBlockchainsToDerive,
action.messageResId,
)

View file

@ -32,7 +32,7 @@ import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
@ -45,7 +45,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler?,
currenciesRepository: CurrenciesRepository,
userTokensRepository: UserTokensRepository,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
@ -53,8 +53,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
runnable = ScanProductTask(null, currenciesRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message
runnable = ScanProductTask(null, userTokensRepository, additionalBlockchainsToDerive),
cardId = null, initialMessage = message,
).also { sendScanResultsToAnalytics(analyticsHandler, it) }
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.domain
import androidx.annotation.StringRes
import com.tangem.common.core.TangemError
import com.tangem.network.api.tangemTech.TangemTechError
import com.tangem.wallet.R
interface TapErrors
@ -31,13 +32,13 @@ sealed class TapError(
object InsufficientBalance : TapError(R.string.send_error_insufficient_balance)
object BlockchainInternalError : TapError(R.string.send_error_blockchain_internal)
object AmountExceedsBalance : TapError(R.string.send_validation_amount_exceeds_balance)
data class AmountLowerExistentialDeposit(override val args: List<Any>) : TapError(R.string.send_error_minimum_balance_format)
object FeeExceedsBalance : TapError(R.string.send_validation_invalid_fee)
object TotalExceedsBalance : TapError(R.string.send_validation_invalid_total)
object InvalidAmountValue : TapError(R.string.send_validation_invalid_amount)
object InvalidFeeValue : TapError(R.string.send_error_invalid_fee_value)
data class DustAmount(override val args: List<Any>) : TapError(R.string.send_error_dust_amount_format)
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
data class UnsupportedState(
val stateError: String,
@ -54,6 +55,7 @@ sealed class TapError(
sealed class WalletConnect {
object UnsupportedDapp : TapError(R.string.wallet_connect_error_unsupported_dapp)
object UnsupportedLink : TapError(R.string.wallet_connect_error_failed_to_connect)
}
data class ValidateTransactionErrors(
@ -70,7 +72,6 @@ sealed class TapSdkError(override val messageResId: Int?) : Throwable(), TangemE
object CardNotSupportedByRelease : TapSdkError(R.string.error_update_app)
}
fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
val idList = mutableListOf<Pair<Int, List<Any>?>>()
when (this) {
@ -78,4 +79,13 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}
}
fun TangemTechError.toTapError(): TapError {
return when (this.code) {
404 -> NoDataError(this.description)
else -> TapError.CustomError(customMessage = this.description)
}
}
class NoDataError(message: String) : TapError.CustomError(customMessage = message)

View file

@ -16,30 +16,28 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
by lazy { WalletManagerFactory(blockchainSdkConfig) }
val rates: RatesRepository = RatesRepository()
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler =
ThrottlerWithValues<BlockchainNetwork, Result<Wallet>>(10000)
@ -64,16 +62,16 @@ class TapWalletManager {
WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
blockchainNetwork,
(result.error as TapError.WalletManager.NoAccountError).customMessage
)
(result.error as TapError.WalletManager.NoAccountError).customMessage,
),
)
}
else -> {
dispatchOnMain(
WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
)
result.error.localizedMessage,
),
)
}
}
@ -88,14 +86,16 @@ class TapWalletManager {
withMainContext {
store.dispatch(WalletAction.ResetState(data.card.cardId))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
store.dispatch(WalletConnectAction.RestoreSessions(data))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
&& data.card.backupStatus == Card.BackupStatus.NoBackup
)
&& data.card.backupStatus == Card.BackupStatus.NoBackup,
),
)
loadData(data)
}
@ -136,23 +136,24 @@ class TapWalletManager {
}
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse
scanResponse: ScanResponse,
) {
val savedCurrencies = currenciesRepository.loadSavedCurrencies(
scanResponse.card.cardId, scanResponse.card.settings.isHDWalletAllowed
)
if (savedCurrencies.isEmpty()) return
loadUserCurrencies(scanResponse, walletManagerFactory)
}
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(scanResponse, savedCurrencies)
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(savedCurrencies, walletManagers),
)
savedCurrencies.map {
private fun checkIfDerivationsAreMissing(blockchainNetworks: List<BlockchainNetwork>, scanResponse: ScanResponse) {
blockchainNetworks.map {
if (it.tokens.isNotEmpty()) {
dispatchOnMain(WalletAction.MultiWallet.AddTokens(it.tokens, it))
WalletAction.MultiWallet.AddTokens(it.tokens, it, false)
}
}
val missingDerivations = blockchainNetworks
.filter {
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
}
if (missingDerivations.isNotEmpty()) {
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
}
}
private suspend fun loadSingleWalletData(data: ScanResponse) {
@ -169,14 +170,43 @@ class TapWalletManager {
}
dispatchOnMain(
WalletAction.MultiWallet.AddBlockchains(
listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
listOf(primaryWalletManager)
)
blockchains = listOf(BlockchainNetwork.fromWalletManager(primaryWalletManager)),
walletManagers = listOf(primaryWalletManager),
save = false,
),
)
}
}
private suspend fun loadUserCurrencies(scanResponse: ScanResponse, walletManagerFactory: WalletManagerFactory) {
val userTokens = userTokensRepository.getUserTokens(scanResponse.card)
withMainContext {
val blockchainNetworks = userTokens.toBlockchainNetworks()
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, userTokens)
store.dispatch(
WalletAction.MultiWallet.AddBlockchains(
blockchains = blockchainNetworks,
walletManagers = walletManagers,
save = false,
),
)
blockchainNetworks.filter { it.tokens.isNotEmpty() }
.map {
store.dispatch(
WalletAction.MultiWallet.AddTokens(
tokens = it.tokens,
blockchain = it,
save = false,
),
)
}
checkIfDerivationsAreMissing(blockchainNetworks, scanResponse)
}
}
suspend fun reloadData(data: ScanResponse) {
loadUserCurrencies(data, walletManagerFactory)
withContext(Dispatchers.Main) {
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)

View file

@ -1,12 +1,8 @@
package com.tangem.tap.domain.configurable.config
import android.content.Context
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.common.AssetReader
import com.tangem.tap.domain.configurable.Loader
import timber.log.Timber
@ -14,8 +10,8 @@ import timber.log.Timber
[REDACTED_AUTHOR]
*/
class FeaturesLocalLoader(
private val context: Context,
private val moshi: Moshi,
private val assetReader: AssetReader,
private val moshi: Moshi,
) : Loader<ConfigModel> {
override fun load(onComplete: (ConfigModel) -> Unit) {
@ -23,8 +19,8 @@ class FeaturesLocalLoader(
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = context.readAssetAsString(Loader.featuresName)
val jsonConfigValues = context.readAssetAsString(Loader.configValuesName)
val jsonFeatures = assetReader.readAssetAsString(Loader.featuresName)
val jsonConfigValues = assetReader.readAssetAsString(Loader.configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {
@ -33,31 +29,4 @@ class FeaturesLocalLoader(
}
onComplete(config)
}
}
class FeaturesRemoteLoader(
private val moshi: Moshi,
) : Loader<ConfigModel> {
override fun load(onComplete: (ConfigModel) -> Unit) {
val emptyConfig = ConfigModel.empty()
val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener {
if (it.isSuccessful) {
val config = remoteConfig.getValue(Loader.featuresName)
val jsonConfig = config.asString()
if (jsonConfig.isEmpty()) {
onComplete(emptyConfig)
return@addOnCompleteListener
}
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
onComplete(ConfigModel(featureAdapter.fromJson(jsonConfig), null))
} else {
onComplete(emptyConfig)
}
}.addOnFailureListener {
FirebaseAnalyticsHandler.logException("remote_config_error.features", it)
onComplete(emptyConfig)
}
}
}

View file

@ -24,26 +24,19 @@ data class Config(
val zendesk: ZendeskConfig? = null,
)
class ConfigManager(
private val localLoader: Loader<ConfigModel>,
private val remoteLoader: Loader<ConfigModel>
) {
class ConfigManager {
var config: Config = Config()
private set
private var defaultConfig = Config()
fun load(onComplete: ((config: Config) -> Unit)? = null) {
localLoader.load { configModel ->
fun load(configLoader: Loader<ConfigModel>, onComplete: ((config: Config) -> Unit)? = null) {
configLoader.load { configModel ->
setupFeature(configModel.features)
setupKey(configModel.configValues)
onComplete?.invoke(config)
}
// Uncomment to enable remote config
// remoteLoader.load { config ->
// setupFeature(config.features)
// }
}
fun turnOff(name: String) {

View file

@ -1,52 +0,0 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.configurable.Loader
/**
[REDACTED_AUTHOR]
*/
class RemoteWarningLoader(
private val moshi: Moshi,
) : Loader<List<WarningMessage>> {
override fun load(onComplete: (List<WarningMessage>) -> Unit) {
val emptyConfig = listOf<WarningMessage>()
val remoteConfig = Firebase.remoteConfig
remoteConfig.fetchAndActivate().addOnCompleteListener {
if (!it.isSuccessful) {
onComplete(emptyConfig)
return@addOnCompleteListener
}
val config = remoteConfig.getValue(Loader.warnings)
val jsonConfig = config.asString()
if (jsonConfig.isEmpty()) {
onComplete(emptyConfig)
return@addOnCompleteListener
}
val adapterType = Types.newParameterizedType(List::class.java, WarningMessage::class.java)
val warningsAdapter: JsonAdapter<List<WarningMessage>> = moshi.adapter(adapterType)
try {
val warnings = warningsAdapter.fromJson(jsonConfig) ?: listOf()
onComplete(warnings)
} catch (ex: Exception) {
handleError(ex)
onComplete(emptyConfig)
}
}.addOnFailureListener {
handleError(it)
onComplete(emptyConfig)
}
}
private fun handleError(ex: Exception) {
FirebaseAnalyticsHandler.logException("remote_config_error.warnings", ex)
}
}

View file

@ -1,35 +1,17 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class WarningMessagesManager(
private val warningLoader: RemoteWarningLoader,
) {
class WarningMessagesManager {
private val warningsList: MutableList<WarningMessage> = mutableListOf()
fun load(onComplete: VoidCallback? = null) {
// exclude annoying remote debug warnings
if (BuildConfig.DEBUG) {
onComplete?.invoke()
return
}
warningLoader.load { remoteList ->
warningsList.clear()
warningsList.addAll(remoteList)
sortByPriority()
onComplete?.invoke()
}
}
fun addWarning(warning: WarningMessage) {
if (findWarning(warning) == null) {
warningsList.add(warning)
@ -37,7 +19,10 @@ class WarningMessagesManager(
}
}
fun getWarnings(location: WarningMessage.Location, forBlockchains: List<Blockchain> = emptyList()): List<WarningMessage> {
fun getWarnings(
location: WarningMessage.Location,
forBlockchains: List<Blockchain> = emptyList(),
): List<WarningMessage> {
return warningsList
.filter { !it.isHidden && it.location.contains(location) }
.filter {
@ -94,7 +79,7 @@ class WarningMessagesManager(
null,
R.string.alert_title,
R.string.alert_developer_card,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
@ -106,7 +91,7 @@ class WarningMessagesManager(
null,
R.string.alert_title,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
@ -131,7 +116,7 @@ class WarningMessagesManager(
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
@ -147,7 +132,7 @@ class WarningMessagesManager(
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
@ -160,7 +145,7 @@ class WarningMessagesManager(
titleResId = R.string.alert_title,
messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString()
messageFormatArg = remainingSignatures.toString(),
)
fun testCardWarning(): WarningMessage = WarningMessage(
@ -172,7 +157,7 @@ class WarningMessagesManager(
null,
R.string.alert_title,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
fun demoCardWarning(): WarningMessage = WarningMessage(
@ -184,7 +169,7 @@ class WarningMessagesManager(
null,
R.string.alert_title,
R.string.alert_demo_message,
WarningMessage.Origin.Local
WarningMessage.Origin.Local,
)
const val REMAINING_SIGNATURES_WARNING = 10

View file

@ -1,6 +1,10 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationParams
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
@ -99,9 +103,11 @@ fun WalletManagerFactory.makeWalletManagerForApp(
}
fun WalletManagerFactory.makeWalletManagersForApp(
scanResponse: ScanResponse, blockchains: List<BlockchainNetwork>,
scanResponse: ScanResponse, blockchains: List<Currency>,
): List<WalletManager> {
return blockchains.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
return blockchains
.filter { it.isBlockchain() }
.mapNotNull { this.makeWalletManagerForApp(scanResponse, it) }
}
fun WalletManagerFactory.makePrimaryWalletManager(

View file

@ -32,16 +32,14 @@ import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import kotlinx.coroutines.launch
class ScanProductTask(
val card: Card? = null,
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
private val userTokensRepository: UserTokensRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
) : CardSessionRunnable<ScanResponse> {
override fun run(
@ -62,7 +60,7 @@ class ScanProductTask(
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
else -> ScanWalletProcessor(currenciesRepository, additionalBlockchainsToDerive)
else -> ScanWalletProcessor(userTokensRepository, additionalBlockchainsToDerive)
}
commandProcessor.proceed(card, session) { processorResult ->
when (processorResult) {
@ -111,8 +109,8 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
}
private class ScanWalletProcessor(
private val currenciesRepository: CurrenciesRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null
private val userTokensRepository: UserTokensRepository?,
private val additionalBlockchainsToDerive: Collection<Blockchain>? = null,
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
@ -184,47 +182,41 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
scope.launch {
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard
)
)
)
return@launch
}
val derivations = collectDerivations(card)
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
primaryCard = primaryCard,
),
),
)
return
}
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val response = ScanResponse(
card = card,
productType = ProductType.Wallet,
walletData = session.environment.walletData,
derivedKeys = result.data.entries,
primaryCard = primaryCard,
)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
private fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val userTokensRepository = userTokensRepository ?: return emptyList()
val blockchainsToDerive = userTokensRepository.loadBlockchainsToDerive(card).toMutableList().ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card),
@ -236,7 +228,7 @@ private class ScanWalletProcessor(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card),
)
),
)
}
if (additionalBlockchainsToDerive != null) {
@ -256,16 +248,14 @@ private class ScanWalletProcessor(
return blockchainsToDerive.distinct()
}
private suspend fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
private fun collectDerivations(card: Card): Map<ByteArrayKey, List<DerivationPath>> {
val blockchains = getBlockchainsToDerive(card)
val derivations = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
blockchains.forEach { blockchain ->
val curve = blockchain.blockchain.getPrimaryCurve()
val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach
if (wallet.chainCode == null) return@forEach
val key = wallet.publicKey.toMapKey()
val path = blockchain.derivationPath?.let { DerivationPath(it) }
if (path != null) {

View file

@ -1,247 +1,12 @@
package com.tangem.tap.domain.tokens
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.ObsoleteTokenDao
import com.tangem.tap.domain.tokens.models.TokenDao
import com.tangem.tap.features.demo.DemoHelper
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import java.util.*
class CurrenciesRepository(
private val context: Application,
private val tangemNetworkService: TangemTechService
) {
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
)
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
moshi.adapter(CurrenciesFromJson::class.java)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
fun saveUpdatedCurrency(cardId: String, blockchainNetwork: BlockchainNetwork) {
var changed = false
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
changed = true
blockchainNetwork
} else {
it
}
}
val updatedCurrencies = if (changed) currencies else currencies + blockchainNetwork
saveCurrencies(cardId, updatedCurrencies.distinct())
}
fun removeToken(cardId: String, token: Token, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrenciesWithoutMigration(cardId).map {
if (it == blockchainNetwork) {
it.copy(tokens = it.tokens.filterNot { it == token })
} else {
it
}
}
saveCurrencies(cardId, currencies)
}
fun removeBlockchain(cardId: String, blockchainNetwork: BlockchainNetwork) {
val currencies = loadSavedCurrenciesWithoutMigration(cardId)
.filterNot { it == blockchainNetwork }
saveCurrencies(cardId, currencies)
}
fun removeCurrencies(cardId: String) {
saveCurrencies(cardId, emptyList())
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptyList()
}
return try {
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
fun loadSavedCurrenciesWithoutMigration(
cardId: String,
): List<BlockchainNetwork> {
if (DemoHelper.isDemoCardId(cardId)) {
return loadDemoCurrencies()
}
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
private fun loadDemoCurrencies(): List<BlockchainNetwork> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList()
)
}
}
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported
)
} catch (exception: Exception) {
emptyList()
}
}
private suspend fun loadSavedCurrenciesOldWay(
cardId: String, isHdWalletSupported: Boolean = false
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
token.copy(id = ids[token.contractAddress])
}
)
}
saveCurrencies(cardId, blockchainNetworks) // migrate saved currencies
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
tokens.map {
async {
tangemNetworkService.getTokens(
contractAddress = it.contractAddress,
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}.map { it.await() }
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}.toMap()
}
fun saveCurrencies(cardId: String, currencies: List<BlockchainNetwork>) {
val json = blockchainNetworkAdapter.toJson(currencies)
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
private fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
fun getTestnetCoins(): List<Currency> {
val json = context.assets.readJsonFileToString(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun loadTokensJson(blockchain: Blockchain): String? {
val fileName = getFileName(blockchain)
return try {
context.assets.readJsonFileToString(fileName)
} catch (ex: Exception) {
Timber.e(ex, "Tokens with the file name %s not found", fileName)
null
}
}
private fun getFileName(blockchain: Blockchain): String {
return StringBuilder().apply {
append(blockchain.id.lowercase(Locale.getDefault()).replace("/test", ""))
append("_tokens")
appendIf("_testnet") { blockchain.isTestnet() }
}.toString()
}
private fun fromJsonToTokensDao(tokenJson: String, blockchain: Blockchain): List<TokenDao> {
return obsoleteTokensAdapter.fromJson(tokenJson)!!.map { it.toTokenDao(blockchain) }
}
object CurrenciesRepository {
fun getBlockchains(
cardFirmware: FirmwareVersion,
isTestNet: Boolean = false
isTestNet: Boolean = false,
): List<Blockchain> {
val blockchains = if (cardFirmware < FirmwareVersion.MultiWalletAvailable) {
Blockchain.secp256k1Blockchains(isTestNet)
@ -261,21 +26,6 @@ class CurrenciesRepository(
)
}
}
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}
fun Blockchain.getTokensName(): String {
return when (this) {
Blockchain.Fantom -> "Fantom Opera"
else -> this.fullName
}
}

View file

@ -1,29 +1,36 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getListOfCoins
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.CoinsResponse
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.AssetReader
class LoadAvailableCoinsService(
private val networkService: TangemTechService,
private val currenciesRepository: CurrenciesRepository
private val assetReader: AssetReader,
) {
private val moshi: Moshi by lazy { MoshiConverter.defaultMoshi() }
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
moshi.adapter(CurrenciesFromJson::class.java)
suspend fun getSupportedTokens(
isTestNet: Boolean = false,
supportedBlockchains: List<Blockchain>,
page: Int,
searchInput: String? = null
searchInput: String? = null,
): Result<LoadedCoins> {
if (isTestNet) {
return Result.Success(
LoadedCoins(
currencies = currenciesRepository.getTestnetCoins().filter(searchInput),
currencies = getTestnetCoins().filter(searchInput),
moreAvailable = false,
)
),
)
}
val offset = page * LOAD_PER_PAGE
@ -56,21 +63,28 @@ class LoadAvailableCoinsService(
active = true,
offset = offset,
limit = LOAD_PER_PAGE,
searchText = searchInput
searchText = searchInput,
)
}
fun getTestnetCoins(): List<Currency> {
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
if (searchInput.isNullOrBlank()) return this
return filter{
return filter {
it.symbol.contains(searchInput, ignoreCase = true) ||
it.name.contains(searchInput, ignoreCase = true)
it.name.contains(searchInput, ignoreCase = true)
}
}
companion object {
const val LOAD_PER_PAGE = 100
private const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
}
}

View file

@ -0,0 +1,130 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.getTokens
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
@Deprecated("Use this only for migration")
class OldUserTokensRepository(
private val fileReader: FileReader,
private val tangemNetworkService: TangemTechService,
) {
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java),
)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java),
)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
fileReader.readFile(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptyList()
}
return try {
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use TokensRepository instead")
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported,
)
} catch (exception: Exception) {
emptyList()
}
}
private suspend fun loadSavedCurrenciesOldWay(
cardId: String, isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
token.copy(id = ids[token.contractAddress])
},
)
}
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = coroutineScope {
tokens.map {
async {
tangemNetworkService.getTokens(
contractAddress = it.contractAddress,
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}.map { it.await() }
.map { (it as? Result.Success)?.data?.coins?.firstOrNull()?.id }
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}.toMap()
}
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
private fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.domain.tokens
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.features.wallet.models.Currency
class UserTokensNetworkService(private val tangemTechService: TangemTechService) {
suspend fun getUserTokens(userId: String): Result<UserTokensResponse> {
return when (val result = tangemTechService.getUserTokens(userId)) {
is Result.Success -> result
is Result.Failure -> {
val error = result.error
if (error is TangemSdkError.NetworkError && error.customMessage.contains("404")) {
return Result.Failure(NoDataError(error.customMessage))
} else {
return result
}
}
}
}
suspend fun saveUserTokens(userId: String, tokens: List<Currency>): Result<Unit> {
val tokensResponse = tokens.map { it.toTokenResponse() }
val data = UserTokensResponse(tokens = tokensResponse)
return tangemTechService.putUserTokens(userId, data)
}
}

View file

@ -0,0 +1,117 @@
package com.tangem.tap.domain.tokens
import android.content.Context
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.network.api.tangemTech.TangemTechService
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.NoDataError
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val networkService: UserTokensNetworkService,
) {
suspend fun getUserTokens(card: Card): List<Currency> {
if (DemoHelper.isDemoCardId(card.cardId)) {
return loadDemoCurrencies()
}
val userId = card.getUserId()
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
return loadTokensOffline(card, userId)
}
return when (val networkResult = networkService.getUserTokens(userId)) {
is Result.Success -> {
val tokens = networkResult.data.tokens.map { Currency.fromTokenResponse(it) }
storageService.saveUserTokens(card.getUserId(), tokens)
tokens
}
is Result.Failure -> {
handleGetUserTokensFailure(card = card, userId = userId, error = networkResult.error)
}
}
}
suspend fun saveUserTokens(card: Card, tokens: List<Currency>) {
networkService.saveUserTokens(card.getUserId(), tokens)
storageService.saveUserTokens(card.getUserId(), tokens)
}
suspend fun removeUserTokens(card: Card) {
networkService.saveUserTokens(card.getUserId(), emptyList())
storageService.saveUserTokens(card.getUserId(), emptyList())
}
fun loadBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
return storageService.getUserTokens(card.getUserId())?.toBlockchainNetworks() ?: emptyList()
}
private fun loadDemoCurrencies(): List<Currency> {
return DemoHelper.config.demoBlockchains.map {
BlockchainNetwork(
blockchain = it,
derivationPath = it.derivationPath(DerivationStyle.LEGACY)?.rawPath,
tokens = emptyList(),
)
}.flatMap { it.toCurrencies() }
}
private suspend fun handleGetUserTokensFailure(
card: Card,
userId: String,
error: Throwable,
): List<Currency> {
return when (error) {
is NoDataError -> {
val tokens = storageService.getUserTokens(card)
coroutineScope { launch { networkService.saveUserTokens(userId = userId, tokens = tokens) } }
tokens
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens
}
}
}
private suspend fun loadTokensOffline(card: Card, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
}
private fun Card.getUserId(): String {
val walletPublicKey = this.wallets.firstOrNull()?.publicKey ?: return ""
return calculateUserId(walletPublicKey)
}
private fun calculateUserId(walletPublicKey: ByteArray): String {
val message = MESSAGE.toByteArray()
val keyHash = walletPublicKey.calculateSha256()
return message.calculateHmacSha256(keyHash).toHexString()
}
companion object {
const val MESSAGE = "UserWalletID"
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
val fileReader = AndroidFileReader(context)
val oldUserTokensRepository = OldUserTokensRepository(
fileReader, store.state.domainNetworks.tangemTechService,
)
val storageService = UserTokensStorageService(oldUserTokensRepository, fileReader)
val networkService = UserTokensNetworkService(tangemTechService)
return UserTokensRepository(storageService, networkService)
}
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.Log
import com.tangem.common.card.Card
import com.tangem.network.api.tangemTech.UserTokensResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
class UserTokensStorageService(
private val oldUserTokensRepository: OldUserTokensRepository,
private val fileReader: FileReader,
) {
private val moshi = MoshiConverter.defaultMoshi()
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
moshi.adapter(UserTokensResponse::class.java)
fun getUserTokens(userId: String): List<Currency>? {
return try {
val json = fileReader.readFile(getFileNameForUserTokens(userId))
userTokensAdapter.fromJson(json)?.tokens?.map { Currency.fromTokenResponse(it) }
} catch (exception: Exception) {
Log.error { exception.stackTraceToString() }
null
}
}
@Deprecated("")
suspend fun getUserTokens(card: Card): List<Currency> {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }
}
fun saveUserTokens(userId: String, tokens: List<Currency>) {
val tokensResponse = tokens.map { it.toTokenResponse() }
val data = UserTokensResponse(tokens = tokensResponse)
val json = userTokensAdapter.toJson(data)
fileReader.rewriteFile(json, getFileNameForUserTokens(userId))
}
companion object {
private const val FILE_NAME_PREFIX_USER_TOKENS = "user_tokens"
private fun getFileNameForUserTokens(userId: String): String = "${FILE_NAME_PREFIX_USER_TOKENS}_$userId"
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.domain.common.ScanResponse
import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.tangemSdkManager
@ -119,10 +120,6 @@ class TwinCardsManager(
}
}
interface AssetReader {
fun readAssetAsString(name: String): String
}
private class Issuer(
val id: String,
val privateKey: String,

View file

@ -1,7 +1,9 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
@ -57,7 +59,15 @@ class WalletConnectManager {
private var sessions: MutableMap<WCSession, WalletConnectActiveData> = mutableMapOf()
fun connect(wcUri: String) {
val session = WCSession.from(wcUri) ?: return
val session = WCSession.from(wcUri).guard {
store.dispatchOnMain(
WalletConnectAction.FailureEstablishingSession(
session = null,
error = TapError.WalletConnect.UnsupportedLink,
),
)
return
}
if (sessions[session] != null) {
store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession)
return
@ -65,13 +75,25 @@ class WalletConnectManager {
val client = WCClient(httpClient = okHttpClient)
setListeners(client)
val peerId = UUID.randomUUID().toString()
client.connect(session, tangemPeerMeta, peerId)
try {
client.connect(session, tangemPeerMeta, peerId)
} catch (exception: IllegalArgumentException) {
store.dispatchOnMain(
WalletConnectAction.FailureEstablishingSession(
session = null,
error = TapError.WalletConnect.UnsupportedLink,
),
)
return
}
sessions[session] = WalletConnectActiveData(
peerId = peerId,
remotePeerId = null,
session = session,
client = client,
wallet = WalletForSession(cardId = "")
wallet = WalletForSession(),
)
setupConnectionTimeoutCheck(session)
}
@ -112,8 +134,12 @@ class WalletConnectManager {
}
}
fun restoreSessions() {
fun restoreSessions(scanResponse: ScanResponse) {
val walletPublicKey = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey
?: return
val sessions = walletConnectRepository.loadSavedSessions()
// filter sessions for this particular card
.filter { it.wallet.walletPublicKey.contentEquals(walletPublicKey) }
this.sessions = sessions
.map { session ->
WalletConnectActiveData(
@ -128,8 +154,8 @@ class WalletConnectManager {
setListeners(it.client)
it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId)
}
}
.map { it.session to it }.toMap().toMutableMap()
}.associateBy { it.session }.toMutableMap()
store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions))
}

View file

@ -12,6 +12,9 @@ class WalletConnectNetworkUtils {
peer: WCPeerMeta,
): Blockchain? {
return when {
peer.url.contains("pancakeswap.finance") -> {
Blockchain.BSC
}
chainId != null -> {
Blockchain.fromChainId(chainId)
}

View file

@ -92,7 +92,6 @@ class WalletConnectSdkHelper {
)
)
val dialogData = TransactionRequestDialogData(
cardId = session.wallet.cardId,
dAppName = session.peerMeta.name,
dAppUrl = session.peerMeta.url,
amount = value.toFormattedString(decimals),
@ -235,7 +234,6 @@ class WalletConnectSdkHelper {
val dialogData = PersonalSignDialogData(
cardId = session.wallet.cardId,
dAppName = session.peerMeta.name,
message = messageString,
session = session.session,
@ -272,11 +270,11 @@ class WalletConnectSdkHelper {
suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? {
val key = wallet.derivedPublicKey ?: wallet.walletPublicKey
val command = SignHashCommand(hashToSign, wallet.walletPublicKey!!, wallet.derivationPath)
return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) {
return when (val result = tangemSdkManager.runTaskAsync(command)) {
is CompletionResult.Success -> {
val hash = result.data.signature
return EthereumUtils.prepareSignedMessageData(
hash, hashToSign, CryptoUtils.decompressPublicKey(key!!)
hash, hashToSign, CryptoUtils.decompressPublicKey(key!!),
)
}
is CompletionResult.Failure -> {

View file

@ -1,94 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.EllipticCurve
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.features.wallet.redux.WalletState
class WcWalletManagerFactory(
private val factory: WalletManagerFactory,
private val currenciesRepository: CurrenciesRepository,
) {
suspend fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = wallet.derivationPath?.rawPath,
tokens = emptyList()
)
return if (walletState.cardId == wallet.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
val blockchainNetworkWithTokens = currenciesRepository
.loadSavedCurrencies(
cardId = wallet.cardId,
isHdWalletSupported = wallet.derivationPath != null
).firstOrNull { it == blockchainNetwork }
if (blockchainNetworkWithTokens != null) {
factory.makeWalletManager(
blockchain = blockchainToMake,
publicKey = Wallet.PublicKey(
wallet.walletPublicKey!!,
wallet.derivedPublicKey,
wallet.derivationPath
),
tokens = blockchainNetworkWithTokens.tokens,
curve = blockchainToMake.getPrimaryCurve() ?: EllipticCurve.Secp256k1
)
} else {
null
}
}
}
suspend fun getWalletManager(
scanResponse: ScanResponse, blockchain: Blockchain, walletState: WalletState
): WalletManager? {
val card = scanResponse.card
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
card = card
)
return if (walletState.cardId == card.cardId) {
walletState.getWalletManager(blockchainNetwork)
} else {
if (currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.contains(blockchainNetwork)
) {
factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchainNetwork = blockchainNetwork
)
} else {
null
}
}
}
}

View file

@ -11,7 +11,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -98,21 +97,21 @@ class DetailsMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Start -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins()) {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
scope.launch {
val result = tangemSdkManager.resetToFactorySettings(card)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
currenciesRepository.removeCurrencies(card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
@ -123,7 +122,6 @@ class DetailsMiddleware {
)
}
}
}
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.wallet.R
@ -12,11 +13,9 @@ import com.trustwallet.walletconnect.models.session.WCSession
import org.rekotlin.Action
sealed class WalletConnectAction : Action {
object ResetState : WalletConnectAction()
data class HandleDeepLink(val wcUri: String?) : WalletConnectAction()
object RestoreSessions : WalletConnectAction()
data class RestoreSessions(val scanResponse: ScanResponse) : WalletConnectAction()
data class StartWalletConnect(
val copiedUri: String?,
) : WalletConnectAction()
@ -105,6 +104,6 @@ sealed class WalletConnectAction : Action {
data class Sign(
val id: Long, val data: ByteArray, val sessionData: WCSession,
) : WalletConnectAction()
) : WalletConnectAction()
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
@ -13,12 +14,11 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.domain.walletconnect.WcWalletManagerFactory
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.scope
@ -30,7 +30,7 @@ import org.rekotlin.Middleware
import timber.log.Timber
class WalletConnectMiddleware {
private val walletConnectManager = WalletConnectManager()
private var walletConnectManager = WalletConnectManager()
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
@ -44,8 +44,9 @@ class WalletConnectMiddleware {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletConnectAction.ResetState -> walletConnectManager = WalletConnectManager()
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
walletConnectManager.restoreSessions(action.scanResponse)
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
@ -75,7 +76,13 @@ class WalletConnectMiddleware {
is WalletConnectAction.ChooseNetwork -> {
val data = state()?.walletConnectState?.newSessionData ?: return
scope.launch {
prepareWalletManager(data.scanResponse, store.state.walletState, action.blockchain, data.session)
prepareWalletManager(
scanResponse = data.scanResponse,
walletState = store.state.walletState,
blockchain = action.blockchain,
session = data.session,
walletConnectManager = walletConnectManager,
)
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
@ -121,7 +128,8 @@ class WalletConnectMiddleware {
)
}
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
val scanResponse = store.state.globalState.scanResponse ?: return
scanCard(scanResponse, action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
@ -161,7 +169,6 @@ class WalletConnectMiddleware {
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name,
),
),
@ -175,7 +182,6 @@ class WalletConnectMiddleware {
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name,
),
),
@ -195,33 +201,26 @@ class WalletConnectMiddleware {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletState = store.state.walletState
scope.launch {
val walletManager = factory.getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName),
),
)
return@launch
}
val updatedWallet = action.session.wallet.copy(
walletPublicKey = walletManager.wallet.publicKey.seedKey,
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
derivationPath = walletManager.wallet.publicKey.derivationPath,
blockchain = action.blockchain,
val walletManager = getWalletManager(
wallet = action.session.wallet,
blockchain = blockchain,
walletState = store.state.walletState,
).guard {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchain.fullName),
),
)
val updatedSession = action.session.copy(wallet = updatedWallet)
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
return
}
val updatedWallet = action.session.wallet.copy(
walletPublicKey = walletManager.wallet.publicKey.seedKey,
derivedPublicKey = walletManager.wallet.publicKey.derivedKey,
derivationPath = walletManager.wallet.publicKey.derivationPath,
blockchain = action.blockchain,
)
val updatedSession = action.session.copy(wallet = updatedWallet)
store.dispatchOnMain(WalletConnectAction.UpdateBlockchain(updatedSession))
}
is WalletConnectAction.UpdateBlockchain -> {
walletConnectManager.updateBlockchain(action.updatedSession)
@ -229,40 +228,22 @@ class WalletConnectMiddleware {
}
}
private fun scanCard(session: WalletConnectSession, chainId: Int?) {
private fun scanCard(scanResponse: ScanResponse, session: WalletConnectSession, chainId: Int?) {
val blockchain = WalletConnectNetworkUtils.parseBlockchain(
chainId = chainId,
peer = session.peerMeta,
) ?: Blockchain.Ethereum
).guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
return
}
store.dispatch(
GlobalAction.ScanCard(
additionalBlockchainsToDerive = listOf(blockchain),
onSuccess = { scanResponse ->
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
},
onFailure = {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
},
R.string.wallet_connect_scan_card_message,
),
)
handleScanResponse(scanResponse = scanResponse, session = session, blockchain = blockchain)
}
private suspend fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
return if (walletState.cardId == card.cardId) {
walletState.currencies.filter {
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
}.map { it.blockchain }
} else {
currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
.filter {
it.blockchain.isEvm() &&
it.derivationPath == it.blockchain.derivationPath(card.derivationStyle)?.rawPath
}
.map { it.blockchain }
}
private fun getAvailableBlockchains(card: Card, walletState: WalletState): List<Blockchain> {
return walletState.currencies.filter {
it.isBlockchain() && !it.isCustomCurrency(card.derivationStyle) && it.blockchain.isEvm()
}.map { it.blockchain }
}
private suspend fun prepareWalletManager(
@ -270,12 +251,9 @@ class WalletConnectMiddleware {
walletState: WalletState,
blockchain: Blockchain,
session: WalletConnectSession,
walletConnectManager: WalletConnectManager,
) {
val factory = WcWalletManagerFactory(
factory = store.state.globalState.tapWalletManager.walletManagerFactory,
currenciesRepository = currenciesRepository,
)
val walletManager = factory.getWalletManager(scanResponse, blockchain, walletState).guard {
val walletManager = getWalletManager(session.wallet, blockchain, walletState).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
store.dispatchOnMain(
GlobalAction.ShowDialog(
@ -292,7 +270,6 @@ class WalletConnectMiddleware {
walletManager.wallet.publicKey.blockchainKey
}
val walletForSession = WalletForSession(
cardId = scanResponse.card.cardId,
walletPublicKey = wallet.publicKey.seedKey,
derivedPublicKey = derivedKey,
derivationPath = wallet.publicKey.derivationPath,
@ -325,17 +302,29 @@ class WalletConnectMiddleware {
NewWcSessionData(session = updatedSession, scanResponse = scanResponse, blockchain = blockchain),
),
)
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
),
)
}
scope.launch {
val blockchains = if (blockchain.isEvm()) getAvailableBlockchains(card, walletState) else emptyList()
withMainContext {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ApproveWcSession(session = updatedSession, networks = blockchains),
),
)
}
private fun getWalletManager(
wallet: WalletForSession, blockchain: Blockchain, walletState: WalletState,
): WalletManager? {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && wallet.isTestNet) {
Blockchain.EthereumTestnet
} else {
blockchain
}
val derivation = blockchainToMake.derivationPath(store.state.globalState.scanResponse?.card?.derivationStyle)
?.rawPath
val blockchainNetwork = BlockchainNetwork(
blockchain = blockchainToMake,
derivationPath = derivation,
tokens = emptyList(),
)
return walletState.getWalletManager(blockchainNetwork)
}
}

View file

@ -10,6 +10,7 @@ class WalletConnectReducer {
if (action !is WalletConnectAction) return state
return when (action) {
is WalletConnectAction.ResetState -> return WalletConnectState()
is WalletConnectAction.ApproveSession.Success -> {
state.copy(
loading = false,

View file

@ -40,7 +40,6 @@ data class WalletConnectSession(
@JsonClass(generateAdapter = true)
data class WalletForSession(
val cardId: String,
val walletPublicKey: ByteArray? = null,
val derivedPublicKey: ByteArray? = null,
val derivationPath: DerivationPath? = null,
@ -59,7 +58,6 @@ data class WalletForSession(
other as WalletForSession
if (cardId != other.cardId) return false
if (walletPublicKey != null) {
if (other.walletPublicKey == null) return false
if (!walletPublicKey.contentEquals(other.walletPublicKey)) return false
@ -76,8 +74,7 @@ data class WalletForSession(
}
override fun hashCode(): Int {
var result = cardId.hashCode()
result = 31 * result + (walletPublicKey?.contentHashCode() ?: 0)
var result = (walletPublicKey?.contentHashCode() ?: 0)
result = 31 * result + (derivedPublicKey?.contentHashCode() ?: 0)
result = 31 * result + (derivationPath?.hashCode() ?: 0)
result = 31 * result + isTestNet.hashCode()
@ -112,7 +109,6 @@ sealed class WalletConnectDialog : StateDialog {
val data: BinanceMessageData,
val session: WCSession,
val sessionId: Long,
val cardId: String,
val dAppName: String,
) : WalletConnectDialog()
}

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -86,7 +87,7 @@ fun WalletConnectDetailsItem(
) {
Row(
modifier = modifier
.height(84.dp)
.defaultMinSize(minHeight = 84.dp)
.fillMaxWidth()
.clickable { onItemsClick(SettingsElement.WalletConnect) },
horizontalArrangement = Arrangement.Start,
@ -99,7 +100,7 @@ fun WalletConnectDetailsItem(
tint = colorResource(id = R.color.all_colors_azure),
)
Column(
modifier = modifier.height(56.dp),
modifier = modifier.defaultMinSize(minHeight = 56.dp),
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Center,
) {

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -89,11 +91,14 @@ fun ResetCardView(
color = colorResource(id = R.color.text_secondary),
)
Spacer(modifier = modifier.size(44.dp))
Spacer(modifier = modifier.size(28.dp))
Row(
modifier = modifier
.fillMaxWidth()
.padding(end = 20.dp),
.clickable(
onClick = { state.onAcceptWarningToggleClick(!state.accepted) },
)
.padding(top = 16.dp, bottom = 16.dp),
) {
IconToggleButton(
checked = state.accepted,
@ -120,13 +125,15 @@ fun ResetCardView(
text = stringResource(id = R.string.reset_card_to_factory_warning_message),
style = TangemTypography.body2,
color = colorResource(id = R.color.text_secondary),
modifier = modifier
.padding(end = 20.dp),
)
}
Spacer(modifier = modifier.size(32.dp))
Spacer(modifier = modifier.size(16.dp))
Box(
modifier = modifier
.padding(start = 16.dp, end = 16.dp),
.padding(start = 16.dp, end = 16.dp, bottom = 32.dp),
) {
DetailsMainButton(
title = stringResource(id = R.string.reset_card_to_factory_button_title),

View file

@ -9,6 +9,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import com.google.zxing.Result
import com.otaliastudios.cameraview.CameraView
@ -23,6 +24,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
@ -54,7 +56,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result.text))
}

View file

@ -1,55 +0,0 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
class WalletConnectSessionsAdapter
: ListAdapter<WalletConnectSession, WalletConnectSessionsAdapter.SessionsViewHolder>(
DiffUtilCallback
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SessionsViewHolder {
val layout = LayoutInflater.from(parent.context)
.inflate(R.layout.item_wallet_connect_session, parent, false)
return SessionsViewHolder(layout)
}
override fun onBindViewHolder(holder: SessionsViewHolder, position: Int) {
holder.bind(currentList[position])
}
object DiffUtilCallback : DiffUtil.ItemCallback<WalletConnectSession>() {
override fun areContentsTheSame(
oldItem: WalletConnectSession, newItem: WalletConnectSession,
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: WalletConnectSession, newItem: WalletConnectSession,
) = oldItem == newItem
}
class SessionsViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(session: WalletConnectSession) {
view.findViewById<TextView>(R.id.tv_card_id).text = view.context.getString(
R.string.wallet_connect_card_number, session.wallet.cardId
)
view.findViewById<TextView>(R.id.tv_d_app_name).text = session.peerMeta.name
view.findViewById<Chip>(R.id.btn_disconnect).setOnClickListener {
store.dispatch(WalletConnectAction.DisconnectSession(session.session))
}
}
}
}

View file

@ -140,13 +140,12 @@ private fun WalletConnectSessions(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = modifier) {
Text(
text = session.description,
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
}
Text(
text = session.description,
style = TangemTypography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
modifier = modifier.weight(1f),
)
IconButton(
onClick = { state.onRemoveSession(session.sessionId) },
) {
@ -169,7 +168,6 @@ fun WalletConnectScreenPreview() {
sessions = listOf(
WcSessionForScreen(
description = "session from some dApp",
cardId = "12312312321",
sessionId = "",
),
),

View file

@ -11,14 +11,12 @@ data class WalletConnectScreenState(
data class WcSessionForScreen(
val description: String,
val cardId: String,
val sessionId: String,
) {
companion object {
fun fromSession(session: WalletConnectSession): WcSessionForScreen {
return WcSessionForScreen(
description = session.peerMeta.name,
cardId = session.wallet.cardId,
sessionId = session.session.toUri(),
)
}

View file

@ -14,7 +14,6 @@ class ApproveWcSessionDialog {
fun create(session: WalletConnectSession, networks: List<Blockchain>, context: Context): AlertDialog {
val message = context.getString(
R.string.wallet_connect_request_session_start,
session.wallet.cardId,
session.peerMeta.name,
session.peerMeta.url,
)

View file

@ -15,7 +15,6 @@ class BnbTransactionDialog {
data: BinanceMessageData,
session: WCSession,
sessionId: Long,
cardId: String,
dAppName: String,
context: Context,
): AlertDialog {
@ -40,7 +39,7 @@ class BnbTransactionDialog {
val fullMessage = context.getString(
R.string.wallet_connect_bnb_sign_message,
dAppName, cardId, message
dAppName, message,
)
val positiveButtonTitle = context.getText(R.string.common_sign)

View file

@ -15,10 +15,12 @@ class PersonalSignDialog {
context: Context,
): AlertDialog {
val message =
context.getString(R.string.wallet_connect_alert_sign_message, data.cardId) +
context.getString(R.string.wallet_connect_personal_sign_message,
data.dAppName,
data.message)
context.getString(R.string.wallet_connect_alert_sign_message) +
context.getString(
R.string.wallet_connect_personal_sign_message,
data.dAppName,
data.message,
)
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.wallet_connect))
setMessage(message)
@ -37,7 +39,6 @@ class PersonalSignDialog {
}
data class PersonalSignDialogData(
val cardId: String,
val dAppName: String,
val message: String,
val session: WCSession,

View file

@ -17,7 +17,6 @@ class TransactionDialog {
): AlertDialog {
val message = context.getString(
R.string.wallet_connect_create_tx_message,
data.cardId,
data.dAppName,
data.dAppUrl,
data.amount,
@ -53,7 +52,6 @@ class TransactionDialog {
}
data class TransactionRequestDialogData(
val cardId: String,
val dAppName: String,
val dAppUrl: String,
val amount: String,

View file

@ -45,7 +45,6 @@ data class ButtonData(
interface DialogMessageData
data class WcTransactionDialogMessageData(
val cardId: String,
val dAppName: String,
val dAppUrl: String,
val amount: String,

View file

@ -78,30 +78,35 @@ private fun handleReadCard() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard(onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(
GlobalAction.ScanCard(
onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo)
} else {
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo)
} else {
scope.launch {
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
}
store.onCardScanned(scanResponse)
}
}
}
}
}, onFailure = {
changeButtonState(ButtonState.ENABLED)
}))
},
onFailure = {
changeButtonState(ButtonState.ENABLED)
},
),
)
}
}

View file

@ -79,17 +79,21 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
val primaryBlockchain = updatedResponse.getBlockchain()
val blockchainNetworks = if (primaryBlockchain != Blockchain.Unknown) {
val primaryToken = updatedResponse.getPrimaryToken()
val blockchainNetwork = BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
listOfNotNull(primaryToken))
val blockchainNetwork =
BlockchainNetwork(primaryBlockchain, updatedResponse.card).updateTokens(
listOfNotNull(primaryToken),
)
listOf(blockchainNetwork)
} else {
listOf(
BlockchainNetwork(Blockchain.Bitcoin, updatedResponse.card),
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card)
BlockchainNetwork(Blockchain.Ethereum, updatedResponse.card),
)
}
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
store.dispatch(
WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks, updatedResponse.card),
)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))

View file

@ -4,9 +4,9 @@ import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.AssetReader
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action

View file

@ -18,7 +18,6 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.twins.TwinCardsManager
@ -271,7 +270,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
currenciesRepository.removeCurrencies(scanResponse.card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}

View file

@ -15,10 +15,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.AndroidAssetReader
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.navigation.AppScreen
@ -26,7 +26,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
@ -46,13 +45,6 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private val assetReader: AssetReader by lazy {
object : AssetReader {
override fun readAssetAsString(name: String): String =
requireContext().readAssetAsString(name)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
@ -250,7 +242,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
assetReader
AndroidAssetReader(requireContext()),
)
)
}

View file

@ -94,8 +94,8 @@ private fun handleWalletAction(action: Action) {
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks,
cardId = result.data.card.cardId
)
card = result.data.card,
),
)
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingWalletAction.ProceedBackup)
@ -273,15 +273,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
backupService.proceedBackup { result ->
when (result) {
is CompletionResult.Success -> {
val blockchainNetworks = listOf(
BlockchainNetwork(Blockchain.Bitcoin, result.data),
BlockchainNetwork(Blockchain.Ethereum, result.data)
)
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks, cardId = result.data.cardId
)
)
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {

View file

@ -113,15 +113,11 @@ sealed class FeeActionUi : SendScreenActionUi {
}
sealed class FeeAction : SendScreenAction {
enum class Error {
ADDRESS_OR_AMOUNT_IS_EMPTY,
REQUEST_FAILED
}
object RequestFee : FeeAction()
sealed class FeeCalculation : FeeAction() {
data class SetFeeResult(val fee: List<Amount>) : FeeCalculation()
data class SetFeeError(val error: Error) : FeeCalculation()
object ClearResult : FeeCalculation()
}
data class ChangeLayoutVisibility(
@ -160,6 +156,12 @@ sealed class SendAction : SendScreenAction {
data class CardSdkError(val error: TangemSdkError): Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError): Dialog()
}
data class RequestFeeError(
val error: com.tangem.blockchain.common.BlockchainSdkError,
val onRetry: () -> Unit,
) : Dialog()
object Hide : Dialog()
}

View file

@ -4,11 +4,17 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionError
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import org.rekotlin.Action
import java.math.BigDecimal
import java.util.*
/**
[REDACTED_AUTHOR]
@ -62,18 +68,11 @@ class AmountMiddleware {
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto))
val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee)
transactionErrors.remove(TransactionError.TezosSendAll)
if (transactionErrors.isEmpty()) {
val amountFieldErrors = filterErrorsForAmountField(transactionErrors)
if (amountFieldErrors.isEmpty()) {
dispatch(AmountAction.SetAmountError(null))
} else {
val amountErrors = extractErrorsForAmountField(transactionErrors)
if (amountErrors.isNotEmpty()) {
transactionErrors.removeAll(amountErrors)
dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountErrors, walletManager)))
}
if (transactionErrors.isNotEmpty()) {
dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager)))
}
dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountFieldErrors, walletManager)))
}
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
@ -104,4 +103,29 @@ class AmountMiddleware {
dispatch(AmountActionUi.SetMainCurrency(type))
}
}
private fun filterErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {
when (it) {
TransactionError.AmountExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.FeeExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.TotalExceedsBalance -> {
val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance)
if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it)
showIntoAmountField.remove(TransactionError.AmountLowerExistentialDeposit)
}
else -> showIntoAmountField.add(it)
}
}
showIntoAmountField.remove(TransactionError.TezosSendAll)
return showIntoAmountField
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
@ -31,8 +32,7 @@ class RequestFeeMiddleware {
val scanResponse = appState.globalState.scanResponse ?: return
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
// dispatch(FeeAction.ChangeLayoutVisibility(main = false, chipGroup = true))
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
@ -66,14 +66,21 @@ class RequestFeeMiddleware {
}
}
is Result.Failure -> {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.REQUEST_FAILED))
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext
dispatch(
SendAction.Dialog.RequestFeeError(
error = blockchainSdkError,
onRetry = { dispatch(FeeAction.RequestFee) },
),
)
}
}
dispatch(AmountActionUi.CheckAmountToSend)
}
}
}
}
@ -93,13 +100,13 @@ class FeeMock {
suspend fun feeStandard(blockchain: Blockchain): List<Amount> = listOf(
Amount(0.001500.toBigDecimal(), blockchain),
Amount(0.0030.toBigDecimal(), blockchain),
Amount(0.0045001.toBigDecimal(), blockchain)
Amount(0.0045001.toBigDecimal(), blockchain),
)
suspend fun feeStandardBig(blockchain: Blockchain): List<Amount> = listOf(
Amount(1.76.toBigDecimal(), blockchain),
Amount(2.30.toBigDecimal(), blockchain),
Amount(3.45.toBigDecimal(), blockchain)
Amount(3.45.toBigDecimal(), blockchain),
)
suspend fun feeSingle(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0015.toBigDecimal(), blockchain))

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.Amount
@ -114,9 +115,8 @@ private fun verifyAndSendTransaction(
val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend())
val transactionErrors = walletManager.validateTransaction(amountToSend, feeAmount)
val hadTezosError = transactionErrors.remove(TransactionError.TezosSendAll)
when {
hadTezosError -> {
transactionErrors.contains(TransactionError.TezosSendAll) -> {
val reduceAmount = walletManager.wallet.blockchain.minimalAmount()
dispatch(
SendAction.Dialog.TezosWarningDialog(
@ -135,9 +135,6 @@ private fun verifyAndSendTransaction(
),
)
}
transactionErrors.isNotEmpty() -> {
dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager)))
}
else -> {
sendTransaction(
action, walletManager, amountToSend, feeAmount, destinationAddress,
@ -249,8 +246,7 @@ private fun sendTransaction(
}
is SimpleResult.Failure -> {
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
wallet = walletManager.wallet,
host = walletManager.currentHost,
walletManager = walletManager,
amountToSend = amountToSend,
feeAmount = feeAmount,
destinationAddress = destinationAddress,
@ -272,10 +268,8 @@ private fun sendTransaction(
dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError))
}
is BlockchainSdkError.CreateAccountUnderfunded -> {
// from XLM, XRP
val reserve = error.minReserve.value?.stripZeroPlainString() ?: "0"
val symbol = error.minReserve.currencySymbol
dispatch(SendAction.SendError(TapError.CreateAccountUnderfunded(listOf(reserve, symbol))))
// from XLM, XRP, Polkadot
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error))
}
else -> {
when {
@ -300,29 +294,6 @@ private fun sendTransaction(
}
}
fun extractErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<TransactionError> {
val showIntoAmountField = EnumSet.noneOf(TransactionError::class.java)
errors.forEach {
when (it) {
TransactionError.AmountExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.FeeExceedsBalance -> {
showIntoAmountField.remove(TransactionError.TotalExceedsBalance)
showIntoAmountField.add(it)
}
TransactionError.TotalExceedsBalance -> {
val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance)
if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it)
}
TransactionError.InvalidAmountValue -> showIntoAmountField.add(it)
TransactionError.InvalidFeeValue -> showIntoAmountField.add(it)
}
}
return showIntoAmountField
}
fun createValidateTransactionError(
errorList: EnumSet<TransactionError>,
walletManager: WalletManager,
@ -330,12 +301,21 @@ fun createValidateTransactionError(
val tapErrors = errorList.map {
when (it) {
TransactionError.AmountExceedsBalance -> TapError.AmountExceedsBalance
TransactionError.AmountLowerExistentialDeposit -> {
if (walletManager is ExistentialDepositProvider) {
val args = listOf(walletManager.getExistentialDeposit().stripZeroPlainString())
TapError.AmountLowerExistentialDeposit(args)
} else {
TapError.UnknownError
}
}
TransactionError.FeeExceedsBalance -> TapError.FeeExceedsBalance
TransactionError.TotalExceedsBalance -> TapError.TotalExceedsBalance
TransactionError.InvalidAmountValue -> TapError.InvalidAmountValue
TransactionError.InvalidFeeValue -> TapError.InvalidFeeValue
TransactionError.DustAmount -> {
TapError.DustAmount(listOf(walletManager.dustValue?.stripZeroPlainString() ?: "0"))
val args = listOf(walletManager.dustValue?.stripZeroPlainString() ?: "0")
TapError.DustAmount(args)
}
TransactionError.DustChange -> TapError.DustChange
else -> TapError.UnknownError

View file

@ -1,12 +1,9 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.FeePrecision
import com.tangem.tap.features.send.redux.states.FeeState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.SendState
@ -27,10 +24,9 @@ class FeeReducer : SendInternalReducer {
is FeeActionUi.ChangeSelectedFee -> {
val currentFee = createValueOfFeeAmount(action.feeType, state.feeList)
state.copy(
selectedFeeType = action.feeType,
currentFee = currentFee
selectedFeeType = action.feeType,
currentFee = currentFee,
)
}
is FeeActionUi.ChangeIncludeFee -> state.copy(feeIsIncluded = action.isIncluded)
}
@ -40,14 +36,14 @@ class FeeReducer : SendInternalReducer {
private fun handleAction(action: FeeAction, sendState: SendState, state: FeeState): SendState {
val result = when (action) {
is FeeAction.RequestFee -> {
state.copy(error = null)
state
}
is FeeAction.ChangeLayoutVisibility -> {
fun getVisibility(current: Boolean, proposed: Boolean?): Boolean = proposed ?: current
state.copy(
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup)
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup),
)
}
is FeeAction.FeeCalculation.SetFeeResult -> {
@ -57,30 +53,27 @@ class FeeReducer : SendInternalReducer {
val currentFee = createValueOfFeeAmount(feeType, fees)
state.copy(
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
error = null,
feePrecision = getFeePrecision(sendState)
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
feeIsApproximate = isFeeApproximate(sendState),
)
} else {
val feeType = getCurrentFeeType(state)
val currentFee = createValueOfFeeAmount(feeType, fees)
state.copy(
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
error = null,
feePrecision = getFeePrecision(sendState)
selectedFeeType = feeType,
feeList = fees,
currentFee = currentFee,
feeIsApproximate = isFeeApproximate(sendState),
)
}
}
is FeeAction.FeeCalculation.SetFeeError -> {
FeeAction.FeeCalculation.ClearResult -> {
state.copy(
feeList = null,
currentFee = null,
error = action.error
feeList = null,
currentFee = null,
)
}
}
@ -103,26 +96,13 @@ class FeeReducer : SendInternalReducer {
}
}
private fun getFeePrecision(sendState: SendState): FeePrecision {
val blockchain = sendState.walletManager?.wallet?.blockchain
private fun isFeeApproximate(sendState: SendState): Boolean {
val blockchain = sendState.walletManager?.wallet?.blockchain ?: return false
val amountType = sendState.amountState.typeOfAmount
val canBeLower = when (blockchain) {
Blockchain.Tron, Blockchain.TronTestnet -> amountType is AmountType.Token
Blockchain.Fantom, Blockchain.FantomTestnet -> amountType is AmountType.Token
Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> true
Blockchain.Stellar, Blockchain.StellarTestnet -> true
else -> false
}
return if (canBeLower) {
FeePrecision.CAN_BE_LOWER
} else {
FeePrecision.PRECISE
}
return blockchain.isFeeApproximate(amountType)
}
private fun getCurrentFeeType(state: FeeState): FeeType {
return if (state.selectedFeeType == FeeType.SINGLE) FeeType.NORMAL else state.selectedFeeType
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.features.send.redux.states.ReceiptSymbols
import com.tangem.tap.features.send.redux.states.ReceiptTokenCrypto
import com.tangem.tap.features.send.redux.states.ReceiptTokenFiat
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.CAN_BE_LOWER_SIGN
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.store
import java.math.BigDecimal
@ -216,6 +217,8 @@ class ReceiptReducer : SendInternalReducer {
}
}
private fun String.addPrecisionSign(): String =
("${feeState.feePrecision.symbol} $this").trim()
private fun String.addPrecisionSign(): String {
val result = if (feeState.feeIsApproximate) "$CAN_BE_LOWER_SIGN $this" else this
return result.trim()
}
}

View file

@ -59,10 +59,10 @@ private class SendReducer : SendInternalReducer {
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
is SendAction.SendSpecificTransaction ->
handleSendSpecificTransactionAction(action, sendState)
is SendAction.SendSpecificTransaction -> handleSendSpecificTransactionAction(action, sendState)
else -> return sendState
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.send.redux.states
import com.tangem.blockchain.common.Amount
import com.tangem.tap.features.send.redux.FeeAction
import java.math.BigDecimal
/**
@ -11,26 +10,21 @@ enum class FeeType {
SINGLE, LOW, NORMAL, PRIORITY
}
enum class FeePrecision(val symbol: String) {
PRECISE(""), CAN_BE_LOWER("<")
}
data class FeeState(
val selectedFeeType: FeeType = FeeType.NORMAL,
val feeList: List<Amount>? = null,
val currentFee: Amount? = null,
val feeIsIncluded: Boolean = false,
val mainLayoutIsVisible: Boolean = false,
val controlsLayoutIsVisible: Boolean = false,
val feeChipGroupIsVisible: Boolean = true,
val includeFeeSwitcherIsEnabled: Boolean = true,
val error: FeeAction.Error? = null,
val feePrecision: FeePrecision = FeePrecision.PRECISE
val selectedFeeType: FeeType = FeeType.NORMAL,
val feeList: List<Amount>? = null,
val currentFee: Amount? = null,
val feeIsIncluded: Boolean = false,
val feeIsApproximate: Boolean = false,
val mainLayoutIsVisible: Boolean = false,
val controlsLayoutIsVisible: Boolean = false,
val feeChipGroupIsVisible: Boolean = true,
val includeFeeSwitcherIsEnabled: Boolean = true,
) : SendScreenState {
override val stateId: StateId = StateId.FEE
fun isReady(): Boolean = error == null && currentFee != null
fun isReady(): Boolean = currentFee != null
fun getCurrentFeeValue(): BigDecimal = currentFee?.value ?: BigDecimal.ZERO
}

View file

@ -0,0 +1,32 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class RequestFeeErrorDialog {
companion object {
fun create(context: Context, dialog: SendAction.Dialog.RequestFeeError): AlertDialog {
val errorMessage = dialog.error.customMessage
return AlertDialog.Builder(context).apply {
setTitle(R.string.send_error_fee_request_failed)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage))
setNegativeButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage)))
}
setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() }
setNeutralButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}
}

View file

@ -2,7 +2,11 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tangem_sdk_new.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.send.redux.SendAction
@ -13,14 +17,14 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
class SendTransactionFailsDialog {
companion object {
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog {
return create(context, dialog.error.localizedDescription(context))
}
fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog {
return create(context, dialog.error.customMessage)
val errorConverter = BlockchainSdkErrorConverter(context)
return create(context, errorConverter.convert(dialog.error))
}
private fun create(context: Context, errorMessage: String): AlertDialog {
@ -35,4 +39,24 @@ class SendTransactionFailsDialog {
}.create()
}
}
}
private class BlockchainSdkErrorConverter(
private val context: Context,
) : ModuleMessageConverter<BlockchainSdkError, String> {
override fun convert(message: BlockchainSdkError): String {
return when (message) {
is BlockchainSdkError.CreateAccountUnderfunded -> {
val resStringId = when (message.blockchain) {
Blockchain.Polkadot, Blockchain.PolkadotTestnet -> R.string.no_account_polkadot
else -> R.string.send_error_no_target_account
}
val reserveValueString = message.minReserve.value?.stripZeroPlainString() ?: "0"
val argument = "$reserveValueString ${message.minReserve.currencySymbol}"
context.getString(resStringId, argument)
}
else -> message.customMessage
}
}
}

View file

@ -19,7 +19,6 @@ import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.AddressPayIdState
import com.tangem.tap.features.send.redux.states.AmountState
@ -33,12 +32,12 @@ import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.RequestFeeErrorDialog
import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.redux.WalletState.Companion.ROUGH_SIGN
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
/**
@ -60,10 +59,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
when (it) {
StateId.SEND_SCREEN -> handleSendScreen(fg, state)
StateId.ADDRESS_PAY_ID -> handleAddressPayIdState(fg, state.addressPayIdState)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(
fg,
state.transactionExtrasState
)
StateId.TRANSACTION_EXTRAS -> handleTransactionExtrasState(fg, state.transactionExtrasState)
StateId.AMOUNT -> handleAmountState(fg, state.amountState)
StateId.FEE -> handleFeeState(fg, state.feeState)
StateId.RECEIPT -> handleReceiptState(fg, state.receiptState)
@ -85,7 +81,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
if (it.error != null) {
if (it.error == TransactionExtraError.INVALID_XLM_MEMO) {
tilXlmMemo.error = fg.getText(R.string.send_error_invalid_memo)
tilXlmMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
}
} else {
tilXlmMemo.error = null
@ -95,7 +91,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
if (infoState.xrpDestinationTag.error != null) {
if (infoState.xrpDestinationTag.error == TransactionExtraError.INVALID_DESTINATION_TAG) {
tilDestinationTag.error =
fg.getText(R.string.send_error_invalid_destination_tag)
fg.getText(R.string.send_extras_error_invalid_destination_tag)
}
} else {
tilDestinationTag.error = null
@ -107,7 +103,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
infoState.binanceMemo?.let {
if (infoState.binanceMemo.error != null) {
if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) {
tilBinanceMemo.error = fg.getText(R.string.send_error_invalid_memo)
tilBinanceMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
}
} else {
tilBinanceMemo.error = null
@ -138,6 +134,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
dialog?.show()
}
}
is SendAction.Dialog.RequestFeeError -> {
if (dialog == null) {
dialog = RequestFeeErrorDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
else -> {
dialog?.dismiss()
dialog = null
@ -155,7 +157,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
toolbar.title = fg.getString(
R.string.send_title_currency_format,
state.amountState.mainCurrency.currencySymbol
state.amountState.mainCurrency.currencySymbol,
)
}
@ -227,11 +229,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
val balanceText = when (state.mainCurrency.type) {
MainCurrencyType.FIAT -> fg.getString(
R.string.send_balance_subtitle_format,
state.viewBalanceValue, state.mainCurrency.currencySymbol
state.viewBalanceValue, state.mainCurrency.currencySymbol,
).remove(":")
MainCurrencyType.CRYPTO -> fg.getString(
R.string.send_balance_subtitle_format,
state.mainCurrency.currencySymbol, state.viewBalanceValue
state.mainCurrency.currencySymbol, state.viewBalanceValue,
)
}
@ -257,14 +259,6 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
swIncludeFee.isChecked = state.feeIsIncluded
}
if (state.error == FeeAction.Error.REQUEST_FAILED) {
fg.showRetrySnackbar(
fg.requireContext().getString(R.string.send_error_fee_request_failed)
) {
store.dispatch(FeeAction.RequestFee)
}
}
val chipId = FeeUiHelper.toId(state.selectedFeeType)
if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId)
}
@ -293,7 +287,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
val willSent = getString(
R.string.send_total_subtitle_format,
receipt.willSentCrypto, receipt.symbols.crypto
receipt.willSentCrypto, receipt.symbols.crypto,
)
llTotalContainer.tvWillBeSentValue.update(willSent)
@ -328,7 +322,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
val willSent = getString(
R.string.send_total_subtitle_asset_format,
receipt.symbols.token ?: "", receipt.willSentToken,
receipt.symbols.crypto, receipt.willSentFeeCoin
receipt.symbols.crypto, receipt.willSentFeeCoin,
)
llTotalContainer.tvWillBeSentValue.update(willSent)
}

View file

@ -19,7 +19,8 @@ import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
@ -32,6 +33,9 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -77,7 +81,7 @@ class TokensMiddleware {
val loadCoinsService = LoadAvailableCoinsService(
store.state.domainNetworks.tangemTechService,
currenciesRepository
assetReader,
)
scope.launch {
@ -285,22 +289,28 @@ class TokensMiddleware {
else -> DerivationParams.Custom(derivationPath)
}
}
val walletManager = factory.makeWalletManagerForApp(
scanResponse = scanResponse,
blockchain = currency.blockchain,
derivationParams = derivationParams
derivationParams = derivationParams,
) ?: return@mapNotNull null
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager)
WalletAction.MultiWallet.AddBlockchain(
blockchain = blockchainNetwork,
walletManager = walletManager,
save = true,
)
}
is Currency.Token -> {
val rawDerivationPath = currency.derivationPath
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
val blockchainNetwork =
BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
BlockchainNetwork(currency.blockchain, rawDerivationPath, listOf(currency.token))
WalletAction.MultiWallet.AddToken(
token = currency.token,
blockchain = blockchainNetwork,
save = true,
)
}
}
}

View file

@ -24,7 +24,10 @@ fun CurrenciesWarning() {
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(16.dp)
.background(Color(0xFFF2F2F2), shape = RoundedCornerShape(10.dp))
.background(
color = Color(0xFFF2F2F2),
shape = RoundedCornerShape(10.dp),
),
)
{
val warning = stringResource(id = R.string.alert_manage_tokens_addresses_message)
@ -33,18 +36,18 @@ fun CurrenciesWarning() {
AnnotatedString.Range(
SpanStyle(fontWeight = FontWeight.Bold),
start = 0,
end = end
)
end = end,
),
)
Text(
text = AnnotatedString(text = warning, spanStyles = spanStyles),
textAlign = TextAlign.Center,
textAlign = TextAlign.Start,
color = Color(0xFF848488),
modifier = Modifier.padding(
top = 8.dp,
bottom = 8.dp,
start = 16.dp,
end = 16.dp
end = 16.dp,
),
fontSize = 13.sp,
fontWeight = FontWeight.Normal,

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@ -50,7 +51,7 @@ fun CurrencyItemHeader(
Row(
modifier = Modifier
.fillMaxWidth()
.height(62.dp)
.heightIn(min = 62.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
@ -172,8 +173,7 @@ private fun BlockchainNetworkItem(
addedBlockchains: List<Blockchain>,
) {
val added = addedBlockchains.contains(blockchain)
val icon =
if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
val icon = if (added) blockchain?.getRoundIconRes() else blockchain?.getGreyedOutIconRes()
if (icon != null) {
Box(
Modifier

View file

@ -1,14 +1,18 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.network.api.tangemTech.TokenResponse
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
sealed interface Currency {
val coinId: String?
get() = when (this) {
is Blockchain -> blockchain.toCoinId()
@ -17,24 +21,28 @@ sealed interface Currency {
val blockchain: com.tangem.blockchain.common.Blockchain
val currencySymbol: CryptoCurrencyName
val derivationPath: String?
val currencyName: String
get() = when (this) {
is Blockchain -> blockchain.fullName
is Token -> token.name
}
val decimals
get() = when (this) {
is Blockchain -> blockchain.decimals()
is Token -> token.decimals
}
data class Token(
val token: com.tangem.blockchain.common.Token,
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
override val derivationPath: String?,
) : Currency {
override val currencySymbol = token.symbol
}
data class Blockchain(
override val blockchain: com.tangem.blockchain.common.Blockchain,
override val derivationPath: String?
override val derivationPath: String?,
) : Currency {
override val currencySymbol: CryptoCurrencyName = blockchain.currency
}
@ -48,24 +56,35 @@ sealed interface Currency {
}
fun isBlockchain(): Boolean = this is Blockchain
fun isToken(): Boolean = this is Token
fun toTokenResponse(): TokenResponse {
return TokenResponse(
id = coinId ?: "",
networkId = blockchain.toNetworkId(),
derivationPath = derivationPath ?: DERIVATION_PATH_RAW_VALUE,
name = currencyName,
symbol = currencySymbol,
decimals = decimals,
contractAddress = if (this is Token) token.contractAddress else null,
)
}
companion object {
private const val DERIVATION_PATH_RAW_VALUE = "m/44/0'/0/0"
fun fromBlockchainNetwork(
blockchainNetwork: BlockchainNetwork,
token: com.tangem.blockchain.common.Token? = null
token: com.tangem.blockchain.common.Token? = null,
): Currency {
return if (token != null) {
Token(
token = token,
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
derivationPath = blockchainNetwork.derivationPath,
)
} else {
Blockchain(
blockchain = blockchainNetwork.blockchain,
derivationPath = blockchainNetwork.derivationPath
derivationPath = blockchainNetwork.derivationPath,
)
}
}
@ -74,7 +93,7 @@ sealed interface Currency {
return when (customCurrency) {
is CustomCurrency.CustomBlockchain -> Blockchain(
blockchain = customCurrency.network,
derivationPath = customCurrency.derivationPath?.rawPath
derivationPath = customCurrency.derivationPath?.rawPath,
)
is CustomCurrency.CustomToken -> Token(
token = customCurrency.token,
@ -88,8 +107,53 @@ sealed interface Currency {
return Token(
token = tokenWithBlockchain.token,
blockchain = tokenWithBlockchain.blockchain,
derivationPath = null
derivationPath = null,
)
}
fun fromTokenResponse(tokenResponse: TokenResponse): Currency {
val derivationPath = if (tokenResponse.derivationPath == DERIVATION_PATH_RAW_VALUE) {
null
} else {
tokenResponse.derivationPath
}
return when {
tokenResponse.contractAddress != null -> Token(
com.tangem.blockchain.common.Token(
name = tokenResponse.name,
symbol = tokenResponse.symbol,
contractAddress = tokenResponse.contractAddress!!,
decimals = tokenResponse.decimals,
id = tokenResponse.id,
),
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
derivationPath = derivationPath,
)
else -> Blockchain(
blockchain = com.tangem.blockchain.common.Blockchain.fromNetworkId(tokenResponse.networkId)!!,
derivationPath = derivationPath,
)
}
}
}
}
}
fun BlockchainNetwork.toCurrencies(): List<Currency> {
val blockchain = Currency.fromBlockchainNetwork(this)
val tokens = this.tokens.map { Currency.fromBlockchainNetwork(this, it) }
return listOf(blockchain) + tokens
}
fun List<BlockchainNetwork>.toCurrencies(): List<Currency> {
return flatMap { it.toCurrencies() }
}
fun List<Currency>.toBlockchainNetworks(): List<BlockchainNetwork> {
return this.filter { it.isBlockchain() }.map { BlockchainNetwork(it.blockchain, it.derivationPath, getTokens(it)) }
}
private fun List<Currency>.getTokens(currency: Currency): List<Token> {
return this
.filter { it.isToken() && it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
.mapNotNull { if (it is Currency.Token) it.token else null }
}

View file

@ -1,6 +1,11 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.isAboveZero
import com.tangem.tap.common.extensions.toFormattedString
import java.math.BigDecimal
@ -10,8 +15,8 @@ data class PendingTransaction(
val type: PendingTransactionType,
) {
val address: String? = when (type) {
PendingTransactionType.Incoming -> transactionData.sourceAddress
PendingTransactionType.Outgoing -> transactionData.destinationAddress
PendingTransactionType.Incoming -> nullIfUnknown(transactionData.sourceAddress)
PendingTransactionType.Outgoing -> nullIfUnknown(transactionData.destinationAddress)
PendingTransactionType.Unknown -> null
}
@ -20,6 +25,8 @@ data class PendingTransaction(
val amountValueUi: String? = amountValue?.toFormattedString(transactionData.amount.decimals)
val currency: String = transactionData.amount.currencySymbol
fun nullIfUnknown(address: String):String? = if (address == "unknown") null else address
}
enum class PendingTransactionType { Incoming, Outgoing, Unknown }

View file

@ -3,6 +3,11 @@ package com.tangem.tap.features.wallet.models
sealed class WalletWarning(
val showingPosition: Int,
) {
data class ExistentialDeposit(
val currencyName: String,
val edStringValueWithSymbol: String,
) : WalletWarning(1)
object TransactionInProgress : WalletWarning(10)
data class BalanceNotEnoughForFee(val blockchainFullName: String) : WalletWarning(30)
data class Rent(val walletRent: WalletRent) : WalletWarning(40)

View file

@ -31,7 +31,6 @@ sealed class WalletAction : Action {
data class Failure(val error: TapError) : WalletAction()
}
data class LoadWallet(
val blockchain: BlockchainNetwork? = null,
val walletManager: WalletManager? = null
@ -40,7 +39,7 @@ sealed class WalletAction : Action {
data class NoAccount(
val wallet: Wallet,
val blockchain: BlockchainNetwork,
val amountToCreateAccount: String
val amountToCreateAccount: String,
) : WalletAction()
data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction()
@ -48,28 +47,31 @@ sealed class WalletAction : Action {
data class SetArtworkId(val artworkId: String?) : WalletAction()
sealed class UserTokens : WalletAction() {
object Loading : UserTokens()
object Loaded : UserTokens()
}
sealed class MultiWallet : WalletAction() {
data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet()
data class AddBlockchain(
val blockchain: BlockchainNetwork,
val walletManager: WalletManager?
val walletManager: WalletManager?,
val save: Boolean,
) : MultiWallet()
data class AddBlockchains(
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>
val blockchains: List<BlockchainNetwork>, val walletManagers: List<WalletManager>, val save: Boolean,
) : MultiWallet()
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork) :
data class AddTokens(val tokens: List<Token>, val blockchain: BlockchainNetwork, val save: Boolean) :
MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork, val save: Boolean) : MultiWallet()
data class SaveCurrencies(
val blockchainNetworks: List<BlockchainNetwork>, val cardId: String? = null
val blockchainNetworks: List<BlockchainNetwork>, val card: Card? = null,
) : MultiWallet()
// object FindTokensInUse : MultiWallet()
// object FindBlockchainsInUse : MultiWallet()
data class TokenLoaded(
val amount: Amount,
@ -82,14 +84,15 @@ sealed class WalletAction : Action {
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(
val currency: Currency,
val fromScreen: AppScreen
val fromScreen: AppScreen,
) : MultiWallet()
data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet()
data class SetPrimaryToken(val token: Token) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
object ScanToGetDerivations : MultiWallet()
}
sealed class Warnings : WalletAction() {

View file

@ -19,9 +19,6 @@ import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.models.hasSendableAmounts
import com.tangem.tap.features.wallet.models.isSendableAmount
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
@ -47,6 +44,8 @@ data class WalletState(
val isTestnet: Boolean = false,
val totalBalance: TotalBalance? = null,
val showBackupWarning: Boolean = false,
val missingDerivations: List<BlockchainNetwork> = emptyList(),
val loadingUserTokens: Boolean = false,
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -131,32 +130,6 @@ data class WalletState(
return walletsData.find { it.currency == selectedCurrency }
}
fun canBeRemoved(walletData: WalletData?): Boolean {
if (walletData == null) return false
if (!isPrimaryCurrency(walletData)) {
val walletManager = getWalletManager(walletData.currency)
?: return true
if (walletData.currency is Currency.Blockchain &&
walletManager.cardTokens.isNotEmpty()
) {
return false
}
val wallet = walletManager.wallet
return when (walletData.currency) {
is Currency.Blockchain -> !wallet.hasPendingTransactions() && !wallet.hasSendableAmounts()
is Currency.Token -> {
val token = walletData.currency.token
!wallet.hasPendingTransactions(token) && !wallet.isSendableAmount(token)
}
}
}
return false
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
@ -220,21 +193,28 @@ data class WalletState(
fun removeWallet(walletData: WalletData?): WalletState {
if (walletData == null) return this
return if (walletData.currency is Currency.Blockchain) {
val walletStores = wallets.filterNot {
it.blockchainNetwork.blockchain == walletData.currency.blockchain
&& it.blockchainNetwork.derivationPath == walletData.currency.derivationPath
return when (val currency = walletData.currency) {
is Currency.Blockchain -> {
val walletStores = wallets.filterNot {
it.blockchainNetwork.blockchain == currency.blockchain
&& it.blockchainNetwork.derivationPath == currency.derivationPath
}
copy(wallets = walletStores)
.updateTotalBalance()
.updateProgressState()
}
is Currency.Token -> {
val walletStore = getWalletStore(walletData.currency)
val walletDataList = walletStore?.walletsData
?.filterNot { it.currency == walletData.currency }
?: emptyList()
val updatedWalletManager = walletStore?.walletManager?.also { it.removeToken(currency.token) }
val updatedWalletStore = walletStore?.copy(
walletsData = walletDataList,
walletManager = updatedWalletManager,
)
updateWalletStore(updatedWalletStore)
}
copy(wallets = walletStores)
.updateTotalBalance()
.updateProgressState()
} else {
val walletStore = getWalletStore(walletData.currency)
val walletDataList = walletStore?.walletsData
?.filterNot { it.currency == walletData.currency }
?: emptyList()
val updatedWalletStore = walletStore?.copy(walletsData = walletDataList)
updateWalletStore(updatedWalletStore)
}
}
@ -287,6 +267,7 @@ data class WalletState(
companion object {
const val UNKNOWN_AMOUNT_SIGN = ""
const val ROUGH_SIGN = ""
const val CAN_BE_LOWER_SIGN = "<"
}
}
@ -325,9 +306,6 @@ data class Artwork(
const val MARTA_CARD_ID = "BC02"
const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png"
const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png"
const val TEMP_CARDANO =
"https://verify.tangem.com/card/artwork?artworkId=card_ru039&CID=CB19000000040976&publicKey=0416E29423A6CC77CD07CBA52873E8F6F894B1AFB18EB3688ACC2C8D8E5AC84B80B0BA1B17B85E578E47044CE96BCFF3FB4499FA4941CAD3C1EF300A492B5B9659"
}
}
@ -342,6 +320,7 @@ data class WalletData(
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency,
val walletRent: WalletRent? = null,
val existentialDepositString: String? = null,
) {
val isAvailableToBuy: Boolean
get() = store.state.globalState.exchangeManager.availableForBuy(currency)
@ -362,18 +341,41 @@ data class WalletData(
fun assembleWarnings(): List<WalletWarning> {
val walletWarnings = mutableListOf<WalletWarning>()
assembleNonTypedWarnings(walletWarnings)
assembleBlockchainWarnings(walletWarnings)
assembleTokenWarnings(walletWarnings)
return walletWarnings.sortedBy { it.showingPosition }
}
private fun assembleNonTypedWarnings(walletWarnings: MutableList<WalletWarning>) {
if (currencyData.status == BalanceStatus.SameCurrencyTransactionInProgress) {
walletWarnings.add(WalletWarning.TransactionInProgress)
}
}
private fun assembleBlockchainWarnings(walletWarnings: MutableList<WalletWarning>) {
if (!currency.isBlockchain()) return
if (existentialDepositString != null) {
val warning = WalletWarning.ExistentialDeposit(
currencyName = currency.currencyName,
edStringValueWithSymbol = "$existentialDepositString ${currency.currencySymbol}",
)
walletWarnings.add(warning)
}
if (walletRent != null) {
walletWarnings.add(WalletWarning.Rent(walletRent))
}
if (!currency.isBlockchain() && (blockchainAmountIsEmpty() && !tokenAmountIsEmpty())) {
val fullName = currency.blockchain.fullName
walletWarnings.add(WalletWarning.BalanceNotEnoughForFee(fullName))
}
}
return walletWarnings.sortedBy { it.showingPosition }
private fun assembleTokenWarnings(walletWarnings: MutableList<WalletWarning>){
if (!currency.isToken()) return
val blockchainFullName = currency.blockchain.fullName
if ((blockchainAmountIsEmpty() && !tokenAmountIsEmpty())) {
walletWarnings.add(WalletWarning.BalanceNotEnoughForFee(blockchainFullName))
}
}
private fun blockchainAmountIsEmpty(): Boolean = currencyData.blockchainAmount?.isZero() == true

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
@ -11,18 +12,20 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.redux.reducers.toWallet
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -44,41 +47,47 @@ class MultiWalletMiddleware {
}
}
is WalletAction.MultiWallet.AddToken -> {
addTokens(listOf(action.token), action.blockchain, walletState, globalState)
addTokens(listOf(action.token), action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddTokens -> {
addTokens(action.tokens, action.blockchain, walletState, globalState)
addTokens(action.tokens, action.blockchain, walletState, globalState, action.save)
}
is WalletAction.MultiWallet.AddBlockchain -> {
action.walletManager?.let {
handleAddingWalletManagers(globalState, listOf(action.walletManager))
}
val currencies: List<Currency> =
(walletState?.currencies ?: emptyList()) + action.blockchain.toCurrencies()
globalState.scanResponse?.let {
currenciesRepository.saveUpdatedCurrency(
cardId = it.card.cardId,
blockchainNetwork = action.blockchain
)
if (action.save && globalState.scanResponse != null) {
scope.launch {
userTokensRepository.saveUserTokens(
card = globalState.scanResponse.card,
tokens = currencies,
)
}
}
store.dispatch(
WalletAction.LoadFiatRate(
coinsList = listOf(
Currency.Blockchain(
action.blockchain.blockchain,
action.blockchain.derivationPath
)
)
)
action.blockchain.derivationPath,
),
),
),
)
store.dispatch(
WalletAction.LoadWallet(
action.blockchain, action.walletManager
)
action.blockchain, action.walletManager,
),
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = action.cardId ?: globalState.scanResponse?.card?.cardId ?: return
currenciesRepository.saveCurrencies(cardId, action.blockchainNetworks)
val card = action.card ?: globalState.scanResponse?.card ?: return
scope.launch { userTokensRepository.saveUserTokens(card, action.blockchainNetworks.toCurrencies()) }
}
is WalletAction.MultiWallet.TryToRemoveWallet -> {
val currency = action.currency
@ -89,54 +98,44 @@ class MultiWalletMiddleware {
}
if (currency.isBlockchain() && walletManager.cardTokens.isNotEmpty()) {
store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol
))
store.dispatchDialogShow(
WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol,
),
)
} else {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.WalletDetails
))
store.dispatch(NavigationAction.PopBackTo())
}
))
store.dispatchDialogShow(
WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
store.dispatch(
WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.WalletDetails,
),
)
store.dispatch(NavigationAction.PopBackTo())
},
),
)
}
}
is WalletAction.MultiWallet.RemoveWallet -> {
val currency = action.currency
val cardId = globalState.scanResponse?.card?.cardId.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("cardId is NULL"))
val card = globalState.scanResponse?.card.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("card is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
when (currency) {
is Currency.Blockchain -> {
currenciesRepository.removeBlockchain(
cardId = cardId,
blockchainNetwork = BlockchainNetwork(
blockchain = currency.blockchain,
derivationPath = currency.derivationPath,
tokens = emptyList()
)
)
}
is Currency.Token -> {
val walletManager = walletState?.getWalletManager(currency)
if (walletManager != null) {
walletManager.removeToken(currency.token)
currenciesRepository.removeToken(
cardId = cardId,
token = currency.token,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
var currencies = walletState?.currencies ?: emptyList()
currencies = currencies.filterNot { it == currency }
if (currency.isBlockchain()) {
currencies
.filter { it.blockchain == currency.blockchain && it.derivationPath == currency.derivationPath }
}
scope.launch { userTokensRepository.saveUserTokens(card, currencies) }
if (action.fromScreen == AppScreen.AddTokens) {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
}
@ -148,94 +147,9 @@ class MultiWalletMiddleware {
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
}
}
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val cardFirmware = scanResponse.card.firmwareVersion
// val blockchains = currenciesRepository.getBlockchains(cardFirmware)
// .filterNot { walletState?.blockchains?.contains(it) == true }
// .map { BlockchainNetwork(it, null, emptyList()) }
// val walletManagers =
// tapWalletManager.walletManagerFactory.makeWalletManagersForApp(
// scanResponse,
// blockchains
// )
//
// scope.launch {
// walletManagers.map { walletManager ->
// async(Dispatchers.IO) {
// walletManager.safeUpdate()
// val wallet = walletManager.wallet
// val coinAmount = wallet.amounts[AmountType.Coin]?.value
// if (coinAmount != null && !coinAmount.isZero()) {
// scope.launch(Dispatchers.Main) {
// val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
// if (walletState?.getWalletData(blockchainNetwork) == null) {
// store.dispatch(WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork, walletManager
// ))
// store.dispatch(WalletAction.LoadWallet.Success(
// wallet = wallet,
// blockchain = blockchainNetwork
// ))
// }
// }
// }
// }
// }
// }
// }
// is WalletAction.MultiWallet.FindTokensInUse -> {
// val scanResponse = globalState.scanResponse ?: return
// if (scanResponse.supportsHdWallet()) return
//
// val walletFactory = tapWalletManager.walletManagerFactory
// val card = scanResponse.card
//
// val walletManager = walletState?.getWalletManager(
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
// ?: walletFactory.makeWalletManagerForApp(
// scanResponse,
// Currency.Blockchain(Blockchain.Ethereum, null)
// )
//
// val tokenFinder = walletManager as? TokenFinder ?: return
// scope.launch {
// val result = tokenFinder.findTokens()
//
// withContext(Dispatchers.Main) {
// when (result) {
// is Result.Success -> {
// if (result.data.isNotEmpty()) {
// val blockchainNetwork = BlockchainNetwork(
// walletManager.wallet.blockchain,
// walletManager.wallet.publicKey.derivationPath?.rawPath,
// walletManager.cardTokens.toList()
// )
// currenciesRepository.saveUpdatedCurrency(
// card.cardId,
// blockchainNetwork
// )
// store.dispatch(
// WalletAction.MultiWallet.AddBlockchain(
// blockchainNetwork,
// walletManager
// )
// )
// store.dispatch(
// WalletAction.MultiWallet.AddTokens(
// walletManager.cardTokens.toList(),
// blockchainNetwork
// )
// )
// }
// }
// }
// }
// }
// }
is WalletAction.MultiWallet.ScanToGetDerivations -> {
store.dispatch(WalletAction.Scan)
}
}
}
@ -249,7 +163,7 @@ class MultiWalletMiddleware {
private fun handleAddingWalletManagers(
globalState: GlobalState,
walletManagers: List<WalletManager>
walletManagers: List<WalletManager>,
) {
globalState.feedbackManager?.infoHolder?.setWalletsInfo(walletManagers)
if (globalState.scanResponse?.isDemoCard() == true) {
@ -259,50 +173,59 @@ class MultiWalletMiddleware {
private fun addTokens(
tokens: List<Token>, blockchainNetwork: BlockchainNetwork,
walletState: WalletState?, globalState: GlobalState?
walletState: WalletState?, globalState: GlobalState?,
save: Boolean,
) {
if (tokens.isEmpty()) return
val scanResponse = globalState?.scanResponse ?: return
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletState = walletState ?: return
val walletManager = walletState.getWalletManager(blockchainNetwork)?.also {
if (save) {
val wallets = tokens.mapNotNull { token -> token.toWallet(walletState, blockchainNetwork) }
val currencies = walletState.updateWalletsData(wallets).currencies
scope.launch { userTokensRepository.saveUserTokens(scanResponse.card, currencies) }
}
} ?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork.updateTokens(tokens), it, save))
}
val walletManager = walletState?.getWalletManager(blockchainNetwork)
?: wmFactory.makeWalletManagerForApp(scanResponse, blockchainNetwork)?.also {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, it))
} ?: return
store.dispatch(WalletAction.LoadFiatRate(coinsList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
)
}))
if (tokens.isNotEmpty()) walletManager.addTokens(tokens)
currenciesRepository.saveUpdatedCurrency(
cardId = scanResponse.card.cardId,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
store.dispatch(
WalletAction.LoadFiatRate(
coinsList = tokens.map { token ->
Currency.Token(
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath,
)
},
),
)
if (tokens.isNotEmpty()) walletManager?.addTokens(tokens)
scope.launch {
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,
blockchainNetwork
)
)
val result = walletManager?.safeUpdate()
withMainContext {
when (result) {
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,
blockchainNetwork,
),
)
}
}
}
else -> {}
}
is com.tangem.common.services.Result.Failure -> {}
}
}
}

View file

@ -108,10 +108,13 @@ class WalletMiddleware {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.blockchain) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null
))
store.dispatch(
WalletAction.MultiWallet.AddBlockchain(
action.blockchain,
null,
true,
),
)
store.dispatch(WalletAction.LoadWallet.Success(
action.wallet,
action.blockchain

View file

@ -1,15 +1,25 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -24,10 +34,10 @@ class MultiWalletReducer {
val walletManager = action.walletManagers.firstOrNull {
it.wallet.blockchain == blockchain.blockchain &&
(it.wallet.publicKey.derivationPath?.rawPath == blockchain.derivationPath)
} ?: return@mapNotNull null
val wallet = walletManager.wallet
}
val wallet = walletManager?.wallet
val cardToken = if (!state.isMultiwalletAllowed) {
wallet.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) }
} else {
null
}
@ -44,6 +54,7 @@ class MultiWalletReducer {
blockchain.blockchain,
blockchain.derivationPath
),
existentialDepositString = getExistentialDeposit(walletManager),
)
WalletStore(
@ -54,7 +65,7 @@ class MultiWalletReducer {
}
val selectedCurrency = if (!state.isMultiwalletAllowed) {
wallets[0].walletsData[0].currency
wallets.firstOrNull()?.walletsData?.firstOrNull()?.currency
} else {
state.selectedCurrency
}
@ -79,6 +90,7 @@ class MultiWalletReducer {
action.blockchain.blockchain,
action.blockchain.derivationPath
),
existentialDepositString = getExistentialDeposit(walletManager),
)
val walletStore = WalletStore(
walletManager = walletManager,
@ -155,9 +167,11 @@ class MultiWalletReducer {
state.copy(primaryToken = action.token)
is WalletAction.MultiWallet.SaveCurrencies -> state
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(
showBackupWarning = action.show
showBackupWarning = action.show,
)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state
}
}
@ -166,6 +180,10 @@ class MultiWalletReducer {
it.walletRent != null
}?.walletRent
}
private fun getExistentialDeposit(walletManager: WalletManager?): String? {
return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit()?.toPlainString()
}
}
private fun addTokens(

View file

@ -145,18 +145,19 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
BalanceStatus.Loading
}
if (action.blockchain == null) {
val wallets = newState.wallets.map {
it.copy(
walletsData = it.walletsData.map { walletData ->
val wallets = newState.wallets.map { walletStore ->
walletStore.copy(
walletsData = walletStore.walletsData.map { walletData ->
walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus,
status =
if (walletStore.walletManager != null) balanceStatus else BalanceStatus.Unreachable,
currency = walletData.currencyData.currency,
currencySymbol = walletData.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
)
}
},
)
}
newState = newState.copy(
@ -313,6 +314,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.AppCurrencyAction -> {
newState = appCurrencyReducer.reduce(action, newState)
}
is WalletAction.UserTokens.Loading -> newState = newState.copy(loadingUserTokens = true)
is WalletAction.UserTokens.Loaded -> newState = newState.copy(loadingUserTokens = false)
else -> { /* no-op */
}
}

View file

@ -15,10 +15,16 @@ class WalletWarningConverter(
override fun convert(message: WalletWarning): WalletWarningDescription {
val warningMessage = when (message) {
is WalletWarning.ExistentialDeposit -> {
context.getString(
R.string.warning_existential_deposit_message,
message.currencyName, message.edStringValueWithSymbol,
)
}
is WalletWarning.BalanceNotEnoughForFee -> {
context.getString(
R.string.token_details_send_blocked_fee_format,
message.blockchainFullName, message.blockchainFullName
message.blockchainFullName, message.blockchainFullName,
)
}
WalletWarning.TransactionInProgress -> {
@ -27,7 +33,7 @@ class WalletWarningConverter(
is WalletWarning.Rent -> {
context.getString(
R.string.solana_rent_warning,
message.walletRent.minRentValue, message.walletRent.rentExemptValue
message.walletRent.minRentValue, message.walletRent.rentExemptValue,
)
}
}

View file

@ -110,8 +110,12 @@ class WalletAdapter
lContent.tvExchangeRate.text = wallet.fiatRateString
?: root.getString(id = R.string.token_item_no_rate)
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
if (wallet.walletAddresses != null) {
cardWallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
}
} else {
cardWallet.setOnClickListener(null)
}
}
}

View file

@ -22,15 +22,20 @@ class CurrencyIconView @JvmOverloads constructor(
LayoutInflater.from(context),
this,
)
val currencyImageView: ImageFilterView
get() = binding.ivCurrency
val currencyTextView: TextView
get() = binding.tvTokenLetter
val blockchainBadge: ImageFilterView
get() = binding.ivBlockchainBadge
var isBlockchainBadgeVisible: Boolean
get() = binding.ivBlockchainBadge.isVisible
set(value) = binding.ivBlockchainBadge::isVisible.set(value)
var isCustomCurrencyBadgeVisible: Boolean
get() = binding.customBadge.isVisible
set(value) = binding.customBadge::isVisible.set(value)

View file

@ -8,11 +8,12 @@ import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
@ -27,7 +28,9 @@ import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
setFragment(fragment, binding)
onViewCreated()
@ -51,8 +54,7 @@ class MultiWalletView : WalletView() {
if (card?.backupStatus is Card.BackupStatus.Active) {
val cardCount = (card.backupStatus as Card.BackupStatus.Active).cardCount + 1
tvTwinCardNumber.show()
tvTwinCardNumber.text =
fragment?.getString(R.string.wallet_twins_chip_format, 1, cardCount)
tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, cardCount)
} else {
tvTwinCardNumber.hide()
}
@ -76,13 +78,16 @@ class MultiWalletView : WalletView() {
handleTotalBalance(binding, state.totalBalance)
handleBackupWarning(binding, state.showBackupWarning)
handleRescanWarning(binding, state.missingDerivations.isNotEmpty())
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
binding.pbLoadingUserTokens.show(state.loadingUserTokens)
binding.btnAddToken.setOnClickListener {
val card = store.state.globalState.scanResponse!!.card
store.dispatch(
TokensAction.LoadCurrencies(
supportedBlockchains = currenciesRepository.getBlockchains(
supportedBlockchains = CurrenciesRepository.getBlockchains(
card.firmwareVersion,
card.isTestCard,
),
@ -111,6 +116,16 @@ class MultiWalletView : WalletView() {
}
}
private fun handleRescanWarning(
binding: FragmentWalletBinding,
showRescanWarning: Boolean,
) = with(binding.lWalletRescanWarning) {
root.isVisible = showRescanWarning
root.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.ScanToGetDerivations)
}
}
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,

View file

@ -20,6 +20,7 @@ class SaltPaySingleWalletView : WalletView() {
btnAddToken.hide()
rvPendingTransaction.hide()
tvTwinCardNumber.hide()
pbLoadingUserTokens.hide()
lCardBalance.root.hide()
lAddress.root.hide()
lSingleWalletBalance.root.show()

View file

@ -6,6 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.fitChipsByGroupWidth
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
@ -36,6 +37,7 @@ class SingleWalletView : WalletView() {
rvMultiwallet.hide()
btnAddToken.hide()
rvPendingTransaction.hide()
pbLoadingUserTokens.hide()
lCardBalance.root.show()
lAddress.root.show()
lSingleWalletBalance.root.hide()
@ -85,12 +87,7 @@ class SingleWalletView : WalletView() {
private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) {
twinCardsState?.cardNumber?.let { cardNumber ->
tvTwinCardNumber.show()
val number = when (cardNumber) {
TwinCardNumber.First -> 1
TwinCardNumber.Second -> 2
}
tvTwinCardNumber.text =
fragment?.getString(R.string.wallet_twins_chip_format, number, 2)
tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, 2)
}
if (twinCardsState?.cardNumber == null) {
tvTwinCardNumber.hide()

View file

@ -20,10 +20,13 @@ class MercuryoService(
private val apiVersion: String,
private val mercuryoWidgetId: String,
private val secret: String,
private val logEnabled: Boolean,
) : ExchangeService, ExchangeUrlBuilder {
private val api: MercuryoApi = createRetrofitInstance(MercuryoApi.BASE_URL)
.create(MercuryoApi::class.java)
private val api: MercuryoApi = createRetrofitInstance(
baseUrl = MercuryoApi.BASE_URL,
logEnabled = logEnabled,
).create(MercuryoApi::class.java)
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
@ -73,7 +76,12 @@ class MercuryoService(
if (!isBuyAllowed()) return false
// blockchains which cant be defined by mercuryo service
val unsupportedBlockchains = listOf(Blockchain.Unknown, Blockchain.Binance, Blockchain.Arbitrum)
val unsupportedBlockchains = listOf(
Blockchain.Unknown,
Blockchain.Binance,
Blockchain.Arbitrum,
Blockchain.Optimism,
)
val blockchain = currency.blockchain
return when (currency) {

View file

@ -20,11 +20,14 @@ import javax.crypto.spec.SecretKeySpec
class MoonPayService(
private val apiKey: String,
private val secretKey: String,
private val logEnabled: Boolean,
) : ExchangeService, ExchangeUrlBuilder {
private val api: MoonPayApi by lazy {
createRetrofitInstance(MoonPayApi.MOOONPAY_BASE_URL)
.create(MoonPayApi::class.java)
createRetrofitInstance(
baseUrl = MoonPayApi.MOOONPAY_BASE_URL,
logEnabled = logEnabled,
).create(MoonPayApi::class.java)
}
private var status: MoonPayStatus? = null

View file

@ -23,7 +23,10 @@ class PayIdService {
return performRequest { payIdApi.setPayId(cardId, publicKey, payId, address, network) }
}
private fun provideRetrofit(): Retrofit = createRetrofitInstance("https://tangem.com/")
private fun provideRetrofit(): Retrofit = createRetrofitInstance(
baseUrl = "https://tangem.com/",
logEnabled = false,
)
}

View file

@ -8,10 +8,13 @@ import com.tangem.network.common.createRetrofitInstance
[REDACTED_AUTHOR]
*/
class PayIdVerifyService(
private val baseUrl: String
private val baseUrl: String,
) {
private val api = createRetrofitInstance(baseUrl).create(PayIdVerifyApi::class.java)
private val api = createRetrofitInstance(
baseUrl = baseUrl,
logEnabled = false,
).create(PayIdVerifyApi::class.java)
suspend fun verifyAddress(user: String, network: String): Result<VerifyPayIdResponse> {
return performRequest { api.verifyAddress(user, createNetworkHeader(network)) }

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M0,0h22v22h-22z"/>
<path
android:pathData="M11,21.988C17.069,21.988 21.988,17.069 21.988,11C21.988,4.931 17.069,0.012 11,0.012C4.931,0.012 0.012,4.931 0.012,11C0.012,17.069 4.931,21.988 11,21.988Z"
android:fillColor="#008DE4"/>
<path
android:pathData="M12.97,4.846H7.66L7.22,7.304L12.012,7.311C14.372,7.311 15.07,8.168 15.05,9.588C15.038,10.317 14.724,11.549 14.588,11.948C14.224,13.012 13.477,14.227 10.677,14.223L6.02,14.221L5.578,16.682H10.875C12.743,16.682 13.538,16.464 14.379,16.076C16.246,15.215 17.356,13.373 17.8,10.97C18.462,7.392 17.636,4.846 12.97,4.846Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M5.369,9.53C3.979,9.53 3.779,10.436 3.649,10.984C3.476,11.701 3.42,11.991 3.42,11.991H8.856C10.247,11.991 10.446,11.085 10.576,10.537C10.749,9.819 10.805,9.53 10.805,9.53H5.369Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path
android:pathData="M0,0h22v22h-22z"/>
<path
android:pathData="M11,21.988C17.069,21.988 21.988,17.069 21.988,11C21.988,4.931 17.069,0.012 11,0.012C4.931,0.012 0.012,4.931 0.012,11C0.012,17.069 4.931,21.988 11,21.988Z"
android:fillColor="#F1F1F1"/>
<path
android:pathData="M12.97,4.846H7.66L7.22,7.304L12.012,7.311C14.372,7.311 15.07,8.168 15.05,9.588C15.038,10.317 14.724,11.549 14.588,11.948C14.224,13.012 13.477,14.227 10.677,14.223L6.02,14.221L5.578,16.682H10.875C12.743,16.682 13.538,16.464 14.379,16.076C16.246,15.215 17.356,13.373 17.8,10.97C18.462,7.392 17.636,4.846 12.97,4.846"
android:fillColor="#A1A1A4"/>
<path
android:pathData="M5.369,9.53C3.979,9.53 3.779,10.436 3.649,10.984C3.476,11.701 3.42,11.991 3.42,11.991H8.856C10.247,11.991 10.446,11.085 10.576,10.537C10.749,9.819 10.805,9.53 10.805,9.53H5.369Z"
android:fillColor="#A1A1A4"/>
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path android:pathData="M0,0h22v22h-22z" />
<path
android:fillColor="#FF0420"
android:pathData="M11,22C17.075,22 22,17.075 22,11C22,4.925 17.075,0 11,0C4.925,0 0,4.925 0,11C0,17.075 4.925,22 11,22Z" />
<path
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M4.244,14.356C4.814,14.777 5.546,14.987 6.439,14.987C7.52,14.987 8.383,14.735 9.029,14.233C9.674,13.722 10.128,12.953 10.391,11.925C10.548,11.294 10.684,10.645 10.796,9.977C10.834,9.737 10.852,9.538 10.852,9.38C10.852,8.855 10.721,8.404 10.458,8.029C10.196,7.646 9.835,7.361 9.378,7.174C8.92,6.978 8.402,6.881 7.824,6.881C5.7,6.881 4.383,7.913 3.872,9.977C3.692,10.735 3.553,11.384 3.456,11.925C3.418,12.165 3.399,12.367 3.399,12.533C3.399,13.321 3.681,13.929 4.244,14.356ZM7.666,12.994C7.374,13.242 7.017,13.366 6.597,13.366C5.876,13.366 5.516,13.02 5.516,12.33C5.516,12.165 5.531,12.007 5.561,11.857C5.696,11.129 5.831,10.514 5.966,10.011C6.094,9.5 6.304,9.121 6.597,8.874C6.897,8.626 7.257,8.502 7.678,8.502C8.391,8.502 8.747,8.844 8.747,9.526C8.747,9.692 8.732,9.853 8.702,10.011C8.612,10.536 8.481,11.151 8.308,11.857C8.18,12.367 7.967,12.746 7.666,12.994ZM11.178,14.795C11.223,14.848 11.287,14.874 11.369,14.874H12.9C12.975,14.874 13.047,14.848 13.114,14.795C13.182,14.743 13.223,14.675 13.238,14.593L13.756,12.127H15.276C16.259,12.127 17.032,11.921 17.595,11.508C18.166,11.095 18.545,10.457 18.732,9.594C18.777,9.391 18.8,9.196 18.8,9.009C18.8,8.356 18.545,7.857 18.034,7.511C17.531,7.166 16.863,6.993 16.03,6.993H13.035C12.96,6.993 12.889,7.02 12.822,7.072C12.754,7.125 12.713,7.192 12.698,7.275L11.144,14.593C11.129,14.668 11.14,14.735 11.178,14.795ZM16.177,10.315C15.944,10.487 15.67,10.573 15.355,10.573H14.06L14.488,8.558H15.839C16.147,8.558 16.364,8.618 16.492,8.738C16.619,8.851 16.683,9.016 16.683,9.234C16.683,9.331 16.672,9.444 16.649,9.572C16.574,9.894 16.417,10.142 16.177,10.315Z" />
</group>
</vector>

View file

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<group>
<clip-path android:pathData="M0,0h22v22h-22z" />
<path
android:fillColor="#F1F1F1"
android:pathData="M11,22C17.075,22 22,17.075 22,11C22,4.925 17.075,0 11,0C4.925,0 0,4.925 0,11C0,17.075 4.925,22 11,22Z" />
<path
android:fillColor="#A1A1A4"
android:fillType="evenOdd"
android:pathData="M4.244,14.356C4.814,14.777 5.546,14.987 6.439,14.987C7.52,14.987 8.383,14.735 9.029,14.233C9.674,13.722 10.128,12.953 10.391,11.925C10.548,11.294 10.684,10.645 10.796,9.977C10.834,9.737 10.852,9.538 10.852,9.38C10.852,8.855 10.721,8.404 10.458,8.029C10.196,7.646 9.835,7.361 9.378,7.174C8.92,6.978 8.402,6.881 7.824,6.881C5.7,6.881 4.383,7.913 3.872,9.977C3.692,10.735 3.553,11.384 3.456,11.925C3.418,12.165 3.399,12.367 3.399,12.533C3.399,13.321 3.681,13.929 4.244,14.356ZM7.666,12.994C7.374,13.242 7.017,13.366 6.597,13.366C5.876,13.366 5.516,13.02 5.516,12.33C5.516,12.165 5.531,12.007 5.561,11.857C5.696,11.129 5.831,10.514 5.966,10.011C6.094,9.5 6.304,9.121 6.597,8.874C6.897,8.626 7.257,8.502 7.678,8.502C8.391,8.502 8.747,8.844 8.747,9.526C8.747,9.692 8.732,9.853 8.702,10.011C8.612,10.536 8.481,11.151 8.308,11.857C8.18,12.367 7.967,12.746 7.666,12.994ZM11.178,14.795C11.223,14.848 11.287,14.874 11.369,14.874H12.9C12.975,14.874 13.047,14.848 13.114,14.795C13.182,14.743 13.223,14.675 13.238,14.593L13.756,12.127H15.276C16.259,12.127 17.032,11.921 17.595,11.508C18.166,11.095 18.545,10.457 18.732,9.594C18.777,9.391 18.8,9.196 18.8,9.009C18.8,8.356 18.545,7.857 18.034,7.511C17.531,7.166 16.863,6.993 16.03,6.993H13.035C12.96,6.993 12.889,7.02 12.822,7.072C12.754,7.125 12.713,7.192 12.698,7.275L11.144,14.593C11.129,14.668 11.14,14.735 11.178,14.795ZM16.177,10.315C15.944,10.487 15.67,10.573 15.355,10.573H14.06L14.488,8.558H15.839C16.147,8.558 16.364,8.618 16.492,8.738C16.619,8.851 16.683,9.016 16.683,9.234C16.683,9.331 16.672,9.444 16.649,9.572C16.574,9.894 16.417,10.142 16.177,10.315Z" />
</group>
</vector>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:autoMirrored="true"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:fillColor="#F4F5F5"
android:pathData="M21,21m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0" />
<group>
<clip-path android:pathData="M21,42C32.598,42 42,32.598 42,21C42,9.402 32.598,0 21,0C9.402,0 0,9.402 0,21C0,32.598 9.402,42 21,42Z" />
<path
android:fillColor="#0F1820"
android:pathData="M0,8L27,8A4,4 0,0 1,31 12L31,30A4,4 0,0 1,27 34L0,34A4,4 0,0 1,-4 30L-4,12A4,4 0,0 1,0 8z" />
<path
android:fillAlpha="0.3"
android:fillColor="#ffffff"
android:fillType="evenOdd"
android:pathData="M9.047,27H24.954C25.231,27 25.497,27.105 25.694,27.293C25.89,27.48 26,27.735 26,28C26,28.265 25.89,28.52 25.694,28.707C25.497,28.895 25.231,29 24.954,29H9.047C8.769,29 8.503,28.895 8.307,28.707C8.11,28.52 8,28.265 8,28C8,27.735 8.11,27.48 8.307,27.293C8.503,27.105 8.769,27 9.047,27Z" />
</group>
</vector>

View file

@ -0,0 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="44dp"
android:height="44dp"
android:autoMirrored="true"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:fillColor="#F5F5F5"
android:pathData="M20,24m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0" />
<path
android:fillColor="#000000"
android:pathData="M13.333,17.25C13.333,16.836 13.684,16.5 14.118,16.5H25.882C26.316,16.5 26.667,16.836 26.667,17.25V21H13.333V17.25Z" />
<path
android:fillColor="#000000"
android:pathData="M13.333,24H18.039V31.5H14.118C13.684,31.5 13.333,31.164 13.333,30.75V24Z" />
<path
android:fillColor="#000000"
android:pathData="M26.667,24H21.961V31.5H25.882C26.316,31.5 26.667,31.164 26.667,30.75V24Z" />
<path
android:fillColor="#FFB71B"
android:pathData="M36,8m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
android:strokeWidth="4"
android:strokeColor="#ffffff" />
</vector>

View file

@ -93,17 +93,6 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="iv_card,tv_twin_card_number" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
@ -116,16 +105,47 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier" />
<LinearLayout
android:id="@+id/ll_warnings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="8dp"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages">
<include
android:id="@+id/l_wallet_rescan_warning"
layout="@layout/layout_wallet_rescan_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:visibility="gone" />
</LinearLayout>
<include
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/l_wallet_backup_warning"
app:layout_constraintTop_toBottomOf="@id/ll_warnings"
app:layout_goneMarginTop="16dp" />
<androidx.recyclerview.widget.RecyclerView
@ -171,11 +191,31 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<ProgressBar
android:id="@+id/pb_loading_user_tokens"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateTint="@color/icon_accent"
android:indeterminateTintMode="src_atop"
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_wallets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="rv_multiwallet,pb_loading_user_tokens" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_token"
style="@style/BaseTapButton"
@ -190,7 +230,7 @@
android:visibility="gone"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_multiwallet"
app:layout_constraintTop_toBottomOf="@id/barrier_wallets"
app:layout_constraintVertical_bias="0.0"
tools:layout_editor_absoluteX="16dp" />

Some files were not shown because too many files have changed in this diff Show more