diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ee0bc5020b..3682efb9f5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,6 +64,9 @@ dependencies { implementation(projects.domain.analytics) implementation(projects.domain.visa) implementation(projects.domain.onboarding) + implementation(projects.domain.feedback) + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) implementation(projects.common) implementation(projects.core.analytics) @@ -93,6 +96,8 @@ dependencies { implementation(projects.data.visa) implementation(projects.data.promo) implementation(projects.data.onboarding) + implementation(projects.data.feedback) + implementation(projects.data.qrScanning) /** Features */ implementation(projects.features.onboarding) @@ -110,9 +115,9 @@ dependencies { implementation(projects.features.wallet.impl) implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) - implementation(projects.features.send.api) implementation(projects.features.manageTokens.api) implementation(projects.features.manageTokens.impl) + implementation(projects.features.send.api) implementation(projects.features.send.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) @@ -183,10 +188,6 @@ dependencies { implementation(deps.kotsonGson) implementation(deps.spongecastle.core) implementation(deps.lottie) - implementation(deps.shopify.buy) { - exclude(group = "com.shopify.graphql.support") - exclude(module = "joda-time") - } implementation(deps.compose.accompanist.appCompatTheme) implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.accompanist.webView) @@ -230,6 +231,9 @@ dependencies { implementation(deps.listenableFuture) implementation(deps.mlKit.barcodeScanning) + /** Leakcanary */ + debugImplementation(deps.leakcanary) + /** Excluded dependencies */ implementation("com.google.guava:guava:30.0-android") { // excludes version 9999.0-empty-to-avoid-conflict-with-guava diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4b57ad94ed..093b3eb247 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -130,7 +130,7 @@ diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index b638128b34..423202eeda 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -638,6 +638,46 @@ "networkId": "pulsechain/test" } ] + }, + { + "id": "zksync-ethereum", + "name": "zkSync", + "symbol": "ETH", + "networks": [ + { + "networkId": "zksync/test" + } + ] + }, + { + "id": "moonbeam", + "name": "Moonbeam", + "symbol": "GLMR", + "networks": [ + { + "networkId": "moonbeam/test" + } + ] + }, + { + "id": "manta-network-ethereum", + "name": "Manta Testnet", + "symbol": "ETH", + "networks": [ + { + "networkId": "manta-network/test" + } + ] + }, + { + "id": "polygon-zkevm-ethereum", + "name": "Polygon zkEvm Testnet", + "symbol": "ETH", + "networks": [ + { + "networkId": "polygon-zkevm/test" + } + ] } ] } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 116e6dcf12..c1d5853c80 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -52,9 +52,6 @@ import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.common.shop.googlepay.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.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -64,7 +61,6 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction @@ -145,9 +141,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles - @Inject - lateinit var generalUserWalletsListManager: UserWalletsListManager - internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -173,8 +166,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac setContentView(R.layout.activity_main) initContent() - checkGooglePayAvailability() - checkForNotificationPermission() observeStateUpdates() @@ -256,14 +247,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } - private fun checkGooglePayAvailability() { - store.dispatch( - ShopAction.CheckIfGooglePayAvailable( - GooglePayService(createPaymentsClient(this), this), - ), - ) - } - private fun createAppThemeModeFlow(): SharedFlow { val tapApplication = application as TapApplication @@ -377,13 +360,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac super.onActivityResult(requestCode, resultCode, data) onActivityResultCallbacks.forEach { it(requestCode, resultCode, data) } - when (requestCode) { - LOAD_PAYMENT_DATA_REQUEST_CODE -> { - store.dispatch( - ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data), - ) - } - } } override fun addOnActivityResultCallback(callback: OnActivityResultCallback) { @@ -432,10 +408,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted - val isShopNotOpened = store.state.shopState.total != null when { - !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> { + !backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive -> { navigateToInitialScreen(intentWhichStartedActivity) } backStackIsEmpty -> { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 051cb803b8..7c26fe7570 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -10,8 +10,10 @@ import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat +import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.filter.OneTimeEventFilter @@ -57,7 +59,6 @@ import com.tangem.tap.common.log.TimberFormatStrategy 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.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation @@ -82,7 +83,6 @@ lateinit var foregroundActivityObserver: ForegroundActivityObserver lateinit var activityResultCaller: ActivityResultCaller lateinit var preferencesStorage: PreferencesDataSource lateinit var walletConnectRepository: WalletConnectRepository -lateinit var shopService: TangemShopService internal lateinit var derivationsFinder: DerivationsFinder @HiltAndroidApp @@ -178,6 +178,12 @@ internal class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles + + @Inject + lateinit var blockchainSDKLogger: BlockchainSDKLogger + + @Inject + lateinit var tangemSdkLogger: TangemSdkLogger // endregion Injected override fun onCreate() { @@ -266,6 +272,8 @@ internal class TapApplication : Application(), ImageLoaderFactory { saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, cardRepository = cardRepository, feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, + tangemSdkLogger = tangemSdkLogger, + blockchainSDKLogger = blockchainSDKLogger, ), ), ) @@ -290,7 +298,6 @@ internal class TapApplication : Application(), ImageLoaderFactory { } private fun initWithConfigDependency(config: Config) { - shopService = TangemShopService(this, config.shopify!!) initAnalytics(this, config) initFeedbackManager(this, foregroundActivityObserver, store) } @@ -343,13 +350,17 @@ internal class TapApplication : Application(), ImageLoaderFactory { Log.Level.View, Log.Level.Network, Log.Level.Error, + Log.Level.Biometric, ) return TangemLogCollector(logLevels, LogFormat.StairsFormatter()) } val additionalFeedbackInfo = initAdditionalFeedbackInfo(context) val tangemLogCollector = initTangemLogCollector() - Log.addLogger(tangemLogCollector) + + Log.addLogger( + logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector, + ) val feedbackManager = FeedbackManager( infoHolder = additionalFeedbackInfo, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt deleted file mode 100644 index fd658029b7..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.common.analytics.converters - -import com.shopify.buy3.Storefront -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.utils.converter.Converter - -/** -[REDACTED_AUTHOR] - */ -class ShopOrderToEventConverter : Converter, Shop.Purchased> { - - override fun convert(value: Pair): Shop.Purchased { - val checkout = value.first - val productType = value.second - - val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku - val count = when (productType) { - ProductType.WALLET_2_CARDS -> "2" - ProductType.WALLET_3_CARDS -> "3" - } - val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}" - val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication) - ?.code - - return Shop.Purchased(sku, count, amount, code) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt index adf3e19fed..ec4f3737f0 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Activity.kt @@ -17,6 +17,7 @@ import java.io.File * * */ +@Deprecated("Use EmailSender instead") fun Activity.sendEmail( email: String, subject: String, diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 019b203988..f6c299acb1 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -24,7 +24,6 @@ import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragm import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment import com.tangem.tap.features.send.ui.SendFragment -import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.redux.DaggerGraphState @@ -122,7 +121,6 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? { private fun fragmentFactory(screen: AppScreen): Fragment { return when (screen) { AppScreen.Home -> HomeFragment() - AppScreen.Shop -> ShopFragment() AppScreen.OnboardingNote -> OnboardingNoteFragment() AppScreen.OnboardingWallet -> OnboardingWalletFragment() AppScreen.OnboardingTwins -> OnboardingTwinsFragment() diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt index 452eb173aa..7e878f6e69 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -73,15 +73,9 @@ class SendTransactionFailedEmail( class FeedbackEmail : FeedbackData { override val subjectResId: Int - get() = if (isS2CCard) s2cSubject else tangemSubject - override val mainMessageResId: Int - get() = if (isS2CCard) s2cMainMessage else tangemMainMessage + get() = if (isS2CCard) R.string.feedback_subject_support else R.string.feedback_subject_support_tangem - private val tangemSubject: Int = R.string.feedback_subject_support_tangem - private val tangemMainMessage: Int = R.string.feedback_preface_support - - private val s2cSubject: Int = R.string.feedback_subject_support - private val s2cMainMessage: Int = R.string.feedback_preface_support + override val mainMessageResId: Int = R.string.feedback_preface_support private var isS2CCard = false diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 91992dd437..7adedbed8f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -12,7 +12,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer -import com.tangem.tap.features.shop.redux.ShopReducer import com.tangem.tap.features.tokens.legacy.redux.TokensReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.AppStateHolder @@ -36,7 +35,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) disclaimerState = DisclaimerReducer.reduce(action, state), tokensState = TokensReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), - shopState = ShopReducer.reduce(action, state.shopState), welcomeState = WelcomeReducer.reduce(action, state), saveWalletState = SaveWalletReducer.reduce(action, state), daggerGraphState = DaggerGraphReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 72726532fe..59b899ee8b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -29,8 +29,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.send.redux.middlewares.SendMiddleware import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.shop.redux.ShopMiddleware -import com.tangem.tap.features.shop.redux.ShopState import com.tangem.tap.features.tokens.legacy.redux.TokensState import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware @@ -54,7 +52,6 @@ data class AppState( val disclaimerState: DisclaimerState = DisclaimerState(), val tokensState: TokensState = TokensState(), val walletConnectState: WalletConnectState = WalletConnectState(), - val shopState: ShopState = ShopState(), val welcomeState: WelcomeState = WelcomeState(), val saveWalletState: SaveWalletState = SaveWalletState(), val daggerGraphState: DaggerGraphState = DaggerGraphState(), @@ -89,7 +86,6 @@ data class AppState( DisclaimerMiddleware().disclaimerMiddleware, WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, - ShopMiddleware().shopMiddleware, WelcomeMiddleware().middleware, SaveWalletMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt deleted file mode 100644 index 1a1b5827f0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ /dev/null @@ -1,222 +0,0 @@ -package com.tangem.tap.common.shop - -import android.app.Application -import android.content.Intent -import com.google.android.gms.wallet.PaymentData -import com.shopify.buy3.Storefront -import com.tangem.core.analytics.Analytics -import com.tangem.datasource.config.models.ShopifyShop -import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter -import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.common.shop.data.TotalSum -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.common.shop.shopify.ShopifyService -import com.tangem.tap.common.shop.shopify.data.CheckoutItem -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import java.math.BigDecimal -import java.util.Currency -import java.util.UUID - -class TangemShopService(application: Application, shopifyShop: ShopifyShop) { - - private val shopifyService = ShopifyService(application, shopifyShop) - - private val checkouts = mutableMapOf() - private val variants = mutableMapOf() - - private lateinit var googlePayService: GooglePayService - - suspend fun getProducts(): Result> { - val result = shopifyService.getProducts() - - return result.mapCatching { product -> - val availableVariants = product - .flatMap { it.variants.edges.map { it.node } } - .filter { SKUS_TO_DISPLAY.contains(it.sku) } - .associateBy { ProductType.fromSku(it.sku) } - .filterNotNull() - variants.putAll(availableVariants) - - if (variants.size < SKUS_TO_DISPLAY.size) { - return Result.failure( - Exception( - "Shopify: products are missing, " + - "\nproducts available: ${variants.keys.map { it.sku }}", - ), - ) - } - - val twoCardsProduct = TangemProduct( - type = ProductType.WALLET_2_CARDS, - totalSum = TotalSum( - finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(), - beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format(), - ), - ) - val threeCardsProduct = TangemProduct( - type = ProductType.WALLET_3_CARDS, - totalSum = TotalSum( - finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(), - beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format(), - ), - ) - createCheckouts() - return Result.success(listOf(twoCardsProduct, threeCardsProduct)) - } - } - - private suspend fun createCheckouts() { - variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll() - } - - private suspend fun createCheckout(productType: ProductType) { - val checkoutItem = CheckoutItem(variants[productType]!!.id, 1) - val result = shopifyService.createCheckout(listOf(checkoutItem)) - result.onSuccess { checkout -> - checkouts[productType] = checkout - } - } - - suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result { - this.googlePayService = googlePayService - return googlePayService.checkIfGooglePayAvailable(shopifyService.shop.merchantID != null) - } - - fun buyWithGooglePay(productType: ProductType) { - shopifyService.shop.merchantID?.let { merchantId -> - val totalPrice = checkouts[productType]!!.totalPriceV2.amount - googlePayService.payWithGooglePay( - totalPriceCents = totalPrice, - currencyCode = checkouts[productType]!!.currencyCode.name, - merchantID = merchantId, - ) - } - } - -// fun subscribeToGooglePayResult( -// productType: ProductType, -// resultCallback: (Result) -> Unit -// ) { -// googlePayService.responseCallback = { result -> -// result.onFailure { } -// result.onSuccess { -// completeTokenizedPayment(it, productType) -// } -// } -// } - - suspend fun handleGooglePayResult(resultCode: Int, data: Intent?, productType: ProductType): Result { - val result = googlePayService.handleResponseFromGooglePay(resultCode, data) - result.onSuccess { - val finalizePaymentResult = completeTokenizedPayment(it, productType) - finalizePaymentResult.onSuccess { - return Result.success(Unit) - } - return Result.failure(finalizePaymentResult.exceptionOrNull()!!) - } - return Result.failure(result.exceptionOrNull()!!) - } - - private suspend fun completeTokenizedPayment( - paymentData: PaymentData, - productType: ProductType, - ): Result { - val checkout = checkouts[productType]!! - val googlePayResponse = - googlePayService.parsePaymentData(paymentData) - ?: return Result.failure(Exception("cannot parse GPay result")) - - val amount = - Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode) - val idempotencyKey = UUID.randomUUID().toString() - val addressGPay = googlePayResponse.billingAddress - - val address = Storefront.MailingAddressInput().apply { - lastName = addressGPay.name - address1 = addressGPay.address1 - address2 = addressGPay.address2 + addressGPay.address3 - province = addressGPay.administrativeArea - zip = addressGPay.postalCode - phone = addressGPay.phoneNumber - } - - val payment = Storefront.TokenizedPaymentInputV3( - amount, - idempotencyKey, - address, - paymentData.toJson(), - Storefront.PaymentTokenType.GOOGLE_PAY, - ) - .setTest(true) - - return shopifyService.completeWithTokenizedPayment( - payment = payment, - checkoutID = checkout.id, - ) - } - - suspend fun applyPromoCode(promoCode: String): Result> { - val products = variants.keys - .map { coroutineScope { async { applyPromoCode(promoCode, it) } } } - .awaitAll() - .map { result -> result.getOrElse { return Result.failure(it) } } - - return Result.success(products) - } - - private suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result { - val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout")) - - val result = if (promoCode.isBlank()) { - shopifyService.removeDiscount(checkout.id) - } else { - shopifyService.applyDiscount(promoCode, checkout.id) - } - - result.onSuccess { - checkouts[productType] = it - return Result.success( - TangemProduct( - productType, - TotalSum( - finalValue = it.totalPriceV2.format(), - beforeDiscount = variants[productType]!!.compareAtPriceV2.format(), - ), - appliedDiscount = it.getAppliedDiscount(), - ), - ) - } - return Result.failure(result.exceptionOrNull()!!) - } - - fun getCheckoutUrl(productType: ProductType): String { - return checkouts[productType]!!.webUrl - } - - suspend fun waitForCheckout(productType: ProductType) { - val result = shopifyService.checkout(true, checkouts[productType]!!.id) - result.onSuccess { checkout -> - if (checkout.order != null && checkout.lineItems != null) { - val event = ShopOrderToEventConverter().convert(checkout to productType) - Analytics.send(event) - } - } - } -} - -private fun Storefront.MoneyV2.format(): String { - val currencySymbol = Currency.getInstance(currencyCode.name).symbol - val amountFormatted = BigDecimal(amount).setScale(2) - return currencySymbol + amountFormatted -} - -private fun Storefront.Checkout.getAppliedDiscount(): String? { - val discountApplication = - discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication - return discountApplication?.code -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt deleted file mode 100644 index 9a01a5df51..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.common.shop.data - -import com.tangem.tap.features.shop.domain.models.ProductType - -data class TangemProduct( - val type: ProductType, - val totalSum: TotalSum? = null, - val appliedDiscount: String? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt deleted file mode 100644 index a9e920745f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TotalSum.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.common.shop.data - -data class TotalSum( - val finalValue: String? = null, - val beforeDiscount: String? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt deleted file mode 100644 index 7526224414..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayService.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.tap.common.shop.googlepay - -import android.app.Activity -import android.app.Activity.RESULT_CANCELED -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.util.Log -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.wallet.* -import com.tangem.common.core.TangemSdkError -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONException -import org.json.JSONObject -import timber.log.Timber -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) { - -// var responseCallback: ((Result) -> Unit)? = null - - suspend fun checkIfGooglePayAvailable(isMerchantAvailable: Boolean): Result { - if (!isMerchantAvailable) return Result.success(false) - val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false) - val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString()) - - val task = paymentsClient.isReadyToPay(request) - return withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - task.addOnCompleteListener { completedTask -> - try { - val result = completedTask.getResult(ApiException::class.java) - continuation.resume(Result.success(true)) - } catch (exception: ApiException) { - // Process error - Timber.w("isReadyToPay failed: $exception") - continuation.resume(Result.failure(exception)) - } - } - } - } - } - - fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) { - val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest( - totalPriceCents, - currencyCode = currencyCode, - countryCode = "RU", - merchantID = merchantID, - ) - if (paymentDataRequestJson == null) { - Timber.e("RequestPayment: can't fetch payment data request") - return - } - val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString()) - - AutoResolveHelper.resolveTask( - paymentsClient.loadPaymentData(request), - activity, - LOAD_PAYMENT_DATA_REQUEST_CODE, - ) - } - - fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result { - val result = when (resultCode) { - RESULT_OK -> { - val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) } - if (paymentData == null) { - Result.failure(Exception("No payment data")) - } else { - Result.success(paymentData) - } - } - RESULT_CANCELED -> { - Result.failure(TangemSdkError.UserCancelled()) - } - AutoResolveHelper.RESULT_ERROR -> { - val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode - if (statusCode == null) { - Result.failure(Exception("Unknown Status")) - } else { - Result.failure(Exception("$statusCode")) - } - } - else -> Result.failure(Exception("Unknown Status")) - } -// responseCallback?.invoke(result) - return result - } - - fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? { - val paymentInformation = paymentData.toJson() - - try { - // Token will be null if PaymentDataRequest was not constructed using fromJson(String). - val paymentMethodData = - JSONObject(paymentInformation).getJSONObject("paymentMethodData") - val addressJson = paymentMethodData.getJSONObject("info") - .getJSONObject("billingAddress") - - val address = Address( - name = addressJson.getString("name"), - postalCode = addressJson.getString("postalCode"), - countryCode = addressJson.getString("countryCode"), - phoneNumber = addressJson.getString("phoneNumber"), - address1 = addressJson.getString("address1"), - address2 = addressJson.getString("address2"), - address3 = addressJson.getString("address3"), - locality = addressJson.getString("locality"), - administrativeArea = addressJson.getString("administrativeArea"), - sortingCode = addressJson.getString("sortingCode"), - ) - - val token = paymentMethodData - .getJSONObject("tokenizationData") - .getString("token") - - return GooglePayResponse(address, token) - } catch (e: JSONException) { - Log.e("handlePaymentSuccess", "Error: " + e.toString()) - } - return null - } - - companion object { - const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315 - } -} - -data class GooglePayResponse( - val billingAddress: Address, - val token: String, -) - -data class Address( - val name: String, - val postalCode: String, - val countryCode: String, - val phoneNumber: String, - val address1: String, - val address2: String, - val address3: String, - val locality: String, - val administrativeArea: String, - val sortingCode: String, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt b/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt deleted file mode 100644 index 1e5cfba714..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/googlepay/GooglePayUtil.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.tap.common.shop.googlepay - -import android.app.Activity -import com.google.android.gms.wallet.PaymentsClient -import com.google.android.gms.wallet.Wallet -import com.google.android.gms.wallet.WalletConstants -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject - -object GooglePayUtil { - private val baseRequest = JSONObject().apply { - put("apiVersion", 2) - put("apiVersionMinor", 0) - } - - @Suppress("MagicNumber") - private val allowedCardNetworks = JSONArray( - listOf( - "AMEX", - "DISCOVER", - "INTERAC", - "JCB", - "MASTERCARD", - "VISA", - ), - ) - - private val allowedCardAuthMethods = JSONArray( - listOf( - "PAN_ONLY", - "CRYPTOGRAM_3DS", - ), - ) - - private val merchantInfo: JSONObject = JSONObject().put("merchantName", "Example Merchant") - - private fun gatewayTokenizationSpecification(merchantID: String): JSONObject { - return JSONObject().apply { - put("type", "PAYMENT_GATEWAY") - put( - "parameters", - JSONObject( - mapOf( - "gateway" to "shopify", - "gatewayMerchantId" to merchantID, - ), - ), - ) - } - } - - private fun baseCardPaymentMethod(): JSONObject { - return JSONObject().apply { - val parameters = JSONObject().apply { - put("allowedAuthMethods", allowedCardAuthMethods) - put("allowedCardNetworks", allowedCardNetworks) - put("billingAddressRequired", true) - put( - "billingAddressParameters", - JSONObject().apply { - put("format", "FULL") - }, - ) - } - - put("type", "CARD") - put("parameters", parameters) - } - } - - private fun cardPaymentMethod(merchantID: String): JSONObject { - val cardPaymentMethod = baseCardPaymentMethod() - cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID)) - - return cardPaymentMethod - } - - fun createPaymentsClient(activity: Activity): PaymentsClient { - val walletOptions = Wallet.WalletOptions.Builder() - .setEnvironment(PAYMENTS_ENVIRONMENT) - .build() - - return Wallet.getPaymentsClient(activity, walletOptions) - } - - fun isReadyToPayRequest(): JSONObject? { - return try { - baseRequest.apply { - put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod())) - } - } catch (e: JSONException) { - null - } - } - - private fun getTransactionInfo(price: String, countryCode: String, currencyCode: String): JSONObject { - return JSONObject().apply { - put("totalPrice", price) - put("totalPriceStatus", "FINAL") - put("countryCode", countryCode) - put("currencyCode", currencyCode) - } - } - - fun getPaymentDataRequest( - price: String, - countryCode: String, - currencyCode: String, - merchantID: String, - ): JSONObject? { - try { - return baseRequest.apply { - put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID))) - put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode)) - put("merchantInfo", merchantInfo) - - // An optional shipping address requirement is a top-level property of the - // PaymentDataRequest JSON object. - val shippingAddressParameters = JSONObject().apply { - put("phoneNumberRequired", false) -// put("allowedCountryCodes", JSONArray(listOf("US", "GB"))) - } - put("shippingAddressParameters", shippingAddressParameters) - put("shippingAddressRequired", true) - } - } catch (e: JSONException) { - return null - } - } -} - -const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt deleted file mode 100644 index 705bd067a0..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/ShopifyService.kt +++ /dev/null @@ -1,267 +0,0 @@ -package com.tangem.tap.common.shop.shopify - -import android.app.Application -import com.shopify.buy3.GraphCallResult -import com.shopify.buy3.GraphClient -import com.shopify.buy3.RetryHandler -import com.shopify.buy3.Storefront.* -import com.shopify.graphql.support.ID -import com.shopify.graphql.support.Input -import com.tangem.datasource.config.models.ShopifyShop -import com.tangem.tap.common.shop.shopify.data.CheckoutItem -import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment -import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.concurrent.TimeUnit -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -@Suppress("LargeClass") -class ShopifyService(private val application: Application, val shop: ShopifyShop) { - private val client: GraphClient by lazy { initClient() } - - @Suppress("MagicNumber") - suspend fun getProducts(collectionTitleFilter: String? = null): Result> { - val filter = collectionTitleFilter?.let { "title:\"$it\"" } - - val query = query { rootQuery: QueryRootQuery -> - rootQuery.collections( - { arg -> arg.first(250).query(filter) }, - CollectionConnectionQuery::collectionFieldsFragment, - ) - } - return when (val result = queryAsync(query)) { - is GraphCallResult.Success -> { - val products = result.response.data!!.collections.edges - .map { it.node.products } - .flatMap { it.edges } - .map { it.node } - Result.success(products) - } - is GraphCallResult.Failure -> { - Result.failure(result.error) - } - } - } - - suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result { - val query = query { rootQuery: QueryRootQuery -> - rootQuery - .node(checkoutID) { query -> - query.onCheckout { checkoutQuery -> - with(checkoutQuery) { - checkoutFieldsFragment() - } - } - } - } - val retryHandler = RetryHandler.build( - delay = 1, - timeUnit = TimeUnit.SECONDS, - ) { - this.retryWhen { result -> - when (result) { - is GraphCallResult.Success -> { - val checkout = result.response.data?.node as? Checkout - checkout?.order == null - } - is GraphCallResult.Failure -> false - } - } - } - - val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query) - return when (result) { - is GraphCallResult.Success -> { - val checkout = result.response.data!!.node as? Checkout - if (checkout != null) { - Result.success(checkout) - } else { - Result.failure(ShopifyError.Unknown) - } - } - is GraphCallResult.Failure -> { - Result.failure(result.error) - } - } - } - - suspend fun createCheckout(checkoutItems: List, checkoutID: ID? = null): Result { - val storefrontLineItems: MutableList = checkoutItems - .map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList() - - val query = if (checkoutID != null) { - mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutLineItemsReplace( - storefrontLineItems, - checkoutID, - ) { payloadQuery: CheckoutLineItemsReplacePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .userErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - } else { - val input = CheckoutCreateInput() - .setLineItemsInput( - Input.value(storefrontLineItems), - ) - mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutCreate( - input, - ) { payloadQuery: CheckoutCreatePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - } - return runCheckoutMutation(query) - } - - suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutDiscountCodeApplyV2( - discountCode, - checkoutID, - ) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - suspend fun removeDiscount(checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutDiscountCodeRemove( - checkoutID, - ) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery -> - payloadQuery - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery.checkoutFieldsFragment() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - suspend fun completeWithTokenizedPayment(payment: TokenizedPaymentInputV3, checkoutID: ID): Result { - val query = mutation { mutationQuery: MutationQuery -> - mutationQuery - .checkoutCompleteWithTokenizedPaymentV3( - checkoutID, - payment, - ) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery -> - payloadQuery - .payment { paymentQuery: PaymentQuery -> - paymentQuery - .ready() - .errorMessage() - } - .checkout { checkoutQuery: CheckoutQuery -> - checkoutQuery - .ready() - } - .checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery -> - userErrorQuery - .field() - .message() - } - } - } - return runCheckoutMutation(query) - } - - private suspend fun runCheckoutMutation(mutation: MutationQuery): Result { - return when (val result = mutationQueryAsync(mutation)) { - is GraphCallResult.Success -> { - val checkout = result.response.data!!.checkoutCreate?.checkout - ?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout - ?: result.response.data!!.checkoutDiscountCodeRemove?.checkout - ?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout - ?: result.response.data!!.checkoutEmailUpdateV2?.checkout - ?: result.response.data!!.checkoutShippingLineUpdate?.checkout - ?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout - - Result.success(checkout) - } - is GraphCallResult.Failure -> Result.failure(result.error) - } - } - - private suspend fun queryAsync( - query: QueryRootQuery, - retryHandler: RetryHandler, - ): GraphCallResult = withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.queryGraph(query).enqueue(retryHandler = retryHandler) { result -> - continuation.resume(result) - } - } - } - - private suspend fun queryAsync(query: QueryRootQuery): GraphCallResult = withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.queryGraph(query).enqueue { result -> - continuation.resume(result) - } - } - } - - private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult = - withContext(Dispatchers.IO) { - suspendCoroutine { continuation -> - client.mutateGraph(query).enqueue { result -> - continuation.resume(result) - } - } - } - - private fun initClient(): GraphClient { - return GraphClient.build( - application, - shop.domain, - shop.storefrontApiKey, - ) { -// httpCache(application.filesDir) { -// cacheMaxSizeBytes = (1024 * 1024 * 10) -// defaultCachePolicy = -// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES) -// } - } - } -} - -sealed class ShopifyError : Throwable() { - object Unknown : ShopifyError() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt deleted file mode 100644 index 7565db2447..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Checkout.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.buy3.Storefront - -@Suppress("LongMethod", "MagicNumber") -fun Storefront.CheckoutQuery.checkoutFieldsFragment() { - ready() - webUrl() - currencyCode() - lineItemsSubtotalPrice { it.amount() } - totalPriceV2 { - it.currencyCode() - it.amount() - } - lineItems({ arg -> arg.first(250) }) { - it.edges { - it.node { -// it.id() - it.title() - it.quantity() - it.variant { - it.priceV2 { it.amount() } - } - } - } - } - shippingLine { - it.handle() - it.title() - it.priceV2 { it.amount() } - } - availableShippingRates { - it.ready() - it.shippingRates { - it.handle() - it.title() - it.priceV2 { it.amount() } - } - } - shippingAddress { - it.address1() - it.address2() - it.city() -// .company() - it.country() -// .countryCodeV2() - it.firstName() -// .formatted() -// .formattedArea() -// .id() - it.lastName() -// .latitude() -// .longitude() -// .name() - it.phone() - it.province() -// .provinceCode() - it.zip() - } - discountApplications({ arg -> arg.first(250) }) { - it.edges { - it.node { - it.onDiscountCodeApplication { - it.code() -// .applicable() -// .allocationMethod() -// .targetSelection() -// .targetType() - it.value { - it.onMoneyV2 { - it.amount() - } - it.onPricingPercentageValue { - it.percentage() - } - } - } - } - } - } - order { - it.cancelReason() - it.canceledAt() - it.currencyCode() -// .currentSubtotalPrice() -// .currentTotalDuties() -// .currentTotalPrice() -// .currentTotalTax() - it.customerLocale() - it.customerUrl() -// .discountApplications() - it.edited() - it.email() - it.financialStatus() - it.fulfillmentStatus() -// .id() -// .lineItems() -// .metafield() -// .metafields() - it.name() - it.orderNumber() -// .originalTotalDuties() -// .originalTotalPrice() - it.phone() - it.processedAt() - it.shippingAddress { - it.address1() - it.address2() - it.city() - it.company() - it.country() - it.countryCodeV2() - it.firstName() - it.formatted() - it.formattedArea() -// .id() - it.lastName() - it.latitude() - it.longitude() - it.name() - it.phone() - it.province() - it.provinceCode() - it.zip() - } -// .shippingDiscountAllocations() - it.statusUrl() -// .subtotalPriceV2() -// .successfulFulfillments() -// .totalPriceV2() -// .totalRefundedV2() -// .totalShippingPriceV2() -// .totalTaxV2() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt deleted file mode 100644 index 6d16e66908..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/CheckoutItem.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.graphql.support.ID - -data class CheckoutItem( - val id: ID, - val quantity: Int, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt b/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt deleted file mode 100644 index 1527fc9558..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/shopify/data/Collection.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.common.shop.shopify.data - -import com.shopify.buy3.Storefront - -@Suppress("MagicNumber") -fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() { - edges { collectionEdgeQuery -> - collectionEdgeQuery - .node { collectionQuery -> - collectionQuery - .title() - .products( - { arg -> arg.first(250) }, - ) { productConnectionQuery -> - productConnectionQuery - .edges { productEdgeQuery -> - productEdgeQuery - .node { productQuery -> - productQuery.title() - .productType() - .description() - .variants({ arg -> arg.first(10) }) { variantConnectionQuery -> - variantConnectionQuery.edges { variantQuery -> - variantQuery.node { - it.title() - it.sku() - it.currentlyNotInStock() - it.priceV2 { - it.amount() - it.currencyCode() - } - it.compareAtPriceV2 { - it.amount() - it.currencyCode() - } - it.compareAtPriceV2 { - it.amount() - } - it.product { - it.title() - .productType() - .description() - } - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 884fa65323..87b5944231 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -22,6 +22,12 @@ internal class RuntimeUserWalletsStore( ?.singleOrNull { it.walletId == key } } + override suspend fun getAllSyncOrNull(): List? { + return walletsStateHolder.userWalletsListManager + ?.userWallets + ?.firstOrNull() + } + override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) { walletsStateHolder.userWalletsListManager?.update(userWalletId, update) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 76aa092682..084134986d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -69,4 +69,12 @@ internal object CardDomainModule { fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase { return IsNeedToBackupUseCase(walletStateHolder) } + + @Provides + @ViewModelScoped + fun provideGetExtendedPublicKeyForCurrencyUseCase( + derivationsRepository: DerivationsRepository, + ): GetExtendedPublicKeyForCurrencyUseCase { + return GetExtendedPublicKeyForCurrencyUseCase(derivationsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt new file mode 100644 index 0000000000..f2aea82f57 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.di.domain + +import android.content.Context +import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase +import com.tangem.domain.feedback.repository.FeedbackRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeedbackDomainModule { + + @Provides + @Singleton + fun provideGetFeedbackToSupportUseCase( + feedbackRepository: FeedbackRepository, + @ApplicationContext context: Context, + ): GetSupportFeedbackEmailUseCase { + return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt index ba2c69965b..ec86de8c46 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/QrScanningDomainModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +19,16 @@ internal object QrScanningDomainModule { fun provideListenToQrScanUseCase(repository: QrScanningEventsRepository): ListenToQrScanningUseCase { return ListenToQrScanningUseCase(repository) } + + @Provides + @Singleton + fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { + return EmitQrScannedEventUseCase(repository) + } + + @Provides + @Singleton + fun provideParseQrCodeUseCase(repository: QrScanningEventsRepository): ParseQrCodeUseCase { + return ParseQrCodeUseCase(repository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 7334227d99..2d8764fc7c 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -120,4 +120,10 @@ internal object SettingsDomainModule { ): ShouldShowSwapPromoTokenUseCase { return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) } + + @Provides + @ViewModelScoped + fun provideDeleteDeprecatedLogsUseCase(settingsRepository: SettingsRepository): DeleteDeprecatedLogsUseCase { + return DeleteDeprecatedLogsUseCase(settingsRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index c67960a860..7c830de869 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -278,9 +278,17 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideCheckTokenCompatibilityUseCase( - repository: NetworksCompatibilityRepository, + networksCompatibilityRepository: NetworksCompatibilityRepository, ): CheckCurrencyCompatibilityUseCase { - return CheckCurrencyCompatibilityUseCase(repository) + return CheckCurrencyCompatibilityUseCase(networksCompatibilityRepository) + } + + @Provides + @ViewModelScoped + fun provideNeedHardenedDerivationUseCase( + networksCompatibilityRepository: NetworksCompatibilityRepository, + ): RequiresHardenedDerivationOnlyUseCase { + return RequiresHardenedDerivationOnlyUseCase(networksCompatibilityRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 3fedeef4ca..319ddd2c0e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -3,11 +3,13 @@ package com.tangem.tap.di.domain import com.squareup.moshi.Moshi import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.onboarding.data.MnemonicRepository @@ -32,6 +34,8 @@ internal object WalletManagersFacadeModule { mnemonicRepository: MnemonicRepository, assetReader: AssetReader, @SdkMoshi moshi: Moshi, + blockchainSDKLogger: BlockchainSDKLogger, + feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, ): WalletManagersFacade { return DefaultWalletManagersFacade( walletManagersStore = walletManagersStore, @@ -42,6 +46,8 @@ internal object WalletManagersFacadeModule { moshi = moshi, mnemonic = mnemonicRepository.generateDefaultMnemonic(), accountCreator = accountCreator, + blockchainSDKLogger = blockchainSDKLogger, + feedbackManagerFeatureToggles = feedbackManagerFeatureToggles, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 94b72fcf1e..548f91c4ad 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -26,6 +26,8 @@ import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -138,6 +140,17 @@ class TangemSdkManager( return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } + suspend fun deriveExtendedPublicKey( + cardId: String?, + walletPublicKey: ByteArray, + derivation: DerivationPath, + ): CompletionResult = withContext(Dispatchers.Main) { + runTaskAsyncReturnOnMain( + DeriveWalletPublicKeyTask(walletPublicKey, derivation), + cardId, + ) + } + suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, @@ -264,7 +277,7 @@ class TangemSdkManager( allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), batchIdFilter = CardFilter.Companion.ItemFilter.Deny( - items = setOf("0027", "0030", "0031", "0035", "DA88"), + items = setOf("0027", "0030", "0031", "0035"), ), ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 03f3022a51..f8a93381e8 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -47,6 +47,11 @@ class TapWalletManager( config = blockchainSdkConfig, accountCreator = store.inject(DaggerGraphState::accountCreator), blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage), + loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { + listOf(store.inject(DaggerGraphState::blockchainSDKLogger)) + } else { + emptyList() + }, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 4b2f3852b6..84d55370ed 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,12 +1,15 @@ package com.tangem.tap.domain.card +import com.tangem.common.CompletionResult import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -51,6 +54,28 @@ internal class DefaultDerivationsRepository( error("This code should never be reached") } + override suspend fun deriveExtendedPublicKey( + userWalletId: UserWalletId, + derivation: DerivationPath, + ): ExtendedPublicKey? { + val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") + val walletCard = userWallet.scanResponse.card.wallets.firstOrNull { + UserWalletIdBuilder.scanResponse(userWallet.scanResponse).build()?.value + .contentEquals(userWallet.walletId.value) + } ?: return null + + val result = tangemSdkManager.deriveExtendedPublicKey( + cardId = null, + walletPublicKey = walletCard.publicKey, + derivation = derivation, + ) + + return when (result) { + is CompletionResult.Failure -> throw result.error + is CompletionResult.Success -> result.data + } + } + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { return runCatching(dispatchers.io) { userWalletsStore.update( diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 1bac186d83..06df7c2274 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -30,7 +30,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, @@ -41,7 +40,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent, cardId, onProgressStateChange, - onScanStateChange, onWalletNotCreated, disclaimerWillShow, onFailure, @@ -52,7 +50,6 @@ internal class DefaultScanCardProcessor : ScanCardProcessor { analyticsEvent, cardId, onProgressStateChange, - onScanStateChange, onWalletNotCreated, disclaimerWillShow, onFailure, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 757e78cb34..cf1c9f95be 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -48,14 +48,12 @@ internal object LegacyScanProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) = withMainContext { onProgressStateChange(true) - onScanStateChange(true) tangemSdkManager.changeDisplayedCardIdNumbersCount(null) @@ -65,13 +63,11 @@ internal object LegacyScanProcessor { result .doOnFailure { error -> - onScanStateChange(false) onFailure(error) } .doOnSuccess { scanResponse -> tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) - onScanStateChange(false) sendAnalytics(analyticsEvent, scanResponse) showDisclaimerIfNeed( diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 02e1701894..3a0902520a 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -3,11 +3,15 @@ package com.tangem.tap.domain.scanCard import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.Basic import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.StateDialog import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* @@ -25,9 +29,15 @@ internal object UseCaseScanProcessor { allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) + return scanCardUseCase(cardId, allowsRequestAccessCodeFromRepository) .fold( - ifLeft = { CompletionResult.Failure(scanCardExceptionConverter.convertBack(it)) }, + ifLeft = { + val error = scanCardExceptionConverter.convertBack(it) + + Analytics.send(Basic.ScanError(error)) + CompletionResult.Failure(error) + }, ifRight = { CompletionResult.Success(it) }, ) } @@ -37,17 +47,14 @@ internal object UseCaseScanProcessor { analyticsEvent: AnalyticsEvent?, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, disclaimerWillShow: () -> Unit, onFailure: suspend (error: TangemError) -> Unit, onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) = progressScope(onProgressStateChange) { - onScanStateChange(true) - val scanCardUseCase = store.inject(DaggerGraphState::scanCardUseCase) val chains = buildList { - add(ScanningFinishedChain { onScanStateChange(false) }) + add(FailedScansCounterChain(UseCaseScanProcessor::showMaxUnsuccessfulScansReachedDialog)) if (analyticsEvent != null) { add(AnalyticsChain(analyticsEvent)) } @@ -55,9 +62,14 @@ internal object UseCaseScanProcessor { add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) } - scanCardUseCase(cardId, afterScanChains = chains) - .map { onSuccess(it) } - .mapLeft { proceedWithException(it, onWalletNotCreated, onFailure) } + scanCardUseCase(cardId, afterScanChains = chains).fold( + ifLeft = { proceedWithException(it, onWalletNotCreated, onFailure) }, + ifRight = { onSuccess(it) }, + ) + } + + private fun showMaxUnsuccessfulScansReachedDialog() { + store.dispatchDialogShow(StateDialog.ScanFailsDialog) } private suspend fun proceedWithException( @@ -75,7 +87,12 @@ internal object UseCaseScanProcessor { is ScanCardException.UserCancelled, is ScanCardException.WrongAccessCode, is ScanCardException.WrongCardId, - -> onFailure(scanCardExceptionConverter.convertBack(exception)) + -> { + val error = scanCardExceptionConverter.convertBack(exception) + + Analytics.send(Basic.ScanError(error)) + onFailure(error) + } } } @@ -89,8 +106,9 @@ internal object UseCaseScanProcessor { navigateTo(exception.onboardingRoute) onWalletNotCreated() } - is ScanChainException.DisclaimerWasCanceled, - -> onFailure(scanCardExceptionConverter.convertBack(exception)) + is ScanChainException.DisclaimerWasCanceled -> { + onFailure(scanCardExceptionConverter.convertBack(exception)) + } } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index 8f72ce92f8..c7c56864ec 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -1,11 +1,11 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.right import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor @@ -19,11 +19,9 @@ import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor */ class AnalyticsChain( private val event: AnalyticsEvent, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { val interceptor = CardContextInterceptor(previousChainResult) val params = event.params.toMutableMap() interceptor.intercept(params) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 3db1f7a79e..66f708e28d 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.core.analytics.Analytics @@ -9,6 +8,7 @@ import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.addContext import com.tangem.tap.common.extensions.dispatchOnMain @@ -39,11 +39,9 @@ import org.rekotlin.Store class CheckForOnboardingChain( private val store: Store, private val tapWalletManager: TapWalletManager, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { tapWalletManager.updateConfigManager(previousChainResult) store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt index ccf4f495bd..35ad2efbab 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -1,11 +1,11 @@ package com.tangem.tap.domain.scanCard.chains -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain +import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState @@ -31,11 +31,9 @@ import kotlin.coroutines.resume internal class DisclaimerChain( private val store: Store, private val disclaimerWillShow: () -> Unit = {}, -) : Chain { +) : ResultChain() { - override suspend fun invoke( - previousChainResult: ScanResponse, - ): Either { + override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { val disclaimer = previousChainResult.card.createDisclaimer() return if (disclaimer.isAccepted()) { @@ -46,10 +44,7 @@ internal class DisclaimerChain( } } - private suspend fun showDisclaimer( - disclaimer: Disclaimer, - response: ScanResponse, - ): Either { + private suspend fun showDisclaimer(disclaimer: Disclaimer, response: ScanResponse): ScanChainResult { store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer)) return suspendCancellableCoroutine { continuation -> diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt new file mode 100644 index 0000000000..9698824c24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/FailedScansCounterChain.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.domain.scanCard.chains + +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.core.chain.Chain +import com.tangem.domain.models.scan.ScanResponse + +internal class FailedScansCounterChain( + private val onMaxUnsuccessfulScansReached: () -> Unit, + private val maxUnsuccessfulScans: Int = 3, +) : Chain { + + override suspend fun launch(previousChainResult: ScanChainResult): ScanChainResult { + if (previousChainResult.isLeft()) { + unsuccessfulScansCounter = unsuccessfulScansCounter.inc().coerceAtMost(maxUnsuccessfulScans) + if (unsuccessfulScansCounter == maxUnsuccessfulScans) { + onMaxUnsuccessfulScansReached() + } + } else { + unsuccessfulScansCounter = 0 + } + + return previousChainResult + } + + private companion object { + + var unsuccessfulScansCounter = 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt index 588dc809e2..ae5e9f8ad3 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -8,7 +8,11 @@ sealed class ScanChainException : ScanCardException.ChainException() { /** * May be returned from [DisclaimerChain] * */ - object DisclaimerWasCanceled : ScanChainException() + data object DisclaimerWasCanceled : ScanChainException() { + + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = DisclaimerWasCanceled + } /** * May be returned from [CheckForOnboardingChain] diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt new file mode 100644 index 0000000000..e83f4a1578 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainResult.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.domain.scanCard.chains + +import arrow.core.Either +import com.tangem.domain.card.ScanCardException +import com.tangem.domain.models.scan.ScanResponse + +internal typealias ScanChainResult = Either \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt deleted file mode 100644 index a8c116cbe8..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanningFinishedChain.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.domain.scanCard.chains - -import arrow.core.Either -import arrow.core.right -import com.tangem.domain.card.ScanCardException -import com.tangem.domain.core.chain.Chain -import com.tangem.domain.models.scan.ScanResponse - -/** - * Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in - * after card scanning chains. Always returns result of previous chain. - * - * @param onScanningFinished a suspending function to be called when the scanning process has finished. - * - * @see Chain for more information about the Chain interface. - */ -internal class ScanningFinishedChain( - private val onScanningFinished: suspend () -> Unit, -) : Chain { - - override suspend fun invoke(previousChainResult: ScanResponse): Either { - onScanningFinished() - return previousChainResult.right() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index f74d706775..8aeacbb2c9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -1,32 +1,27 @@ package com.tangem.tap.domain.scanCard.repository -import arrow.core.Either -import arrow.core.raise.either import com.tangem.common.CompletionResult -import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter +// TODO: Move to the :data:card module internal class DefaultScanCardRepository( private val tangemSdkManager: TangemSdkManager, ) : ScanCardRepository { private val exceptionConverter = ScanCardExceptionConverter() - override suspend fun scanCard( - cardId: String?, - allowRequestAccessCodeFromStorage: Boolean, - ): Either = either { - when ( + override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse { + return when ( val result = tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, ) ) { is CompletionResult.Success -> result.data - is CompletionResult.Failure -> raise(exceptionConverter.convert(result.error)) + is CompletionResult.Failure -> throw exceptionConverter.convert(result.error) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt index 96b0593c4e..09a32da708 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -33,7 +33,7 @@ class ResetToFactorySettingsTask( } private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { - if (session.environment.card?.backupStatus?.isActive != true) { + if (session.environment.card?.backupStatus == Card.BackupStatus.NoBackup) { callback(CompletionResult.Success(session.environment.card!!)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt index fe064832d6..8a3f2fe559 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt @@ -18,4 +18,5 @@ internal data class UserWalletPublicInformation( val cardsInWallet: Set, val scanResponse: ScanResponse, val isMultiCurrency: Boolean, + val hasBackupError: Boolean = false, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 4116cde096..451e717e9e 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -20,6 +20,7 @@ internal val UserWallet.publicInformation: UserWalletPublicInformation wallets = emptyList(), ), ), + hasBackupError = hasBackupError, ) internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { @@ -30,6 +31,7 @@ internal fun UserWalletPublicInformation.toUserWallet(): UserWallet { cardsInWallet = cardsInWallet, scanResponse = scanResponse, isMultiCurrency = isMultiCurrency, + hasBackupError = hasBackupError, ) } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt index 16fba8e3f2..6ba979beab 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt @@ -291,12 +291,12 @@ enum class DerivationPathSelectorType { internal sealed class AddCustomTokenWarning(val description: TextReference) { /** Potential scam warning */ - object PotentialScamToken : AddCustomTokenWarning( + data object PotentialScamToken : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_validation_error_not_found), ) /** Token already added warning */ - object TokenAlreadyAdded : AddCustomTokenWarning( + data object TokenAlreadyAdded : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_validation_error_already_added), ) @@ -305,7 +305,7 @@ internal sealed class AddCustomTokenWarning(val description: TextReference) { description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName), ) - object WrongDerivationPath : AddCustomTokenWarning( + data object WrongDerivationPath : AddCustomTokenWarning( description = TextReference.Res(R.string.custom_token_invalid_derivation_path), ) } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 9fdc0581da..3dd2804b29 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -15,7 +15,6 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver @@ -674,7 +673,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { return try { DerivationPath(rawPath) - } catch (error: HDWalletError) { + } catch (error: Throwable) { null } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 627b9bf0d5..05c586372d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -16,29 +16,31 @@ sealed class DetailsAction : Action { val shouldSaveUserWallets: Boolean, ) : DetailsAction() - object ReCreateTwinsWallet : DetailsAction() + data object ReCreateTwinsWallet : DetailsAction() sealed class ResetToFactory : DetailsAction() { - object Start : ResetToFactory() - object Proceed : ResetToFactory() + data object Start : ResetToFactory() + data object Proceed : ResetToFactory() data class AcceptCondition1(val accepted: Boolean) : ResetToFactory() data class AcceptCondition2(val accepted: Boolean) : ResetToFactory() - object Failure : ResetToFactory() - object Success : ResetToFactory() + data object Failure : ResetToFactory() + data object Success : ResetToFactory() + + data class LastWarningDialogVisibility(val isShown: Boolean) : ResetToFactory() } - object ScanCard : DetailsAction() + data object ScanCard : DetailsAction() data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() - object ResetCardSettingsData : DetailsAction() - object ScanAndSaveUserWallet : DetailsAction() { + data object ResetCardSettingsData : DetailsAction() + data object ScanAndSaveUserWallet : DetailsAction() { - object Success : DetailsAction() + data object Success : DetailsAction() data class Error(val error: TextReference?) : DetailsAction() } - object DismissError : DetailsAction() + data object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() @@ -50,14 +52,14 @@ sealed class DetailsAction : Action { } sealed class ManageSecurity : DetailsAction() { - object OpenSecurity : ManageSecurity() + data object OpenSecurity : ManageSecurity() data class SelectOption(val option: SecurityOption) : ManageSecurity() - object SaveChanges : ManageSecurity() { - object Success : ManageSecurity() - object Failure : ManageSecurity() + data object SaveChanges : ManageSecurity() { + data object Success : ManageSecurity() + data object Failure : ManageSecurity() } - object ChangeAccessCode : ManageSecurity() + data object ChangeAccessCode : ManageSecurity() } sealed class AppSettings : DetailsAction() { @@ -65,7 +67,7 @@ sealed class DetailsAction : Action { val enable: Boolean, val setting: AppSetting, ) : AppSettings() { - object Success : AppSettings() + data object Success : AppSettings() data class Failure( val prevState: Boolean, @@ -77,7 +79,7 @@ sealed class DetailsAction : Action { val lifecycleScope: LifecycleCoroutineScope, ) : AppSettings() - object EnrollBiometrics : AppSettings() + data object EnrollBiometrics : AppSettings() data class BiometricsStatusChanged( val needEnrollBiometrics: Boolean, ) : AppSettings() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index e829d9c937..5b3f2fafd3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -114,6 +114,7 @@ private fun handlePrepareCardSettingsScreen( null }, isShowPasswordResetRadioButton = isShowPasswordResetRadioButton, + isLastWarningDialogShown = false, ) return state.copy(cardSettingsState = cardSettingsState) } @@ -181,6 +182,13 @@ private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: Detai ), ) } + is DetailsAction.ResetToFactory.LastWarningDialogVisibility -> { + state.copy( + cardSettingsState = cardSettingsState?.copy( + isLastWarningDialogShown = action.isShown, + ), + ) + } else -> state } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 1eab7ec856..e1c523e759 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -46,6 +46,7 @@ data class CardSettingsState( val condition2Checked: Boolean, val accessCodeRecovery: AccessCodeRecoveryState? = null, val isShowPasswordResetRadioButton: Boolean, + val isLastWarningDialogShown: Boolean, ) data class ManageSecurityState( diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index c6422a0706..c45ec9dae3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -13,12 +13,12 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletconnect.WalletConnectActions import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 57376f6c70..d050b873e5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -5,8 +5,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.features.details.redux.DetailsState @@ -24,11 +27,26 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber, private val walletsRepository: WalletsRepository, + private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, + private val getSupportFeedbackEmailUseCase: GetSupportFeedbackEmailUseCase, + private val emailSender: EmailSender, ) { var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) @@ -139,7 +147,21 @@ internal class DetailsViewModel( private fun sendFeedback() { Analytics.send(Basic.ButtonSupport()) - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { + mainScope.launch { + val email = getSupportFeedbackEmailUseCase() + emailSender.send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } else { + store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + } } private fun navigateToAppSettings() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index 1320abaed7..35b4c9bae8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -11,9 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -35,6 +33,8 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni }, onBackClick = onBackClick, ) + + LastWarningDialog(state = state) } @Composable @@ -181,6 +181,26 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) { ) } +@Composable +private fun LastWarningDialog(state: ResetCardScreenState) { + if (state is ResetCardScreenState.ResetCardScreenContent && state.lastWarningDialog.isShown) { + BasicDialog( + title = stringResource(id = R.string.common_attention), + message = stringResource(id = R.string.card_settings_action_sheet_title), + dismissButton = DialogButton( + title = stringResource(id = R.string.card_settings_action_sheet_reset), + warning = true, + onClick = state.lastWarningDialog.onResetButtonClick, + ), + confirmButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = state.lastWarningDialog.onDismiss, + ), + onDismissDialog = state.lastWarningDialog.onDismiss, + ) + } +} + // region Preview @Composable private fun ResetCardScreenSample(modifier: Modifier = Modifier) { @@ -196,6 +216,11 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) { onAcceptCondition1ToggleClick = {}, onAcceptCondition2ToggleClick = {}, onResetButtonClick = {}, + lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( + isShown = false, + onResetButtonClick = {}, + onDismiss = {}, + ), ), onBackClick = {}, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index a12decbeef..d70071ed49 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -15,9 +15,16 @@ internal sealed class ResetCardScreenState { val onAcceptCondition1ToggleClick: (Boolean) -> Unit, val onAcceptCondition2ToggleClick: (Boolean) -> Unit, val onResetButtonClick: () -> Unit, + val lastWarningDialog: LastWarningDialog, ) : ResetCardScreenState() { val resetButtonEnabled: Boolean get() = accepted + + data class LastWarningDialog( + val isShown: Boolean, + val onResetButtonClick: () -> Unit, + val onDismiss: () -> Unit, + ) } internal enum class WarningsToReset { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 62218c3752..02e00914e1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -30,7 +30,25 @@ internal class ResetCardViewModel(private val store: Store) { acceptCondition2Checked = state?.condition2Checked ?: false, onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) }, onAcceptCondition2ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition2(it)) }, - onResetButtonClick = { store.dispatch(DetailsAction.ResetToFactory.Proceed) }, + onResetButtonClick = { showLastWarningDialog() }, + lastWarningDialog = ResetCardScreenState.ResetCardScreenContent.LastWarningDialog( + isShown = state?.isLastWarningDialogShown ?: false, + onResetButtonClick = ::onLastWarningDialogResetClicked, + onDismiss = ::onLastWarningDialogDismiss, + ), ) } + + private fun showLastWarningDialog() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = true)) + } + + private fun onLastWarningDialogResetClicked() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) + store.dispatch(DetailsAction.ResetToFactory.Proceed) + } + + private fun onLastWarningDialogDismiss() { + store.dispatch(DetailsAction.ResetToFactory.LastWarningDialogVisibility(isShown = false)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index c096d0531e..cb6a206195 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -2,8 +2,8 @@ package com.tangem.tap.features.details.ui.walletconnect import androidx.lifecycle.* import arrow.core.getOrElse -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index d3f92e4286..23a605e2d5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -7,7 +7,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index e87ba1ee99..cb2d157825 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.home.redux import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.tap.common.entities.IndeterminateProgressButton import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action @@ -29,6 +28,4 @@ sealed class HomeAction : Action { data class GoToShop(val userCountryCode: String?) : HomeAction() data class UpdateCountryCode(val userCountryCode: String) : HomeAction() - - data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index be5b026c2d..18ec4fb565 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -15,12 +15,10 @@ import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.entities.IndeterminateProgressButton import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL -import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope @@ -32,6 +30,8 @@ import org.rekotlin.Action import org.rekotlin.Middleware import timber.log.Timber +private const val HIDE_PROGRESS_DELAY = 400L + object HomeMiddleware { val handler = homeMiddleware @@ -85,17 +85,16 @@ private suspend fun readCard(analyticsEvent: AnalyticsEvent?) { analyticsEvent = analyticsEvent, onProgressStateChange = { showProgress -> if (showProgress) { - changeButtonState(ButtonState.PROGRESS) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = true)) } else { - changeButtonState(ButtonState.ENABLED) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } }, - onScanStateChange = { scanInProgress -> - store.dispatch(HomeAction.ScanInProgress(scanInProgress)) - }, onFailure = { Timber.e(it, "Unable to scan card") - changeButtonState(ButtonState.ENABLED) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) }, onSuccess = { scanResponse -> proceedWithScanResponse(scanResponse) @@ -142,10 +141,6 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { private suspend fun navigateTo(appScreen: AppScreen) { store.dispatchOnMain(NavigationAction.NavigateTo(appScreen)) - delay(timeMillis = 200) - changeButtonState(ButtonState.ENABLED) -} - -private fun changeButtonState(state: ButtonState) { - store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state))) + delay(HIDE_PROGRESS_DELAY) + store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt index 81f1adee30..bc71e1fef2 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeReducer.kt @@ -22,9 +22,6 @@ private fun internalReduce(action: Action, appState: AppState): HomeState { is HomeAction.ScanInProgress -> { state = state.copy(scanInProgress = action.scanInProgress) } - is HomeAction.ChangeScanCardButtonState -> { - state = state.copy(btnScanState = action.state) - } is HomeAction.UpdateCountryCode -> { state.onCountryCodeUpdate(state, action.userCountryCode) } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt index 7ffd5d139e..2e584d3d35 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeState.kt @@ -1,15 +1,14 @@ package com.tangem.tap.features.home.redux +import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import com.tangem.tap.common.entities.IndeterminateProgressButton -import com.tangem.tap.features.send.redux.states.ButtonState import org.rekotlin.StateType import java.util.Locale +@Immutable data class HomeState( val scanInProgress: Boolean = false, - val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED), val stories: List = initDefaultStories(), ) : StateType { diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index dc5b58c12a..6f5d289dd9 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -10,6 +10,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.tap.features.main.model.MainScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -17,12 +18,14 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class MainViewModel @Inject constructor( private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, private val reduxNavController: ReduxNavController, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, + private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val dispatchers: CoroutineDispatcherProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -41,6 +44,10 @@ internal class MainViewModel @Inject constructor( observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() + + viewModelScope.launch(dispatchers.main) { + deleteDeprecatedLogsUseCase() + } } private fun updateAppCurrencies() { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 6d69e20e28..4edb21a332 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -9,6 +9,7 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.userwallets.UserWalletBuilder @@ -41,8 +42,8 @@ object OnboardingHelper { response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() val activationInProgress = onboardingManager?.isActivationInProgress(cardId) - val backupNotActive = response.card.backupStatus?.isActive != true - emptyWallets || activationInProgress == true || backupNotActive + val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup + emptyWallets || activationInProgress == true || isNoBackup } response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false @@ -72,6 +73,7 @@ object OnboardingHelper { scanResponse: ScanResponse, accessCode: String? = null, backupCardsIds: List? = null, + hasBackupError: Boolean = false, ) { Analytics.setContext(scanResponse) scope.launch { @@ -96,7 +98,7 @@ object OnboardingHelper { // When should not save user wallets but device has biometry and save wallet screen has not been shown, // then open save wallet screen tangemSdkManager.canUseBiometry && preferencesStorage.shouldShowSaveUserWalletScreen -> { - proceedWithScanResponse(scanResponse, backupCardsIds) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) delay(timeMillis = 1_200) @@ -113,7 +115,7 @@ object OnboardingHelper { } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> { - proceedWithScanResponse(scanResponse, backupCardsIds) + proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) } } @@ -133,8 +135,13 @@ object OnboardingHelper { } } - private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List?) { - val userWallet = UserWalletBuilder(scanResponse) + private suspend fun proceedWithScanResponse( + scanResponse: ScanResponse, + backupCardsIds: List?, + hasBackupError: Boolean, + ) { + val userWallet = UserWalletBuilder(scanResponse = scanResponse) + .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() .guard { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 6b536fc754..5bd2df0285 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -9,23 +9,23 @@ import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class OnboardingWalletAction : Action { - object Init : OnboardingWalletAction() - object GetToCreateWalletStep : OnboardingWalletAction() - object CreateWallet : OnboardingWalletAction() + data object Init : OnboardingWalletAction() + data object GetToCreateWalletStep : OnboardingWalletAction() + data object CreateWallet : OnboardingWalletAction() data class WalletWasCreated( val shouldSendAnalyticsEvent: Boolean, val result: CompletionResult, ) : OnboardingWalletAction() - object Done : OnboardingWalletAction() + data object Done : OnboardingWalletAction() data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction() - object ResumeBackup : OnboardingWalletAction() + data object ResumeBackup : OnboardingWalletAction() data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction() class SetArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() - object OnBackPressed : OnboardingWalletAction() + data object OnBackPressed : OnboardingWalletAction() } sealed class OnboardingWallet2Action : OnboardingWalletAction() { @@ -49,45 +49,46 @@ sealed class OnboardingWallet2Action : OnboardingWalletAction() { sealed class BackupAction : Action { - object IntroduceBackup : BackupAction() - object StartBackup : BackupAction() - object SkipBackup : BackupAction() + data object IntroduceBackup : BackupAction() + data object StartBackup : BackupAction() + data object SkipBackup : BackupAction() - object StartAddingPrimaryCard : BackupAction() - object ScanPrimaryCard : BackupAction() + data object ErrorInBackupCard : BackupAction() + data object StartAddingPrimaryCard : BackupAction() + data object ScanPrimaryCard : BackupAction() /** * Check for unfinished backup of standard Wallets * See more GlobalAction.Onboarding.StartForUnfinishedBackup */ - object CheckForUnfinishedBackup : BackupAction() + data object CheckForUnfinishedBackup : BackupAction() - object StartAddingBackupCards : BackupAction() - object AddBackupCard : BackupAction() { - object Success : BackupAction() + data object StartAddingBackupCards : BackupAction() + data object AddBackupCard : BackupAction() { + data object Success : BackupAction() data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction() } - object FinishAddingBackupCards : BackupAction() + data object FinishAddingBackupCards : BackupAction() - object ShowAccessCodeInfoScreen : BackupAction() - object ShowEnterAccessCodeScreen : BackupAction() + data object ShowAccessCodeInfoScreen : BackupAction() + data object ShowEnterAccessCodeScreen : BackupAction() data class CheckAccessCode(val accessCode: String) : BackupAction() data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction() data class SaveFirstAccessCode(val accessCode: String) : BackupAction() data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction() - object OnAccessCodeDialogClosed : BackupAction() + data object OnAccessCodeDialogClosed : BackupAction() - object PrepareToWritePrimaryCard : BackupAction() - object WritePrimaryCard : BackupAction() + data object PrepareToWritePrimaryCard : BackupAction() + data object WritePrimaryCard : BackupAction() data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() data class WriteBackupCard(val cardNumber: Int) : BackupAction() data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction() - object DiscardBackup : BackupAction() - object DiscardSavedBackup : BackupAction() - object ResumeFoundUnfinishedBackup : BackupAction() + data object DiscardBackup : BackupAction() + data object DiscardSavedBackup : BackupAction() + data object ResumeFoundUnfinishedBackup : BackupAction() data class ResetBackupCard(val cardId: String) : BackupAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 460734903f..16045b38c8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.domain.userwallets.Artwork import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource +import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.backup.BackupService import com.tangem.tap.* @@ -173,6 +174,7 @@ private fun handleWalletAction(action: Action) { scanResponse = updatedScanResponse, accessCode = backupState.accessCode, backupCardsIds = backupState.backupCardIds, + hasBackupError = backupState.hasBackupError, ) } } @@ -484,6 +486,10 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) backupService.proceedBackup { result -> when (result) { is CompletionResult.Success -> { + val backupValidator = BackupValidator() + if (!backupValidator.isValid(CardDTO(result.data))) { + store.dispatchOnMain(BackupAction.ErrorInBackupCard) + } if (backupService.currentState == BackupService.State.Finished) { store.dispatchOnMain(BackupAction.FinishBackup()) } else { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 61bee3be0f..1a400ea778 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -112,6 +112,7 @@ private object BackupReducer { } else { state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber)) } + is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true) is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished) is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished) BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 0920fb4a9b..64b81fc771 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -70,6 +70,7 @@ data class BackupState( val canSkipBackup: Boolean = true, val isInterruptedBackup: Boolean = false, val showBtnLoading: Boolean = false, + val hasBackupError: Boolean = false, ) enum class AccessCodeError { diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 775a5cc578..0402c5e2de 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -25,10 +25,14 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.features.send.api.navigation.SendRouter.Companion.CRYPTO_CURRENCY_KEY import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token @@ -60,6 +64,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import java.text.DecimalFormatSymbols import javax.inject.Inject @@ -82,11 +87,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { private val sendSubscriber = SendStateSubscriber(this) private lateinit var keyboardObserver: KeyboardObserver + private val cryptoCurrency: CryptoCurrency? + get() = arguments?.getParcelable(CRYPTO_CURRENCY_KEY) + val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind) @Inject lateinit var listenToQrScanningUseCase: ListenToQrScanningUseCase + @Inject + lateinit var parseQrCodeUseCase: ParseQrCodeUseCase + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) @@ -177,13 +188,21 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } .flowWithLifecycle(this@SendFragment.lifecycle, minActiveState = Lifecycle.State.CREATED) - .collect { + .collect { rawQr -> delay(200) // Delayed launch is needed in order for the UI to be drawn and to process the sent events. // If do not use the delay, then etAmount error field is not displayed when // inserting an incorrect amount by shareUri - onCodeScanned(it) + cryptoCurrency?.let { cryptoCurrency -> + parseQrCodeUseCase(rawQr, cryptoCurrency = cryptoCurrency).fold( + ifLeft = { + onCodeScanned(QrResult(address = rawQr)) + Timber.w(it) + }, + ifRight = { onCodeScanned(it) }, + ) + } ?: onCodeScanned(QrResult(address = rawQr)) } } } @@ -254,15 +273,18 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { .launchIn(mainScope) } - private fun onCodeScanned(scannedCode: String) { - if (scannedCode.isEmpty()) return + private fun onCodeScanned(parsedQr: QrResult) { + if (parsedQr.address.isEmpty()) return store.dispatch( PasteAddress( - data = scannedCode, + data = parsedQr.address, sourceType = Token.Send.AddressEntered.SourceType.QRCode, ), ) + parsedQr.amount?.let { amount -> + store.dispatchOnMain(AmountAction.SetAmount(amount, isUserInput = false)) + } store.dispatch(TruncateOrRestore(!binding.lSendAddress.etAddress.isFocused)) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt index 5e9f50747f..a86e233c8b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/KaspaWarningDialog.kt @@ -2,17 +2,20 @@ 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.tap.features.send.redux.SendAction import com.tangem.tap.store import com.tangem.wallet.R object KaspaWarningDialog { + fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.common_warning) setMessage( context.getString( - R.string.kaspa_withdrawal_message_warning, + R.string.common_utxo_validate_withdrawal_message_warning, + Blockchain.Kaspa.fullName, dialog.maxOutputs, dialog.maxAmount.toPlainString(), ), @@ -23,7 +26,6 @@ object KaspaWarningDialog { setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } - } - .create() + }.create() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt deleted file mode 100644 index 1abbd3d17c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.tangem.tap.features.shop.data - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.ShopResponse -import com.tangem.domain.common.extensions.withIOContext -import com.tangem.tap.features.shop.domain.ShopRepository -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import timber.log.Timber -import java.util.Locale - -/** - * Default implementation of shop feature repository - * - * @property tangemTechApi TangemTech API - * @property dispatchers coroutine dispatchers provider - * -[REDACTED_AUTHOR] - */ -internal class DefaultShopRepository( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, -) : ShopRepository { - - private val salesProductConverter = SalesProductConverter() - - override suspend fun isShopifyOrderingAvailable(): Boolean { - return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) } - .fold( - onSuccess = ShopResponse::isOrderingAvailable, - onFailure = { - Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)") - true - }, - ) - } - - override suspend fun getSalesProductInfo(): List { - return withIOContext { - val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME) - salesProductConverter.convert(salesInfo) - } - } - - private fun getLocaleName(): String { - return if (Locale.getDefault().language == "ru") { - RU_LOCALE - } else { - EN_LOCALE - } - } - - private companion object { - private const val SHOPIFY_NAME = "shopify" - private const val RU_LOCALE = "ru" - private const val EN_LOCALE = "en" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt deleted file mode 100644 index 9d8f440da6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.features.shop.data - -import com.tangem.datasource.api.tangemTech.models.SalesResponse -import com.tangem.tap.features.shop.domain.models.Notification -import com.tangem.tap.features.shop.domain.models.ProductState -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.utils.converter.Converter - -internal class SalesProductConverter : Converter> { - - override fun convert(value: SalesResponse): List { - return value.sales.map { sales -> - val productState = when (sales.state) { - "order" -> ProductState.ORDER - "pre-order" -> ProductState.PRE_ORDER - "sold-out" -> ProductState.SOLD_OUT - else -> ProductState.SOLD_OUT - } - val productType = when (sales.product.code) { - "pack2" -> ProductType.WALLET_2_CARDS - "pack3" -> ProductType.WALLET_3_CARDS - else -> ProductType.WALLET_3_CARDS - } - SalesProduct( - id = sales.id, - productType = productType, - state = productState, - name = sales.product.name, - notification = sales.notification?.let { notification -> - Notification( - type = notification.type, - title = notification.title, - description = notification.description, - ) - }, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt deleted file mode 100644 index a6f22bec79..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.tap.features.shop.di - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.tap.features.shop.data.DefaultShopRepository -import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase -import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase -import com.tangem.tap.features.shop.domain.ShopRepository -import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ViewModelComponent -import dagger.hilt.android.scopes.ViewModelScoped - -@Module -@InstallIn(ViewModelComponent::class) -internal object ShopUseCaseModule { - - @Provides - @ViewModelScoped - fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase { - return DefaultShopifyOrderingAvailabilityUseCase( - shopRepository = shopRepository, - ) - } - - @Provides - @ViewModelScoped - fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase { - return GetShopifySalesProductsUseCase( - shopRepository = shopRepository, - ) - } - - @Provides - @ViewModelScoped - fun provideDefaultShopRepository( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - ): ShopRepository { - return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt deleted file mode 100644 index 97b5839f47..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.shop.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager -import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object ShopifyTogglesModule { - - @Provides - @Singleton - fun provideDefaultShopifyFeatureToggleManager( - featureToggleManager: FeatureTogglesManager, - ): ShopifyFeatureToggleManager { - return DefaultShopifyFeatureToggleManager(featureToggleManager) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt deleted file mode 100644 index 41bbe3d449..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/DefaultShopifyOrderingAvailabilityUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.tap.features.shop.domain - -/** - * Default implementation of use case to define shopify ordering availability - * - * @property shopRepository shop feature repository - * -[REDACTED_AUTHOR] - */ -internal class DefaultShopifyOrderingAvailabilityUseCase( - private val shopRepository: ShopRepository, -) : ShopifyOrderingAvailabilityUseCase { - - override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt deleted file mode 100644 index 7881b82dea..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.shop.domain - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.tap.features.shop.domain.models.SalesError -import com.tangem.tap.features.shop.domain.models.SalesProduct - -/** - * Use case to get shopify available products - * - * @property shopRepository shop feature repository - */ -class GetShopifySalesProductsUseCase( - private val shopRepository: ShopRepository, -) { - - suspend operator fun invoke(): Either> { - return try { - shopRepository.getSalesProductInfo().right() - } catch (e: Exception) { - SalesError.DataError(e).left() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt deleted file mode 100644 index 67803630b8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.tap.features.shop.domain - -import com.tangem.tap.features.shop.domain.models.SalesProduct - -/** - * Shop feature repository - * -[REDACTED_AUTHOR] - */ -interface ShopRepository { - - /** Get shopify ordering availability */ - suspend fun isShopifyOrderingAvailable(): Boolean - - /** Get actual sales product info */ - suspend fun getSalesProductInfo(): List -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt deleted file mode 100644 index 5f6c351ce2..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopifyOrderingAvailabilityUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.shop.domain - -/** - * Use case to define shopify ordering availability - * -[REDACTED_AUTHOR] - */ -internal interface ShopifyOrderingAvailabilityUseCase { - - /** Get availability */ - suspend operator fun invoke(): Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt deleted file mode 100644 index 0987fdd8bd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" -private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" - -enum class ProductType(val sku: String) { - - WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU), - WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU), - ; - - companion object { - val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) - fun fromSku(sku: String): ProductType? { - return when (sku) { - WALLET_2_CARDS.sku -> WALLET_2_CARDS - WALLET_3_CARDS.sku -> WALLET_3_CARDS - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt deleted file mode 100644 index 373b8d14c8..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -sealed class SalesError { - data class DataError(val cause: Throwable) : SalesError() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt deleted file mode 100644 index b3b5a083d3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.shop.domain.models - -/** - * Sales product - * - * @property id product id - * @property productType shows TW2 cards or 3 cards - * @property state state as order available etc - * @property name product name - * @property notification optional notification - */ -data class SalesProduct( - val id: String, - val productType: ProductType, - val state: ProductState, - val name: String, - val notification: Notification?, -) - -data class Notification( - val type: String, - val title: String, - val description: String, -) - -enum class ProductState { - ORDER, - SOLD_OUT, - PRE_ORDER, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt deleted file mode 100644 index 2e39e2bd4f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.features.shop.presentation - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase -import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase -import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch -import javax.inject.Inject - -/** - * Shop screen view model - * - * @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability - * @property getShopifySalesProductsUseCase use case to get actual sales info - * @property dispatchers coroutine dispatchers provider - * @property appStateHolder redux state holder - * -[REDACTED_AUTHOR] - */ -@HiltViewModel -internal class ShopViewModel @Inject constructor( - private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase, - private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase, - private val dispatchers: CoroutineDispatcherProvider, - private val appStateHolder: AppStateHolder, -) : ViewModel() { - - /** Check ordering delay block visibility */ - fun checkOrderingDelayBlockVisibility() { - viewModelScope.launch(dispatchers.main) { - val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() } - .fold(onSuccess = { !it }, onFailure = { false }) - - appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility)) - } - } - - /** - * Get actual sales products info - * to configure view dynamically - */ - fun getActualSalesInfo() { - viewModelScope.launch(dispatchers.main) { - val action = getShopifySalesProductsUseCase().fold( - ifLeft = { - ShopAction.SalesProductsError - }, - ifRight = { - ShopAction.SalesProductsLoaded(it) - }, - ) - appStateHolder.mainStore?.dispatch(action) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt deleted file mode 100644 index a72324e845..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import android.content.Intent -import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.common.shop.googlepay.GooglePayService -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.wallet.R -import org.rekotlin.Action - -sealed interface ShopAction : Action { - - object LoadProducts : ShopAction { - data class Success(val products: List) : ShopAction - object Failure : ShopAction, NotificationAction { - override val messageResource = R.string.common_server_unavailable - } - } - - data class ApplyPromoCode(val promoCode: String) : ShopAction { - data class Success(val promoCode: String?, val products: List) : ShopAction - object InvalidPromoCode : ShopAction - } - - object BuyWithGooglePay : ShopAction { - object UserCancelled : ShopAction - data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction - - data class Failure(val exception: Throwable) : ShopAction - object Success : ShopAction - } - - object StartWebCheckout : ShopAction - - data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction { - object Success : ShopAction - object Failure : ShopAction - } - - data class SelectProduct(val productType: ProductType) : ShopAction - - object FinishSuccessfulOrder : ShopAction - - object ResetState : ShopAction - - data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction - - data class SalesProductsLoaded(val salesProducts: List) : ShopAction - - object SalesProductsError : ShopAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt deleted file mode 100644 index f049ef01b6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopMiddleware.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.scope -import com.tangem.tap.shopService -import com.tangem.tap.store -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware -import timber.log.Timber - -class ShopMiddleware { - - val shopMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - handle(action) - next(action) - } - } - } -} - -@Suppress("LongMethod", "ComplexMethod") -private fun handle(action: Action) { - val shopState = store.state.shopState - - if (action is NavigationAction.NavigateTo && action.screen == AppScreen.Shop) { - store.dispatch(ShopAction.LoadProducts) - } - - if (action !is ShopAction) return - - when (action) { - is ShopAction.ApplyPromoCode -> { - scope.launch { - if (action.promoCode.isBlank() && shopState.promoCode == null) { - store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) - return@launch - } - - val result = shopService.applyPromoCode(action.promoCode) - - result.onSuccess { products -> - store.dispatchOnMain( - ShopAction.ApplyPromoCode.Success( - promoCode = products.first { it.type == shopState.selectedProduct }.appliedDiscount, - products = products, - ), - ) - } - result.onFailure { store.dispatchOnMain(ShopAction.ApplyPromoCode.InvalidPromoCode) } - } - } - ShopAction.BuyWithGooglePay -> { - shopService.buyWithGooglePay(shopState.selectedProduct) -// shopService.subscribeToGooglePayResult(productType = shopState.selectedProduct) { result -> -// result.onSuccess { -// store.dispatch(ShopAction.BuyWithGooglePay.Success) -// } -// result.onFailure { error -> -// if (error is TangemSdkError.UserCancelled) { -// store.dispatch(ShopAction.BuyWithGooglePay.UserCancelled) -// } else { -// store.dispatch(ShopAction.BuyWithGooglePay.Failure(error)) -// } -// } -// } - } - is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> { - scope.launch { - val result = shopService.handleGooglePayResult( - action.resultCode, - action.data, - shopState.selectedProduct, - ) - result.onSuccess { - store.dispatchOnMain(ShopAction.BuyWithGooglePay.Success) - } - result.onFailure { - store.dispatchOnMain(ShopAction.BuyWithGooglePay.Failure(it)) - } - } - } - ShopAction.LoadProducts -> { - scope.launch { - shopService.getProducts().fold( - onSuccess = { store.dispatchOnMain(ShopAction.LoadProducts.Success(it)) }, - onFailure = { - Timber.e(it) - store.dispatchOnMain(ShopAction.LoadProducts.Failure) - }, - ) - } - } - is ShopAction.CheckIfGooglePayAvailable -> { - scope.launch { - val isAvailable = - shopService.checkIfGooglePayAvailable(action.googlePayService).getOrNull() - ?: false - val newAction = if (isAvailable) { - ShopAction.CheckIfGooglePayAvailable.Success - } else { - ShopAction.CheckIfGooglePayAvailable.Failure - } - store.dispatchOnMain(newAction) - } - } - ShopAction.StartWebCheckout -> { - Analytics.send(Shop.Redirected(null)) - store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct)) - store.dispatch(ShopAction.FinishSuccessfulOrder) - } - - is ShopAction.FinishSuccessfulOrder -> { - scope.launch { - shopService.waitForCheckout(shopState.selectedProduct) - } - } - else -> {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt deleted file mode 100644 index 11061c7abf..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import org.rekotlin.Action - -object ShopReducer { - fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, state: ShopState): ShopState { - if (action !is ShopAction) return state - - return when (action) { - is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true) - is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products) - is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false) - is ShopAction.ApplyPromoCode.Success -> { - state.copy( - promoCode = action.promoCode, - availableProducts = action.products, - promoCodeLoading = false, - ) - } - is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType) - - // TODO: change when we add support for GPay - is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false) - is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false) - - is ShopAction.ResetState -> ShopState() - is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility) - is ShopAction.BuyWithGooglePay, - is ShopAction.LoadProducts, - is ShopAction.StartWebCheckout, - is ShopAction.CheckIfGooglePayAvailable, - is ShopAction.BuyWithGooglePay.Failure, - is ShopAction.BuyWithGooglePay.HandleGooglePayResponse, - is ShopAction.BuyWithGooglePay.Success, - is ShopAction.BuyWithGooglePay.UserCancelled, - is ShopAction.FinishSuccessfulOrder, - is ShopAction.LoadProducts.Failure, - -> state - is ShopAction.SalesProductsLoaded -> state.copy( - salesProducts = action.salesProducts, - ) - is ShopAction.SalesProductsError -> state.copy( - salesProducts = emptyList(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt deleted file mode 100644 index 08bac48f4d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.tap.features.shop.redux - -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.common.shop.data.TangemProduct -import com.tangem.tap.features.shop.domain.models.SalesProduct -import org.rekotlin.StateType - -data class ShopState( - val availableProducts: List = emptyList(), - val selectedProduct: ProductType = ProductType.WALLET_3_CARDS, - val salesProducts: List = emptyList(), - val promoCode: String? = null, - val promoCodeLoading: Boolean = false, - val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay - val isOrderingDelayBlockVisible: Boolean = false, -) : StateType { - val total: String? - get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue - - val priceBeforeDiscount: String? - get() { - val totalSum = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum - if (totalSum?.finalValue != totalSum?.beforeDiscount) { - return totalSum?.beforeDiscount - } - return null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt deleted file mode 100644 index eda902f707..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.tap.features.shop.toggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class DefaultShopifyFeatureToggleManager( - private val featureTogglesManager: FeatureTogglesManager, -) : ShopifyFeatureToggleManager { - - override val isDynamicSalesProductsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt deleted file mode 100644 index ab3b8fc968..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.shop.toggles - -/** - * Shopify feature toggle manager that provides info about shopify toggle availability - * - */ -interface ShopifyFeatureToggleManager { - - val isDynamicSalesProductsEnabled: Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt deleted file mode 100644 index bfb86a0f8a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.tangem.tap.features.shop.ui - -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.View.OnFocusChangeListener -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import androidx.activity.OnBackPressedCallback -import androidx.fragment.app.viewModels -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.common.GlobalLayoutStateHandler -import com.tangem.tap.common.KeyboardObserver -import com.tangem.tap.common.extensions.getQuantityString -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.shop.domain.models.ProductState -import com.tangem.tap.features.shop.domain.models.ProductType -import com.tangem.tap.features.shop.domain.models.SalesProduct -import com.tangem.tap.features.shop.presentation.ShopViewModel -import com.tangem.tap.features.shop.redux.ShopAction -import com.tangem.tap.features.shop.redux.ShopState -import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentShopBinding -import dagger.hilt.android.AndroidEntryPoint -import org.rekotlin.StoreSubscriber -import javax.inject.Inject - -@AndroidEntryPoint -internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { - - @Inject - lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager - - private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) - private var cardTranslationY = 70f - - private lateinit var keyboardObserver: KeyboardObserver - - private val viewModel by viewModels() - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.shopState == newState.shopState - }.select { it.shopState } - } - storeSubscribersList.add(this) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { - viewModel.getActualSalesInfo() - } else { - viewModel.checkOrderingDelayBlockVisibility() - } - - activity?.onBackPressedDispatcher?.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) - store.dispatch(ShopAction.ResetState) - } - }, - ) - } - - override fun onDestroyView() { - super.onDestroyView() - keyboardObserver.unregisterListener() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - setupCardsImages() - setupProductSelection() - setupPromoCodeEditText() - - binding.toolbar.setNavigationOnClickListener { - requireActivity().onBackPressed() - } - - keyboardObserver = KeyboardObserver(requireActivity()).apply { - registerListener { isVisible -> - binding.flCards.show(!isVisible) - } - } - } - - @Suppress("MagicNumber") - private fun setupCardsImages() { - GlobalLayoutStateHandler(binding.imvSecond).apply { - onStateChanged = { - cardTranslationY = it.height * 0.15f - binding.imvSecond.animate() - .translationY(cardTranslationY) - .scaleX(0.9f) - .scaleY(0.9f) - .start() - binding.imvThird.animate() - .translationY(cardTranslationY * 2) - .scaleX(0.8f) - .scaleY(0.8f) - .start() - detach() - } - } - } - - private fun setupProductSelection() = with(binding) { - chipProduct1.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_3_CARDS)) - } - chipProduct2.setOnCheckedChangeListener { _, isChecked -> - if (isChecked) store.dispatch(ShopAction.SelectProduct(ProductType.WALLET_2_CARDS)) - } - chipProduct1.text = chipProduct1.getQuantityString(R.plurals.card_label_card_count, quantity = 3) - chipProduct2.text = chipProduct2.getQuantityString(R.plurals.card_label_card_count, quantity = 2) - } - - private fun setupPromoCodeEditText() = with(binding) { - etPromoCode.setOnEditorActionListener { view, actionId, event -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - val imm: InputMethodManager = - requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - view.clearFocus() - return@setOnEditorActionListener true - } - return@setOnEditorActionListener false - } - - etPromoCode.onFocusChangeListener = OnFocusChangeListener { _, hasFocus -> - if (!hasFocus) { - store.dispatch(ShopAction.ApplyPromoCode(etPromoCode.text.toString())) - } - } - } - - override fun newState(state: ShopState) { - if (activity == null || view == null) return - - animateProductSelection(state.selectedProduct) - handlePriceState(state) - handlePromoCodeState(state) - // TODO: https://tangem.slack.com/archives/C01HARKDLQ0/p1691421861756069 - // if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { - // handleNotificationBlock(state) - // } else { - // handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) - // } - handleButtonsState(state) - } - - private fun animateProductSelection(selectedProduct: ProductType) { - val show = when (selectedProduct) { - ProductType.WALLET_2_CARDS -> false - ProductType.WALLET_3_CARDS -> true - } - showOrHideThirdCardWithAnimation(show) - } - - private fun showOrHideThirdCardWithAnimation(show: Boolean) = with(binding) { - val translationY = if (show) cardTranslationY * 2 else cardTranslationY - if (show) imvThird.show() - imvThird.animate() - .translationY(translationY) - .setListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - super.onAnimationEnd(animation) - imvThird.show(show) - } - }, - ) - } - - private fun handlePriceState(state: ShopState) = with(binding) { - tvTotal.text = state.total - tvTotalBeforeDiscount.text = state.priceBeforeDiscount - - pbPrice.show(state.total == null) - } - - private fun handlePromoCodeState(state: ShopState) = with(binding) { - if (state.promoCode == null && !etPromoCode.hasFocus()) { - etPromoCode.setText("") - } - pbPromoCode.show(state.promoCodeLoading) - } - - // private fun handleOrderingDelayBlock(isVisible: Boolean) { - // if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() - // } - // - // private fun handleNotificationBlock(state: ShopState) { - // if (isVisible) { - // binding.tvSoldOutDesc.show() - // getSelectedSalesProduct(state)?.notification?.let { notification -> - // binding.tvSoldOutDesc.text = notification.description - // } - // } else { - // binding.tvSoldOutDesc.hide() - // } - // } - - private fun handleButtonsState(state: ShopState) = with(binding) { - btnPayGooglePay.root.show(state.isGooglePayAvailable) - btnAlternativePayment.show(state.isGooglePayAvailable) - btnMainAction.show(!state.isGooglePayAvailable) - - if (state.total != null) { - btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) } - btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - if (state.salesProducts.isNotEmpty()) { - getSelectedSalesProduct(state)?.let { selectedProduct -> - btnMainAction.text = getMainBtnTextByProductState( - productState = selectedProduct.state, - ) - } - } - } - } - - override fun handleOnBackPressed() { - store.dispatch(ShopAction.ResetState) - super.handleOnBackPressed() - } - - private fun getSelectedSalesProduct(state: ShopState): SalesProduct? { - return state.salesProducts.find { - it.productType == state.selectedProduct - } - } - - private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) { - ProductState.ORDER -> getString(R.string.shop_buy_now) - ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR - ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 28a73436b5..a76ae2c37e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -318,6 +318,7 @@ object TradeCryptoMiddleware { SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId, SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress, SendRouter.AMOUNT_KEY to txInfo?.amount, + SendRouter.TAG_KEY to txInfo?.tag, ) store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt new file mode 100644 index 0000000000..c5f04b9039 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.network.exchangeServices.moonpay + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Blockchain.* +import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency + +internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? + get() = when (this) { + Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo") + Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt") + Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum") + Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain") + Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb") + Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc") + BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch") + BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc") + Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada") + Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom") + Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge") + Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth") + EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc") + Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar") + Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc") + Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near") + Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism") + Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot") + Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon") + Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn") + Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol") + Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm") + Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz") + TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton") + Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx") + VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet") + XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp") + else -> null + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 947e8f1b69..3e5a70b2a2 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -12,6 +12,7 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME +import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -35,42 +36,46 @@ class MoonPayService( override suspend fun update() { withIOContext { performRequest { - val userStatusResult = performRequest { api.getUserStatus(apiKey) } - if (userStatusResult is Result.Failure) return@performRequest - - val currenciesResult = performRequest { api.getCurrencies(apiKey) } - if (currenciesResult is Result.Failure) return@performRequest - - val userStatus = (userStatusResult as Result.Success).data - val currencies = (currenciesResult as Result.Success).data - -// val currenciesToBuy = mutableListOf() - val currenciesToSell = mutableListOf() - - currencies.forEach { currencyStatus -> - if (currencyStatus.type != "crypto" || currencyStatus.isSuspended || - !currencyStatus.supportsLiveMode - ) { - return@forEach - } - - if (userStatus.countryCode == "USA") { - if (!currencyStatus.isSupportedInUS) return@forEach - if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach - } - val currencyCode = currencyStatus.code.uppercase() -// currenciesToBuy.add(currencyCode) - - if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode) + val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { + is Result.Failure -> return@performRequest + is Result.Success -> result.data } -// currenciesToBuy.sort() - currenciesToSell.sort() + + val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { + is Result.Failure -> return@performRequest + is Result.Success -> result.data + } + + val currenciesToSell = currencies + .filter { currency -> + checkGeneralRequirements(currency) && checkUSARequirements(userStatus, currency) + } + .mapNotNull { currency -> + MoonPayAvailableCurrency( + currencyCode = currency.code, + networkCode = currency.metadata?.networkCode ?: return@mapNotNull null, + contractAddress = currency.metadata.contractAddress, + ) + } status = MoonPayStatus(currenciesToSell, userStatus, currencies) } } } + private fun checkGeneralRequirements(currency: MoonPayCurrencies): Boolean { + return currency.type == "crypto" && !currency.isSuspended && currency.supportsLiveMode && + currency.isSellSupported + } + + private fun checkUSARequirements(userStatus: MoonPayUserStatus, currency: MoonPayCurrencies): Boolean { + return if (userStatus.countryCode == "USA") { + currency.isSupportedInUS && !currency.notAllowedUSStates.contains(userStatus.stateCode) + } else { + true + } + } + override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean { @@ -80,21 +85,21 @@ class MoonPayService( override fun availableForBuy(currency: Currency): Boolean = false override fun availableForSell(currency: Currency): Boolean { - val availableForSell = status?.availableForSell ?: return false - val metadata = status?.responseCurrencies?.filter { it.isSellSupported }?.map { it.metadata } if (!isSellAllowed()) return false - return when (currency) { - is Currency.Blockchain -> { - val blockchain = currency.blockchain - when { - blockchain.isTestnet() -> false - blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false - else -> availableForSell.contains(currency.currencySymbol) + val availableForSell = status?.availableForSell ?: return false + + val supportedCurrency = currency.blockchain.moonPaySupportedCurrency ?: return false + return availableForSell.any { + when (currency) { + is Currency.Blockchain -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.currencyCode.equals(other = supportedCurrency.currencyCode, ignoreCase = true) + } + is Currency.Token -> { + it.networkCode.equals(other = supportedCurrency.networkCode, ignoreCase = true) && + it.contractAddress.equals(other = currency.token.contractAddress, ignoreCase = true) } - } - is Currency.Token -> { - metadata?.any { it?.contractAddress.equals(currency.token.contractAddress, ignoreCase = true) } ?: false } } } @@ -103,7 +108,7 @@ class MoonPayService( action: CurrencyExchangeManager.Action, blockchain: Blockchain, cryptoCurrencyName: CryptoCurrencyName, - fatCurrency: String, + fiatCurrencyName: String, walletAddress: String, isDarkTheme: Boolean, ): String { @@ -115,7 +120,8 @@ class MoonPayService( .appendQueryParameter("apiKey", apiKey) .appendQueryParameter("baseCurrencyCode", cryptoCurrencyName) .appendQueryParameter("refundWalletAddress", walletAddress) - .appendQueryParameter("redirectURL", "tangem://sell-request.tangem.com") + .appendQueryParameter("redirectURL", "tangem://redirect_sell") + if (isDarkTheme) uri.appendQueryParameter("theme", "dark") val originalQuery = uri.build().encodedQuery ?: uri.build().toString() @@ -147,7 +153,7 @@ class MoonPayService( } private data class MoonPayStatus( - val availableForSell: List, + val availableForSell: List, val responseUserStatus: MoonPayUserStatus, val responseCurrencies: List, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt new file mode 100644 index 0000000000..5f7845f0c9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPayAvailableCurrency.kt @@ -0,0 +1,7 @@ +package com.tangem.tap.network.exchangeServices.moonpay.models + +internal data class MoonPayAvailableCurrency( + val currencyCode: String, + val networkCode: String, + val contractAddress: String?, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt new file mode 100644 index 0000000000..f858942b46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/models/MoonPaySupportedCurrency.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.network.exchangeServices.moonpay.models + +internal data class MoonPaySupportedCurrency(val networkCode: String, val currencyCode: String?) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 0afb10ab8d..c23def20bc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,7 +1,9 @@ package com.tangem.tap.proxy.redux +import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.AccountCreator import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -67,4 +69,6 @@ data class DaggerGraphState( val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val cardRepository: CardRepository? = null, val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles? = null, + val tangemSdkLogger: TangemSdkLogger? = null, + val blockchainSDKLogger: BlockchainSDKLogger? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 1c237b7938..346cab4f03 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -40,7 +40,7 @@ android:layout_height="match_parent" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="90dp" + android:layout_marginBottom="74dp" app:layout_constraintBottom_toTopOf="@id/btn_accept" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -49,7 +49,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt index 730ddb8d96..b1e1189c8e 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt @@ -37,6 +37,7 @@ internal class DefaultDerivationsRepositoryTest { cardsInWallet = setOf(), isMultiCurrency = false, scanResponse = ScanResponseMockFactory.create(cardConfig = GenericCardConfig(2), derivedKeys = emptyMap()), + hasBackupError = false, ) @Test diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 580334eb2b..8ebac44c3b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -102,7 +102,6 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { ), appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey, amplitudeApiKey = configValues.amplitudeApiKey, - shopify = configValues.shopifyShop, sprinklr = configValues.sprinklr, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 8490f44237..25e8595d33 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -14,7 +14,6 @@ data class Config( val isTopUpEnabled: Boolean = false, @Deprecated("Not relevant since version 3.23") val isCreatingTwinCardsAllowed: Boolean = false, - val shopify: ShopifyShop? = null, val sprinklr: SprinklrConfig? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index eaedb32084..0e572bd3e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -31,7 +31,6 @@ class ConfigValueModel( val blockcypherTokens: Set?, val infuraProjectId: String?, val appsFlyer: AppsFlyer, - val shopifyShop: ShopifyShop?, val sprinklr: SprinklrConfig?, val tronGridApiKey: String, val amplitudeApiKey: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt deleted file mode 100644 index c4a09adcfe..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/ShopifyShop.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.config.models - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class ShopifyShop( - @Json(name = "domain") - val domain: String, - @Json(name = "storefrontApiKeyAndroid") - val storefrontApiKey: String, - @Json(name = "merchantID") - val merchantID: String?, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index f3b1c56267..bfee20e460 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -9,6 +9,8 @@ interface UserWalletsStore { suspend fun getSyncOrNull(key: UserWalletId): UserWallet? + suspend fun getAllSyncOrNull(): List? + @Throws suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) } \ No newline at end of file diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt index 5d9047a83b..579c72689a 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/SellCurrencyDeepLink.kt @@ -4,13 +4,14 @@ import com.tangem.core.deeplink.DeepLink class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { - override val uri: String = "tangem://sell-request.tangem.com" + override val uri: String = "tangem://redirect_sell" override fun onReceive(params: Map) { val data = Data( transactionId = params["transactionId"] ?: return, baseCurrencyAmount = params["baseCurrencyAmount"] ?: return, depositWalletAddress = params["depositWalletAddress"] ?: return, + depositWalletAddressTag = params["depositWalletAddressTag"], ) onReceive(data) @@ -20,5 +21,6 @@ class SellCurrencyDeepLink(val onReceive: (data: Data) -> Unit) : DeepLink { val transactionId: String, val baseCurrencyAmount: String, val depositWalletAddress: String, + val depositWalletAddressTag: String?, ) } \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 639da533da..8f979c41f0 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "SHOPIFY_DYNAMIC_ENABLED", - "version": "undefined" - }, { "name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED", "version": "undefined" @@ -22,5 +18,9 @@ { "name": "LOCAL_USER_LOGS_ENABLED", "version": "5.8.0" + }, + { + "name": "GENERATE_XPUB_ENABLED", + "version": "undefined" } ] diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index bb00481f67..8dc7d5437d 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -9,6 +11,10 @@ android { } dependencies { + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + implementation(deps.material) implementation(deps.reKotlin) + implementation(deps.timber) } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt index 7ca534e15c..a0a6d64f25 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt @@ -11,7 +11,6 @@ data class NavigationState( enum class AppScreen(val isDialogFragment: Boolean = false) { Home, - Shop, Disclaimer, OnboardingNote, OnboardingWallet, diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt new file mode 100644 index 0000000000..0651ea8926 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/di/EmailSenderModule.kt @@ -0,0 +1,22 @@ +package com.tangem.core.navigation.di + +import android.content.Context +import com.tangem.core.navigation.email.AndroidEmailSender +import com.tangem.core.navigation.email.EmailSender +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal object EmailSenderModule { + + @Provides + @ActivityScoped + fun provideEmailSender(@ActivityContext context: Context): EmailSender { + return AndroidEmailSender(context) + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt new file mode 100644 index 0000000000..d6fe4b6d35 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/AndroidEmailSender.kt @@ -0,0 +1,65 @@ +package com.tangem.core.navigation.email + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import timber.log.Timber + +/** + * Implementation of email sender for Android + * + * @property context context + * +[REDACTED_AUTHOR] + */ +internal class AndroidEmailSender(private val context: Context) : EmailSender { + + override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) { + val originalIntent = createEmailShareIntent(email) + val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) + + val packageManager = context.packageManager + val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0) + val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0) + + val targetedIntents = originalIntentResults + .filter { originalResult -> + emailFilterIntentResults.any { + originalResult.activityInfo.packageName == it.activityInfo.packageName + } + } + .map { + createEmailShareIntent(email).apply { setPackage(it.activityInfo.packageName) } + } + .toMutableList() + + try { + val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...").apply { + putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray()) + } + + ContextCompat.startActivity(context, chooserIntent, null) + } catch (ex: Exception) { + Timber.e("Failed to send email: $ex") + } + } + + private fun createEmailShareIntent(email: EmailSender.Email): Intent { + val builder = ShareCompat.IntentBuilder(context) + .setType("message/rfc822") + .setEmailTo(arrayOf(email.address)) + .setSubject(email.subject) + .setText(email.message) + + email.attachment?.let { + builder.setStream( + FileProvider.getUriForFile(context, "${context.packageName}.provider", it), + ) + } + + return builder.intent + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt new file mode 100644 index 0000000000..3aa803b702 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/email/EmailSender.kt @@ -0,0 +1,21 @@ +package com.tangem.core.navigation.email + +import java.io.File + +/** + * Email sender + * +[REDACTED_AUTHOR] + */ +interface EmailSender { + + /** Send [email] or handle exception [onFail] */ + fun send(email: Email, onFail: ((Exception) -> Unit)? = null) + + data class Email( + val address: String, + val subject: String, + val message: String, + val attachment: File? = null, + ) +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index aaccaf5396..559c09d33c 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,72 +1,59 @@ - Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet - Tangem Bot - Akzeptieren - Bilanz: %s - Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an - Abbrechen - Entfernen - Erledigt - Fehler - OK - Änderungen speichern - Absenden - Erfolg - Gebühr - Zugangscode - Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. - Langes Tippen - Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert - Passcode - Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. - %s Hasch - KartenID - App Währung - Emittent - Signiert - Details - Nutzungsbedingungen - Karte scannen - Tippen Sie um den Zugangscode zu ändern - Tippen Sie um den Passcode zu ändern - Legen Sie die Karte zum Scannen an - Tippen um zu signieren - Legen Sie die Karte an - Der Betrag enthält nicht einige Ihrer Mittel - Ein wallet erstellen - Betrag - Adresse - Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein - Tag - Memo - inkl. Gebühr - Niedrig - Normal - Priorität - Höchstbetrag - Netzgebühr - Gesamt - %1$s und %2$s werden gesendet - ≈ %1$s (inkl. Gebühr: %2$s) - %s wird gesendet - Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert - Ungültige Adresse - Tangem Wallet - Tangem Twin - Die Bilanz wird aufgeladen… - Die Transaktion läuft… - Verifizierte Bilanz - WalletConnect - Das Konto ist nicht erstellt - Diese Karte wird nicht unterstützt - Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App - Die Adresse wurde erfolgreich kopiert - Keine Internetverbindung - Erhalten - von %s - Senden - an %s - Tangem - OK, ich hab\'s! + Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet + Akzeptieren + Bilanz: %s + Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an + Abbrechen + Entfernen + Erledigt + Fehler + Gebühr + Netzgebühr + OK + Änderungen speichern + Absenden + Erfolg + Zugangscode + Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen. + Langes Tippen + Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert + Passcode + Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben. + %s Hasch + KartenID + App Währung + Emittent + Signiert + Details + Nutzungsbedingungen + Karte scannen + Tippen Sie um den Zugangscode zu ändern + Tippen Sie um den Passcode zu ändern + Legen Sie die Karte zum Scannen an + Tippen um zu signieren + Legen Sie die Karte an + Ein wallet erstellen + Betrag + Adresse + Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein + Tag + Memo + inkl. Gebühr + Niedrig + Normal + Priorität + Höchstbetrag + Gesamt + %1$s und %2$s werden gesendet + ≈ %1$s (inkl. Gebühr: %2$s) + %s wird gesendet + Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert + Ungültige Adresse + Tangem Twin + WalletConnect + Die Adresse wurde erfolgreich kopiert + Keine Internetverbindung + Tangem + OK, ich hab\'s! diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 680543d79c..e7454edeb7 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,72 +1,59 @@ - Cette carte n\'est pas conçue pour fonctionner avec Tangem - Tangem Bot - J\'accepte - Solde : %s - Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité - Annuler - Supprimer - Exécuté - Erreur - OK - Sauvegarder les modifications - Envoyer - Avec succès - Commissions - Code d\'accès - Vous devrez entrer le mot de passe correct avant de scanner la carte - Tenez la carte fermement - Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. - Mot de passe - Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. - %s hashes - ID de la carte - Monnaie de l\'application - Emetteur - Signé - Référénces - Conditions d\'utilisation - Scannez la carte - Touchez, pour modifier le code d\'accès - Touchez, pour modifier le mot de passe - Posez pour scanner - Touchez pour signer - Posez la carte - Le montant n\'inclut pas certains de vos fonds - Créer un portefeuille - Somme - Adresse - L\'adresse est la même que celle de votre portefeuille - Tag - Memo - Inclure les commissions - Bas - Normal - Priorité - Somme maximale - Commissions du réseau - Total - Sera envoyé %1$s et %2$s - ≈ %1$s (incl. les commissions : %2$s) - Sera envoyé %s - La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps - Adresse incorrecte - Tangem Wallet - Tangem Twin - Solde est en cours de téléchargement… - Transaction en cours… - Solde confirmé - WalletConnect - Compte n\'est pas créé - Cette carte n\'est pas prise en charge - Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente - L\'adresse a été copiée avec succès - Pas de connexion internet - Réception - de %s - Envoi - jusqu\'à %s - Tangem - Ok, je l\'ai! + Cette carte n\'est pas conçue pour fonctionner avec Tangem + J\'accepte + Solde : %s + Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité + Annuler + Supprimer + Exécuté + Erreur + Commissions + Commissions du réseau + OK + Sauvegarder les modifications + Envoyer + Avec succès + Code d\'accès + Vous devrez entrer le mot de passe correct avant de scanner la carte + Tenez la carte fermement + Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. + Mot de passe + Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. + %s hashes + ID de la carte + Monnaie de l\'application + Emetteur + Signé + Référénces + Conditions d\'utilisation + Scannez la carte + Touchez, pour modifier le code d\'accès + Touchez, pour modifier le mot de passe + Posez pour scanner + Touchez pour signer + Posez la carte + Créer un portefeuille + Somme + Adresse + L\'adresse est la même que celle de votre portefeuille + Tag + Memo + Inclure les commissions + Bas + Normal + Priorité + Somme maximale + Total + Sera envoyé %1$s et %2$s + ≈ %1$s (incl. les commissions : %2$s) + Sera envoyé %s + La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + Adresse incorrecte + Tangem Twin + WalletConnect + L\'adresse a été copiée avec succès + Pas de connexion internet + Tangem + Ok, je l\'ai! diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 6ff58173b7..e7caa7829e 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -1,72 +1,59 @@ - Questa carta non è progettata per funzionare con Tangem - Tangem Bot - Accetta - Saldo: %s - Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy - Annulla - Rimuovere - Fatto - Errore - OK - Mantieni le modifiche - Invia - Con successo - Commissione - Codice di accesso - Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto - Mantenimento della carta - Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. - Password - Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. - Hash %s - ID carta - Valuta dell\'applicazione - Emittente - Firmato - Requisiti - Termini del servizio - Scansiona carta - Avvicina per modificare il codice di accesso - Avvicina per modificare la password - Avvicina per scansionare - Avvicina per firmare - Avvicina la carta - L\'importo non include alcuni dei tuoi fondi - Crea portafoglio - Importo - Indirizzo - L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio - Tag - Memo - Includi commissione - Insufficiente - Normale - Prioritario - Importo totale - Costi della rete - Totale - Sarà inviato %1$s e %2$s - ≈ %1$s (inc. commissione: %2$s) - Sarà inviato %s - La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo - Indirizzo non valido - Tangem Wallet - Tangem Twin - Il saldo sta per essere caricato… - Transazione in corso… - Saldo verificato - WalletConnect - Conto non creato - Questa carta non è supportata - La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta - L\'indirizzo è stato copiato con successo - Nessuna connessione a Internet - Riceviu - da %s - Inviau - su %s - Tangem - Ok, ho capito! + Questa carta non è progettata per funzionare con Tangem + Accetta + Saldo: %s + Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy + Annulla + Rimuovere + Fatto + Errore + Commissione + Costi della rete + OK + Mantieni le modifiche + Invia + Con successo + Codice di accesso + Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto + Mantenimento della carta + Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente. + Password + Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta. + Hash %s + ID carta + Valuta dell\'applicazione + Emittente + Firmato + Requisiti + Termini del servizio + Scansiona carta + Avvicina per modificare il codice di accesso + Avvicina per modificare la password + Avvicina per scansionare + Avvicina per firmare + Avvicina la carta + Crea portafoglio + Importo + Indirizzo + L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio + Tag + Memo + Includi commissione + Insufficiente + Normale + Prioritario + Importo totale + Totale + Sarà inviato %1$s e %2$s + ≈ %1$s (inc. commissione: %2$s) + Sarà inviato %s + La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo + Indirizzo non valido + Tangem Twin + WalletConnect + L\'indirizzo è stato copiato con successo + Nessuna connessione a Internet + Tangem + Ok, ho capito! diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml index c58315db3f..f589ed1f9f 100644 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ b/core/res/src/main/res/values-ru/strings-blockchain.xml @@ -6,9 +6,9 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Произошла ошибка. Код: %s. - Для создания аккаунта отправьте средства на этот адрес Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. + Для создания аккаунта отправьте средства на этот адрес Минимальная сумма: %s Сдача слишком мала Неверная комиссия diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index ea038372cd..95cd7e9498 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,753 +1,676 @@ - Добавить токен - Валюты - Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. - Спасибо за ваш отзыв - Отправлено успешно - Обратиться в поддержку - Отправить отзыв - Эта функция недоступна в демонстрационном режиме - Не удалось отправить письмо - Причина: %s - Не могу отправить транзакцию - Выбранный кошелёк не поддерживает сеть %1$s - Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. - Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. - У вас возникли трудности со сканированием карты? - Эта карта не предназначена для работы с этим приложением - Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem - Включите биометрическую аутентификацию - Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. - При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. - Сохранение кода доступа - Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. - Cохранение кошелька - Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. - Тёмная - Светлая - Как в системе - Тема - Настройки приложения - Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" - Больше не показывать - Понятно - Балансы скрыты - Пожалуйста, отсканируйте карту - Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту - Слишком много попыток - Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. - Начать резервное копирование - - %d карта - %d карты - %d карт - %d карт - - Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. - Использовать эту карту для сброса кода доступа на других картах в этом кошельке - Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька - Восстановление кода доступа - Сбросить - Вы уверены, что хотите это сделать? - Смена кода доступа - Код доступа будет изменен только на данной карте - Заводские настройки - Тип безопасности - Выбранный способ защиты приложения - Настройки карты - Tangem Bot - Чат - Отправить логи - Принять - Доступ запрещен - Добавить - Применить - Одобрение - Внимание - Баланс: %s - Баланс - биометрическую аутентификацию - биометрией - Купить - Перейти на %1$s - Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. - Отмена - Закрыть - Продолжить - Копировать - Скопировать адрес - Создать - Удалить - Отключено - Не нравится - Готово - Включить - Включено - Ошибка - Обменять - Обозреватель - Посмотреть историю - Посмотреть историю транзакций - Обозреватель - Комиссия - Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s - Свое - Быстро - По рынку - Медленно - Скорость и комиссия - Получить адреса - К провайдеру - Импортировать - Нравится - Заблокирован - Основная сеть - Далее - Нет - Нет адреса - Нет данных - OK - Основная карта - Вставить - Подробнее - Получить - Отклонить - Перезагрузить - Переименовать - Сохранить изменения - Искать - Поиск токенов - Seed-фраза - Выберите действие - Продать - Отправить - Сервер недоступен, повторите попытку позднее - Поделиться - Подписать - Подписать и отправить - Стейкинг - Начать - Отправить - Успешно - Поддержка - Обмен - условия участия - Ошибка транзакции - Транзакции - Перевод - Я понял - Недоступно - Произошла ошибка. Пожалуйста, попробуйте снова. - Да - Адрес контракта скопирован! - Доступные сети - Добавить токен - Адрес контракта - Адрес контракта некорректен - Путь деривации некорректен - Пожалуйста, выберите сеть - Обязательное поле - Количество знаков после запятой должно быть корректным числом не больше %d - Своя деривация - Например m/00\'/0000\'/0\'/0/0 - Введите свою деривацию - Знаков после запятой - Путь деривации - По умолчанию - Деривация по BIP44 - Введенный путь деривации некорректен - Например, USD Coin - Название токена - Не выбрано - Сеть - Сеть - Вы можете добавить токен в ручную, если он не поддерживается Tangem - Например, USDC - Символ - Символ токена - Этот токен/сеть уже находится в вашем списке - Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить - Остерегайтесь мошеннических токенов, они могут ничего не стоить - Токены могут быть созданы кем угодно - Чат - Код доступа - Перед сканированием карты вам нужно будет ввести правильный код доступа. - Задержка сканирования - Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. - Пароль - Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. - Реферальная программа - Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы - Privacy policy - %s хэшей - Номер карты - Обратиться в поддержку - Добавить еще карты - Валюта приложения - Скрывать балансы жестом переворота - Эмитент - Подписано - Подробности - Проверьте подключение с интернетом или переключитесь на другую сеть - Условия использования - К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений - Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. - Мои токены - У вас нет добавленных токенов. Добавьте токены для обмена - Токены не найдены. Пожалуйста, попробуйте другой запрос - Недоступен для обмена с %s - Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес - Статус - Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами - Выберите провайдера - Курс обмена - Больше провайдеров на подходе.\nСледите за обновлениями! - Чтобы вернуть ваши деньги, посетите сайт провайдера - Операция не выполнена провайдером - Посетите сайт провайдера для проверки - Провайдер запрашивает прохождение верификации - Отменен - Подтверждено - Подтверждение - Подтверждение... - Обменяно - Обмен - Обмен... - Неудачно - Депозит получен - Ожидание депозита - Ожидаем пополнения... - Возвращено - Отправляем - Отправка средств... - Отправлено - Верифицировано - Требуется верификация - Список токенов в вашем кошельке - Получение наилучших курсов... - Плавающая ставка - К провайдеру - Пользуясь сервисом вы соглашаетесь с %s - Пользуясь сервисом вы соглашаетесь с %1$s и %2$s - Политикой конфиденциальности - Провайдер - Лучший курс - Доступно с %s - Доступно до %s - Недоступно для этой пары - Требуется разрешение - Условиями использования - Предоставлено - Информация ниже не является обязательной. Вы можете стереть её, если хотите. - Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. - Скажите, пожалуйста, какая у вас карта? - Привет, команда поддержки, - Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь. - Мои предложения - Не могу отсканировать карту - Обращение в поддержку - Обращение в поддержку Tangem - Не могу отправить транзакцию - Купить - Сканировать - Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции - Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции - Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции - Чтобы сбросить настройки до заводских, приложите карту как показано выше и не убирайте до окончания операции - Приложите, чтобы отсканировать - Чтобы подписать транзакцию, приложите карту как показано выше и не убирайте до окончания операции - Приложите, чтобы подписать - Приложите карту - Внутренняя ошибка: не удается найти менеджер кошельков - Вы обновили данные биометрии, отсканируйте свою карту для входа - Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены - Вы успешно прошли все уроки и теперь можете получить 1INCH токены - - Пройдите 3 урока и получите %d 1INCH токен на свой кошелек - Пройдите 3 урока и получите %d 1INCH токена на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - Пройдите 3 урока и получите %d 1INCH токенов на свой кошелек - - Управление токенами - Баланс - В сумме учтены не все монеты - 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов - Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту - Отсканируйте карту - Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. - Обмен с Changelly, %s комиссии - Токены - Добавить - Изменить - Основная сеть - Не основной или основной блокчейн, на котором размещен токен - Не основные сети - Сети - Выберите сети - Кошелек - Не удалось найти этот токен, вы можете добавить его вручную. - - %1$d из %2$d кошелька - %1$d из %2$d кошельков - %1$d из %2$d кошельков - %1$d из %2$d кошельков - - например Bitcoin - Рыночная капитализация - Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. - Голосовать - Выберите кошелек - Кошелёк не поддерживает более одной сети - Вам необходимо установить единый код доступа для защиты всех ваших карт - Защита - Позже вы сможете установить индивидуальный код доступа для каждой карты - Персонализация - Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте - Восстановление - Выберите любое слово, фразу или число в качестве кода доступа - Создайте код доступа - Повторно введите код доступа - Код доступа должен состоять не менее чем из 4 символов. - Введенные коды доступа не совпадают - Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам - Ошибка активации - Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? - Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. - Добавить резервную карту - Сканировать карту #%d - Создать резервную копию - Сканировать основную карту - Запросить - Перейти к моему кошельку - Завершение бэкапа - Установить Код доступа - Получить криптовалюту - Сканировать основную карту - Пропустить - Как это работает? - Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек - Создать кошелек - Создать кошелек - Другие опции - Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее. - Cоздавайте ключи приватно - Ваша карта активирована и готова к использованию - Успешно! - В этом случае вам будет необходимо начать процесс заново. - Вы хотите выйти из процесса активации? - Подготовка - Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? - Код доступа - Подключиться - Резервная копия - Прочитать о seed-фразе - Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. - Ваша seed-фраза - Чтобы импортировать кошелек, введите seed-фразу в поле ниже - Создать seed-фразу - Импорт кошелька - Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. - Использовать seed-фразу - Неверная seed-фраза. Пожалуйста, проверьте порядок слов. - Неверная seed-фраза. Пожалуйста, проверьте орфографию. - Устаревший - Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова - Итак, проверим - Для начала работы просто запросите начисление wxDAI на свой кошелек - Это займет несколько секунд - Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. - Вы можете добавить еще одну карту или завершить процесс резервного копирования - Подготовьте резервную карту с номером %s - Отсканируйте основную карту, чтобы начать процесс резервного копирования. - Подготовьте основную карту с номером %s - Поздравляем! Ваша платежная крипто карта теперь активирована! - Ваша карта настроена и готова к использованию. - Добавлено максимальное количество карт. Завершите процесс резервного копирования. - Активация карты - Резервная карта #%d - Запросить %s - Запрашивается - Нет резервных карт - Добавлена ​​одна резервная карта - Код доступа - Подготовьте свою карту - Добавлены две резервные карты - Пополните кошелек на любую сумму, чтобы начать пользоваться картой - Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой - Купить криптовалюту - Показать адрес кошелька - Активация кошелька - Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. - Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала - Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet. - Код доступа можно восстановить с помощью одной из резервных карт. - Все резервные карты являются полнофункциональными и содержат одинаковые ключи. - Вы сможете установить код доступа для защиты своих кошельков. - Резервная копия карты - Восстановление кода доступа - Идентичные карты - Код доступа - Группы - По балансу - Сортировка токенов - Список - %1$s %2$s адрес в сети %3$s - %1$s (%2$s) в сети %3$s - Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. - Участвовать - Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. - Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. - Грядущие выплаты - Ваши друзья купили - Меньше - Больше - Нет грядущих выплат - - за %d кошелек - за %d кошелька - за %d кошельков - за %d кошельков - - Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг - Получите - на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг - Вы - Получит - при покупке кошелька на сайте tangem.com - %s скидку - Ваш друг - Персональный код скопирован! - Ваш персональный код - Купи Tangem Wallet со скидкой!\n%s - Приведи друга в Tangem - Вы приняли - Нажимая на эту кнопку, вы принимаете - в реферальной программе - - %d кошелек - %d кошелька - %d кошельков - %d кошельков - - Пожалуйста, удерживайте карту до завершения операции - Сбросить карту - Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку - Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду - Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа. - Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек. - У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ? - Карты банков РФ в данный момент не принимаются - Войдите в приложение и следите за своим балансом без сканирования карты - Доступ в приложение - Использовать биометрию - Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты - Код доступа - Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков - Включите биометрическую аутентификацию - Вы хотите использовать биометрию? - Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта - Ошибка сканирования карты. Попробуй снова. - Сканировать - Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. - Приготовьте свою карту - Уже содержится в введенном адресе - Сумма - Транзакция не выполнена - Причина: %1$s\nКод: %2$s - Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? - Вычесть - %1$s в %2$s - Адрес - Код назначения - Введите адрес - Адрес совпадает с адресом кошелька - Недопустимый Tag. Он не будет добавлен в транзакцию. - Недопустимый Memo. Он не будет добавлен в транзакцию. - Tag - Memo - Включая комиссию - Низкая - Нормальная - Приоритетная - Лимит газа - Цена газа - Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. - Всё - Максимальная сумма - Комиссия не превысит - Максимальная cумма комиссии - Сетевая комиссия - Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. - Покрытие сетевой комиссии - Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса - Недостаточно средств - Увеличение комиссии - Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01. - Установлена высокая комиссия - Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. - Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению - Недопустимая сумма - Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s. - Адрес получателя не активирован. Пожалуйста, измените сумму отправки, чтобы продолжить. - Сумма отправки не может быть менее %1$s - Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции - Возможны задержки по транзакции - Лимит транзакции - Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. - Уменьшить до %s - Уменьшить на %s - Необязательное - Последние - Получатель - Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов - Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. - Мои кошельки - Отправка %s - Всего - %1$s и %2$s будет отправлено - ≈ %1$s (вкл. комиссию: %2$s) - %s будет отправлено - Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время - Неверный адрес - Купить сейчас - У меня есть промо-код… - Tangem Wallet - Другие способы оплаты - Сделать предзаказ - Итого - Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. - Революционный аппаратный кошелек - До трех карт с одним кошельком - До - трех карт - с одним кошельком - Все ключи в безопасности - Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте - Тысячи криптовалют - Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. - Кошелек для каждого - Пройдите 3 урока, получите скидку на покупку Tangem Wallet и 1INCH токены - Пройти обучение - Встречайте Tangem - Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах - Поддержка Web 3.0 - Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. - Подтвердить - Ошибка: %s - Вы отправляете - Дать разрешение - Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. - Недостаточно средств - Комиссии - В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя - В сумму включена комиссия провайдера сервиса. - Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии - Недостаточно средств для оплаты комиссии на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. - Транзакция в процессе… - Подождите - Подтвердить - Текущая транзакция - Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. - Дать разрешение - Укажите лимит доступа к выбранному токену - Количество %s - Отправитель - Ваш кошелек - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s - Безлимитно - Открыть в обозревателе - В процессе - Обменять - Обмен %s на - Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. - Вы получите - Другие токены - Выберите токен - Ваши токены - не доступен - Балансы скрыты - Балансы показаны - Отменить - - %d токен - %d токена - %d токенов - %d токенов - - Скрыть - Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. - Скрыть %s - Скрыть токен - %1$s токен в сети %%image%% %2$s - Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. - Невозможно скрыть %s - Нет цены - Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. - Обмен с Changelly, %s комиссии - Обменять - контракт: %s - У вас еще нет транзакций - Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. - Несколько адресов - История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. - Операция - от: %s - на: %s - В процессе… - Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d - Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую - Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. - Один кошелек. Две карты. - Сканировать карту #%s - Создание кошелька - Отсканируйте twin-карту #%s - Подготовка карты - Tangem Twin - Это действие необратимо. У вас не будет доступа к старому кошельку. - Приложите twin-карту с номером %s и не убирайте до окончания операции - Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку - Добавить новый кошелек - Вы уверены, что хотите удалить этот кошелек? - %d выбрано - Произошла ошибка, пожалуйста, отсканируйте свою карту для входа - Этот кошелек уже был сохранен, вы можете добавить другой - Мультивалютные - Имя кошелька - Переименование кошелька - Одновалютные - Мои кошельки - Разблокировать все - Разблокировать все с %s - История транзакций - Блокчейн недоступен. Попробуйте позже. - Баланс загружается… - Отсканируйте карту - Транзакция подтверждается… - Подтвержденный баланс - Действия - Что отправить? - Вы хотите купить или продать криптовалюту? - Запрос на подпись сообщения.\n\n%s - Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s - Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s - Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s - Транзакция с BNB успешно подписана и отправлена ​​в Dapp. - Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код - Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s - Невозможно отправить транзакцию. Недостаточно средств. - Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже. - Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n - Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз - Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. - Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n - Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. - Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. - Неверная карта выбрана в приложении Tangem - Не удалось создать транзакцию из данных Dapp. Код: %s - Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. - Сообщение было успешно подписано и отправлено в Dapp - Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. - Нет открытых сессий WalletConnect - Упс. Нет сессий. - Не удалось создать пару WalletConnect: %1$s - Вставить из буфера обмена - Сообщение для %1$s:\n%2$s - Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s - Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами. - Сканировать новый код - Эту карту нельзя использовать с WalletConnect. - Сеть не поддерживается. Пожалуйста, выберите другую сеть. - Выберите сеть - Dapp не предоставил необходимые данные для открытия сессии WalletConnect - Не удалось найти сессию для обработки запроса - Сессии WalletConnect - Подключение к dApps - WalletConnect - Транзакция успешно подписана и отправлена ​​в Dapp - Транзакция успешно подписана и отправлена ​​в блокчейн - Не удалось найти хэш транзакции - Сеть %s - Аккаунт не создан - Эта карта не поддерживается - Ваша карта Tangem предназначена для работы с другим приложением. Пожалуйста, ознакомьтесь с названием и инструкциями на вашей карте и установите правильное приложение. - Рыночная цена %s - за 24 часа - Сеть %s - Адрес скопирован в буфер обмена - Нет соединения с интернетом - Получение - от %s - Отправка - к %s - Tangem - Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку - Можно лучше - Узнать больше - Нравится - Понятно! - Очень круто! - Обновить - Вы находитесь в режиме демо - Демо режим включен - Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. - Не для пользователя! - Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. - Для работы с сетью необходим депозит - Обмен будет доступен после завершения %s транзакции - У вас есть активная транзакция - У вас в списке нет монет доступных для обмена с %s - Нет доступных токенов для обмена - Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s - Невозможно покрыть комиссию %s - Cервис временно недоступен - Пожалуйста, измените сумму для обмена - Сумма для обмена должна быть не менее %s - Сумма для обмена должна быть не более %s - Возможно, данная карта - образец или подделка - Ошибка проверки подлинности - На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. - Малое количество подписей - Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. - - Используйте вашу карту, чтобы получить адрес для %d сети - Используйте вашу карту, чтобы получить адреса для %d сетей - Используйте вашу карту, чтобы получить адреса для %d сетей - Используйте вашу карту, чтобы получить адреса для %d сетей - - Некоторые адреса отсутствуют - В данный момент сеть недоступна. Пожалуйста, попробуйте позже. - Сеть недоступна - Пополните ваш кошелек - Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы. - Резервная копия отсутствует - Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. - Карта уже подписывала транзакции - Устройство несовместимо - Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше - Нравится Tangem? - Необходима плата за аренду сети - %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. - Недостаточно %1$s для оплаты комиссии сети - Отправка средств станет доступной после завершения транзакции %s - Транзакция в обработке - Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. - Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. - Некоторые сети недоступны - Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. - Только для целей тестирования - Отказаться - Вы не закончили резервное копирование. Хотите продолжить? - Да, возобновить - Отказаться - Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново - Возобновить резервное копирование - Это необратимое действие - Войти с %s - Сканировать карту - Используйте %s или отсканируйте карту для входа в приложение - C возвращением! - Обмен через %s - Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. - Статус обмена + Добавить токен + Валюты + Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + Обратиться в поддержку + Эта функция недоступна в демонстрационном режиме + Причина: %s + Не могу отправить транзакцию + Выбранный кошелёк не поддерживает сеть %1$s + Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен. + Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки. + У вас возникли трудности со сканированием карты? + Эта карта не предназначена для работы с этим приложением + Подключите функцию комиссии по умолчанию и при формировании транзакции на отправку средств комиссия будет выставлена автоматически, а экран комиссии пропущен. Вы всегда сможете на него вернуться. + Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem + Включите биометрическую аутентификацию + Все сохраненные коды доступа будут удалены. Вам потребуется вводить код доступа при работе с кошельком. + При отключении функции сохранения кошелька все ранее сохраненные кошельки будут удалены из приложения. + Сохранение кода доступа + Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. + Cохранение кошелька + Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. + Тёмная + Светлая + Как в системе + Тема + Настройки приложения + Чтобы скрыть или показать баланс, просто поверните ваше устройство вниз или отключите опцию его в разделе \"Настройки\" + Больше не показывать + Понятно + Балансы скрыты + Пожалуйста, отсканируйте карту + Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту + Слишком много попыток + Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Начать резервное копирование + + %d карта + %d карты + %d карт + %d карт + + Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте. + Использовать эту карту для сброса кода доступа на других картах в этом кошельке + Восстановление кода доступа + Сбросить + Вы уверены, что хотите это сделать? + Смена кода доступа + Код доступа будет изменен только на данной карте + Заводские настройки + Тип безопасности + Настройки карты + Принять + Доступ запрещен + Применить + Одобрение + Внимание + Баланс: %s + Баланс + биометрическую аутентификацию + биометрией + Купить + Перейти на %1$s + Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. + Отмена + Закрыть + Продолжить + Копировать + Скопировать адрес + Создать + Удалить + Отключено + Готово + Включить + Включено + Ошибка + Обозреватель + Посмотреть историю транзакций + Обозреватель + Комиссия + Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s + Свое + Быстро + По рынку + Медленно + Скорость и комиссия + Получить адреса + К провайдеру + Импортировать + Заблокирован + Основная сеть + Сетевая комиссия + Далее + Нет + Нет адреса + OK + Основная карта + Кодовая фраза + Подробнее + Получить + Отклонить + Перезагрузить + Переименовать + Сохранить изменения + Искать + Поиск токенов + Seed-фраза + Выберите действие + Продать + Отправить + Сервер недоступен, повторите попытку позднее + Поделиться + Подписать + Подписать и отправить + Начать + Отправить + Успешно + Поддержка + Обмен + условия участия + Ошибка транзакции + Транзакции + Перевод + Я понял + Произошла ошибка. Пожалуйста, попробуйте снова. + Недоступно + Да + Адрес контракта скопирован! + Доступные сети + Добавить токен + Адрес контракта + Адрес контракта некорректен + Пожалуйста, выберите сеть + Десятичное число должно быть действительным целым числом, до %li + Своя деривация + Например m/00\'/0000\'/0\'/0/0 + Введите свою деривацию + Знаков после запятой + Путь деривации + По умолчанию + Деривация по BIP44 + Введенный путь деривации некорректен + Например, USD Coin + Название токена + Не выбрано + Сеть + Сеть + Вы можете добавить токен в ручную, если он не поддерживается Tangem + Например, USDC + Символ + Символ токена + Этот токен/сеть уже находится в вашем списке + Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Остерегайтесь мошеннических токенов, они могут ничего не стоить + Токены могут быть созданы кем угодно + Чат + Код доступа + Перед сканированием карты вам нужно будет ввести правильный код доступа. + Задержка сканирования + Этот механизм защищает карту от бесконтактных атак. Между сканированием карты и выполнением команды будет добавлена задержка. + Пароль + Перед выполнением любой команды, влекущей за собой изменение состояния карты, вам необходимо будет ввести пароль. + Реферальная программа + Переверните экран вашего устройства вниз, чтобы быстро скрыть и отобразить балансы + %s хэшей + Номер карты + Обратиться в поддержку + Добавить еще карты + Валюта приложения + Скрывать балансы жестом переворота + Эмитент + Подписано + Подробности + Проверьте подключение с интернетом или переключитесь на другую сеть + Условия использования + Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. + Мои токены + У вас нет добавленных токенов. Добавьте токены для обмена + Недоступен для обмена с %s + Предоставлено + Статус + Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами + Выберите провайдера + Произошла ошибка. Код: %s + К сожалению обмен указанной пары через выбранного провайдера на данный момент не возможен. Попробуйте совершить обмен позже. (Код:\u00A0%s) + Выбранный провайдер не доступен для обмена. Попробуйте позже. (Код:\u00A0%s) + В данный момент обмен не возможен. Попробуйте позже. (Код:\u00A0%s) + Курс обмена + Обмен через %s + Чтобы вернуть ваши деньги, посетите сайт провайдера + Операция не выполнена провайдером + Посетите сайт провайдера для проверки + Провайдер запрашивает прохождение верификации + Отменен + Подтверждено + Подтверждение + Подтверждение... + Обменяно + Обмен + Обмен... + Неудачно + Депозит получен + Ожидание депозита + Ожидаем пополнения... + Возвращено + Отправляем + Отправка средств... + Отправлено + Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. + Статус обмена + Требуется верификация + Список токенов в вашем кошельке + Получение наилучших курсов... + Плавающая ставка + Пользуясь сервисом, вы соглашаетесь с %s + Пользуясь сервисом, вы соглашаетесь с %1$s и %2$s + Больше провайдеров на подходе.\nСледите за обновлениями! + Политикой конфиденциальности + Провайдер + Лучший курс + Доступно до %s + Доступно с %s + Недоступно для этой пары + Требуется разрешение + Условиями использования + Токены не найдены. Пожалуйста, попробуйте другой запрос + ID транзакции скопирован + Информация ниже не является обязательной. Вы можете стереть её, если хотите. + Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. + Скажите, пожалуйста, какая у вас карта? + Привет, команда поддержки, + Пожалуйста, расскажите нам больше о вашей проблеме. Каждая маленькая деталь может помочь. + Мои предложения + Не могу отсканировать карту + Обращение в поддержку + Обращение в поддержку Tangem + Не могу отправить транзакцию + Купить + Сканировать + Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции + Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции + Чтобы создать кошелек, приложите карту как показано выше и не убирайте до окончания операции + Приложите, чтобы отсканировать + Приложите, чтобы подписать + Приложите карту + Вы обновили данные биометрии, отсканируйте свою карту для входа + Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены + Управление токенами + Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту + Отсканируйте карту + Обменивайте свои токены с %1$s комиссии провайдера через Changelly с %2$s по %3$s февраля. + Обмен с Changelly, %s комиссии + Токены + Добавить + Изменить + Рыночная капитализация + Основная сеть + Не основной или основной блокчейн, на котором размещен токен + Не основные сети + Выберите сети + Кошелек + Не удалось найти этот токен, вы можете добавить его вручную. + + %1$d из %2$d кошелька + %1$d из %2$d кошельков + %1$d из %2$d кошельков + %1$d из %2$d кошельков + + например Bitcoin + Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. + Голосовать + Выберите кошелек + Кошелёк не поддерживает более одной сети + Вам необходимо установить единый код доступа для защиты всех ваших карт + Защита + Позже вы сможете установить индивидуальный код доступа для каждой карты + Персонализация + Код доступа можно восстановить с помощью привязанной карты. Не храните все карты в одном месте. + Восстановление + Выберите любое слово, фразу или число в качестве кода доступа + Создайте код доступа + Введите код доступа еще раз, чтобы избежать ошибки + Повторно введите код доступа + Код доступа должен состоять не менее чем из 4 символов. + Введенные коды доступа не совпадают + Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам + Ошибка активации + Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить? + Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. + Кодовая фраза — это расширенная функция безопасности, которую используют криптокошельки. Она добавляет дополнительное слово или фразу по вашему выбору к уже существующей seed - фразе, чтобы разблокировать совершенно новый набор адресов. + Добавить резервную карту + Сканировать карту #%d + Создать резервную копию + Сканировать основную карту + Перейти к моему кошельку + Завершение бэкапа + Получить криптовалюту + Сканировать основную карту + Пропустить + Как это работает? + Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек + Создать кошелек + Создать кошелек + Другие опции + Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее. + Cоздавайте ключи приватно + Ваша карта активирована и готова к использованию + Успешно! + В этом случае вам будет необходимо начать процесс заново. + Вы хотите выйти из процесса активации? + Подготовка + Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа? + Резервная копия + Прочитать о seed-фразе + Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. + + + Запишите эти %d слова в порядке, указанном ниже, и сохраните их в надежном месте. + Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте. + Запишите эти %d слов в порядке, указанном ниже, и сохраните их в надежном месте. + + Ваша seed-фраза + + + %d слова + %d слов + %d слов + + Чтобы импортировать кошелек, введите seed-фразу в поле ниже + Создать seed-фразу + Импорт кошелька + Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. + Использовать seed-фразу + Неверная seed-фраза. Пожалуйста, проверьте порядок слов. + Неверная seed-фраза. Пожалуйста, проверьте орфографию. + Устаревший + Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова + Итак, проверим + Чтобы начать процесс резервного копирования, добавьте одну или две резервные карты. + Вы можете добавить еще одну карту или завершить процесс резервного копирования + Подготовьте резервную карту с номером %s + Отсканируйте основную карту, чтобы начать процесс резервного копирования. + Подготовьте основную карту с номером %s + Ваша карта настроена и готова к использованию. + Добавлено максимальное количество карт. Завершите процесс резервного копирования. + Активация карты + Резервная карта #%d + Нет резервных карт + Добавлена ​​одна резервная карта + Подготовьте свою карту + Добавлены две резервные карты + Пополните кошелек на любую сумму, чтобы начать пользоваться картой + Пополните кошелек более чем на %1$s %2$s, чтобы начать пользоваться картой + Купить криптовалюту + Показать адрес кошелька + Активация кошелька + Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас. + Если процесc создания кошелька каким-либо образом прервется, вам придется начинать сначала + Вы можете сделать резервную копию своих ключей на одной или двух других пустых картах Wallet. + Код доступа можно восстановить с помощью одной из резервных карт. + Все резервные карты являются полнофункциональными и содержат одинаковые ключи. + Вы сможете установить код доступа для защиты своих кошельков. + Резервная копия карты + Восстановление кода доступа + Идентичные карты + Код доступа + Группы + По балансу + Сортировка токенов + Список + Нравится наше приложение? + %1$s (%2$s) в сети %3$s + Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. + Участвовать + Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. + Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. + Грядущие выплаты + Ваши друзья купили + Меньше + Больше + Нет грядущих выплат + + за %d кошелек + за %d кошелька + за %d кошельков + за %d кошельков + + Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг + Вы + Получит + при покупке кошелька на сайте tangem.com + %s скидку + Ваш друг + Персональный код скопирован! + Ваш персональный код + Купи Tangem Wallet со скидкой!\n%s + Приведи друга в Tangem + Вы приняли + Нажимая на эту кнопку, вы принимаете + в реферальной программе + + %d кошелек + %d кошелька + %d кошельков + %d кошельков + + Сбросить карту + Я понимаю, что после выполнения этого действия у меня больше не будет доступа к текущему кошельку + Я понимаю, что не смогу этой картой восстановить пароль на остальных картах этого кошелька, если я его забуду + Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек или использовать данную карту для восстановления кода доступа. + Сброс к заводским настройкам приведет к полному удалению кошелька с выбранной карты. Вы не сможете восстановить текущий кошелек. + У вас есть карта банка другой страны, а также вид на жительство или регистрация вне РФ? + Карты банков РФ в данный момент не принимаются + Войдите в приложение и следите за своим балансом без сканирования карты + Доступ в приложение + Использовать биометрию + Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты + Код доступа + Похоже, что у вас отключена биометрическая аутентификация, она необходима для сохранения кошельков + Включите биометрическую аутентификацию + Вы хотите использовать биометрию? + Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта + Сканировать + Отсканируйте карту, чтобы изменить ее настройки. Изменения затронут только ту карту, которую вы отсканировали, и не повлияют на другие карты, привязанные к вашему кошельку. + Приготовьте свою карту + Уже содержится в введенном адресе + Вычесть + Недостаточно средств для покрытия комиссии сети. Вычесть комиссию из отправляемой сумму? + Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? + Причина: %1$s\nКод: %2$s + Транзакция не выполнена + Сумма + Подтвердить + %1$s, %2$s + Адрес + Код назначения + Введите адрес + Адрес совпадает с адресом кошелька + Недопустимый Tag. Он не будет добавлен в транзакцию. + Недопустимый Memo. Он не будет добавлен в транзакцию. + Tag + Memo + Включая комиссию + Низкая + Нормальная + Приоритетная + Лимит газа + Цена газа + Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана. + Всё + Максимальная сумма + Комиссия не превысит + Максимальная cумма комиссии + Сумма отправки будет уменьшена на %1$s для покрытия выбранного уровня комиссии. Получателю будет отправлено %2$s. + Покрытие сетевой комиссии + Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса + Недостаточно средств + Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. + Установлена высокая комиссия + Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %s. + Комиссия увеличилась + Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению + Недопустимая сумма + Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. + Адрес получателя не активирован. \nПожалуйста, измените сумму отправки, чтобы продолжить. + Сумма отправки не может быть менее %s + Уменьшить на %s + Уменьшить до %s + Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции + Возможны задержки по транзакции + Из-за ограничений %1$s в одну транзакцию может поместиться только %2$s UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. + Лимит транзакции + Необязательное + Последние + Получатель + Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов + Отправить + Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств. + Мои кошельки + Отправка %s + Всего + %1$s и %2$s будет отправлено + ≈ %1$s (вкл. комиссию: %2$s) + %s будет отправлено + Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время + Неверный адрес + Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. + Революционный аппаратный кошелек + До трех карт с одним кошельком + Все ключи в безопасности + Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте + Тысячи криптовалют + Используйте его на ходу, в любом месте, в любое время. Без проводов и батареек. Как только понадобится крипта, просто приложите карту к телефону. + Кошелек для каждого + Встречайте Tangem + Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах + Поддержка Web 3.0 + В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя + В сумму включена комиссия провайдера сервиса. + Комиссии + Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. + Подтвердить + Вы отправляете + Дать разрешение + Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. + Недостаточно средств + Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии + Подтвердить + Текущая транзакция + Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. + Дать разрешение + Укажите лимит доступа к выбранному токену + Количество %s + Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Безлимитно + В процессе + Обменять + Вы получите + Выберите токен + не доступен + Балансы скрыты + Балансы показаны + Отменить + Сгенерировать XPUB + Скрыть + Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. + Скрыть %s + Скрыть токен + %1$s токен в сети %%image%% %2$s + Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. + Невозможно скрыть %s + Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. + Обмен с Changelly, %s комиссии + Обменять + контракт: %s + У вас еще нет транзакций + Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. + Несколько адресов + История транзакций в настоящее время не поддерживается для этого блокчейна. Но не волнуйтесь, мы работаем над этим! А пока вы можете проверить ее в обозревателе. + Операция + от: %s + на: %s + Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d + Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую + Это карта, которую вы держите в руках. У парной карты номер %s.\n\nОбе карты можно использовать для вывода средств из этого кошелька. + Один кошелек. Две карты. + Сканировать карту #%s + Создание кошелька + Отсканируйте twin-карту #%s + Подготовка карты + Tangem Twin + Это действие необратимо. У вас не будет доступа к старому кошельку. + Приложите twin-карту с номером %s и не убирайте до окончания операции + Используйте %s или отсканируйте карту, чтобы получить доступ к своему кошельку + Добавить новый кошелек + Вы уверены, что хотите удалить этот кошелек? + Произошла ошибка, пожалуйста, отсканируйте свою карту для входа + Этот кошелек уже был сохранен, вы можете добавить другой + Имя кошелька + Переименование кошелька + Разблокировать все + Разблокировать все с %s + Блокчейн недоступен. Попробуйте позже. + Отсканируйте карту + Запрос на подпись сообщения.\n\n%s + Dapp %1$s, запрос на\nподпись транзакции с BNB.\n\n%2$s + Торговый ордер на %1$s\nЦена: %2$s\nСумма к получению: %3$s\nСумма к оплате: %4$s + Детали транзакции:\nОт: %1$s\nК: %2$s\nСумма: %3$s + Буфер обмена содержит код WalletConnect. Использовать скопированное значение или отсканировать QR-код + Запрос на создание транзакции для %1$s\n%2$s\n\nСумма: %3$s\nКомиссия: %4$s\nВсего: %5$s\nБаланс: %6$s + Невозможно отправить транзакцию. Недостаточно средств. + Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже. + Не все токены добавлены в ваш список. Пожалуйста, добавьте их в начале, а потом попробуйте снова. Недостающие токены: \n + Не удалось подписать сообщение.\nПожалуйста, попробуйте еще раз + Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже. + Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n + Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации. + Произошла непредвиденная ошибка. Сообщение ошибки: %s Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Неверная карта выбрана в приложении Tangem + Не удалось создать транзакцию из данных Dapp. Код: %s + Произошла непредвиденная ошибка. Код ошибки: %d Попробуйте, пожалуйста, позже. Если проблема будет продолжать возникать - обратитесь в службу поддержки. + Нет открытых сессий WalletConnect + Упс. Нет сессий. + Не удалось создать пару WalletConnect: %1$s + Вставить из буфера обмена + Сообщение для %1$s:\n%2$s + Запрос на открытие сессии для\n%1$s\n\nСЕТЬ: %2$s\n\nURL: %3$s + Операция не может быть завершена.\n\nВы уже установили сеанс WalletConnect с этими параметрами. + Сканировать новый код + Эту карту нельзя использовать с WalletConnect. + Сеть не поддерживается. Пожалуйста, выберите другую сеть. + Выберите сеть + Сессии WalletConnect + Подключение к dApps + WalletConnect + Рыночная цена %s + за 24 часа + Сеть %s + Адрес скопирован в буфер обмена + Нет соединения с интернетом + Tangem + Используйте %s или отсканируйте карту, чтобы разблокировать доступ к вашему кошельку + Пожалуйста, выведите все средства из этого кошелька, сбросьте его к заводским настройкам и создайте новый. Доступ к текущему кошельку будет утерян. + Ошибка активации + Можно лучше + Нравится + Понятно! + Очень круто! + Обновить + Вы находитесь в режиме демо + Демо режим включен + Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька. + Не для пользователя! + Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. + Для работы с сетью необходим депозит + Обмен будет доступен после завершения %s транзакции + У вас есть активная транзакция + Разрешение обмена в процессе и будет скоро завершено + Разрешение в процессе + Минимальная сумма обмена - %1$s. Пожалуйста, убедитесь, что остаток после обмена также не будет меньше %2$s. + У вас в списке нет монет доступных для обмена с %s + Нет доступных для обмена токенов + Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s + Невозможно покрыть комиссию %s + Сумма получения не может быть менее %s + Cервис временно недоступен + Сумма для обмена должна быть не более %s + Сумма для обмена должна быть не менее %s + Пожалуйста, измените сумму для обмена + Возможно, данная карта - образец или подделка + Ошибка проверки подлинности + На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. + Малое количество подписей + Токены на разных сетях могут иметь разные адреса. Пожалуйста, убедитесь при переводе средств, что ваш адрес соответствует сети. + + Используйте вашу карту, чтобы получить адрес для %d сети + Используйте вашу карту, чтобы получить адреса для %d сетей + Используйте вашу карту, чтобы получить адреса для %d сетей + Используйте вашу карту, чтобы получить адреса для %d сетей + + Некоторые адреса отсутствуют + В данный момент сеть недоступна. Пожалуйста, попробуйте позже. + Сеть недоступна + Пополните ваш кошелек + Ваш кошелек не имеет резервной копии. Проведите эту процедуру сейчас, чтобы защитить ваши активы. + Резервная копия отсутствует + Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. + Карта уже подписывала транзакции + Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше + Нравится Tangem? + Необходима плата за аренду сети + %1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети. + Недостаточно %1$s для оплаты комиссии сети + Отправка средств станет доступной после завершения транзакции(-ий) в сети %s + Отправка средств станет доступной после завершения транзакции %s + Транзакция в обработке + Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. + Оповещение сети Солана + Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. + Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Некоторые сети недоступны + Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. + Только для целей тестирования + Отказаться + Вы не закончили резервное копирование. Хотите продолжить? + Да, возобновить + Отказаться + Если сейчас отказаться, то придётся сбрасывать карты до заводских настроек, чтобы начать заново + Возобновить резервное копирование + Это необратимое действие + Войти с %s + Сканировать карту + Используйте %s или отсканируйте карту для входа в приложение + C возвращением! diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml index eb43a8cf6f..7545da58e5 100644 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml @@ -3,9 +3,12 @@ 默認 遺留資產 獲取費用失敗 - 由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。 - 要創建帳戶,請將資金發送到此地址 + 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 + 沒有足夠的資金進行交易。請先入金 + + 目標帳戶未激活。發送 %s 或更多以激活帳戶 + 要創建帳戶,請將資金發送到此地址 最小數量是 %s 更動太小 無效費用 diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index bef56ab91b..841f0df3bb 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -1,463 +1,372 @@ - 添加自定義代幣 - 管理代幣 - 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 - 謝謝您的反饋 - 成功送出 - 請求支持 - 發送反饋 - 此功能不在展示模式中提供 - 發送電子郵件失敗 - 原因:%s - 無法發送交易 - 此卡不支持Solana網路上的代幣因為韌體限制 - 有困難在掃描卡上嗎? - 此卡不適用於此app - 轉到設置以在 Tangem App 中啟用生物識別身份驗證 - 啟用生物辨識 - 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。 - 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼 - 保存訪問密碼 - 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼 - 將錢包保存在應用程序中 - 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片 - APP設置 - 請掃描卡片 - 請30秒後重試或刷卡 - 嘗試次數過多 - - %d 卡片 - - 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 - 允許您使用此卡重置此錢包中其他卡上的訪問密碼 - 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 - 恢復訪問密碼 - 重置 - 您確定要這麼做嗎? - 更改訪問密碼 - 訪問密碼將僅在此卡上更改 - 回復至原廠設置 - 安全模式 - 選定的應用程序保護方法 - 卡片設置 - Tangem 機器人 - 支援 - 請評價 - 發送logs - 請選擇一個操作 - 請評價我們的服務 - 接受 - 添加 - 注意 - 餘額: %s - 生物識別 - 生物 - 購買 - 您尚未授予相機訪問權限,請更改您的隱私設置 - 刪除 - 關閉 - 繼續 - 複製 - 複製地址 - 創造 - 刪除 - 禁用 - 不喜歡 - 完成 - 允許 - 啟用 - 錯誤 - 交易 - 導入 - 喜歡 - - OK - 主卡片 - 拒絕 - 重新命名 - 保存設置 - 搜索 - 搜尋代幣 - 選擇 - 銷售 - 發送 - 伺服器不可用,請稍後在試 - 分享 - 簽署 - 簽署和發送 - 開始 - 提交 - 成功 - 交換 - 費用 - 條款和條件 - 交易 - 我了解 - 無法觸達 - - 已複製代幣地址 - 支持的網路 - 添加代幣 - 代幣地址 - 無效地址 - 衍生路徑錯誤 - 請選擇網路 - 必填 - 十進制數必須是有效整數,不大於 %d - 小數點 - 默認 - BIP44 幣種 - 如 USD Coin - 名稱 - 未選擇 - 網路 - 如 USDC - 代幣符號 - 此代幣/網路已被加入 - 請注意代幣可以被任何人創造。小心詐騙 - 交談 - 訪問密碼 - 在掃描卡之前,您必須提交正確的訪問密碼 - 長按 - 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。 - 密碼 - 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 - 推薦計畫 - 隱私政策 - %s hashes - 卡號 - 連結更多錢包 - App Currency - 發行人 - 簽署 - 更多 - 檢查您的網路連接或切換到其他網絡 - 服務條款 - 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 - 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 - 以下信息是可選的。如果不想共享,可以將其刪除 - 告訴我們您缺少哪些功能,我們會盡力幫助您 - 請告訴我們你有什麼卡 - 嗨! 幫助團隊, - 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助 - 我的建議 - 無法掃卡 - 反饋 - Tangem反饋 - 無法發送交易 - 掃描卡片 - 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 - 要更改密碼,請完全按照上圖所示連接手機和卡 - 要創建錢包,請完全按照上圖所示連接手機和卡 - 要重置為出廠設置,請完全按照上圖所示連接手機和卡片 - 點擊掃描 - 要簽名,請完全按照上圖所示連接手機和卡 - 點擊簽名 - 點按卡片 - 內部錯誤:找不到錢包管理器 - 您已更新生物識別登入,掃描您的卡進入 - 管理代幣 - 總餘額 - 該金額不包括您的部分資金 - 要訪問所有的網路您需要掃描卡片 - 掃描卡片 - 代幣 - 您必須設置一個單一的訪問代碼來保護您的所有錢包 - 保護 - 您可以稍後在每張卡上設置單獨的訪問密碼 - 個人化 - 門禁密碼可以用聯動卡恢復,不要把所有卡都放在一個地方 - 恢復 - 選擇您想要的任何單詞、短語或數字作為您的訪問代碼 - 建立訪問密碼 - 重新輸入訪問密碼 - 訪問代碼的長度必須至少為 4 個字符 - 輸入的訪問密碼與初始訪問密碼不匹配 - 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎? - 備份過程已部分完成。你現在不能退出 - 餘額 - 添加備用卡 - 掃描卡片 #%d - 立即備份 - 掃描主卡 - 獲取 - 繼續至我的錢包 - 完成備份過程 - 設置PIN碼 - 接收貨幣 - 掃描主卡 - 暫時略過 - 它是如何運作的? - 讓我們生成您卡上的所有密鑰並創建一個安全的錢包 - 創建錢包 - 創造錢包 - 其他選項 - 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。 - 私下生成密鑰 - 您的卡已激活並可以使用 - 成功! - 這此情況,您必須要重新開始 - 您想要離開啟用程序嗎? - 開始 - 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? - PIN 碼 - 連接 - 創建備份 - 閱讀更多關於助記詞的訊息 - 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 - 生成助記詞 - 導入錢包 - 使用助記詞 - Legacy - 那麼,讓我們檢查一下 - 要開始,只需將 wxDAI 獲取到您的錢包 - 這會花上幾秒 - 要開始備份過程,最多可添加兩張備份卡。 - 您可以再添加一張卡或完成備份過程 - 準備編號為 %s 的備份卡 - 掃描主卡以啟動備份過程 - 準備編號為 %s 的主卡 - 恭喜! 您的第一張支付加密卡已啟用! - 您的錢包卡已配置完畢,可以使用了 - 已添加至最大數量。完成備份過程 - 啟用卡片 - 備份卡 #%d - 獲取%s - 獲取中 - 沒有備用卡 - 添加了一張備用卡 - PIN 碼 - 準備好你的卡片 - 添加了兩張備用卡 - 要開始使用,只需為錢包充值任意金額 - 要開始,只需為錢包充值超過 %1$s %2$s - 購買加密貨幣 - 顯示錢包地址 - 啟動錢包 - 結對過程已部分完成。你現在不能退出 - 如果創建錢包的過程以任何方式中斷,您將得重新開始 - 您最多可以額外備份兩張空白的 Tangem冷錢包 - 可以使用其中一張備用卡恢復訪問密碼 - 所有備用卡都可以使用相同的密鑰作為全功能使用 - 您將能夠設置訪問密碼來保護您的錢包 - 備援錢包 - 訪問密碼還原 - 相同的卡片 - 訪問密碼 - 參與 - 無法加載有關推薦計劃的消息。請稍後再試 - 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 - 您的朋友買 - 會得到 - 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 - - 得到 - %s 折扣 - 你的朋友 - 個人促銷碼已複製! - 您的優惠碼 - 以折扣價購買 Tangem 錢包!%s - 推薦您的好友至Tangem - 你接受了 - 點擊此按鈕即表示您接受 - 在推薦計劃 - - %d 錢包 - - 請持有卡片直至操作完成 - 重置卡片 - 我了解執行此操作後,我將無法再訪問當前錢包 - 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 - 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 - 目前不接受俄羅斯銀行卡 - 登錄應用程序並在不掃描卡片的情況下檢查您的資產 - 訪問應用程序 - 允許使用生物辨識 - 將要求生物識別而不是訪問密碼來與您的錢包進行交互 - 訪問密碼 - 您好像禁用了生物識別,有必要保存錢包 - 啟用生物識別授權 - 您想使用生物識別技術嗎? - 請注意,用您的資金進行交易仍需要您的卡片 - 掃卡失敗。再試一次 - 掃描卡片 - 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 - 準備好您的卡! - 數量 - 地址 - 地址與錢包地址相同 - 標籤無效。它不會被添加到交易中 - Memo無效。 它不會被添加到交易中 - Tag - Memo - 包含費用 - - 正常 - 優先 - 最大值 - 網路費 - 發送 %s - 總計 - %1$s 和 %2$s 將被發送 - ≈ %1$s (費用包含: %2$s) - %s 將被發送 - 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 - 無效地址 - 現在購買 - 輸入折扣碼 - Tangem Wallet - 其他付款方式 - 總計 - 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 - 創新式的硬體錢包 - 最多 3張實體卡片 到一個錢包 - 最多 - 3張實體卡片 - 到一個錢包 - 更多的安全備份 - 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中 - 千種虛擬貨幣 - 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機 - 適合每個人的冷錢包 - 認識Tangem - 交易、購買NFT、自由穿梭於100+去中心化服務 - 兼容Web3.0 - 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 - 批准 - 錯誤: %s - 賦予權限 - 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 - 餘額不足 - 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 - 交易進行中... - 等待中 - 允許 - 賦予權限 - 數量 %s - 花費者 - 您的錢包 - 要繼續,您需要允許 1inch 智能合約使用您的 %s - 在瀏覽器中查看 - 進行中 - 交易 - 交易 %s 至 - 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品 - 其他代幣 - 選擇代幣 - 您的代幣 - 無法使用 - - %d 代幣 - - 隱藏 - 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 - 隱藏 %s - 隱藏代幣 - %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 - 無法隱藏 %s - 無費用 - 您還沒有任何交易 - 無法加載交易 - 進行中… - 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 - 你掃描錯了胞胎卡。請嘗試另一個 - 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 - 一個錢包,兩張卡片 - 掃描卡片 #%s - 創建錢包 - 掃描 #%s 雙胞胎卡 - 準備卡片 - Tangem Twin - 這個動作是不可逆的。您將無法訪問舊錢包 - 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 - 添加新錢包 - 您確定要刪除此錢包? - 已選擇 %d - 發生錯誤,請掃卡登錄 - 此錢包已保存,您可以再添加一個 - 多幣種 - 錢包名稱 - 重新命名錢包 - 單一幣種 - 我的錢包 - 用 %s 解鎖全部 - 交易記錄 - 區塊鍊無法使用。稍後再試 - 餘額加載中 - 掃描卡片 - 交易進行中 - 檢視餘額 - 動作 - 選擇錢包選項 - 您想要購買或賣出交易貨幣? - 請求籤署消息。%s - Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s - %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s - 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s - BNB 交易已成功簽署並發送至 Dapp - 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼 - 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s - 無法交易,無足夠資金 - 未能建立 WalletConnect 連接。請稍後再試 - 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n - 無法簽署消息。請重試 - 無法建立 WalletConnect 連接:超時錯誤。請稍後再試 - 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n - 由於技術問題,無法與此 Dapp 建立連接 - 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員 - 在 Tangem App 中選擇了錯誤的卡 - 無法從 Dapp 數據創建交易。代碼: %s - 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 - 消息已成功簽名並發送至Dapp - 沒有 %s 網路,請先加入後再試一次 - 沒有已連結的WalletConnect - Ooops, 沒有連接 - 從剪貼板貼上 - 給 %1$s 的消息:%2$s - 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s - 無法完成執行,您已經使用此參數建立了 WalletConnect 連接 - 掃描新密碼 - 此卡不能用於建立 WalletConnect 連接 - 不支持此網絡。請選擇其他網絡 - 選擇網路 - Dapp 沒有提供必要的數據來建立 WalletConnect 連接 - 找不到請求的連接 - WalletConnect 連接 - 連結到dApps - WalletConnect - 交易已成功簽署並發送至 Dapp - 交易已成功簽署並發送至區塊鏈 - 未能找到交易哈希 - %s 網路 - 帳號尚未被創造 - 不支持此卡 - 您的 Tangem 卡是為與不同的應用程序一起工作而設計的。請查看卡片上的名稱和說明,並安裝正確的應用程序 - %s市場價格 - 地址已復製到剪貼板 - 無網路 - 接收中 - 來自%s - 發送中 - 至%s - Tangem - 可以更好 - 了解更多 - 了解 - 真的很酷! - 此卡可能是生產樣本或偽造品 - 認證檢查失敗 - 網路無法使用 - Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 - 捨棄 - 您有一個備份中斷了,您想繼續嗎? - 是的,恢復 - 捨棄 - 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始 - 恢復備份 - 此動作不可逆 - 使用 %s 登錄 - 掃描卡片 - 使用 %s 或掃描卡片以訪問該應用程序 - 歡迎回來! + 添加自定義代幣 + 管理代幣 + 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 + 請求支持 + 此功能不在展示模式中提供 + 原因:%s + 無法發送交易 + 此卡不支持%1$s網路上的代幣因為韌體限制 + 有困難在掃描卡上嗎? + 此卡不適用於此app + 轉到設置以在 Tangem App 中啟用生物識別身份驗證 + 啟用生物辨識 + 這將刪除所有保存的錢包訪問代碼。對錢包的任何進一步操作都需要提交訪問代碼。 + 移除已保存的卡會刪除所有已保存的錢包及其訪問密碼 + 保存訪問密碼 + 在與您的卡進行交互時,將要求進行生物識別身份驗證,而不是訪問密碼 + 將錢包保存在應用程序中 + 啟用以將所有錢包鏈接到 Tangem 應用程序。解鎖應用程序需要生物識別身份驗證。交易簽名需要輕觸您的 Tangem 卡片 + APP設置 + 請掃描卡片 + 請30秒後重試或刷卡 + 嘗試次數過多 + + %d 卡片 + + 如果您不希望使用此卡重置此錢包中其他卡上的訪問密碼,請禁用此選項。請注意,這也會阻止您重置此卡上的訪問密碼。 + 允許您使用此卡重置此錢包中其他卡上的訪問密碼 + 恢復訪問密碼 + 重置 + 您確定要這麼做嗎? + 更改訪問密碼 + 訪問密碼將僅在此卡上更改 + 回復至原廠設置 + 安全模式 + 卡片設置 + 接受 + 注意 + 餘額: %s + 餘額 + 生物識別 + 生物 + 購買 + 您尚未授予相機訪問權限,請更改您的隱私設置 + 刪除 + 關閉 + 繼續 + 複製 + 複製地址 + 創造 + 刪除 + 禁用 + 完成 + 允許 + 啟用 + 錯誤 + 費用 + 導入 + 網路費 + + OK + 主卡片 + 拒絕 + 重新命名 + 保存設置 + 搜索 + 搜尋代幣 + 選擇 + 銷售 + 發送 + 伺服器不可用,請稍後在試 + 分享 + 簽署 + 簽署和發送 + 開始 + 提交 + 成功 + 交換 + 條款和條件 + 交易 + 我了解 + 無法觸達 + + 已複製代幣地址 + 支持的網路 + 添加代幣 + 代幣地址 + 無效地址 + 請選擇網路 + 小數點 + 默認 + BIP44 幣種 + 如 USD Coin + 名稱 + 未選擇 + 網路 + 如 USDC + 代幣符號 + 此代幣/網路已被加入 + 請注意代幣可以被任何人創造。小心詐騙 + 交談 + 訪問密碼 + 在掃描卡之前,您必須提交正確的訪問密碼 + 長按 + 此機制可防止對卡進行鄰近攻擊。它將強制執行命令的接收和執行之間的延遲。 + 密碼 + 在執行任何需要更改卡狀態的命令之前,您必須輸入密碼 + 推薦計畫 + %s hashes + 卡號 + 連結更多錢包 + App Currency + 發行人 + 簽署 + 更多 + 檢查您的網路連接或切換到其他網絡 + 服務條款 + 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 + 以下信息是可選的。如果不想共享,可以將其刪除 + 告訴我們您缺少哪些功能,我們會盡力幫助您 + 請告訴我們你有什麼卡 + 嗨! 幫助團隊, + 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助 + 我的建議 + 無法掃卡 + 反饋 + Tangem反饋 + 無法發送交易 + 掃描卡片 + 要更改訪問密碼,請完全按照上圖所示連接手機和卡片 + 要更改密碼,請完全按照上圖所示連接手機和卡 + 要創建錢包,請完全按照上圖所示連接手機和卡 + 點擊掃描 + 點擊簽名 + 點按卡片 + 您已更新生物識別登入,掃描您的卡進入 + 管理代幣 + 要訪問所有的網路您需要掃描卡片 + 掃描卡片 + 代幣 + 保護 + 您可以稍後在每張卡上設置單獨的訪問密碼 + 個人化 + 恢復 + 選擇您想要的任何單詞、短語或數字作為您的訪問代碼 + 建立訪問密碼 + 重新輸入訪問密碼 + 訪問代碼的長度必須至少為 4 個字符 + 輸入的訪問密碼與初始訪問密碼不匹配 + 您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎? + 備份過程已部分完成。你現在不能退出 + 添加備用卡 + 掃描卡片 #%d + 立即備份 + 掃描主卡 + 繼續至我的錢包 + 完成備份過程 + 接收貨幣 + 掃描主卡 + 暫時略過 + 它是如何運作的? + 讓我們生成您卡上的所有密鑰並創建一個安全的錢包 + 創建錢包 + 創造錢包 + 其他選項 + 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。 + 私下生成密鑰 + 您的卡已激活並可以使用 + 成功! + 這此情況,您必須要重新開始 + 您想要離開啟用程序嗎? + 開始 + 您要添加的卡上已經創建了另一個錢包。你想重置它並將卡用於新錢包嗎? + 創建備份 + 閱讀更多關於助記詞的訊息 + 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 + + 按照下面給出的順序寫下這 %d 個單詞,並將它們存放在安全秘密的地方。 + + 生成助記詞 + 導入錢包 + 使用助記詞 + Legacy + 那麼,讓我們檢查一下 + 要開始備份過程,最多可添加兩張備份卡。 + 您可以再添加一張卡或完成備份過程 + 準備編號為 %s 的備份卡 + 掃描主卡以啟動備份過程 + 準備編號為 %s 的主卡 + 您的錢包卡已配置完畢,可以使用了 + 已添加至最大數量。完成備份過程 + 啟用卡片 + 備份卡 #%d + 沒有備用卡 + 添加了一張備用卡 + 準備好你的卡片 + 添加了兩張備用卡 + 要開始使用,只需為錢包充值任意金額 + 要開始,只需為錢包充值超過 %1$s %2$s + 購買加密貨幣 + 顯示錢包地址 + 啟動錢包 + 結對過程已部分完成。你現在不能退出 + 如果創建錢包的過程以任何方式中斷,您將得重新開始 + 您最多可以額外備份兩張空白的 Tangem冷錢包 + 可以使用其中一張備用卡恢復訪問密碼 + 所有備用卡都可以使用相同的密鑰作為全功能使用 + 您將能夠設置訪問密碼來保護您的錢包 + 備援錢包 + 訪問密碼還原 + 相同的卡片 + 訪問密碼 + 參與 + 無法加載有關推薦計劃的消息。請稍後再試 + 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 + 您的朋友買 + + 得到 + %s 折扣 + 你的朋友 + 個人促銷碼已複製! + 您的優惠碼 + 以折扣價購買 Tangem 錢包!%s + 推薦您的好友至Tangem + 你接受了 + 點擊此按鈕即表示您接受 + 在推薦計劃 + + %d 錢包 + + 重置卡片 + 我了解執行此操作後,我將無法再訪問當前錢包 + 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 + 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 + 目前不接受俄羅斯銀行卡 + 登錄應用程序並在不掃描卡片的情況下檢查您的資產 + 訪問應用程序 + 允許使用生物辨識 + 將要求生物識別而不是訪問密碼來與您的錢包進行交互 + 訪問密碼 + 您好像禁用了生物識別,有必要保存錢包 + 啟用生物識別授權 + 您想使用生物識別技術嗎? + 請注意,用您的資金進行交易仍需要您的卡片 + 掃描卡片 + 掃描卡片以更改其設置。這些更改只會影響您掃描過的卡,不會影響綁定到您錢包的其他卡。 + 準備好您的卡! + 數量 + 地址 + 地址與錢包地址相同 + 標籤無效。它不會被添加到交易中 + Memo無效。 它不會被添加到交易中 + Tag + Memo + 包含費用 + + 正常 + 優先 + 最大值 + 發送 %s + 總計 + %1$s 和 %2$s 將被發送 + ≈ %1$s (費用包含: %2$s) + %s 將被發送 + 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 + 無效地址 + 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 + 創新式的硬體錢包 + 最多 3張實體卡片 到一個錢包 + 更多的安全備份 + 一個冷錢包,可同時將您的比特幣、以太幣和其他幣種-全部存在一張卡中 + 千種虛擬貨幣 + 隨時隨地使用它,不需要接線或電池,只需要你的卡片和手機 + 適合每個人的冷錢包 + 認識Tangem + 交易、購買NFT、自由穿梭於100+去中心化服務 + 兼容Web3.0 + 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 + 批准 + 賦予權限 + 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 + 餘額不足 + 允許 + 賦予權限 + 數量 %s + 要繼續,您需要允許 1inch 智能合約使用您的 %s + 進行中 + 交易 + 選擇代幣 + 無法使用 + 隱藏 + 您即將在主屏幕上隱藏此代幣。您可以隨時通過管理代幣頁面將其添加回來。 + 隱藏 %s + 隱藏代幣 + %1$s 代幣是 %2$s 網絡上的主要貨幣,只要列表中還有該網絡上的其他代幣,它就無法被隱藏。 + 無法隱藏 %s + 您還沒有任何交易 + 無法加載交易 + 您掃描了同一張卡片。要創建雙錢包,您需要掃描編號為 %d 的卡 + 你掃描錯了胞胎卡。請嘗試另一個 + 這一個是你手裡拿著的,另一個是編號為 %s 的,這兩張卡都可以用來從這個錢包中提取資金 + 一個錢包,兩張卡片 + 掃描卡片 #%s + 創建錢包 + 掃描 #%s 雙胞胎卡 + 準備卡片 + Tangem Twin + 這個動作是不可逆的。您將無法訪問舊錢包 + 將您的 iPhone 靠近編號為 %s 的雙胞胎卡 + 添加新錢包 + 您確定要刪除此錢包? + 發生錯誤,請掃卡登錄 + 此錢包已保存,您可以再添加一個 + 錢包名稱 + 重新命名錢包 + 用 %s 解鎖全部 + 區塊鍊無法使用。稍後再試 + 掃描卡片 + 請求籤署消息。%s + Dapp %1$s,請求\n簽署 BNB 交易。\n%2$s + %1$s 的交易訂單\n價格: %2$s\n接收金額:%3$s\n支付數量: %4$s + 交易明細:\n從: %1$s\n到: %2$s\n數量: %3$s + 剪貼板包含 WalletConnect 代碼。使用複制的值或掃描二維碼 + 請求為 %1$s 創建交易\n%2$s\n\n數量: %3$s\n費用: %4$s\n全部的: %5$s\n餘額: %6$s + 無法交易,無足夠資金 + 未能建立 WalletConnect 連接。請稍後再試 + 並非所有代幣都已添加到您的列表中。請先添加它們,然後重試。缺少標記:\n + 無法簽署消息。請重試 + 無法建立 WalletConnect 連接:超時錯誤。請稍後再試 + 會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n + 由於技術問題,無法與此 Dapp 建立連接 + 我們遇到了未知錯誤。錯誤信息:%s。如果問題仍然存在-請隨時聯繫我們的客服人員 + 在 Tangem App 中選擇了錯誤的卡 + 無法從 Dapp 數據創建交易。代碼: %s + 我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員 + 沒有已連結的WalletConnect + Ooops, 沒有連接 + 從剪貼板貼上 + 給 %1$s 的消息:%2$s + 請求開始會話\n%1$s\n\n網絡: %2$s\n\n網址:%3$s + 無法完成執行,您已經使用此參數建立了 WalletConnect 連接 + 掃描新密碼 + 此卡不能用於建立 WalletConnect 連接 + 不支持此網絡。請選擇其他網絡 + 選擇網路 + WalletConnect 連接 + 連結到Dapps + WalletConnect + %s市場價格 + 地址已復製到剪貼板 + 無網路 + Tangem + 可以更好 + 了解 + 真的很酷! + 此卡可能是生產樣本或偽造品 + 認證檢查失敗 + 網路無法使用 + Solana 網絡每 2 天收取 %1$s 的費用。無法付此費用的帳戶將從網絡中清除。向您的帳戶存入超過 %2$s 即可免費使用 + 捨棄 + 您有一個備份中斷了,您想繼續嗎? + 是的,恢復 + 捨棄 + 如果您現在要放棄備份,則必須將卡重置為出廠設置才能重新開始 + 恢復備份 + 此動作不可逆 + 使用 %s 登錄 + 掃描卡片 + 使用 %s 或掃描卡片以訪問該應用程序 + 歡迎回來! diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml index 2aeb56c771..ef4e2b2edb 100644 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ b/core/res/src/main/res/values/strings-blockchain.xml @@ -6,10 +6,9 @@ Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Not enough funds for the transaction. Please top up your account. An error occurred. Code: %s. - Due to Kaspa limitations only %1$d UTXOs can fit in a single transaction. This means you can only send %2$s or less. You need to reduce the amount. - To create account send funds to this address To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely Destination account is not active. Send %s or more to activate the account. + To create account send funds to this address Minimum amount is %s Change is too small Invalid Fee diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index cada81a5ff..597b184e40 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,762 +1,684 @@ - Add custom token - Manage tokens - Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. - Thank you for your feedback - Sent successfully - Request support - Send feedback - This feature is disabled in Demo mode - Failed to send the email - Reason: %s - Can\'t send a transaction - The selected does not support the %1$s network - To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. - Tokens in %1$s network are not supported by this card due to firmware limitation. - Are you having difficulty scanning your card? - This card is not designed to work with this app - Go to settings to enable biometric authentication in the Tangem App - Enable biometric authentication - This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. - Removing the saved card deletes all the saved wallets and their access codes from the app. - Save Access Code - Biometric authentication will be requested instead of the access code for interactions with your card. - Keep the wallet in the app - Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. - Dark - Light - System default - Theme - App Settings - To hide or show your balances, simply flip your device screen down, or switch it off in Settings - Don\'t show again - Got it - Balances are hidden - Please scan the card - Please try again in 30 seconds or scan the card - Too many attempts - You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. - Start backup process - - %d card - %d cards - - Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. - Allows you to use this card to reset access code on other cards in this wallet - Disable the ability to reset the access code on this card or other cards in this wallet - Access code recovery - Reset - Are you sure you want to do this? - Change Access Code - Access code will be changed on this card only - Reset to Factory Settings - Security Mode - Selected application protection method - Card Settings - Tangem Bot - Support - Send logs - Accept - Access denied - Add - Apply - Approval - Attention - Balance: %s - Balance - biometric authentication - biometrics - Buy - Go to %1$s - You have not given access to your camera, please adjust your privacy settings - Cancel - Close - Continue - Copy - Copy address - Create - Delete - Disabled - Dislike - Done - Enable - Enabled - Error - Exchange - Explore - Explore history - Explore transaction history - Explorer - Fee - Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s - Custom - Fast - Market - Slow - Speed and fee - Go to provider - Get addresses - Import - Learn & Earn - Like - Locked - Main network - Next - No - No address - No data - OK - Primary Card - Paste - Read more - Receive - Reject - Reload - Rename - Save changes - Search - Search tokens - Seed phrase - Select action - Sell - Send - The server is not available, please try again later - Share - Sign - Sign and send - Stake - Start - Submit - Success - Support - Swap - terms and conditions - Transaction failed - Transactions - Transfer - I understand - Unreachable - There was an error. Please try again. - Yes - Contract address copied! - Available networks - Add token - Contract address - Contract address is invalid - Derivation path is invalid - Please select the network - Required field - Decimal number must be a valid integer, no higher than %d - Custom derivation - E. g. m/00\'/0000\'/0\'/0/0 - Enter custom derivation - Decimals - Derivation Path - Default - BIP44 coin type - The derivation path you\'ve entered is not valid - E.g. USD Coin - Name - Not selected - Network - Token network - You can manually add a token that is not natively supported by Tangem - E.g. USDC - Symbol - Token symbol - This token/network has already been added to your list - Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. - Be aware of adding scam tokens, they can cost nothing - Note that tokens can be created by anyone - Chat - Access code - You will have to submit the correct access code before scanning the card - Long Tap - This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. - Passcode - Before executing any command entailing a change of the card state, you will have to enter the passcode. - Referral program - Flip your device screen down to quickly hide and show balances - Privacy policy - %s hashes - Card ID - Contact to Support - Link More Cards - App Currency - Flip-to-Hide Balances - Issuer - Signed - Details - Check your internet connection or switch to a different network - Terms of Service - Oops, the current version of the application is not ready to work with this card, please check for updates. - You have used a card from another wallet. Tap the card associated with this wallet - My tokens - You haven\'t added any tokens yet. Add tokens via Market to swap - No tokens found. Please try another request - Cannot be swapped for %s - Additionally, the network fee for sending the exchanged funds back to your address is included in the rate - Status - Providers facilitate transactions, ensuring smooth and efficient token swaps - Choose provider - More providers are coming soon.\nStay tuned! - Estimated amount - Exchange by %s - Provided by - Visit provider’s website to refund your money - Operation failed by provider - Visit provider’s website for verification - KYC verification required by provider - Canceled - Confirmed - Confirming - Confirming… - Exchanged - Exchanging - Exchanging… - Failed - Deposit received - Awaiting deposit - Awaiting deposit… - Refunded - Sending to you - Sending to you… - Sent - Provider-sourced data. Estimated amount subject to change due to market conditions. - Exchange status - Verified - Verification required - List of all tokens added to your wallet - Fetching best rates... - Floating rate - Go to provider - By using swap functionality, you agree with provider’s %s - By using swap functionality, you agree with provider’s %1$s and %2$s - Privacy Policy - Provider - Best rate - Available from %s - Available up to %s - Unavailable for this pair - Permission Required - Terms of Use - The following information is optional. You can erase it if you don\'t want to share it. - Tell us what functions you are missing, and we will try to help you. - Please tell us what card do you have - Hi support team, - Please tell us more about your issue. Every small detail can help. - My suggestions - Can\'t scan a card - Feedback - Tangem feedback - Can\'t send a transaction - Order card - Scan card - To change the access code tap the card as shown above and do not remove until the end of the operation - To change the passcode tap the card as shown above and do not remove until the end of the operation - To create the wallet tap the card as shown above and do not remove until the end of the operation - To reset to factory settings tap the card as shown above and do not remove until the end of the operation - Tap to scan - To sign tap the card as shown above and do not remove until the end of the operation - Tap to sign - Tap the card - Internal error: wallet manager not found - You have updated biometrics, scan your card to enter - To begin tracking your crypto assets and transactions, add tokens - You have completed all of the lessons, and are now eligible to receive your 1INCH tokens - - Complete three lessons and receive %d 1INCH token to your wallet - Complete three lessons and receive %d 1INCH tokens to your wallet - - Manage tokens - Total balance - The amount does not include some of your funds - 1INCH tokens will be credited to your %s wallet address within 48 hours - To access all the networks you need to scan the card - Scan your card - Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s - Swap with Changelly, %s fees - Tokens - Add - Edit - Couldn’t find this token, you can add it manually - Coin market cap - Blockchain the cryptocurrency was initially created - Native network - Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. - Not original or primary blockchain the token is hosted - Non-native networks - Networks - Choose networks - Wallet - Couldn’t find this token, you can add it manually - - %1$d of %2$d wallet - %1$d of %2$d wallets - - e.g. BTC I trust, hodl I must - Coin market cap - The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. - Upvote - Choose wallet - The wallet doesn\'t support more than one network - You have to set up a single access code to protect all your wallets - Protect - You can set up an individual access code on each card later - Personalize - The access code can be restored with a linked card, don\'t keep all cards in one place - Restore - Choose any word, phrase, or number you want as your access code - Create Access Code - Re-enter your Access Code - Access code must be at least 4 characters long - Entered access code didn\'t match the initial access code - Please repeat the operation. The card will be reset to factory settings. - Activation error - You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? - The backup process is partly complete. You can\'t exit it now. - Add a backup card - Scan the card #%d - Backup now - Scan the primary card - Claim - Continue to my wallet - Finalize the backup - Set PIN code - Receive crypto - Scan primary card - Skip for later - How does it work? - Let\'s generate all the keys on your card and create a secure wallet - Create wallet - Create a wallet - Other options - Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. - Generate keys privately - Your card is activated and ready to be used - Success! - In this case, you will need to start from the beginning. - Do you want to exit the activation process? - Getting started - Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? - Pin code - Connect - Creating a backup - Read more about seed phrase - Write these 12 words down in the order given below and store them in a safe and secret place. - Your seed phrase - To import your wallet, enter your seed phrase in the field below - Generate seed phrase - Import wallet - A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk. - Use seed phrase - Invalid seed phrase. Please check the word order. - Invalid seed phrase. Please check your spelling. - Legacy - To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words - So, let’s check - To get started, simply claim wxDAI to your wallet - It will take a few seconds - To start the backup process add up to two backup cards. - You can add one more card or finalize the backup process - Prepare the backup card with number %s - Scan the primary card to start the backup process. - Prepare the primary card with number %s - Congratulations! Your first payment crypto card has been activated! - Your wallet card is configured and ready for use. - Max number of cards added. Finalize the backup process. - Activating card - Backup card #%d - Claim %s - Claiming - No backup cards - One backup card added - PIN Code - Prepare your card - Two backup cards added - To get started, simply top up the wallet with any amount - To get started, simply top up the wallet with more than %1$s %2$s - Buy crypto - Show the wallet\'s address - Activate a wallet - The twinning process is partly complete. You can\'t exit it now. - If the process of creating the wallet gets interrupted in any way, you\'ll have to start over - You can backup your keys up to two other blank Tangem Wallet cards. - Access code can be restored with one of backup cards. - All the backup cards can be used as full-functional with the identical keys. - You will be able to set an access code to protect your wallets. - Backup wallet - Access code restore - Identical cards - Access code - Group - By balance - Organize tokens - Ungroup - Select from the gallery - Settings - Camera access denied - You have not given access to your camera - %1$s %2$s address on %3$s network - %1$s (%2$s) on %3$s network - Send only %s to this address. Sending any other currency will result in its irreversible loss. - Participate - Failed to load the information about the referral program. Please try again later. - Failed to load the information about the referral program. Error code: %s. Please try again later. - Upcoming payments - Your friends bought - Less - More - No upcoming payments - - for %d wallet - for %d wallets - - Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that - Will get - for each wallet bought by your friend on your %1$s network address%2$s - You - Will get a - when buying a wallet on tangem.com - %s discount - Your friend - Personal code copied! - Your personal code - Buy Tangem Wallet with discount!\n%s - Refer your friends to Tangem - You\'ve accepted - By tapping this button you accept - of the referral program - - %d wallet - %d wallets - - Please hold the card until the operation complete - Reset the Card - I understand that after performing this action, I will no longer have access to the current wallet - I realize that I can\'t use this card to recover my access code on the other cards of the current wallet - Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. - Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet. - Do you have a bank card from another country and a residence permit or registration outside the Russian Federation? - Russian bank cards are not currently accepted - Log into the app and check your balance without scanning the card - Access the app - Allow to use biometrics - Biometrics will be requested instead of the access code for interactions with your wallet - Access code - It looks like you have biometric authentication disabled, it is necessary to save wallets - Enable biometric authorization - Would you like to use biometrics? - Note that making a transaction with your funds will still require your card - Scan card failed. Try again - Scan Card - Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. - Get your card ready! - Already included in the entered address - Amount - The transaction is not completed - Reason: %1$s\nCode: %2$s - Not enough funds to cover the network commission. Subtract the commission from the amount sent? - Subtract - Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? - Confirm - %1$s at %2$s - Address - Destination Tag - Enter address - Address is the same as wallet address - Invalid Tag. It won\'t be added to the transaction. - Invalid Memo. It won\'t be added to the transaction. - Tag - Memo - Include fee - Low - Normal - Priority - Check your network connection - Network fee info unreachable - From - Gas limit - Gas Limit is auto-calculated; raise it during network congestion - Gas price - Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed - Max - Maximum amount - Max fee - Maximum fee amount - Numbers only for Destination Tag - Network fee - Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. - Network fee coverage - Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance - Total exceeds balance - Fee is increased - The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01. - Custom fee is high - The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. - The included commission exceeds the transfer amount, leading to a negative value - Invalid amount - The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s. - Target account is not created. Please change the amount to send. - The amount to send must be at least %1$s - Kindly be aware that your transaction may experience delays under specific fee settings - Transaction delays are possible - Transaction limitation - Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. - Existential deposit - The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. - Reduce by %s - Reduce to %s - Optional - Please align your QR code with the square to scan it. Ensure you scan %s network address. - Recent - Recipient - Send to - Not a valid address - Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens - A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds - My wallets - Sending %s - Total - %1$s and %2$s will be sent - ≈ %1$s (inc. fee: %2$s) - %s will be sent - Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while - Invalid address - Transaction sent - %s (%s) - Buy now - I have a promo code… - Tangem Wallet - Other payment methods - Pre-order now - Total - Store your crypto assets secure while keeping private keys contained in your card - Revolutionary Hardware Wallet - Up to 3 physical cards to one wallet - Up to - 3 physical cards - to one wallet - Ultra Secure Backup - A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card - Thousands of Currencies - Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. - The Wallet for Everyone - Take three lessons, get a discount on your Tangem Wallet, and receive 1INCH tokens to your wallet - Learn - Meet Tangem - Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services - Web 3.0 Compatible - All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. - Approve - Error: %s - You swap - Give Permission - Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. - Insufficient funds - Fees - The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. - The amount includes the service provider\'s fee. - Sending amount will be reduced to cover the selected fee level - Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first. - Transaction in progress... - Waiting - Approve - Current transaction - The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. - Give Permission - Specify the approve limit for the selected token - Amount %s - Spender - Your wallet - To continue, grant 1inch smart contracts permission to use your %s - Unlimited - View in Explorer - In progress - Swap - Swap %s for - Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. - You receive - Other tokens - Choose token - Your tokens - not available - Balances hidden - Balances shown - Undo - - %d token - %d tokens - - Hide - You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. - Hide %s - Hide token - %1$s token in %%image%% %2$s network - The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. - Unable to hide %s - No rate - Exchange this token for another at %1$s service fees from February %2$s-%3$s. - Swap with Changelly, %s fees - Swap now - contract: %s - You don\'t have any transactions yet - Failed to load transaction history.\nClick on reload button to update the information. - Multiple addresses - Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. - Operation - from: %s - to: %s - In progress… - You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d - You\'ve scanned wrong twin card. Please try another one - This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. - One wallet. Two cards. - Scan the card #%s - Creating wallet - Scan the #%s twin card - Preparing card - Tangem Twin - This action is irreversible. You will not have access to the old wallet. - Tap the twin card with number %s and do not remove until the end of the operation - Use %s or scan a card to have an access to your wallet - Add new wallet - Are you sure you want to delete this wallet? - %d selected - An error has occurred, please scan your card to log in - This wallet has already been saved, you can add another one - Multi-currency - Wallet name - Rename Wallet - Single-currency - My Wallets - Unlock all - Unlock all with %s - Transaction history - Blockchain is unreachable. Try later - Balance is loading… - Scan the card - Transaction is in progress… - Verified Balance - Actions - Choose wallet option - Do you want to buy or sell crypto? - Requesting to sign a message.\n\n%s - Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s - Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s - Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s - The BNB transaction has been successfully signed and sent to the Dapp - Clipboard contain WalletConnect code. Use copied value or scan QR-code - Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s - Can\'t send transaction. Not enough funds. - Failed to establish WalletConnect session. Please, try again later. - Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n - Failed to sign message.\nPlease, try again - Failed to establish WalletConnect session: timeout error. Please, try again later. - Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n - Connection with this Dapp cannot be established due to its technical implementation. - We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support - Wrong card selected in Tangem App - Failed to create transaction from Dapp data. Code: %s - We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support - The message has been successfully signed and sent to the Dapp - %s network not found. Please, add it first and try again. - No opened WalletConnect sessions - Ooops. No Sessions. - Failed to pairing WalletConnect session: %1$s - Paste from clipboard - Message for %1$s:\n%2$s - Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s - The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters. - Scan new code - This card can\'t be used to establish WalletConnect session - This network is not supported. Please select another network. - Select network - Dapp didn\'t provide essential data to establish WalletConnect session - Failed to find session for request - WalletConnect Sessions - Connect to dApps - WalletConnect - The transaction has been successfully signed and sent to the Dapp - The transaction has been succesfully signed and sent to the blockchain - Failed to find transaction hash - %s network - Account is not created - This card is not supported - Your Tangem card was made to work with a different application. Please see the name and instructions on your card, and install the correct app - %s Market Price - last 24h - %s network - Address was copied to clipboard - No internet connection - Receiving - from %s - Sending - to %s - Tangem - Use %s or scan a card to unlock access to your wallet - Could be better - Learn more - Like it - Ok, Got it! - Really cool! - Refresh - You are currently in the Demo mode - Demo mode active - The card you scanned is a developer card. Do not use it to create your wallet. - Not for users! - %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. - Network requires Existential Deposit - Swap will be available after the %s transaction is complete - You have active transaction - You do not have any %s exchangeable coins in your list - No available tokens to swap - To make a transaction you need to deposit some %1$s %2$s - Unable to cover %s fee - Service temporarily unavailable - Please change the amount to swap - The amount to swap must be at least %s - The amount of tokens to be swapped must not exceed %s - This card might be a production sample or counterfeit - Authenticity check failed - Only %s signatures are left on this card. You must withdraw all of your funds. - Low signature count - Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. - - Use your card to get an address for %d network - Use your card to get an addresses for %d networks - - Some addresses are missing - The network is currently unreachable. Please try again later. - Network is unreachable - Top up your wallet - Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now. - Missing backup - This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. - Card has already signed transactions - Device incompatibility detected - Your review keeps us motivated to make Tangem Wallet even better - Enjoying Tangem? - Network rent fee required - %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. - Insufficient %1$s to cover network fee - Sending funds will be available once the %s transaction is complete - Transaction pending - Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. - Some networks currently are unreachable. Please try again later. - Some networks are unreachable - This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. - For testing purposes only - Discard - You have an interrupted backup. Do you want to resume? - Yes, resume - Discard - If you will discard the backup now, then you will have to reset the cards to factory settings to start over again - Resume backup - This is an irreversible action - Log in with %s - Scan card - Use %s or scan a card to access the app - Welcome back! + Add custom token + Manage tokens + Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + Request support + This feature is disabled in Demo mode + Reason: %s + Can\'t send a transaction + The selected does not support the %1$s network + To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset. + Tokens in %1$s network are not supported by this card due to firmware limitation. + Are you having difficulty scanning your card? + This card is not designed to work with this app + Default Fee + Enable Default Fee to set transaction fees automatically and skip the Fee page when sending funds. You can always go back to this page if necessary. + Go to settings to enable biometric authentication in the Tangem App + Enable biometric authentication + This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code. + Removing the saved card deletes all the saved wallets and their access codes from the app. + Save Access Code + Biometric authentication will be requested instead of the access code for interactions with your card. + Keep the wallet in the app + Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Dark + Light + System default + Theme + App Settings + To hide or show your balances, simply flip your device screen down, or switch it off in Settings + Don\'t show again + Got it + Balances are hidden + Please scan the card + Please try again in 30 seconds or scan the card + Too many attempts + You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + Start backup process + + %d card + %d cards + + Disable this option if you don\'t want this card to be used to reset access codes on other cards in this wallet. Please note that this will also prevent you from resetting the access code on this card. + Allows you to use this card to reset access code on other cards in this wallet + Access code recovery + Reset + Are you sure you want to do this? + Change Access Code + Access code will be changed on this card only + Reset to Factory Settings + Security Mode + Card Settings + Accept + Access denied + Apply + Approval + Attention + Balance: %s + Balance + biometric authentication + biometrics + Buy + Go to %1$s + You have not given access to your camera, please adjust your privacy settings + Cancel + Close + Continue + Copy + Copy address + Create + Delete + Disabled + Done + Enable + Enabled + Error + Explore + Explore transaction history + Explorer + Fee + Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s + Custom + Fast + Market + Slow + Speed and fee + Get addresses + Go to provider + Import + Locked + Main network + Network fee + Next + No + No address + OK + Primary Card + Passphrase + Read more + Receive + Reject + Reload + Rename + Save changes + Search + Search tokens + Seed phrase + Select action + Sell + Send + The server is not available, please try again later + Share + Sign + Sign and send + Start + Submit + Success + Support + Swap + terms and conditions + Transaction failed + Transactions + Transfer + I understand + There was an error. Please try again. + Unreachable + Yes + Contract address copied! + Available networks + Add token + Contract address + Contract address is invalid + Please select the network + Decimal must be a valid integer, up to %li + Custom derivation + E. g. m/00\'/0000\'/0\'/0/0 + Enter custom derivation + Decimals + Derivation Path + Default + BIP44 coin type + The derivation path you\'ve entered is not valid + E.g. USD Coin + Name + Not selected + Network + Token network + You can manually add a token that is not natively supported by Tangem + E.g. USDC + Symbol + Token symbol + This token/network has already been added to your list + Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + Be aware of adding scam tokens, they can cost nothing + Note that tokens can be created by anyone + Chat + Access code + You will have to submit the correct access code before scanning the card + Long Tap + This mechanism protects against proximity attacks on a card. It will enforce a delay between reception and execution of a command. + Passcode + Before executing any command entailing a change of the card state, you will have to enter the passcode. + Referral program + Flip your device screen down to quickly hide and show balances + %s hashes + Card ID + Contact to Support + Link More Cards + App Currency + Flip-to-Hide Balances + Issuer + Signed + Details + Check your internet connection or switch to a different network + Terms of Service + You have used a card from another wallet. Tap the card associated with this wallet + My tokens + You haven\'t added any tokens yet. Add tokens via Market to swap + Cannot be swapped for %s + Provided by + Status + Tangem offers token swaps via 3rd-party providers according to each provider\'s terms + Choose provider + An error occurred. Code: %s + Oops! Swapping the selected pair through the chosen provider is temporarily unavailable. Please try again later. (Code:\u00A0%s) + Selected provider is unavailable at the moment. Please try again later. (Code:\u00A0%s) + Swaps are unavailable at the moment. Please try again later. (Code:\u00A0%s) + Estimated amount + Exchange by %s + Visit provider’s website to refund your money + Operation failed by provider + Visit provider’s website for verification + KYC verification required by provider + Canceled + Confirmed + Confirming + Confirming... + Exchanged + Exchanging + Exchanging... + Failed + Deposit received + Awaiting deposit + Awaiting deposit... + Refunded + Sending to you + Sending to you... + Sent + Provider-sourced data. Estimated amount subject to change due to market conditions. + Exchange status + Verification required + List of all tokens added to your wallet + Fetching best rates... + Floating rate + By using swap functionality, you agree with provider’s %s + By using swap functionality, you agree with provider’s %1$s and %2$s + More providers are coming soon.\nStay tuned! + Privacy Policy + Provider + Best rate + Available up to %s + Available from %s + Unavailable for this pair + Permission Required + Terms of Use + No tokens found. Please try another request + ID: %s + Transaction ID copied + The following information is optional. You can erase it if you don\'t want to share it. + Tell us what functions you are missing, and we will try to help you. + Please tell us what card do you have + Hi support team, + Please tell us more about your issue. Every small detail can help. + My suggestions + Can\'t scan a card + Feedback + Tangem feedback + Can\'t send a transaction + Order card + Scan card + To change the access code tap the card as shown above and do not remove until the end of the operation + To change the passcode tap the card as shown above and do not remove until the end of the operation + To create the wallet tap the card as shown above and do not remove until the end of the operation + Tap to scan + Tap to sign + Tap the card + You have updated biometrics, scan your card to enter + To begin tracking your crypto assets and transactions, add tokens + Manage tokens + To access all the networks you need to scan the card + Scan your card + Enjoy %1$s service fees on swaps via Changelly from February %2$s-%3$s + Swap with Changelly, %s fees + Tokens + Add + Edit + Coin market cap + Blockchain the cryptocurrency was initially created + Native network + Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk. + Not original or primary blockchain the token is hosted + Non-native networks + Choose networks + Wallet + Couldn’t find this token, you can add it manually + + %1$d of %2$d wallet + %1$d of %2$d wallets + + e.g. BTC I trust, hodl I must + The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. + Upvote + Choose wallet + The wallet doesn\'t support more than one network + You have to set up a single access code to protect all your cards + Protect + You can set up an individual access code on each card later + Personalize + The access code can be restored with a linked card. Don’t keep all cards at one place. + Restore + Choose any word, phrase, or number you want as your access code + Create Access Code + Enter your access code one more time to avoid a mistake + Re-enter your Access Code + Access code must be at least 4 characters long + Entered access code didn\'t match the initial access code + Please repeat the operation. The card will be reset to factory settings. + Activation error + You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process? + The backup process is partly complete. You can\'t exit it now. + The passphrase is an advanced security feature that crypto wallets use. It adds an extra word or phrase of your own choosing to your already existing recovery phrase to unlock a brand-new set of addresses. + Add a backup card + Scan the card #%d + Backup now + Scan the primary card + Continue to my wallet + Finalize the backup + Receive crypto + Scan primary card + Skip for later + How does it work? + Let\'s generate all the keys on your card and create a secure wallet + Create wallet + Create a wallet + Other options + Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. + Generate keys privately + Your card is activated and ready to be used + Success! + In this case, you will need to start from the beginning. + Do you want to exit the activation process? + Getting started + Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet? + Creating a backup + Read more about seed phrase + Write these 12 words down in the order given below and store them in a safe and secret place. + + + Write these %d words down in the order given below and store them in a safe and secret place. + + Your seed phrase + + + %d words + + To import your wallet, enter your seed phrase in the field below + Generate seed phrase + Import wallet + A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk. + Use seed phrase + Invalid seed phrase. Please check the word order. + Invalid seed phrase. Please check your spelling. + Legacy + To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words + So, let’s check + To start the backup process add up to two backup cards. + You can add one more card or finalize the backup process + Prepare the backup card with number %s + Scan the primary card to start the backup process. + Prepare the primary card with number %s + Your wallet card is configured and ready for use. + Max number of cards added. Finalize the backup process. + Activating card + Backup card #%d + No backup cards + One backup card added + Prepare your card + Two backup cards added + To get started, simply top up the wallet with any amount + To get started, simply top up the wallet with more than %1$s %2$s + Buy crypto + Show the wallet\'s address + Activate a wallet + The twinning process is partly complete. You can\'t exit it now. + If the process of creating the wallet gets interrupted in any way, you\'ll have to start over + You can backup your keys up to two other blank Tangem Wallet cards. + Access code can be restored with one of backup cards. + All the backup cards can be used as full-functional with the identical keys. + You will be able to set an access code to protect your wallets. + Backup wallet + Access code restore + Identical cards + Access code + Group + By balance + Organize tokens + Ungroup + Select from the gallery + Settings + You have not given access to your camera + Camera access denied + Enjoying our app? + %1$s (%2$s) on %3$s network + Send only %s to this address. Sending any other currency will result in its irreversible loss. + Participate + Failed to load the information about the referral program. Please try again later. + Failed to load the information about the referral program. Error code: %s. Please try again later. + Upcoming payments + Your friends bought + Less + More + No upcoming payments + + for %d wallet + for %d wallets + + Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that + You + Will get a + when buying a wallet on tangem.com + %s discount + Your friend + Personal code copied! + Your personal code + Buy Tangem Wallet with discount!\n%s + Refer your friends to Tangem + You\'ve accepted + By tapping this button you accept + of the referral program + + %d wallet + %d wallets + + Reset the Card + I understand that after performing this action, I will no longer have access to the current wallet + I realize that I can\'t use this card to recover my access code on the other cards of the current wallet + Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code. + Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet. + Do you have a bank card from another country and a residence permit or registration outside the Russian Federation? + Russian bank cards are not currently accepted + Log into the app and check your balance without scanning the card + Access the app + Allow to use biometrics + Biometrics will be requested instead of the access code for interactions with your wallet + Access code + It looks like you have biometric authentication disabled, it is necessary to save wallets + Enable biometric authorization + Would you like to use biometrics? + Note that making a transaction with your funds will still require your card + Scan Card + Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet. + Get your card ready! + Already included in the entered address + Subtract + Not enough funds to cover the network commission. Subtract the commission from the amount sent? + You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? + Reason: %1$s\nCode: %2$s + The transaction is not completed + Amount + Confirm + %1$s, %2$s + Address + Destination Tag + Enter address + Address is the same as wallet address + Invalid Tag. It won\'t be added to the transaction. + Invalid Memo. It won\'t be added to the transaction. + Tag + Memo + Include fee + Low + Normal + Priority + Check your network connection + Network fee info unreachable + From + Gas limit + Gas Limit is auto-calculated; raise it during network congestion + Gas price + Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed + Max + Maximum amount + Max fee + Maximum fee amount + Numbers only for Destination Tag + Sending amount will be reduced by %1$s to cover the selected commission level. The recipient will get %2$s. + Network fee coverage + Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance + Total exceeds balance + The account will be wiped from the blockchain if a balance goes below the existential deposit. Please ensure that the remaining balance after sending will not be less than %s. + Existential deposit + The commission amount is %s times the recommended amount. Make sure that the custom settings are correct. + Custom fee is high + The fee for transferring the entire balance is higher. To reduce the commission, you can leave %s. + Fee is increased + The included commission exceeds the transfer amount, leading to a negative value + Invalid amount + The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. + Target account is not created. Please change the amount to send. + The amount to send must be at least %s + Reduce by %s + Reduce to %s + Kindly be aware that your transaction may experience delays under specific fee settings + Transaction delays are possible + Due to %1$s limitations only %2$s UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. + Transaction limitation + Optional + Please align your QR code with the square to scan it. Ensure you scan %s network address. + Recent + Recipient + Not a valid address + Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens + Send to + A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds + My wallets + Tap any field to change it + Send %s + You are sending %1$s including a network fee of %2$s + Sending %s + Total + %1$s and %2$s will be sent + ≈ %1$s (inc. fee: %2$s) + %s will be sent + Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while + Invalid address + %1$s (%2$s) + Transaction sent + Store your crypto assets secure while keeping private keys contained in your card + Revolutionary Hardware Wallet + Up to 3 physical cards to one wallet + Ultra Secure Backup + A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card + Thousands of Currencies + Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto. + The Wallet for Everyone + Meet Tangem + Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services + Web 3.0 Compatible + The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. + The amount includes the service provider\'s fee. + Fees + All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. + Approve + You swap + Give Permission + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + Insufficient funds + Sending amount will be reduced to cover the selected fee level + Approve + Current transaction + The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. + Give Permission + Specify the approve limit for the selected token + Amount %s + To continue, grant 1inch smart contracts permission to use your %s + Unlimited + In progress + Swap + You receive + Choose token + not available + Balances hidden + Balances shown + Undo + Generate XPUB + Hide + You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. + Hide %s + Hide token + %1$s token in %%image%% %2$s network + The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. + Unable to hide %s + Exchange this token for another at %1$s service fees from February %2$s-%3$s. + Swap with Changelly, %s fees + Swap now + contract: %s + You don\'t have any transactions yet + Failed to load transaction history.\nClick on reload button to update the information. + Multiple addresses + Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. + Operation + from: %s + to: %s + You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d + You\'ve scanned wrong twin card. Please try another one + This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet. + One wallet. Two cards. + Scan the card #%s + Creating wallet + Scan the #%s twin card + Preparing card + Tangem Twin + This action is irreversible. You will not have access to the old wallet. + Tap the twin card with number %s and do not remove until the end of the operation + Use %s or scan a card to have an access to your wallet + Add new wallet + Are you sure you want to delete this wallet? + An error has occurred, please scan your card to log in + This wallet has already been saved, you can add another one + Wallet name + Rename Wallet + Unlock all + Unlock all with %s + Blockchain is unreachable. Try later + Scan the card + Requesting to sign a message.\n\n%s + Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s + Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s + Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s + Clipboard contain WalletConnect code. Use copied value or scan QR-code + Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s + Can\'t send transaction. Not enough funds. + Failed to establish WalletConnect session. Please, try again later. + Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n + Failed to sign message.\nPlease, try again + Failed to establish WalletConnect session: timeout error. Please, try again later. + Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n + Connection with this Dapp cannot be established due to its technical implementation. + We\'ve encountered unknown error. Error message: %s. If the problem persists — feel free to contact our support + Wrong card selected in Tangem App + Failed to create transaction from Dapp data. Code: %s + We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support + No opened WalletConnect sessions + Ooops. No Sessions. + Failed to pairing WalletConnect session: %1$s + Paste from clipboard + Message for %1$s:\n%2$s + Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s + The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters. + Scan new code + This card can\'t be used to establish WalletConnect session + This network is not supported. Please select another network. + Select network + WalletConnect Sessions + Connect to dApps + WalletConnect + %s Market Price + last 24h + %s network + Address was copied to clipboard + No internet connection + Tangem + Use %s or scan a card to unlock access to your wallet + Please withdraw all funds from this wallet, reset it to factory settings, and create a new one. Access to the current wallet will be lost. + Activation error + Could be better + Like it + Ok, Got it! + Really cool! + Refresh + You are currently in the Demo mode + Demo mode active + The card you scanned is a developer card. Do not use it to create your wallet. + Not for users! + %1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed. + Network requires Existential Deposit + Swap will be available after the %s transaction is complete + You have active transaction + Swap approval is underway and will be completed shortly + Approval in progress + The minimum swapping amount is %1$s. Please ensure that the remaining balance after the swap will not be less than %2$s. + You do not have any %s exchangeable coins in your list + No available tokens to swap + To make a transaction you need to deposit some %1$s %2$s + Unable to cover %s fee + The amount to receive must be at least %s + Service temporarily unavailable + The amount of tokens to be swapped must not exceed %s + The amount to swap must be at least %s + Please change the amount to swap + This card might be a production sample or counterfeit + Authenticity check failed + Only %s signatures are left on this card. You must withdraw all of your funds. + Low signature count + Tokens on different networks can have different addresses. Double-check that your address matches the network when you transfer funds. + + Use your card to get an address for %d network + Use your card to get an addresses for %d networks + + Some addresses are missing + The network is currently unreachable. Please try again later. + Network is unreachable + Top up your wallet + Your wallet hasn\'t been backed up. Carry out this procedure to protect your assets now. + Missing backup + This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. + Card has already signed transactions + Your review keeps us motivated to make Tangem Wallet even better + Enjoying Tangem? + Network rent fee required + %1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee. + Insufficient %1$s to cover network fee + Sending funds will be available once the pending transaction(s) in network %s is complete + Sending funds will be available once the %s transaction is complete + Transaction pending + The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. + Solana Network Alert + Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. + Some networks currently are unreachable. Please try again later. + Some networks are unreachable + This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. + For testing purposes only + Discard + You have an interrupted backup. Do you want to resume? + Yes, resume + Discard + If you will discard the backup now, then you will have to reset the cards to factory settings to start over again + Resume backup + This is an irreversible action + Log in with %s + Scan card + Use %s or scan a card to access the app + Welcome back! diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt index d0d3315f20..fe2a681d08 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Warnings.kt @@ -102,7 +102,7 @@ fun ClickableWarningCard( * >Figma component */ @Composable -fun RefreshableWaringCard( +fun RefreshableWarningCard( title: String, description: String, icon: @Composable (() -> Unit)? = null, @@ -193,7 +193,7 @@ private fun WarningsPreview() { onClick = {}, ) SpacerH32() - RefreshableWaringCard( + RefreshableWarningCard( title = "Exchange rate has expired", description = "To access all the networks, you need to scan the card.", onClick = {}, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt index c9e0526b4e..405217433f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIcon.kt @@ -18,6 +18,7 @@ fun AppBarWithBackButtonAndIcon( onBackClick: () -> Unit, modifier: Modifier = Modifier, text: String? = null, + subtitle: String? = null, @DrawableRes backIconRes: Int? = null, @DrawableRes iconRes: Int? = null, onIconClick: (() -> Unit)? = null, @@ -27,6 +28,7 @@ fun AppBarWithBackButtonAndIcon( onBackClick = onBackClick, modifier = modifier, text = text, + subtitle = subtitle, backIconRes = backIconRes, backgroundColor = backgroundColor, iconContent = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt index 6402068907..229fbe444c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithBackButtonAndIconContent.kt @@ -1,10 +1,10 @@ package com.tangem.core.ui.components.appbar import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -37,6 +37,7 @@ fun AppBarWithBackButtonAndIconContent( onBackClick: () -> Unit, modifier: Modifier = Modifier, text: String? = null, + subtitle: String? = null, @DrawableRes backIconRes: Int? = null, backIconTint: Color = TangemTheme.colors.icon.primary1, backgroundColor: Color = TangemTheme.colors.background.secondary, @@ -46,7 +47,7 @@ fun AppBarWithBackButtonAndIconContent( modifier = modifier .background(color = backgroundColor) .fillMaxWidth() - .padding(all = TangemTheme.dimens.spacing16), + .padding(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), verticalAlignment = Alignment.CenterVertically, ) { @@ -54,6 +55,7 @@ fun AppBarWithBackButtonAndIconContent( painter = painterResource(backIconRes ?: R.drawable.ic_back_24), contentDescription = null, modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing16) .size(size = TangemTheme.dimens.size24) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -61,20 +63,37 @@ fun AppBarWithBackButtonAndIconContent( ) { onBackClick() }, tint = backIconTint, ) - AnimatedContent( - targetState = text, - modifier = Modifier.weight(1f), - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Toolbar title change", + Column( + verticalArrangement = Arrangement.Center, + modifier = Modifier.weight(1f) + .animateContentSize(), ) { - if (!it.isNullOrBlank()) { + AnimatedVisibility( + visible = !text.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), + label = "Toolbar title change", + ) { Text( - text = it, + text = text.orEmpty(), color = TangemTheme.colors.text.primary1, maxLines = 1, style = TangemTheme.typography.subtitle1, ) } + AnimatedVisibility( + visible = !subtitle.isNullOrBlank(), + enter = fadeIn(), + exit = fadeOut(), + label = "Toolbar subtitle change", + ) { + Text( + text = subtitle.orEmpty(), + color = TangemTheme.colors.text.secondary, + maxLines = 1, + style = TangemTheme.typography.caption2, + ) + } } iconContent() } @@ -111,6 +130,7 @@ private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() { TangemTheme(isDark = true) { AppBarWithBackButtonAndIconContent( text = "Title", + subtitle = "Subtitle", onBackClick = {}, iconContent = { Row { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt index db255b710f..5a555dfcf7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonsDefaults.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemTheme -internal object TangemButtonsDefaults { +object TangemButtonsDefaults { val elevation: ButtonElevation @Composable diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt index 4bf1b72d84..fee185d0ab 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/AmountTextField.kt @@ -1,7 +1,9 @@ package com.tangem.core.ui.components.fields +import androidx.annotation.FloatRange import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -12,9 +14,15 @@ import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Alignment.Companion.TopStart import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.ParagraphIntrinsics import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -56,51 +64,77 @@ fun AmountTextField( keyboardType = KeyboardType.Number, ), keyboardActions: KeyboardActions = KeyboardActions.Default, + isAutoResize: Boolean = false, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, ) { val decimalFormat = rememberDecimalFormat() - + val visualTransformation = remember { AmountVisualTransformation(decimals, symbol, decimalFormat) } val placeholderTextAlign = if (placeholderAlignment == TopCenter) { TextAlign.Center } else { TextAlign.Start } - SimpleTextField( - value = value, - onValueChange = { newText -> - if (decimalFormat.isValidSymbols(newText)) { - val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) - onValueChange(trimmed) + BoxWithConstraints(modifier = modifier) { + var fontSize = textStyle.fontSize + if (isAutoResize) { + val calculateIntrinsics = @Composable { + val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text + ParagraphIntrinsics( + text = transformedText, + style = textStyle.copy(fontSize = fontSize), + density = LocalDensity.current, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + ) } - }, - modifier = modifier - .background(TangemTheme.colors.background.action), - textStyle = textStyle, - color = color, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = true, - visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), - decorationBox = { innerTextField -> - Box { - if (value.isBlank() && showPlaceholder) { - val placeholder = if (symbol != null) { - decimalFormat.defaultFormat().plus(" $symbol") - } else { - decimalFormat.defaultFormat() - } - Text( - text = placeholder, - style = textStyle, - color = TangemTheme.colors.text.disabled, - textAlign = placeholderTextAlign, - modifier = Modifier - .align(placeholderAlignment), - ) + var intrinsics = calculateIntrinsics() + with(LocalDensity.current) { + while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) { + fontSize *= reduceFactor + intrinsics = calculateIntrinsics() } - innerTextField() } - }, - ) + } + SimpleTextField( + value = value, + onValueChange = { newText -> + if (decimalFormat.isValidSymbols(newText)) { + val trimmed = decimalFormat.getValidatedNumberWithFixedDecimals(newText, decimals) + onValueChange(trimmed) + } + }, + textStyle = textStyle.copy( + fontSize = fontSize, + textDirection = TextDirection.ContentOrLtr, + textAlign = TextAlign.Center, + ), + color = color, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = true, + visualTransformation = AmountVisualTransformation(decimals, symbol, decimalFormat), + modifier = Modifier.background(TangemTheme.colors.background.action), + decorationBox = { innerTextField -> + Box { + if (value.isBlank() && showPlaceholder) { + val placeholder = if (symbol != null) { + decimalFormat.defaultFormat().plus(" $symbol") + } else { + decimalFormat.defaultFormat() + } + Text( + text = placeholder, + style = textStyle.copy(textDirection = TextDirection.ContentOrLtr), + color = TangemTheme.colors.text.disabled, + textAlign = placeholderTextAlign, + modifier = Modifier.align(placeholderAlignment), + ) + } + innerTextField() + } + }, + ) + } } private fun DecimalFormat.isValidSymbols(text: String): Boolean { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt index 4a3fdbff20..6d31ebeaef 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SimpleTextField.kt @@ -67,9 +67,7 @@ fun SimpleTextField( LaunchedEffect(key1 = isSelectionChanged) { if (isSelectionChanged) { - textFieldValueState = textFieldValue.copy( - selection = getValueRange(value), - ) + textFieldValueState = textFieldValue } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index 1d7d0fab95..88ed54192f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -36,7 +36,12 @@ import com.tangem.core.ui.utils.DEFAULT_ANIMATION_DURATION fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current - + val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() + val color = if (isPasteEnabled) { + TangemTheme.colors.button.primary + } else { + TangemTheme.colors.button.secondary + } AnimatedVisibility( visible = isPasteButtonVisible, label = "Paste Button Visibility Animation", @@ -50,7 +55,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi color = TangemTheme.colors.text.primary2, modifier = Modifier .background( - color = TangemTheme.colors.button.primary, + color = color, shape = TangemTheme.shapes.roundedCornersXMedium, ) .padding( @@ -60,6 +65,7 @@ fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifi .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(radius = TangemTheme.dimens.radius8), + enabled = isPasteEnabled, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) onClick( diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index f49147879e..d7b3fd7f1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -57,6 +57,11 @@ fun getActiveIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "zkSyncEra", "zkSyncEra/test" -> R.drawable.img_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 + "manta", "manta/test" -> R.drawable.img_manta_22 + "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.img_polygon_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -115,6 +120,11 @@ fun getActiveIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.img_aurora_22 "areon", "areon/test" -> R.drawable.img_areon_22 "pls", "pls/test" -> R.drawable.img_pls_22 + "zksync", "zksync/test" -> R.drawable.img_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 + "manta-network", "manta-network/test" -> R.drawable.img_manta_22 + "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -170,6 +180,11 @@ fun getActiveIconResByCoinId(coinId: String): Int { "aurora-near" -> R.drawable.img_aurora_22 "areon" -> R.drawable.img_areon_22 "pls" -> R.drawable.img_pls_22 + "zksync-ethereum" -> R.drawable.img_zksync_22 + "moonbeam" -> R.drawable.img_moonbeam_22 + "manta-network-ethereum" -> R.drawable.img_manta_22 + "polygon-zkevm-ethereum" -> R.drawable.img_polygon_22 + "radiant" -> R.drawable.img_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -190,7 +205,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "XRP" -> R.drawable.ic_xrp_22 "XLM", "XLM/test" -> R.drawable.ic_stellar_16 "AVALANCHE", "AVALANCHE/test" -> R.drawable.ic_avalanche_22 - "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_16 + "POLYGON", "POLYGON/test" -> R.drawable.ic_polygon_22 "SOLANA", "SOLANA/test" -> R.drawable.ic_solana_16 "FTM", "FTM/test" -> R.drawable.ic_fantom_22 "BSC", "BSC/test", "BINANCE", "BINANCE/test" -> R.drawable.ic_bsc_16 @@ -228,6 +243,11 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "zkSyncEra", "zkSyncEra/test" -> R.drawable.ic_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 + "manta", "manta/test" -> R.drawable.ic_manta_22 + "polygonZkEVM", "polygonZkEVM/test" -> R.drawable.ic_polygon_22 + "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } } @@ -249,7 +269,7 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "tezos" -> R.drawable.ic_tezos_16 "xrp", "ripple" -> R.drawable.ic_xrp_22 "stellar", "stellar/test" -> R.drawable.ic_stellar_16 - "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_16 + "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_22 "solana", "solana/test" -> R.drawable.ic_solana_16 "fantom", "fantom/test" -> R.drawable.ic_fantom_22 "dogecoin" -> R.drawable.ic_dogecoin_16 @@ -286,6 +306,11 @@ fun getGreyedOutIconResByNetworkId(networkId: String): Int { "aurora", "aurora/test" -> R.drawable.ic_aurora_22 "areon", "areon/test" -> R.drawable.ic_areon_22 "pls", "pls/test" -> R.drawable.ic_pls_22 + "zksync", "zksync/test" -> R.drawable.ic_zksync_22 + "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 + "manta-network", "manta-network/test" -> R.drawable.ic_manta_22 + "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22 + "radiant" -> R.drawable.ic_radiant_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt index 3a0c083a41..e58126708e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Fragment.kt @@ -1,14 +1,43 @@ package com.tangem.core.ui.extensions +import android.R +import android.content.Context +import android.graphics.Color.* import android.view.WindowManager import androidx.annotation.ColorRes import androidx.core.content.ContextCompat +import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment +import kotlin.math.sqrt +@Deprecated("Use only in legacy fragments") fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) { with(requireActivity().window) { clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) statusBarColor = ContextCompat.getColor(requireContext(), colorResId) + val view = view ?: return + val windowInsetsController = WindowCompat.getInsetsController(this, view) + windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId) } +} + +// TODO replace by android.graphics.luminance() after bump min API to 24 +@Suppress("MagicNumber") +fun luminance(context: Context, @ColorRes colorRes: Int): Boolean { + val color = context.resources.getColor(colorRes, null) + if (R.color.transparent == color) return true + var rtnValue = false + val rgb = intArrayOf(red(color), green(color), blue(color)) + val brightness = sqrt( + rgb[0] * rgb[0] * .241 + + rgb[1] * rgb[1] * .691 + + rgb[2] * rgb[2] * .068, + ).toInt() + + // color is light + if (brightness >= 200) { + rtnValue = true + } + return rtnValue } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt index f5cc3cb971..be264409bc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -29,4 +29,6 @@ fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { } } return bmp -} \ No newline at end of file +} + +fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index ce8b600e55..0ea40469e2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -59,11 +59,7 @@ object DateTimeFormatters { DateTimeFormat.forPattern("dd.MM.yyyy HH:mm") } - fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String { - return formatter.print(time) - } - - fun formatDate(formatter: DateTimeFormatter = dateFormatter, date: DateTime): String { + fun formatDate(date: DateTime, formatter: DateTimeFormatter = dateFormatter): String { return formatter.print(date) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt index b0237e897a..8d596fc41f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateUtils.kt @@ -29,5 +29,5 @@ fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateForm * Returns formatted time according to [formatter]. */ fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String { - return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault())) + return DateTimeFormatters.formatDate(date = DateTime(this, DateTimeZone.getDefault()), formatter = formatter) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_manta_22.xml b/core/ui/src/main/res/drawable/ic_manta_22.xml new file mode 100644 index 0000000000..0cdfc8df09 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_manta_22.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_moonbeam_22.xml b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml new file mode 100644 index 0000000000..71e2200421 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_moonbeam_22.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_polygon_16.xml b/core/ui/src/main/res/drawable/ic_polygon_16.xml deleted file mode 100644 index ddc6ff8adf..0000000000 --- a/core/ui/src/main/res/drawable/ic_polygon_16.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/core/ui/src/main/res/drawable/ic_polygon_22.xml b/core/ui/src/main/res/drawable/ic_polygon_22.xml new file mode 100644 index 0000000000..73d1249583 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_polygon_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_radiant_22.xml b/core/ui/src/main/res/drawable/ic_radiant_22.xml new file mode 100644 index 0000000000..53d3f81f81 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_radiant_22.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_zksync_22.xml b/core/ui/src/main/res/drawable/ic_zksync_22.xml new file mode 100644 index 0000000000..16964a64d6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_zksync_22.xml @@ -0,0 +1,14 @@ + + + + diff --git a/core/ui/src/main/res/drawable/img_manta_22.xml b/core/ui/src/main/res/drawable/img_manta_22.xml new file mode 100644 index 0000000000..93200c11c3 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_manta_22.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_moonbeam_22.xml b/core/ui/src/main/res/drawable/img_moonbeam_22.xml new file mode 100644 index 0000000000..7c010ab7e4 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_moonbeam_22.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_polygon_22.xml b/core/ui/src/main/res/drawable/img_polygon_22.xml index efb2fff8c1..455e3cbe53 100644 --- a/core/ui/src/main/res/drawable/img_polygon_22.xml +++ b/core/ui/src/main/res/drawable/img_polygon_22.xml @@ -1,13 +1,29 @@ + + + android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" + android:fillColor="#F5F0FD"/> + android:pathData="M7.435,8.666C7.694,8.514 8.03,8.514 8.314,8.666L10.328,9.833L11.697,10.594L13.712,11.761C13.97,11.913 14.306,11.913 14.59,11.761L16.192,10.848C16.45,10.696 16.631,10.417 16.631,10.112V8.311C16.631,8.006 16.476,7.727 16.192,7.575L14.616,6.687C14.358,6.535 14.022,6.535 13.738,6.687L12.162,7.575C11.904,7.727 11.723,8.006 11.723,8.311V9.478L10.354,8.691V7.524C10.354,7.22 10.509,6.941 10.793,6.789L13.712,5.114C13.97,4.962 14.306,4.962 14.59,5.114L17.561,6.789C17.845,6.941 18,7.22 18,7.524V10.899C18,11.203 17.845,11.482 17.561,11.634L14.59,13.309C14.332,13.461 13.996,13.461 13.712,13.309L11.697,12.167L10.328,11.38L8.314,10.239C8.055,10.087 7.72,10.087 7.435,10.239L5.86,11.127C5.601,11.279 5.421,11.558 5.421,11.863V13.664C5.421,13.968 5.576,14.247 5.86,14.4L7.435,15.313C7.694,15.465 8.03,15.465 8.314,15.313L9.889,14.425C10.148,14.273 10.328,13.994 10.328,13.689V12.522L11.697,13.309V14.476C11.697,14.78 11.542,15.059 11.258,15.211L8.288,16.886C8.03,17.038 7.694,17.038 7.41,16.886L4.439,15.211C4.181,15.059 4,14.78 4,14.476V11.101C4,10.797 4.155,10.518 4.439,10.366L7.435,8.666Z"> + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_radiant_22.xml b/core/ui/src/main/res/drawable/img_radiant_22.xml new file mode 100644 index 0000000000..8b5ea3383f --- /dev/null +++ b/core/ui/src/main/res/drawable/img_radiant_22.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_zksync_22.xml b/core/ui/src/main/res/drawable/img_zksync_22.xml new file mode 100644 index 0000000000..ffb720dae9 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_zksync_22.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/core/utils/src/main/AndroidManifest.xml b/core/utils/src/main/AndroidManifest.xml deleted file mode 100644 index 02f6bf24ad..0000000000 --- a/core/utils/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index d37487c40e..d8427a907f 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -43,7 +43,7 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), maxFirmwareVersion = FirmwareVersion(major = 6, minor = 33), batchIdFilter = CardFilter.Companion.ItemFilter.Deny( - items = setOf("0027", "0030", "0031", "0035", "DA88"), + items = setOf("0027", "0030", "0031", "0035"), ), ), ) diff --git a/data/feedback/.gitignore b/data/feedback/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/feedback/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts new file mode 100644 index 0000000000..306603b7c2 --- /dev/null +++ b/data/feedback/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.feedback" +} + +dependencies { + + // region AndroidX libraries + implementation(deps.androidx.datastore) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Tangem libraries + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + // endregion + + // Other libraries + implementation(deps.timber) + // endregion + + // region Core modules + implementation(projects.core.datasource) + // endregion + + implementation(projects.core.utils) + + implementation(projects.domain.feedback) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt new file mode 100644 index 0000000000..9f6c1aa883 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -0,0 +1,109 @@ +package com.tangem.data.feedback + +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Build +import com.tangem.data.feedback.converters.BlockchainInfoConverter +import com.tangem.data.feedback.converters.CardInfoConverter +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.models.* +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.wallets.models.UserWallet +import timber.log.Timber +import java.io.File +import java.io.FileWriter +import java.io.StringWriter + +/** + * Implementation of [FeedbackRepository] + * + * @property appPreferencesStore application preferences store + * @property userWalletsStore user wallets store + * @property walletManagersStore wallet managers store + * @property context context for getting app version + * +[REDACTED_AUTHOR] + */ +internal class DefaultFeedbackRepository( + private val appPreferencesStore: AppPreferencesStore, + private val userWalletsStore: UserWalletsStore, + private val walletManagersStore: WalletManagersStore, + private val context: Context, +) : FeedbackRepository { + + override suspend fun getUserWalletsInfo(): UserWalletsInfo { + return UserWalletsInfo( + selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, + totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"), + ) + } + + override suspend fun getCardInfo(): CardInfo { + return CardInfoConverter.convert(value = getSelectedUserWallet()) + } + + override suspend fun getBlockchainInfoList(): List { + return walletManagersStore + .getAllSync(userWalletId = getSelectedUserWallet().walletId) + .map(BlockchainInfoConverter::convert) + } + + override fun getPhoneInfo(): PhoneInfo { + return PhoneInfo( + phoneModel = Build.MODEL, + osVersion = Build.VERSION.SDK_INT.toString(), + appVersion = getAppVersion(), + ) + } + + override suspend fun getAppLogs(): List { + return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) + .map { AppLogModel(timestamp = it.key.toLong(), message = it.value) } + .sortedBy(AppLogModel::timestamp) + } + + override suspend fun createLogFile(logs: String): File? { + return try { + val file = File(context.filesDir, LOGS_FILE) + file.delete() + file.createNewFile() + + val stringWriter = StringWriter() + + stringWriter.append(logs) + + val fileWriter = FileWriter(file) + fileWriter.write(stringWriter.toString()) + fileWriter.close() + + file + } catch (ex: Exception) { + Timber.e(ex, "Logs file isn't created") + null + } + } + + private fun getAppVersion(): String { + return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) } + .fold( + onSuccess = PackageInfo::versionName, + onFailure = { + Timber.e(it) + "x.y.z" + }, + ) + } + + private fun getSelectedUserWallet(): UserWallet { + return userWalletsStore.selectedUserWalletOrNull + ?: error("UserWallet is not selected") + } + + private companion object { + const val LOGS_FILE = "logs.txt" + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt new file mode 100644 index 0000000000..de7f9b6d04 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt @@ -0,0 +1,43 @@ +package com.tangem.data.feedback.converters + +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.address.Address +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo +import com.tangem.utils.converter.Converter +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses + +/** + * Converter from [WalletManager] to [BlockchainInfo] + * +[REDACTED_AUTHOR] + */ +internal object BlockchainInfoConverter : Converter { + + override fun convert(value: WalletManager): BlockchainInfo { + return BlockchainInfo( + blockchain = value.wallet.blockchain.fullName, + derivationPath = value.wallet.publicKey.derivationPath?.rawPath ?: "", + outputsCount = value.outputsCount?.toString(), + host = value.currentHost, + addresses = value.wallet.mapAddresses(Address::value), + explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, + tokens = value.cardTokens.map { token -> + BlockchainInfo.TokenInfo(id = token.id, name = token.name, contractAddress = token.contractAddress) + }, + ) + } + + private inline fun Wallet.mapAddresses(map: (Address) -> String): BlockchainAddresses { + return if (addresses.size == 1) { + BlockchainAddresses.Single(value = map(addresses.first())) + } else { + BlockchainAddresses.Multiple( + addresses.map { + AddressInfo(type = it.type.name, value = map(it)) + }, + ) + } + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt new file mode 100644 index 0000000000..7d16454467 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.feedback.converters + +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.utils.converter.Converter + +/** + * Converter from [UserWallet] to [CardInfo] + * +[REDACTED_AUTHOR] + */ +internal object CardInfoConverter : Converter { + + override fun convert(value: UserWallet): CardInfo { + return with(value.scanResponse) { + CardInfo( + cardId = card.cardId, + firmwareVersion = card.firmwareVersion.stringValue, + cardBlockchain = walletData?.blockchain, + signedHashesList = card.wallets.map { + CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) + }, + isImported = value.isImported, + isStart2Coin = value.scanResponse.card.isStart2Coin, + ) + } + } +} \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt new file mode 100644 index 0000000000..5a00938959 --- /dev/null +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt @@ -0,0 +1,30 @@ +package com.tangem.data.feedback.di + +import android.content.Context +import com.tangem.data.feedback.DefaultFeedbackRepository +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.feedback.repository.FeedbackRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object FeedbackRepositoryModule { + + @Provides + @Singleton + fun provideFeedbackRepository( + appPreferencesStore: AppPreferencesStore, + userWalletsStore: UserWalletsStore, + walletManagersStore: WalletManagersStore, + @ApplicationContext context: Context, + ): FeedbackRepository { + return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context) + } +} \ No newline at end of file diff --git a/data/qr-scanning/.gitignore b/data/qr-scanning/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/qr-scanning/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts new file mode 100644 index 0000000000..1c801589b7 --- /dev/null +++ b/data/qr-scanning/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.qrscanning" +} + +dependencies { + + /** Domain */ + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + + implementation(projects.core.ui) + + /** SdK */ + implementation(deps.tangem.blockchain) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) +} \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt new file mode 100644 index 0000000000..efe697f667 --- /dev/null +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/di/QrScanningDataModule.kt @@ -0,0 +1,20 @@ +package com.tangem.data.qrscanning.di + +import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object QrScanningDataModule { + + @Provides + @Singleton + fun provideQrScanningEventsRepository(): QrScanningEventsRepository { + return DefaultQrScanningEventsRepository() + } +} \ No newline at end of file diff --git a/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt new file mode 100644 index 0000000000..87729e8bc4 --- /dev/null +++ b/data/qr-scanning/src/main/java/com/tangem/data/qrscanning/repository/DefaultQrScanningEventsRepository.kt @@ -0,0 +1,145 @@ +package com.tangem.data.qrscanning.repository + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import java.math.BigDecimal +import java.net.URLDecoder + +internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { + + private data class QrScanningEvent(val type: SourceType, val qrCode: String) + + private val scannedEvents = MutableSharedFlow() + + override suspend fun emitResult(type: SourceType, qrCode: String) { + scannedEvents.emit(QrScanningEvent(type, qrCode)) + } + + override fun subscribeToScanningResults(type: SourceType) = scannedEvents + .filter { it.type == type } + .map { it.qrCode } + + override fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult { + val withoutSchema = stripSchema(qrCode, cryptoCurrency) + + // A poor man's ERC-681 parser: we want to extract only the destination address, and we don't care + // about other parts of the ERC-681 payload string like `chain_id` and/or `function_name`. + // + // We're extracting the destination address by parsing the given string until we meet + // any of the possible string delimiters (@ ? /). + val address = withoutSchema.takeWhile { char -> + char != CHAIN_DELIMITER && char != FUNCTION_DELIMITER && char != PARAM_DELIMITER + } + + val result = QrResult(address = address) + + extractParameters(withoutSchema) + .forEach { + when (it.key) { + Parameter.Amount -> { + // According to BIP-0021, the value is specified in decimals. No conversion needed + result.amount = it.value.toBigDecimalOrNull() + } + Parameter.Message, + Parameter.Memo, + -> { + result.memo = URLDecoder.decode(it.value, "UTF-8") + } + Parameter.Address -> { + // Overrides destination address for token transfers (ERC-681) + if (cryptoCurrency is CryptoCurrency.Token) { + // `address` parameter is used only if the contract address, encoded in the QR, + // matches the contract address of the token. + // Otherwise, the scanned string is likely malformed, and we stop the entire parsing routine + if (cryptoCurrency.contractAddress.equals(address, ignoreCase = true)) { + result.address = it.value + } else { + return QrResult() + } + } + } + Parameter.Value, + Parameter.Uint256, + -> { + // Extra convert parses scientific notation to decimal + // This is necessary to be able comparing BigDecimal values + result.amount = it.value.toBigDecimalOrNull() + ?.toPlainString()?.toBigDecimalOrNull() + ?.divide(BigDecimal.TEN.pow(cryptoCurrency.decimals)) + } + } + } + + return result + } + + private fun stripSchema(raw: String, currency: CryptoCurrency): String { + val qrSchemas = Blockchain.fromId(currency.network.id.value).getShareScheme() + + // The most specific (i.e. the most lengthy) prefixes always come first + qrSchemas + .sortedByDescending { it.length } + .forEach { schema -> + val stripped = raw.split(schema) + + if (stripped.size > 1) return stripped.last() + } + + return raw + } + + private fun extractParameters(from: String): Map { + val parametersBlock = from.substringAfter(PARAM_DELIMITER) + if (parametersBlock.isBlank()) return emptyMap() + + val paramList = parametersBlock.split(PARAMS_DELIMITER) + .mapNotNull { param -> + val parameterWithValue = param.split(PARAM_VALUE_DELIMITER) + if (parameterWithValue.size == 2) { + val name = Parameter.getParam(parameterWithValue.first()) + val value = parameterWithValue.last() + + if (name != null) { + name to value + } else { + null + } + } else { + null + } + }.associate { it } + + return paramList + } + + private enum class Parameter { + Amount, + Message, + Memo, + Address, + Value, + Uint256, + ; + + companion object { + fun getParam(name: String): Parameter? { + return Parameter.entries.firstOrNull { it.name.equals(name, ignoreCase = true) } + } + } + } + + private companion object { + // See https://eips.ethereum.org/EIPS/eip-681 for details. + const val CHAIN_DELIMITER = '@' // ERC-681 [ "@" chain_id ] + const val FUNCTION_DELIMITER = '/' // ERC-681 [ "/" function_name ] + const val PARAM_DELIMITER = '?' // BIP-021, ERC-681 + const val PARAMS_DELIMITER = '&' + const val PARAM_VALUE_DELIMITER = '=' + } +} \ No newline at end of file diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt new file mode 100644 index 0000000000..ad1f6244a7 --- /dev/null +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -0,0 +1,253 @@ +package com.tangem.data.qrscanning + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import java.math.BigDecimal + +internal class DefaultQrScanningEventsRepositoryTest { + + private val repository = DefaultQrScanningEventsRepository() + + private val cryptoCurrencyId = mockk() + private val network = mockk() + private val cryptoCurrency = CryptoCurrency.Coin( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 18, + iconUrl = null, + isCustom = false, + ) + private val tokenCryptoCurrency = CryptoCurrency.Token( + id = cryptoCurrencyId, + network = network, + name = "blockchain", + symbol = "symbol", + decimals = 7, + iconUrl = null, + isCustom = false, + contractAddress = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7", + ) + + private val garbage = "some_garbage=" + + private val schema1 = "bitcoin" + private val schema2 = "ethereum" + + private val address1 = "bc1pw83rs5s75na2g7ec8yqgekr3ae209ye7ck2ftakjnh8tv3xzw8ls6wgt62" + private val address2 = "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address3 = "pay-0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb" + private val address4 = "0x89205A3A3b2A69De6Dbf7f01ED13B2108B2c43e7" + + private val function = "/transfer" + + private val someParam = "someParam" + private val someParamValue = "someParamValue" + + private val addressParam = "address" + private val addressParamValue = "0xc00f86ab93cd0bd3a60213583d0fe35aaa1ace23" + + private val amountParam = "amount" + private val someAmountParamValue = "amount" + private val amountParamValue = "123.123" + + private val valueParam = "value" + private val someValueParamValue = "amount" + private val valueParamValue2 = "1.88e10" + private val valueParamValue3 = "1.68E11" + private val valueParamValue4 = "23000000000" + + private val memoParam = "memo" + private val memoParamValue = "a%20random%20memo" + private val memoParamValueUtf8 = "a random memo" + + private val messageParam = "message" + private val messageParamValue = "some%20message" + private val messageParamValueUft8 = "some message" + private val messageParamValue2 = "message" + + @Test + fun testBip021() { + every { network.id } returns Network.ID(Blockchain.Bitcoin.id) + positiveCase( + "$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema1:$address1", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$someAmountParamValue", + QrResult(address = address1), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue)), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue&$memoParam=$memoParamValue", + QrResult(address = address1, amount = BigDecimal(amountParamValue), memo = memoParamValueUtf8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue", + QrResult(address = address1, memo = messageParamValueUft8), + cryptoCurrency, + ) + positiveCase( + "$address1?$someParam=$someParamValue&$messageParam=$messageParamValue2", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + negativeCase( + "$address1?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address1, memo = messageParamValue2), + cryptoCurrency, + ) + } + + @Test + fun testErc681Coin() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2$function?$addressParam=$addressParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$someAmountParamValue", + QrResult(address = address2), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("0.0000000188")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("0.000000168")), + cryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("0.000000023")), + cryptoCurrency, + ) + } + + @Test + fun testErc681Token() { + every { network.id } returns Network.ID(Blockchain.Ethereum.id) + positiveCase( + address2, + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address3", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$garbage$schema2:$address2", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$schema2:$address4$function?$addressParam=$addressParamValue", + QrResult(address = addressParamValue), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$someValueParamValue", + QrResult(address = address2), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue2", + QrResult(address = address2, amount = BigDecimal("1880")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue3", + QrResult(address = address2, amount = BigDecimal("16800")), + tokenCryptoCurrency, + ) + positiveCase( + "$address2?$someParam=$someParamValue&$valueParam=$valueParamValue4", + QrResult(address = address2, amount = BigDecimal("2300")), + tokenCryptoCurrency, + ) + negativeCase( + "$address2?$someParam=$someParamValue&$amountParam=$amountParamValue", + QrResult(address = address2, amount = BigDecimal("123.123"), memo = memoParamValueUtf8), + tokenCryptoCurrency, + ) + } + + private fun positiveCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual.address).isEqualTo(expected.address) + Truth.assertThat(actual.amount).isEqualTo(expected.amount) + Truth.assertThat(actual.memo).isEqualTo(expected.memo) + } + + private fun negativeCase(input: String, expected: QrResult, cryptoCurrency: CryptoCurrency) { + val actual = repository.parseQrCode(input, cryptoCurrency) + Truth.assertThat(actual).isNotEqualTo(expected) + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 4e7ff03252..277d4fc31f 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -3,10 +3,8 @@ package com.tangem.data.settings import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.settings.models.AppLogsModel import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -36,11 +34,6 @@ internal class DefaultSettingsRepository( ) } - override suspend fun getAppLogs(): List { - return appPreferencesStore.getObjectMap(key = PreferencesKeys.APP_LOGS_KEY) - .map { AppLogsModel(timestamp = it.key.toLong(), message = it.value) } - } - override suspend fun updateAppLogs(message: String) { val newLogs = DateTime.now().millis.toString() to message @@ -51,22 +44,21 @@ internal class DefaultSettingsRepository( } } - override suspend fun deleteDeprecatedLogs() { - val threeDaysAgo = getThreeDaysAgoTimestamp() - + override suspend fun deleteDeprecatedLogs(maxSize: Int) { appPreferencesStore.editData { preferences -> val savedLogs = preferences.getObjectMap(PreferencesKeys.APP_LOGS_KEY) + var sum = 0 preferences.setObjectMap( key = PreferencesKeys.APP_LOGS_KEY, - value = savedLogs.filterKeys { it.toLong() > threeDaysAgo }, + value = savedLogs.entries + .sortedBy(Map.Entry::key) + .takeLastWhile { + sum += it.value.length + sum < maxSize + } + .associate { it.key to it.value }, ) } } - - private fun getThreeDaysAgoTimestamp(): Long = DateTime.now().minusDays(THREE_DAYS).millis - - private companion object { - const val THREE_DAYS = 3 - } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt index f17fe671b9..006ca60bb9 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksCompatibilityRepository.kt @@ -1,8 +1,10 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.EllipticCurve import com.tangem.data.tokens.utils.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.extensions.* import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.derivationStyleProvider @@ -71,6 +73,15 @@ internal class DefaultNetworksCompatibilityRepository( } } + override suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + val scanResponse = getWalletOrThrow(userWalletId).scanResponse + val config = CardConfig.createConfig(scanResponse.card) + val blockchain = Blockchain.fromNetworkId(networkId) ?: return false + + return config.primaryCurve(blockchain) == EllipticCurve.Ed25519Slip0010 && + scanResponse.cardTypesResolver.isWallet2() + } + override fun areTokensSupportedByNetwork(networkId: String): Boolean { return Blockchain.fromNetworkId(networkId)?.canHandleTokens() ?: false } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt index e6d573b353..6b2779299c 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesUnsupportedCurrenciesIdAdapter.kt @@ -51,6 +51,9 @@ internal class QuotesUnsupportedCurrenciesIdAdapter { private val UNSUPPORTED_IDS_WITH_REPLACEMENTS = mapOf( "optimistic-ethereum" to "ethereum", "arbitrum-one" to "ethereum", + "zksync-ethereum" to "ethereum", + "manta-network-ethereum" to "ethereum", + "polygon-zkevm-ethereum" to "ethereum", "aurora-ethereum" to "ethereum", ) } diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index 2709c26dfc..d534e238ec 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -2,10 +2,8 @@ package com.tangem.data.txhistory.repository.paging import androidx.paging.PagingSource import androidx.paging.PagingState -import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper @@ -82,17 +80,9 @@ internal class TxHistoryPagingSource( } private suspend fun PaginationWrapper.addRecentTransactions(): PaginationWrapper { - val blockchain = Blockchain.fromNetworkId(sourceParams.currency.network.backendId) - - if (blockchain == null) { - Timber.e("Blockchain not found") - return this - } - val recentItems = walletManagersFacade.getRecentTransactions( userWalletId = sourceParams.userWalletId, - blockchain = blockchain, - derivationPath = sourceParams.currency.network.derivationPath.value, + currency = sourceParams.currency, ) .filterUnconfirmedTransaction() .filterIfTxAlreadyAdded(apiItems = items) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index d41cf5f9c7..ac55319dfe 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation(projects.domain.legacy) /** Project - Libs */ - implementation(projects.libs.visa) + debugImplementation(projects.libs.visa) /** Libs - Other */ implementation(deps.kotlin.coroutines) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt similarity index 95% rename from data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index d14a201a2b..89752a8b76 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -58,7 +58,7 @@ internal class DefaultVisaRepository( override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { val address = makeAddress(userWalletId) - // val address = "0x143fe062a538176aa0bf162f13d390208f90898f" // for testing + // val address = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" // for testing fetchVisaCurrencyIfExpired(address, isRefresh) @@ -97,7 +97,7 @@ internal class DefaultVisaRepository( ): Flow> { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing + // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -125,7 +125,7 @@ internal class DefaultVisaRepository( return withContext(dispatchers.io) { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet).toHexString() - // val cardPubKey = "03DEF02B1FECC8BD3CFD52CE93235194479E1DE931EF0F55DC194967E7CCC3D12C" // for testing + // val cardPubKey = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" // for testing val transaction = fetchedHistoryItems.value[cardPubKey]?.firstOrNull { it.transactionId.toString() == txId } diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt new file mode 100644 index 0000000000..a3022a444a --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -0,0 +1,53 @@ +package com.tangem.data.visa.di + +import com.squareup.moshi.Moshi +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.visa.BuildConfig +import com.tangem.data.visa.DefaultVisaRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApiBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ImplementedVisaDataModule { + + @Provides + @Singleton + @ImplementedVisaRepository + fun provideVisaRepository( + @NetworkMoshi moshi: Moshi, + tangemTechApi: TangemTechApi, + cacheRegistry: CacheRegistry, + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): VisaRepository { + val contractInfoProvider = VisaContractInfoProvider.Builder( + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + dispatchers = dispatchers, + ).build() + val visaApi = VisaApiBuilder( + useDevApi = true, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + ).build() + + return DefaultVisaRepository( + contractInfoProvider, + tangemTechApi, + visaApi, + cacheRegistry, + userWalletsStore, + dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaConfig.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt similarity index 92% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 92bb35d307..8f3e95d73a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -17,6 +17,7 @@ internal class VisaCurrencyFactory { } else { balancesAndLimits.newLimits } + val remainingOtpLimit = getRemainingOtp(currentLimit, now) return VisaCurrency( symbol = VisaConfig.TOKEN_SYMBOL, @@ -35,8 +36,8 @@ internal class VisaCurrencyFactory { ) }, limits = VisaCurrency.Limits( - remainingOtp = getRemainingOtp(currentLimit, now), - remainingNoOtp = getRemainingNoOtp(currentLimit, now), + remainingOtp = remainingOtpLimit, + remainingNoOtp = minOf(remainingOtpLimit, getRemainingNoOtp(currentLimit, now)), singleTransaction = currentLimit.singleTransactionLimit, expirationDate = getLimitsExpirationDate(currentLimit, now), ), diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxDetailsFactory.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryItemFactory.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt similarity index 100% rename from data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt rename to data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt new file mode 100644 index 0000000000..e965d71e93 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DummyVisaRepository.kt @@ -0,0 +1,28 @@ +package com.tangem.data.visa + +import androidx.paging.PagingData +import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.visa.model.VisaTxDetails +import com.tangem.domain.visa.model.VisaTxHistoryItem +import com.tangem.domain.visa.repository.VisaRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +internal class DummyVisaRepository : VisaRepository { + + override suspend fun getVisaCurrency(userWalletId: UserWalletId, isRefresh: Boolean): VisaCurrency { + TODO("Not implemented for this build type") + } + + override suspend fun getTxHistory( + userWalletId: UserWalletId, + pageSize: Int, + isRefresh: Boolean, + ): Flow> { + TODO("Not implemented for this build type") + } + + override suspend fun getTxDetails(userWalletId: UserWalletId, txId: String): VisaTxDetails { + TODO("Not implemented for this build type") + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt new file mode 100644 index 0000000000..074af6e72d --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.data.visa.di + +import javax.inject.Qualifier + +@Qualifier +internal annotation class ImplementedVisaRepository \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt new file mode 100644 index 0000000000..96de38914e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/ImplementedVisaRepositoryModule.kt @@ -0,0 +1,16 @@ +package com.tangem.data.visa.di + +import com.tangem.domain.visa.repository.VisaRepository +import dagger.BindsOptionalOf +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface ImplementedVisaRepositoryModule { + + @BindsOptionalOf + @ImplementedVisaRepository + fun bindImplementedVisaRepository(): VisaRepository +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index a0227da794..a8587868f9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,21 +1,14 @@ package com.tangem.data.visa.di -import com.squareup.moshi.Moshi -import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.BuildConfig -import com.tangem.data.visa.DefaultVisaRepository -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.data.visa.DummyVisaRepository import com.tangem.domain.visa.repository.VisaRepository -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApiBuilder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import java.util.Optional import javax.inject.Singleton +import kotlin.jvm.optionals.getOrNull @Module @InstallIn(SingletonComponent::class) @@ -24,29 +17,8 @@ internal object VisaDataModule { @Provides @Singleton fun provideVisaRepository( - @NetworkMoshi moshi: Moshi, - tangemTechApi: TangemTechApi, - cacheRegistry: CacheRegistry, - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, + @ImplementedVisaRepository implementedVisaRepository: Optional, ): VisaRepository { - val contractInfoProvider = VisaContractInfoProvider.Builder( - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - dispatchers = dispatchers, - ).build() - val visaApi = VisaApiBuilder( - useDevApi = true, - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - moshi = moshi, - ).build() - - return DefaultVisaRepository( - contractInfoProvider, - tangemTechApi, - visaApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) + return implementedVisaRepository.getOrNull() ?: DummyVisaRepository() } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt new file mode 100644 index 0000000000..4f107a0948 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.card + +import arrow.core.Either +import com.tangem.crypto.NetworkType +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Derivates an exteneded public key (xpub) based on blockchain hardened derivation + */ +class GetExtendedPublicKeyForCurrencyUseCase( + private val derivationsRepository: DerivationsRepository, +) { + suspend operator fun invoke( + userWalletId: UserWalletId, + derivation: Network.DerivationPath, + ): Either { + return Either.catch { + val derivationPath = requireNotNull(derivation.value?.let { DerivationPath(it) }) { + error("Derivation is null") + } + val hardenedNodes = derivationPath.nodes.filter { it.isHardened } + val hardenedDerivation = DerivationPath(hardenedNodes) + derivationsRepository.deriveExtendedPublicKey(userWalletId, hardenedDerivation) + ?.serialize(NetworkType.Mainnet).orEmpty() + } + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt index 3f153ee689..667e29f062 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt @@ -1,14 +1,20 @@ package com.tangem.domain.card -// TODO: May be add new error types sealed class ScanCardException : Exception() { - object UserCancelled : ScanCardException() - object WrongAccessCode : ScanCardException() + data object UserCancelled : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = UserCancelled + } + + data object WrongAccessCode : ScanCardException() { + @Suppress("UnusedPrivateMember") + private fun readResolve(): Any = WrongAccessCode + } open class ChainException : ScanCardException() - data class UnknownException(override val cause: Exception) : ScanCardException() + data class UnknownException(override val cause: Throwable) : ScanCardException() data class WrongCardId(val cardId: String) : ScanCardException() } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index 961d9acd07..4dfc1c9668 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -16,7 +16,6 @@ interface ScanCardProcessor { analyticsEvent: AnalyticsEvent? = null, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, disclaimerWillShow: () -> Unit = {}, onFailure: suspend (error: TangemError) -> Unit = {}, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 69dbc7a617..04abfb1fce 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -1,9 +1,7 @@ package com.tangem.domain.card import arrow.core.Either -import arrow.core.EitherNel -import arrow.core.flatMap -import com.tangem.TangemSdk +import arrow.core.raise.either import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.core.chain.Chain @@ -11,12 +9,7 @@ import com.tangem.domain.core.chain.ChainProcessor import com.tangem.domain.models.scan.ScanResponse /** - * Use case responsible for scanning a card and returning a [ScanResponse] object - * - * @property scanCardRepository a repository object implementing [ScanCardRepository] interface - * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID - * - * @constructor create a new instance of [ScanCardUseCase] with the given dependencies + * Use case responsible for scanning a card and returning a [ScanResponse] object. */ class ScanCardUseCase( private val scanCardRepository: ScanCardRepository, @@ -24,37 +17,41 @@ class ScanCardUseCase( ) { /** A [ChainProcessor] object to launch the after-scan chains */ - private val scanChainProcessor by lazy { - ChainProcessor() - } + private val scanChainProcessor = ChainProcessor() /** - * Scan a card and return a [ScanResponse] object. + * Scan a card. * - * @param cardId an optional card ID to scan. If null, can scan any card present - * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed - * @param afterScanChains a list of chains that should be executed after a successful card scan - * operation. Defaults to an empty array + * @param cardId An optional card ID to scan. If `null`, can scan any card present + * @param allowRequestAccessCodeFromStorage Whether to prompt the user for an access code if needed + * @param afterScanChains A list of chains that should be executed after a card scan + * operation. Defaults to an empty array. * - * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse] + * @return [Either] object with either a [ScanCardException] or a [ScanResponse] object. */ suspend operator fun invoke( cardId: String? = null, allowRequestAccessCodeFromStorage: Boolean = false, - afterScanChains: List> = emptyList(), - ): Either { + afterScanChains: List> = emptyList(), + ): Either = either { cardSdkConfigRepository.resetCardIdDisplayFormat() - scanChainProcessor.addChains(afterScanChains) + scanChainProcessor.setChains(afterScanChains) - return scanCardRepository.scanCard( - cardId = cardId, - allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, - ) - .onRight { scanResponse -> - cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) - } - .flatMap { response -> - scanChainProcessor.launchChains(initial = response) + val maybeScanResponse = scanCard(cardId, allowRequestAccessCodeFromStorage) + val scanResponse = scanChainProcessor.launchChains(maybeScanResponse).bind() + + cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) + + scanResponse + } + + private suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + ): Either { + return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) } + .mapLeft { e -> + e as? ScanCardException ?: ScanCardException.UnknownException(e) } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt index cd1af0a789..209b68817a 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/DerivationsRepository.kt @@ -1,5 +1,7 @@ package com.tangem.domain.card.repository +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @@ -7,4 +9,7 @@ interface DerivationsRepository { @Throws suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) + + @Throws + suspend fun deriveExtendedPublicKey(userWalletId: UserWalletId, derivation: DerivationPath): ExtendedPublicKey? } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt index 2de00090cf..90f2016383 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt @@ -1,6 +1,5 @@ package com.tangem.domain.card.repository -import arrow.core.Either import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse @@ -15,10 +14,8 @@ interface ScanCardRepository { * @param cardId an optional card ID to scan. If null, the repository should scan any card present. * @param allowRequestAccessCodeFromStorage whether the access code can be requested from * internal storage during the scan process - * @return an [Either] that contains a [ScanCardException] in case of an error or a [ScanResponse]. + * @return a [ScanResponse] object with the result of the scan. + * @throws [ScanCardException] if the scan process fails. */ - suspend fun scanCard( - cardId: String?, - allowRequestAccessCodeFromStorage: Boolean, - ): Either + suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt deleted file mode 100644 index a7e0d5bc38..0000000000 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.domain.core.chain - -import arrow.core.Either - -/** - * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. - * @param E the type of error - * @param R the type of result - */ -interface Chain { - - /** - * Invokes the chain with the previous chain result as input and returns an [Either] result. - * @param previousChainResult the previous chain result as an [R] - * @return the result of the chain processing as an [Either] - */ - suspend operator fun invoke(previousChainResult: R): Either -} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt index c1cac09eb3..4e95318b6c 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt @@ -1,7 +1,6 @@ package com.tangem.domain.core.chain import arrow.core.Either -import arrow.core.raise.either // TODO: Create Ior implementation // TODO: Create accumulate implementation @@ -10,7 +9,7 @@ import arrow.core.raise.either * @param E the type of error * @param R the type of result */ -class ChainProcessor { +class ChainProcessor { /** * The list of chains to be executed in order. @@ -18,18 +17,20 @@ class ChainProcessor { private val chains: MutableList> = mutableListOf() /** - * Adds chains to the existing list of chains to be executed. - * @param chains the chains to be added to the list + * Sets the list of chains to be executed. + * @param chains the chains to be set */ - fun addChains(chains: List>) { + fun setChains(chains: List>) { + this.chains.clear() this.chains.addAll(chains) } - suspend fun launchChains(initial: R): Either { - return either { - chains.fold(initial) { prevChainResult, chain -> - chain.invoke(prevChainResult).bind() - } + /** + * Launches the chains with the [initial] value. + * */ + suspend fun launchChains(initial: Either): Either { + return chains.fold(initial) { prevChainResult, chain -> + chain.launch(prevChainResult) } } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt new file mode 100644 index 0000000000..02b5da3924 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chains.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.core.chain + +import arrow.core.Either +import arrow.core.flatMap + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * @param E the type of error + * @param R the type of result + */ +interface Chain { + + /** + * Invokes the chain with the previous chain result as input and returns an [Either] result. + * + * @param previousChainResult the previous chain result as an [Either] + * @return the result of the chain processing as an [Either] + */ + suspend fun launch(previousChainResult: Either): Either +} + +/** + * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors. + * + * This chain will be launched only if the previous chains in the [ChainProcessor] have + * returned a right side of [Either]. + * + * @param E the type of error + * @param R the type of result + */ +abstract class ResultChain : Chain { + + final override suspend fun launch(previousChainResult: Either): Either { + return previousChainResult.flatMap { launch(it) } + } + + /** + * Invokes the chain with the previous chain result as input and returns a result. + * + * @param previousChainResult the previous chain result + * @return the result of the chain processing as an [Either] + */ + abstract suspend fun launch(previousChainResult: R): Either +} \ No newline at end of file diff --git a/domain/feedback/.gitignore b/domain/feedback/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/feedback/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts new file mode 100644 index 0000000000..3e7859b754 --- /dev/null +++ b/domain/feedback/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.feedback" +} + +dependencies { + implementation(deps.arrow.core) + implementation(deps.jodatime) + + implementation(projects.core.res) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt new file mode 100644 index 0000000000..5350902e33 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -0,0 +1,111 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.BlockchainInfo +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.PhoneInfo +import com.tangem.domain.feedback.models.UserWalletsInfo +import com.tangem.domain.feedback.utils.breakLine +import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainAddresses + +internal class FeedbackDataBuilder { + + private val builder = StringBuilder() + + fun addUserWalletsInfo(userWalletsInfo: UserWalletsInfo) { + builder.appendKeyValue("User Wallet ID", userWalletsInfo.selectedUserWalletId) + builder.appendKeyValue("Total saved wallets", userWalletsInfo.totalUserWallets.toString()) + } + + fun addCardInfo(cardInfo: CardInfo) { + builder.appendKeyValue("Card ID", cardInfo.cardId) + builder.appendKeyValue("Firmware version", cardInfo.firmwareVersion) + builder.appendKeyValue("Imported wallet", if (cardInfo.isImported) "yes" else "no") + builder.appendKeyValue("Card Blockchain", cardInfo.cardBlockchain) + builder.appendSignedHashes(cardInfo.signedHashesList) + } + + fun addBlockchainInfoList(blockchainInfoList: List) { + blockchainInfoList.forEachBlockchain { isLastIndex -> + builder.appendKeyValue("Blockchain", blockchain) + builder.appendKeyValue("Derivation path", derivationPath) + + // enable later + // if (walletInfo.blockchain == Blockchain.Bitcoin) { + // builder.appendKeyValue("XPUB", infoHolder.extendedPublicKey) + // } + + builder.appendKeyValue("Outputs count", outputsCount) + + if (tokens.isNotEmpty()) { + builder.append("Tokens:") + builder.breakLine() + tokens.forEach { token -> + builder.appendKeyValue("ID", token.id ?: "[custom token]") + builder.appendKeyValue("Name", token.name) + builder.appendKeyValue("Contract address", token.contractAddress) + } + } + + builder.appendKeyValue("Host", host) + + builder.appendAddresses( + key = "Wallet address${addresses.isMultiple(suffix = "es")}", + addresses = addresses, + ) + + builder.appendAddresses( + key = "Explorer link${explorerLinks.isMultiple(suffix = "s")}", + addresses = explorerLinks, + ) + + if (!isLastIndex) builder.appendDelimiter() + } + } + + fun addPhoneInfo(phoneInfo: PhoneInfo) { + builder.appendKeyValue("Phone model", phoneInfo.phoneModel) + builder.appendKeyValue("OS version", phoneInfo.osVersion) + builder.appendKeyValue("App version", phoneInfo.appVersion) + } + + fun addDelimiter(): StringBuilder = builder.appendDelimiter() + + fun build(): String = builder.trimEnd().toString() + + private fun StringBuilder.appendKeyValue(key: String, value: String?) { + if (value.isNullOrBlank()) return + + val keyValuePrefix = if (key.isBlank()) "" else "$key: " + append("$keyValuePrefix$value\n") + } + + private fun StringBuilder.appendSignedHashes(signedHashesList: List) { + signedHashesList.forEach { + appendKeyValue("Signed hashes [${it.curve}]", it.total) + } + } + + private fun StringBuilder.appendAddresses(key: String, addresses: BlockchainAddresses) { + appendKeyValue( + key = key, + value = when (addresses) { + is BlockchainInfo.Addresses.Multiple -> { + addresses.values.joinToString(separator = "\n", prefix = "\n") { + "${it.type} — ${it.value}" + } + } + is BlockchainInfo.Addresses.Single -> addresses.value + }, + ) + } + + private fun BlockchainAddresses.isMultiple(suffix: String): String { + return if (this is BlockchainAddresses.Multiple) suffix else "" + } + + private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n") + + private inline fun List.forEachBlockchain(action: BlockchainInfo.(Boolean) -> Unit) { + return forEachIndexed { index, info -> info.action(index == lastIndex) } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt new file mode 100644 index 0000000000..6ab491c734 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetSupportFeedbackEmailUseCase.kt @@ -0,0 +1,113 @@ +package com.tangem.domain.feedback + +import android.content.res.Resources +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.models.SupportFeedbackEmail +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.feedback.utils.skipLine +import org.joda.time.DateTime +import org.joda.time.format.DateTimeFormatterBuilder +import java.util.Locale + +/** + * Get email with feedback for support + * + * @property feedbackRepository feedback repository + * @property resources resources for getting strings + * +[REDACTED_AUTHOR] + */ +class GetSupportFeedbackEmailUseCase( + private val feedbackRepository: FeedbackRepository, + private val resources: Resources, +) { + + // 00.00 00:00:00.000 + private val dateFormatter = DateTimeFormatterBuilder() + .appendDayOfMonth(2) + .appendLiteral('.') + .appendMonthOfYear(2) + .appendLiteral(' ') + .appendHourOfDay(2) + .appendLiteral(':') + .appendMinuteOfHour(2) + .appendLiteral(':') + .appendSecondOfMinute(2) + .appendLiteral('.') + .appendMillisOfSecond(3) + .toFormatter() + .withLocale(Locale.getDefault()) + + suspend operator fun invoke(): SupportFeedbackEmail { + val cardInfo = feedbackRepository.getCardInfo() + + return SupportFeedbackEmail( + address = getEmail(isStart2Coin = cardInfo.isStart2Coin), + subject = getSubject(isStart2Coin = cardInfo.isStart2Coin), + message = createMessage(cardInfo), + file = feedbackRepository.createLogFile(logs = getLogs()), + ) + } + + private fun getEmail(isStart2Coin: Boolean): String { + return if (isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + } + + private fun getSubject(isStart2Coin: Boolean): String { + return resources.getString( + if (isStart2Coin) { + R.string.feedback_subject_support + } else { + R.string.feedback_subject_support_tangem + }, + ) + } + + private suspend fun createMessage(cardInfo: CardInfo): String { + return StringBuilder().apply { + append(resources.getString(R.string.feedback_preface_support)) + skipLine() + append(resources.getString(R.string.feedback_data_collection_message)) + skipLine() + append( + FeedbackDataBuilder().apply { + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addDelimiter() + addCardInfo(cardInfo) + addDelimiter() + addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) + addDelimiter() + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) + }.build(), + ) + }.toString() + } + + private suspend fun getLogs(): String { + val builder = StringBuilder() + + var sum = 0 + val appLogs = feedbackRepository.getAppLogs() + for (i in appLogs.lastIndex downTo 0) { + val log = appLogs[i] + val date = dateFormatter.print(DateTime(log.timestamp)) + + val formattedLog = "$date: ${log.message}\n" + + sum += formattedLog.length + if (sum < GMAIL_MAX_FILE_SIZE) { + builder.insert(0, formattedLog) + } else { + break + } + } + + return builder.toString() + } + + private companion object { + const val START2COIN_SUPPORT_EMAIL = "cardsupport@start2coin.com" + const val TANGEM_SUPPORT_EMAIL = "support@tangem.com" + const val GMAIL_MAX_FILE_SIZE = 24_900_000 // ≈ 25 MB + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt new file mode 100644 index 0000000000..6688c29a1f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/AppLogModel.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.feedback.models + +data class AppLogModel(val timestamp: Long, val message: String) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt new file mode 100644 index 0000000000..da0a0df2a5 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/BlockchainInfo.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.feedback.models + +data class BlockchainInfo( + val blockchain: String, + val derivationPath: String, + val outputsCount: String?, + val host: String, + val addresses: Addresses, + val explorerLinks: Addresses, + val tokens: List, +) { + + sealed class Addresses { + + data class Single(val value: String) : Addresses() + + data class Multiple(val values: List) : Addresses() { + + data class AddressInfo(val type: String, val value: String) + } + } + + data class TokenInfo( + val id: String?, + val name: String, + val contractAddress: String, + ) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt new file mode 100644 index 0000000000..7ae84ac608 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.feedback.models + +data class CardInfo( + val cardId: String, + val firmwareVersion: String, + val cardBlockchain: String?, + val signedHashesList: List, + val isImported: Boolean, + val isStart2Coin: Boolean, +) { + + data class SignedHashes(val curve: String, val total: String?) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt new file mode 100644 index 0000000000..578d9e2e00 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/PhoneInfo.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.feedback.models + +data class PhoneInfo( + val phoneModel: String, + val osVersion: String, + val appVersion: String, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt new file mode 100644 index 0000000000..35faf18f6f --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/SupportFeedbackEmail.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.feedback.models + +import java.io.File + +data class SupportFeedbackEmail( + val address: String, + val subject: String, + val message: String, + val file: File? = null, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt new file mode 100644 index 0000000000..0438e80ae4 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/UserWalletsInfo.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.feedback.models + +data class UserWalletsInfo( + val selectedUserWalletId: String, + val totalUserWallets: Int, +) \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt new file mode 100644 index 0000000000..58281067c1 --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.feedback.repository + +import com.tangem.domain.feedback.models.* +import java.io.File + +interface FeedbackRepository { + + suspend fun getUserWalletsInfo(): UserWalletsInfo + + suspend fun getCardInfo(): CardInfo + + suspend fun getBlockchainInfoList(): List + + fun getPhoneInfo(): PhoneInfo + + suspend fun getAppLogs(): List + + suspend fun createLogFile(logs: String): File? +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt new file mode 100644 index 0000000000..a2e3cb68ee --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/StringBuilderExt.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.feedback.utils + +fun StringBuilder.breakLine(count: Int = 1): StringBuilder = append("\n".repeat(count)) + +fun StringBuilder.skipLine(): StringBuilder = breakLine(count = 2) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index ce72fcde55..2f27ff5330 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -93,6 +93,17 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "areon-network/test" -> Blockchain.AreonTestnet "pulsechain" -> Blockchain.PulseChain "pulsechain/test" -> Blockchain.PulseChainTestnet + "zksync" -> Blockchain.ZkSyncEra + "zksync/test" -> Blockchain.ZkSyncEraTestnet + "moonbeam" -> Blockchain.Moonbeam + "moonbeam/test" -> Blockchain.MoonbeamTestnet + "manta-network" -> Blockchain.Manta + "manta-network/test" -> Blockchain.MantaTestnet + "polygon-zkevm" -> Blockchain.PolygonZkEVM + "polygon-zkevm/test" -> Blockchain.PolygonZkEVMTestnet + "nexa" -> Blockchain.Nexa // FIXME + "nexa/test" -> Blockchain.NexaTestnet // FIXME + "radiant" -> Blockchain.Radiant else -> null } } @@ -187,6 +198,17 @@ fun Blockchain.toNetworkId(): String { Blockchain.AreonTestnet -> "areon-network/test" Blockchain.PulseChain -> "pulsechain" Blockchain.PulseChainTestnet -> "pulsechain/test" + Blockchain.ZkSyncEra -> "zksync" + Blockchain.ZkSyncEraTestnet -> "zksync/test" + Blockchain.Moonbeam -> "moonbeam" + Blockchain.MoonbeamTestnet -> "moonbeam/test" + Blockchain.Manta -> "manta-network" + Blockchain.MantaTestnet -> "manta-network/test" + Blockchain.PolygonZkEVM -> "polygon-zkevm" + Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm/test" + Blockchain.Nexa -> "nexa" // FIXME + Blockchain.NexaTestnet -> "nexa/test" // FIXME + Blockchain.Radiant -> "radiant" } } @@ -249,6 +271,12 @@ fun Blockchain.toCoinId(): String { Blockchain.Aurora, Blockchain.AuroraTestnet -> "aurora-ethereum" Blockchain.Areon, Blockchain.AreonTestnet -> "areon-network" Blockchain.PulseChain, Blockchain.PulseChainTestnet -> "pulsechain" + Blockchain.ZkSyncEra, Blockchain.ZkSyncEraTestnet -> "zksync-ethereum" + Blockchain.Moonbeam, Blockchain.MoonbeamTestnet -> "moonbeam" + Blockchain.Manta, Blockchain.MantaTestnet -> "manta-network-ethereum" + Blockchain.PolygonZkEVM, Blockchain.PolygonZkEVMTestnet -> "polygon-zkevm-ethereum" + Blockchain.Nexa, Blockchain.NexaTestnet -> "nexa" // FIXME + Blockchain.Radiant -> "radiant" } } @@ -276,4 +304,7 @@ private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Playa3ull, + Blockchain.Nexa, + Blockchain.NexaTestnet, + Blockchain.Radiant, ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 182653ef00..325fb2efa7 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -11,6 +11,7 @@ class UserWalletBuilder( private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), ) { private var backupCardsIds: Set = emptySet() + private var hasBackupError: Boolean = false private val CardDTO.isBackupNotAllowed: Boolean get() = !this.settings.isBackupAllowed @@ -41,6 +42,13 @@ class UserWalletBuilder( } } + /** + * Sets if UserWallet has any backup errors (wrong curves etc). Use in onboarding + */ + fun hasBackupError(hasBackupError: Boolean) = this.apply { + this.hasBackupError = hasBackupError + } + suspend fun build(): UserWallet? { return with(scanResponse) { UserWalletIdBuilder.scanResponse(scanResponse) @@ -53,6 +61,7 @@ class UserWalletBuilder( cardsInWallet = backupCardsIds.plus(card.cardId), scanResponse = this, isMultiCurrency = cardTypesResolver.isMultiwalletAllowed(), + hasBackupError = hasBackupError, ) } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index bb0cc5a3ad..276758748b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -11,6 +11,7 @@ import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.common.address.EstimationFeeAddressFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.common.pagination.Page import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -25,6 +26,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -53,15 +55,18 @@ class DefaultWalletManagersFacade( configManager: ConfigManager, blockchainDataStorage: BlockchainDataStorage, accountCreator: AccountCreator, + blockchainSDKLogger: BlockchainSDKLogger, + feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, ) : WalletManagersFacade { private val demoConfig by lazy { DemoConfig() } private val resultFactory by lazy { UpdateWalletManagerResultFactory() } private val walletManagerFactory by lazy { WalletManagerFactory( - configManager, - accountCreator, - blockchainDataStorage, + configManager = configManager, + accountCreator = accountCreator, + blockchainDataStorage = blockchainDataStorage, + blockchainSDKLogger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) blockchainSDKLogger else null, ) } private val sdkTokenConverter by lazy { SdkTokenConverter() } @@ -515,27 +520,28 @@ class DefaultWalletManagersFacade( override suspend fun getRecentTransactions( userWalletId: UserWalletId, - blockchain: Blockchain, - derivationPath: String?, + currency: CryptoCurrency, ): List { - val walletManager = getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - val feePaidCurrency = blockchain.feePaidCurrency() + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) if (walletManager == null) { - Timber.e("Unable to get a wallet manager for blockchain: $blockchain") + Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") return emptyList() } val transactionDataConverter = TransactionDataToTxHistoryItemConverter( walletAddresses = SdkAddressToAddressConverter.convertList(walletManager.wallet.addresses).toSet(), - feePaidCurrency = feePaidCurrency, + feePaidCurrency = walletManager.wallet.blockchain.feePaidCurrency(), ) - return walletManager.wallet.recentTransactions.mapNotNull(transactionDataConverter::convert) + return walletManager.wallet.recentTransactions + .filter { transaction -> + when (currency) { + is CryptoCurrency.Coin -> transaction.amount.type is AmountType.Coin + is CryptoCurrency.Token -> transaction.contractAddress == currency.contractAddress + } + } + .mapNotNull(transactionDataConverter::convert) } override suspend fun tokenBalance( diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 543ef9006e..4636317765 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -227,12 +227,8 @@ interface WalletManagersFacade { network: Network, ): SimpleResult - /** Get recent transactions of [userWalletId] for [blockchain] with [derivationPath] */ - suspend fun getRecentTransactions( - userWalletId: UserWalletId, - blockchain: Blockchain, - derivationPath: String?, - ): List + /** Get recent transactions of [userWalletId] for [currency] */ + suspend fun getRecentTransactions(userWalletId: UserWalletId, currency: CryptoCurrency): List @Suppress("LongParameterList") suspend fun tokenBalance( diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index e0377cf6a3..89a63b05f6 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationParams import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.datastorage.BlockchainDataStorage +import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager import com.tangem.domain.common.DerivationStyleProvider @@ -18,10 +19,16 @@ internal class WalletManagerFactory( configManager: ConfigManager, accountCreator: AccountCreator, blockchainDataStorage: BlockchainDataStorage, + blockchainSDKLogger: BlockchainSDKLogger? = null, ) { private val sdkWalletManagerFactory by lazy { - BlockchainWalletManagerFactory(configManager.config.blockchainSdkConfig, accountCreator, blockchainDataStorage) + BlockchainWalletManagerFactory( + config = configManager.config.blockchainSdkConfig, + accountCreator = accountCreator, + blockchainDataStorage = blockchainDataStorage, + loggers = listOfNotNull(blockchainSDKLogger), + ) } fun createWalletManager( diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 8863fe8a03..ff2b5be1d0 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -303,7 +303,7 @@ data class CardDTO( data class Active(val cardCount: Int) : BackupStatus() - object NoBackup : BackupStatus() + data object NoBackup : BackupStatus() val isActive: Boolean get() = this is Active diff --git a/domain/qr-scanning/.gitignore b/domain/qr-scanning/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/qr-scanning/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/qr-scanning/build.gradle.kts b/domain/qr-scanning/build.gradle.kts new file mode 100644 index 0000000000..f1ea5e63ec --- /dev/null +++ b/domain/qr-scanning/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.qrscanning" +} + +dependencies { + + /** Domain */ + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) +} \ No newline at end of file diff --git a/domain/qr-scanning/models/.gitignore b/domain/qr-scanning/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/qr-scanning/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/qr-scanning/models/build.gradle.kts b/domain/qr-scanning/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/qr-scanning/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt new file mode 100644 index 0000000000..a97a4a7161 --- /dev/null +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/QrResult.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.qrscanning.models + +import java.math.BigDecimal + +data class QrResult( + var address: String = "", + var amount: BigDecimal? = null, + var memo: String? = null, +) \ No newline at end of file diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt similarity index 56% rename from features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt rename to domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt index 96640d74e6..87be83fd0c 100644 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/SourceType.kt +++ b/domain/qr-scanning/models/src/main/java/com/tangem/domain/qrscanning/models/SourceType.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.qrscanning +package com.tangem.domain.qrscanning.models enum class SourceType { WALLET_CONNECT, diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt new file mode 100644 index 0000000000..15ca99b951 --- /dev/null +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/repository/QrScanningEventsRepository.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.qrscanning.repository + +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.tokens.model.CryptoCurrency +import kotlinx.coroutines.flow.Flow + +interface QrScanningEventsRepository { + + suspend fun emitResult(type: SourceType, qrCode: String) + + fun subscribeToScanningResults(type: SourceType): Flow + + fun parseQrCode(qrCode: String, cryptoCurrency: CryptoCurrency): QrResult +} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt similarity index 65% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt index cc7f7bc6c2..ebe0e5980f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/EmitQrScannedEventUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/EmitQrScannedEventUseCase.kt @@ -1,12 +1,12 @@ -package com.tangem.feature.qrscanning.usecase +package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository -internal class EmitQrScannedEventUseCase( +class EmitQrScannedEventUseCase( private val repository: QrScanningEventsRepository, ) { suspend operator fun invoke(type: SourceType, qrCode: String): Either { diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt similarity index 69% rename from features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt rename to domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt index e3ef9f9dc1..3c42400420 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/usecase/ListenToQrScanningUseCase.kt +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ListenToQrScanningUseCase.kt @@ -1,12 +1,11 @@ -package com.tangem.feature.qrscanning.usecase +package com.tangem.domain.qrscanning.usecases import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository import kotlinx.coroutines.flow.Flow -import java.lang.Exception class ListenToQrScanningUseCase( val repository: QrScanningEventsRepository, diff --git a/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt new file mode 100644 index 0000000000..0849c8bf1c --- /dev/null +++ b/domain/qr-scanning/src/main/java/com/tangem/domain/qrscanning/usecases/ParseQrCodeUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.qrscanning.usecases + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.qrscanning.models.QrResult +import com.tangem.domain.qrscanning.repository.QrScanningEventsRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +class ParseQrCodeUseCase( + val repository: QrScanningEventsRepository, +) { + operator fun invoke(qrCode: String, cryptoCurrency: CryptoCurrency): Either { + return try { + repository.parseQrCode(qrCode, cryptoCurrency).right() + } catch (e: Exception) { + e.left() + } + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt new file mode 100644 index 0000000000..627d3f1124 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DeleteDeprecatedLogsUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class DeleteDeprecatedLogsUseCase( + private val settingsRepository: SettingsRepository, +) { + + suspend operator fun invoke() { + settingsRepository.deleteDeprecatedLogs(maxSize = MAX_LOGS_SIZE) + } + + private companion object { + const val MAX_LOGS_SIZE = 24_900_000 // ≈ 25 MB + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt b/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt deleted file mode 100644 index 746974930f..0000000000 --- a/domain/settings/src/main/java/com/tangem/domain/settings/models/AppLogsModel.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.domain.settings.models - -data class AppLogsModel(val timestamp: Long, val message: String) \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt index f686561f3e..061ee78557 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.settings.repositories -import com.tangem.domain.settings.models.AppLogsModel - interface SettingsRepository { suspend fun shouldShowSaveUserWalletScreen(): Boolean @@ -10,12 +8,9 @@ interface SettingsRepository { suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) - @Throws - suspend fun getAppLogs(): List - @Throws suspend fun updateAppLogs(message: String) @Throws - suspend fun deleteDeprecatedLogs() + suspend fun deleteDeprecatedLogs(maxSize: Int) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt new file mode 100644 index 0000000000..db8dad152a --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RequiresHardenedDerivationOnlyUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.repository.NetworksCompatibilityRepository +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Ed25519Slip0010 requires only hardened derivations on Wallet 2 + */ +class RequiresHardenedDerivationOnlyUseCase( + private val repository: NetworksCompatibilityRepository, +) { + + suspend operator fun invoke(networkId: String, userWalletId: UserWalletId): Either { + return either { + catch( + block = { + repository.requiresHardenedDerivationOnly(networkId, userWalletId) + }, + catch = { throwable -> raise(throwable) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 1657a3ea05..092ad6d3ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -41,8 +41,9 @@ sealed class TradeCryptoAction : Action { data class Swap(val cryptoCurrency: CryptoCurrency) : TradeCryptoAction() data class TransactionInfo( - val amount: String, - val destinationAddress: String, val transactionId: String, + val destinationAddress: String, + val amount: String, + val tag: String? = null, ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt index 1d0f6946b1..a5e2083e05 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksCompatibilityRepository.kt @@ -17,5 +17,7 @@ interface NetworksCompatibilityRepository { @Throws(IllegalArgumentException::class) suspend fun getSupportedNetworks(userWalletId: UserWalletId): List + suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean + fun areTokensSupportedByNetwork(networkId: String): Boolean } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index 710566e799..4904076698 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -20,6 +20,7 @@ data class UserWallet( val artworkUrl: String, val cardsInWallet: Set, val isMultiCurrency: Boolean, + val hasBackupError: Boolean, val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO] ) { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt index 7fe3b1dc8c..6ad1ac8e68 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt @@ -2,6 +2,7 @@ package com.tangem.managetokens.presentation.addcustomtoken.state.factory import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.tokens.error.AddCustomTokenError import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet @@ -15,6 +16,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentSet +@Suppress("LargeClass") internal class AddCustomTokenStateFactory( private val currentStateProvider: Provider, private val clickIntents: AddCustomTokenClickIntents, @@ -72,8 +74,11 @@ internal class AddCustomTokenStateFactory( ) } - private fun getListOfDerivations(supportedNetworks: List): List { - return supportedNetworks.mapNotNull { network -> + private fun getListOfDerivations( + networksListToGenerateDerivations: List, + filterOnlyHardenedDerivations: Boolean = false, + ): List { + return networksListToGenerateDerivations.mapNotNull { network -> network.derivationPath.value?.let { rawPath -> Derivation( networkName = network.name, @@ -82,6 +87,12 @@ internal class AddCustomTokenStateFactory( networkId = network.backendId, onDerivationSelected = clickIntents::onDerivationSelected, ) + }.takeIf { derivation -> + if (filterOnlyHardenedDerivations) { + derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false + } else { + true + } } } } @@ -218,41 +229,52 @@ internal class AddCustomTokenStateFactory( ) } - fun updateStateOnNetworkSelected(networkItemState: NetworkItemState, supportsTokens: Boolean): AddCustomTokenState { + fun updateStateOnNetworkSelected( + networkItemState: NetworkItemState, + supportsTokens: Boolean, + networks: List, + requiresHardenedDerivationOnly: Boolean, + ): AddCustomTokenState { val uiState = currentStateProvider() val tokenData = if (supportsTokens) { - uiState.tokenData - ?: CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) + uiState.tokenData ?: CustomTokenData( + contractAddressTextField = TextFieldState.Editable( + value = "", + isEnabled = true, + onValueChange = clickIntents::onContractAddressChange, + onFocusExit = clickIntents::onContractAddressFocusExit, + ), + nameTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onTokenNameChange, + onFocusExit = clickIntents::onTokenNameFocusExit, + ), + symbolTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onSymbolChange, + onFocusExit = clickIntents::onSymbolFocusExit, + ), + decimalsTextField = TextFieldState.Editable( + value = "", + isEnabled = false, + onValueChange = clickIntents::onDecimalsChange, + onFocusExit = clickIntents::onDecimalsFocusExit, + ), + ) } else { null } + + val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly) + val chooseDerivationState = createChooseDerivationState(derivations) + return uiState.copy( - chooseNetworkState = uiState.chooseNetworkState.copy(selectedNetwork = networkItemState), + chooseNetworkState = uiState.chooseNetworkState.copy( + selectedNetwork = networkItemState, + ), + chooseDerivationState = chooseDerivationState, tokenData = tokenData, addTokenButton = uiState.addTokenButton.copy(isEnabled = true), ) @@ -291,6 +313,47 @@ internal class AddCustomTokenStateFactory( ) } + fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState { + val uiState = currentStateProvider() + val path = createDerivationPathOrNull(input) + val isWrongDerivationForWallet2 = isWrongDerivationForWallet2( + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly, + derivationPath = path, + ) + val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( + confirmButtonEnabled = path != null && !isWrongDerivationForWallet2, + derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2, + ) + return uiState.copy( + chooseDerivationState = uiState.chooseDerivationState?.copy( + enterCustomDerivationState = enterDerivationState, + ), + ) + } + + private fun isWrongDerivationForWallet2( + requiresHardenedDerivationOnly: Boolean, + derivationPath: DerivationPath?, + ): Boolean { + return if (requiresHardenedDerivationOnly) { + !allNodesHardened(derivationPath) + } else { + false + } + } + + private fun allNodesHardened(derivationPath: DerivationPath?): Boolean { + return derivationPath?.nodes?.all { it.isHardened } ?: false + } + + private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { + return try { + DerivationPath(rawPath) + } catch (error: Throwable) { + null + } + } + fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState { return currentStateProvider().copy( tokenData = CustomTokenData( diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt index 1b107076ea..44e55b947d 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt @@ -9,8 +9,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.HDWalletError import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency @@ -56,6 +54,7 @@ internal class AddCustomTokenViewModel @Inject constructor( private val validateContractAddressUseCase: ValidateContractAddressUseCase, private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet, private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase, + private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver { @@ -135,13 +134,25 @@ internal class AddCustomTokenViewModel @Inject constructor( private fun selectNetwork(networkItemState: NetworkItemState) { viewModelScope.launch(dispatchers.io) { - val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + // TODO [REDACTED_TASK_KEY] + val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch val supportsTokens = areTokensSupportedByNetworkUseCase( networkId = networkItemState.id, userWalletId = selectedWalletId, ).getOrNull() ?: false + + val networksForDerivations = getSupportedNetworks(selectedWalletId) + withContext(dispatchers.main) { - uiState = stateFactory.updateStateOnNetworkSelected(networkItemState, supportsTokens) + uiState = stateFactory.updateStateOnNetworkSelected( + networkItemState = networkItemState, + supportsTokens = supportsTokens, + networks = networksForDerivations, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly( + networkId = networkItemState.id, + userWalletId = selectedWalletId, + ), + ) } } } @@ -347,6 +358,8 @@ internal class AddCustomTokenViewModel @Inject constructor( } override fun onCustomDerivationChange(input: String) { + val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return + analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value)) uiState = uiState.copy( chooseDerivationState = uiState.chooseDerivationState?.copy( @@ -356,25 +369,19 @@ internal class AddCustomTokenViewModel @Inject constructor( ), ) debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val path = createDerivationPathOrNull(input) - val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - confirmButtonEnabled = path != null, - derivationIncorrect = input.isNotBlank() && path == null, - ) - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = enterDerivationState, - ), + val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce + uiState = stateFactory.updateOnCustomDerivationEntered( + input = input, + requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId), ) } } - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: HDWalletError) { - null - } + private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { + return requiresHardenedDerivationOnlyUseCase.invoke( + networkId = networkId, + userWalletId = userWalletId, + ).getOrElse { false } } override fun onCustomDerivationSelected() { diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt index d6c2b505ea..ca26ef0621 100644 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt @@ -25,7 +25,7 @@ internal fun TokensList( LazyColumn(modifier = modifier) { item { Text( - text = stringResource(id = R.string.manage_tokens_title), + text = stringResource(id = R.string.manage_tokens_list_header_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index 10248b767a..e0a5b674ad 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -22,7 +22,9 @@ import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R +import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState +import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock @@ -67,7 +69,7 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo PrimaryButtonIconEnd( modifier = Modifier .fillMaxWidth(), - text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet), + text = stringResource(id = R.string.common_import), iconResId = R.drawable.ic_tangem_24, enabled = state.buttonCreateWallet.enabled, showProgress = state.buttonCreateWallet.showProgress, @@ -196,6 +198,28 @@ private fun SuggestionsBlockPreview_Dark( } } +@Preview +@Composable +private fun ImportSeedPhraseScreenPreview_Light( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = false) { + ImportSeedPhraseScreen( + ImportSeedPhraseState( + tvSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), + onSuggestedPhraseClick = {}, + buttonCreateWallet = ButtonState( + enabled = true, + isClickable = true, + showProgress = false, + onClick = {}, + ), + suggestionsList = suggestions, + ), + ) + } +} + private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>( collection = listOf( persistentListOf( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt index 76a7e48923..23e21b9c92 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt @@ -1,5 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -8,16 +9,19 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SpacerW32 import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R +import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList /** [REDACTED_AUTHOR] @@ -88,4 +92,25 @@ private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList) } } } +} + +@Preview +@Composable +private fun YourSeedPhraseScreenPreview_Light() { + TangemTheme(isDark = false) { + YourSeedPhraseScreen( + state = YourSeedPhraseState( + mnemonicGridItems = (1..12).map { MnemonicGridItem(it, it.toString()) }.toImmutableList(), + buttonContinue = ButtonState( + enabled = true, + isClickable = true, + showProgress = false, + onClick = {}, + ), + ), + modifier = Modifier + .fillMaxSize() + .background(color = TangemTheme.colors.background.primary), + ) + } } \ No newline at end of file diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index e1d37506b4..5ad7d8346d 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -10,7 +10,6 @@ android { namespace = "com.tangem.feature.qrscanning.impl" } - dependencies { /** Core */ @@ -40,8 +39,13 @@ dependencies { implementation(deps.hilt.android) kapt(deps.hilt.kapt) + /** Api */ implementation(projects.features.qrScanning.api) + /** Domain */ + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) + /** Compose */ implementation(deps.compose.foundation) implementation(deps.compose.ui) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt deleted file mode 100644 index ad088e8c28..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.qrscanning.di - -import com.tangem.feature.qrscanning.repo.DefaultQrScanningEventsRepository -import com.tangem.feature.qrscanning.repo.QrScanningEventsRepository -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object QrScanningModule { - - @Provides - @Singleton - fun provideQrScannedEventsRepository(): QrScanningEventsRepository { - return DefaultQrScanningEventsRepository() - } - - @Provides - @Singleton - fun provideEmitQrScannedEventUseCase(repository: QrScanningEventsRepository): EmitQrScannedEventUseCase { - return EmitQrScannedEventUseCase(repository) - } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt index c4df653e5a..2f38d3e545 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/presentation/transformers/InitializeQrScanningStateTransformer.kt @@ -2,7 +2,7 @@ package com.tangem.feature.qrscanning.presentation.transformers import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.impl.R import com.tangem.feature.qrscanning.presentation.QrScanningState import com.tangem.feature.qrscanning.viewmodel.QrScanningClickIntents diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt deleted file mode 100644 index 146bee2ef6..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/DefaultQrScanningEventsRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.feature.qrscanning.repo - -import com.tangem.feature.qrscanning.SourceType -import kotlinx.coroutines.flow.* - -internal class DefaultQrScanningEventsRepository : QrScanningEventsRepository { - - private data class QrScanningEvent(val type: SourceType, val qrCode: String) - - private val scannedEvents = MutableSharedFlow() - - override suspend fun emitResult(type: SourceType, qrCode: String) { - scannedEvents.emit(QrScanningEvent(type, qrCode)) - } - - override fun subscribeToScanningResults(type: SourceType) = scannedEvents - .filter { it.type == type } - .map { it.qrCode } -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt deleted file mode 100644 index 3640e79c2a..0000000000 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/repo/QrScanningEventsRepository.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.feature.qrscanning.repo - -import com.tangem.feature.qrscanning.SourceType -import kotlinx.coroutines.flow.Flow - -interface QrScanningEventsRepository { - - suspend fun emitResult(type: SourceType, qrCode: String) - - fun subscribeToScanningResults(type: SourceType): Flow -} \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt index 3ae564a745..a9c37e465c 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/BaseQrScanningClickIntents.kt @@ -1,7 +1,7 @@ package com.tangem.feature.qrscanning.viewmodel import androidx.activity.result.ActivityResultLauncher -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import kotlinx.coroutines.CoroutineScope import kotlin.properties.Delegates diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt index 7ecbdae562..64d837b7f4 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningClickIntents.kt @@ -1,8 +1,8 @@ package com.tangem.feature.qrscanning.viewmodel +import com.tangem.domain.qrscanning.usecases.EmitQrScannedEventUseCase import com.tangem.feature.qrscanning.presentation.QrScanningStateController import com.tangem.feature.qrscanning.presentation.transformers.DismissBottomSheetTransformer -import com.tangem.feature.qrscanning.usecase.EmitQrScannedEventUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.launch diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index 9e17c2f6a7..4320634f4d 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY -import com.tangem.feature.qrscanning.SourceType +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningState import com.tangem.feature.qrscanning.presentation.QrScanningStateController diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 57337c925a..3417b6931f 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme @@ -113,14 +115,22 @@ private fun Awards(expectedAwards: ExpectedAwards) { thickness = TangemTheme.dimens.size0_5, ) AwardText( - startText = stringResource(id = R.string.referral_expected_awards), + startText = if (expectedAwards.expectedAwards.isNotEmpty()) { + stringResource(id = R.string.referral_expected_awards) + } else { + stringResource(id = R.string.referral_no_expected_awards) + }, startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, - endText = pluralStringResource( - id = R.plurals.referral_number_of_wallets, - count = expectedAwards.numberOfWallets, - expectedAwards.numberOfWallets, - ), + endText = if (expectedAwards.expectedAwards.isNotEmpty()) { + pluralStringResource( + id = R.plurals.referral_number_of_wallets, + count = expectedAwards.numberOfWallets, + expectedAwards.numberOfWallets, + ) + } else { + "" + }, endTextColor = TangemTheme.colors.text.tertiary, endTextStyle = TangemTheme.typography.body2, cornersToRound = CornersToRound.ZERO, @@ -309,34 +319,20 @@ private fun Context.shareText(text: String) { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_InLightTheme() { +private fun ParticipateBottomBlockPreview_Light( + @PreviewParameter(ParticipateBottomBlockDataProvider::class) data: ParticipateBottomBlockData, +) { TangemTheme(isDark = false) { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = ExpectedAwards( - numberOfWallets = 3, - expectedAwards = listOf( - ExpectedAward( - amount = "10 USDT", - paymentDate = "Today", - ), - ExpectedAward( - amount = "20 USDT", - paymentDate = "6 Aug 2023", - ), - ExpectedAward( - amount = "30 USDT", - paymentDate = "10 Aug 2023", - ), - ), - ), - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, + purchasedWalletCount = data.purchasedWalletCount, + code = data.code, + shareLink = data.shareLink, + expectedAwards = data.expectedAwards, + onAgreementClick = data.onAgreementClick, + onShowCopySnackbar = data.onShowCopySnackbar, + onCopyClick = data.onCopyClick, + onShareClick = data.onShareClick, ) } } @@ -344,18 +340,20 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { - TangemTheme(isDark = false) { +private fun ParticipateBottomBlockPreview_Dark( + @PreviewParameter(ParticipateBottomBlockDataProvider::class) state: ParticipateBottomBlockData, +) { + TangemTheme(isDark = true) { Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, + purchasedWalletCount = state.purchasedWalletCount, + code = state.code, + shareLink = state.shareLink, + expectedAwards = state.expectedAwards, + onAgreementClick = state.onAgreementClick, + onShowCopySnackbar = state.onShowCopySnackbar, + onCopyClick = state.onCopyClick, + onShareClick = state.onShareClick, ) } } @@ -363,26 +361,7 @@ private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() { - TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = 0, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun LessMoreButton_White() { +private fun LessMoreButton_Light() { TangemTheme(isDark = false) { LessMoreButton( isExpanded = remember { @@ -394,19 +373,64 @@ private fun LessMoreButton_White() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() { +private fun LessMoreButton_Dark() { TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.secondary)) { - ParticipateBottomBlock( - purchasedWalletCount = 3, - code = "x4JdK", - shareLink = "", - expectedAwards = null, - onAgreementClick = {}, - onShowCopySnackbar = {}, - onCopyClick = {}, - onShareClick = {}, - ) - } + LessMoreButton( + isExpanded = remember { + mutableStateOf(false) + }, + ) } -} \ No newline at end of file +} + +private class ParticipateBottomBlockDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), + ), + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = emptyList(), + ), + ), + ParticipateBottomBlockData( + purchasedWalletCount = 3, + expectedAwards = null, + ), + ParticipateBottomBlockData( + purchasedWalletCount = 0, + expectedAwards = null, + ), + + ), +) + +private data class ParticipateBottomBlockData( + val purchasedWalletCount: Int, + val expectedAwards: ExpectedAwards?, + val code: String = "x4JDK", + val shareLink: String = "", + val onAgreementClick: () -> Unit = {}, + val onShowCopySnackbar: () -> Unit = {}, + val onCopyClick: () -> Unit = {}, + val onShareClick: () -> Unit = {}, +) \ No newline at end of file diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index fce7f71d36..4a2940f972 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -11,6 +11,7 @@ interface SendRouter { const val USER_WALLET_ID_KEY = "send_user_wallet_id" const val TRANSACTION_ID_KEY = "send_transaction_id" const val AMOUNT_KEY = "send_amount" + const val TAG_KEY = "send_tag" const val DESTINATION_ADDRESS_KEY = "send_destination_address" } } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 062932fd25..db20b4afc2 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { /** Compose */ implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.material3) + implementation(deps.compose.material) implementation(deps.compose.foundation) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) @@ -69,12 +70,13 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.qrScanning) + implementation(projects.domain.qrScanning.models) /** Feature modules */ implementation(projects.features.send.api) implementation(projects.features.tokendetails.api) implementation(projects.features.qrScanning.api) - implementation(projects.features.qrScanning.impl) /** DI */ implementation(deps.hilt.android) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index db243390ea..0629979fd1 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -5,10 +5,10 @@ import androidx.fragment.app.Fragment import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.feature.qrscanning.SourceType import com.tangem.features.send.impl.presentation.SendFragment import com.tangem.features.tokendetails.navigation.TokenDetailsRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 539fd6d734..0a3521b1eb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -13,8 +13,8 @@ import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.core.ui.theme.AppThemeModeHolder -import com.tangem.feature.qrscanning.SourceType -import com.tangem.feature.qrscanning.usecase.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.state.StateRouter diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt index bf5148d51e..64817ff69e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotification.kt @@ -23,12 +23,12 @@ internal sealed class SendNotification(val config: NotificationConfig) { ), ) { - object TotalExceedsBalance : Error( + data object TotalExceedsBalance : Error( title = resourceReference(R.string.send_notification_exceed_balance_title), subtitle = resourceReference(R.string.send_notification_exceed_balance_text), ) - object InvalidAmount : Error( + data object InvalidAmount : Error( title = resourceReference(R.string.send_notification_invalid_amount_title), subtitle = resourceReference(R.string.send_notification_invalid_amount_text), ) @@ -49,7 +49,7 @@ internal sealed class SendNotification(val config: NotificationConfig) { val amountLimit: String, val onConfirmClick: () -> Unit, ) : Error( - title = resourceReference(R.string.send_notifiaction_transaction_limit_title), + title = resourceReference(R.string.send_notification_transaction_limit_title), subtitle = resourceReference( R.string.send_notification_transaction_limit_text, wrappedList(cryptoCurrency, utxoLimit, amountLimit), @@ -94,17 +94,6 @@ internal sealed class SendNotification(val config: NotificationConfig) { subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), ) - data class NetworkCoverage( - val amountReducedBy: String, - val amountReduced: String, - ) : Warning( - title = resourceReference(id = R.string.send_network_fee_warning_title), - subtitle = resourceReference( - id = R.string.send_network_fee_warning_content, - formatArgs = wrappedList(amountReducedBy, amountReduced), - ), - ) - data object FeeTooLow : Warning( title = resourceReference(id = R.string.send_notification_transaction_delay_title), subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt index 20ad57bca1..426ddab71a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendNotificationFactory.kt @@ -52,7 +52,6 @@ internal class SendNotificationFactory( addDustWarningNotification(feeAmount, sendAmount) addTransactionLimitErrorNotification(feeAmount, sendAmount) // warnings - addFeeCoverageNotification(sendState.isSubtract, sendAmount) addExistentialWarningNotification(feeAmount, sendAmount) addHighFeeWarningNotification(sendAmount, sendState.ignoreAmountReduce) addTooLowNotification(feeState) @@ -257,29 +256,6 @@ internal class SendNotificationFactory( } } - private fun MutableList.addFeeCoverageNotification( - isSubtract: Boolean, - amountValue: BigDecimal, - ) { - val state = currentStateProvider() - val cryptoCurrency = cryptoCurrencyStatusProvider().currency - val feeAmount = state.feeState?.fee?.amount?.value ?: BigDecimal.ZERO - - val amountReducedValue = amountValue.minus(feeAmount) - val amountReducedByValue = amountValue.minus(amountReducedValue) - val amountReducedBy = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amountReducedByValue, - cryptoCurrency = cryptoCurrency, - ) - val amountReduced = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amountReducedValue, - cryptoCurrency = cryptoCurrency, - ) - if (isSubtract) { - add(SendNotification.Warning.NetworkCoverage(amountReducedBy, amountReduced)) - } - } - private fun MutableList.addTooLowNotification(feeState: SendStates.FeeState) { val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return val multipleFees = feeSelectorState.fees as? TransactionFee.Choosable ?: return diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index cbd7ea840b..5f865ed61a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -13,6 +13,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet +import com.tangem.features.send.impl.presentation.state.amount.SendAmountSubtractConverter import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter @@ -31,6 +32,7 @@ internal class SendStateFactory( private val userWalletProvider: Provider, private val appCurrencyProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, ) { @@ -43,7 +45,12 @@ internal class SendStateFactory( appCurrencyProvider = appCurrencyProvider, ) } - + private val amountSubtractConverter by lazy(LazyThreadSafetyMode.NONE) { + SendAmountSubtractConverter( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + ) + } private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendAmountStateConverter( appCurrencyProvider = appCurrencyProvider, @@ -62,7 +69,7 @@ internal class SendStateFactory( private val feeStateConverter by lazy(LazyThreadSafetyMode.NONE) { SendFeeStateConverter( appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } @@ -79,24 +86,29 @@ internal class SendStateFactory( event = consumedEvent(), isEditingDisabled = false, isBalanceHidden = false, + cryptoCurrencySymbol = "", ) fun getReadyState(): SendUiState { val state = currentStateProvider() return state.copy( amountState = state.amountState ?: amountStateConverter.convert(""), - recipientState = state.recipientState ?: recipientStateConverter.convert(""), + recipientState = state.recipientState + ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), + cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, ) } - fun getReadyState(amount: String, destinationAddress: String): SendUiState { + fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState { val state = currentStateProvider() return state.copy( amountState = state.amountState ?: amountStateConverter.convert(amount), - recipientState = state.recipientState ?: recipientStateConverter.convert(destinationAddress), + recipientState = state.recipientState + ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), isEditingDisabled = true, + cryptoCurrencySymbol = cryptoCurrencyStatusProvider().currency.symbol, ) } @@ -214,11 +226,12 @@ internal class SendStateFactory( //endregion //region send - fun onSubtractSelect(isSubtract: Boolean): SendUiState { + fun onSubtractSelect(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() - return state.copy( - sendState = state.sendState.copy(isSubtract = isSubtract), - ) + + if (!isAmountSubtractAvailable) return state + + return amountSubtractConverter.convert(Unit) } fun getSendingStateUpdate(isSending: Boolean): SendUiState { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index 7572a37ae9..00d7a9dbbe 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -25,6 +25,7 @@ import java.math.BigDecimal internal data class SendUiState( val clickIntents: SendClickIntents, val isEditingDisabled: Boolean, + val cryptoCurrencySymbol: String, val amountState: SendStates.AmountState? = null, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt index bbadf98185..e1c163e2d4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt @@ -14,6 +14,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf internal class SendAmountStateConverter( @@ -37,18 +38,22 @@ internal class SendAmountStateConverter( tokenIconState = iconStateConverter.convert(status), amountTextField = sendAmountFieldConverter.convert(value), isPrimaryButtonEnabled = false, - segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( - title = stringReference(status.currency.symbol), - iconState = iconStateConverter.convert(status), - isFiat = false, - ), - SendAmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), + segmentedButtonConfig = if (status.value.fiatRate.isNullOrZero()) { + persistentListOf() + } else { + persistentListOf( + SendAmountSegmentedButtonsConfig( + title = stringReference(status.currency.symbol), + iconState = iconStateConverter.convert(status), + isFiat = false, + ), + SendAmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconUrl = appCurrency.iconSmallUrl, + isFiat = true, + ), + ) + }, ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt new file mode 100644 index 0000000000..4d00bd9b2e --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSubtractConverter.kt @@ -0,0 +1,45 @@ +package com.tangem.features.send.impl.presentation.state.amount + +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.impl.presentation.state.SendUiState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class SendAmountSubtractConverter( + private val currentStateProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + override fun convert(value: Unit): SendUiState { + val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountState = state.amountState ?: return state + val feeValue = state.feeState?.fee?.amount?.value ?: return state + val amountTextField = amountState.amountTextField + val amountValue = amountTextField.cryptoAmount.value ?: return state + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val decimalCryptoValue = amountValue.minus(feeValue) + + if (decimalCryptoValue < BigDecimal.ZERO) return state + + val decimalFiatValue = decimalCryptoValue.multiply(fiatRate) + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val fiatValue = decimalFiatValue.parseBigDecimal(fiatDecimals) + + return state.copy( + sendState = state.sendState.copy(isSubtract = true), + amountState = amountState.copy( + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt index 7b55e0100d..bd8e832a91 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeConverter.kt @@ -13,14 +13,14 @@ import com.tangem.utils.converter.Converter internal class FeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { private val ethereumCustomFeeConverter by lazy { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index 4154ae14a3..6a2804103f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -20,7 +20,7 @@ import kotlinx.collections.immutable.persistentListOf internal class FeeStateFactory( private val clickIntents: SendClickIntents, private val currentStateProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, ) { @@ -28,7 +28,7 @@ internal class FeeStateFactory( SendFeeCustomFieldConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } @@ -36,7 +36,7 @@ internal class FeeStateFactory( FeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } @@ -140,7 +140,7 @@ internal class FeeStateFactory( } private fun isFeeApproximate(fee: Fee): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() return isFeeApproximateUseCase( networkId = cryptoCurrencyStatus.currency.network.id, amountType = fee.amount.type, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 24c85ee305..cdd0fa60e6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -14,14 +14,14 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeCustomFieldConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { private val ethereumCustomFeeConverter by lazy { EthereumCustomFeeConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + feeCryptoCurrencyStatusProvider = feeCryptoCurrencyStatusProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt index 2659080a08..864ae1b501 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeStateConverter.kt @@ -9,16 +9,15 @@ import kotlinx.collections.immutable.persistentListOf internal class SendFeeStateConverter( private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter { override fun convert(value: Unit): SendStates.FeeState { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() return SendStates.FeeState( feeSelectorState = FeeSelectorState.Loading, fee = null, notifications = persistentListOf(), - rate = cryptoCurrencyStatus.value.fiatRate, + rate = feeCryptoCurrencyStatusProvider().value.fiatRate, appCurrency = appCurrencyProvider(), isFeeApproximate = false, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index bf12d6f398..950dd0b135 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -28,7 +28,7 @@ import java.math.RoundingMode internal class EthereumCustomFeeConverter( private val clickIntents: SendClickIntents, private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, ) : Converter> { override fun convert(value: Fee.Ethereum): ImmutableList { @@ -152,7 +152,7 @@ internal class EthereumCustomFeeConverter( private fun getFeeFormatted(fee: BigDecimal?): TextReference { val appCurrency = appCurrencyProvider() - val rate = cryptoCurrencyStatusProvider().value.fiatRate + val rate = feeCryptoCurrencyStatusProvider().value.fiatRate val fiatFee = rate?.let { fee?.multiply(it) } return stringReference( BigDecimalFormatter.formatFiatAmount( @@ -164,7 +164,7 @@ internal class EthereumCustomFeeConverter( } private fun checkExceedBalance(feeAmount: BigDecimal?): Boolean { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrencyStatus = feeCryptoCurrencyStatusProvider() val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO return feeAmount == null || feeAmount.isZero() || feeAmount > currencyCryptoAmount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt index ff622738f7..332a736cd0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt @@ -31,11 +31,13 @@ internal class SendAmountFieldConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val cryptoDecimal = value.toBigDecimalOrDefault() val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) - val fiatValue = if (value.isEmpty()) { - "" + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val (fiatValue, fiatDecimal) = if (value.isEmpty()) { + "" to BigDecimal.ZERO } else { - val fiatDecimal = cryptoCurrencyStatus.value.fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO - fiatDecimal.parseBigDecimal(FIAT_DECIMALS) + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) ?: BigDecimal.ZERO + val fiatValue = fiatDecimal.parseBigDecimal(FIAT_DECIMALS) + fiatValue to fiatDecimal } val isDoneActionEnabled = !cryptoDecimal.isZero() return SendTextField.AmountField( @@ -49,15 +51,16 @@ internal class SendAmountFieldConverter( keyboardActions = KeyboardActions(onDone = { clickIntents.onNextClick() }), isFiatValue = false, cryptoAmount = cryptoAmount, - fiatAmount = getAppCurrencyAmount(appCurrencyProvider()), + fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), isError = false, error = TextReference.Res(R.string.swapping_insufficient_funds), + isFiatUnavailable = fiatRate == null, ) } - private fun getAppCurrencyAmount(appCurrency: AppCurrency) = Amount( + private fun getAppCurrencyAmount(fiatValue: BigDecimal, appCurrency: AppCurrency) = Amount( currencySymbol = appCurrency.symbol, - value = BigDecimal.ZERO, + value = fiatValue, decimals = FIAT_DECIMALS, type = AmountType.FiatType(appCurrency.code), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 97682a9e21..8db8a2fc02 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -27,6 +27,7 @@ internal sealed class SendTextField { val fiatAmount: Amount, val isFiatValue: Boolean, val fiatValue: String, + val isFiatUnavailable: Boolean, val isError: Boolean, val error: TextReference, ) : SendTextField() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index 1eeffb3762..a539d72859 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.recipient +import androidx.annotation.StringRes import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -15,13 +16,18 @@ import com.tangem.utils.converter.Converter internal class SendRecipientMemoFieldConverter( private val clickIntents: SendClickIntents, private val cryptoCurrencyStatus: Provider, -) : Converter { +) : Converter { - fun convertOrNull(): SendTextField.RecipientMemo? { + fun convertOrNull(memoValue: String?): SendTextField.RecipientMemo? { val cryptoCurrency = cryptoCurrencyStatus().currency + val memo = memoValue ?: "" return when (cryptoCurrency.network.id.value) { - Blockchain.XRP.id -> convert(R.string.send_destination_tag_field) + Blockchain.XRP.id -> { + convert( + value = Data(memo = memo, label = R.string.send_destination_tag_field), + ) + } Blockchain.Binance.id, Blockchain.TON.id, Blockchain.Cosmos.id, @@ -30,24 +36,30 @@ internal class SendRecipientMemoFieldConverter( Blockchain.Stellar.id, Blockchain.Hedera.id, Blockchain.Algorand.id, - -> convert(R.string.send_extras_hint_memo) + -> { + convert( + value = Data(memo = memo, label = R.string.send_extras_hint_memo), + ) + } else -> null } } - override fun convert(value: Int): SendTextField.RecipientMemo { + override fun convert(value: Data): SendTextField.RecipientMemo { return SendTextField.RecipientMemo( - value = "", + value = value.memo, onValueChange = clickIntents::onRecipientMemoValueChange, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done, keyboardType = KeyboardType.Text, ), placeholder = resourceReference(R.string.send_optional_field), - label = resourceReference(value), + label = resourceReference(value.label), error = resourceReference(R.string.send_memo_destination_tag_error), disabledText = resourceReference(R.string.send_additional_field_already_included), isEnabled = true, ) } + + data class Data(val memo: String, @StringRes val label: Int) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt index ae375c97e2..dd7db52c45 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientStateConverter.kt @@ -10,7 +10,7 @@ import kotlinx.collections.immutable.persistentListOf internal class SendRecipientStateConverter( private val clickIntents: SendClickIntents, private val cryptoCurrencyStatusProvider: Provider, -) : Converter { +) : Converter { private val addressFieldConverter by lazy { SendRecipientAddressFieldConverter(clickIntents) } private val memoFieldConverter by lazy { @@ -20,14 +20,16 @@ internal class SendRecipientStateConverter( ) } - override fun convert(value: String): SendStates.RecipientState { + override fun convert(value: Data): SendStates.RecipientState { return SendStates.RecipientState( - addressTextField = addressFieldConverter.convert(value), - memoTextField = memoFieldConverter.convertOrNull(), + addressTextField = addressFieldConverter.convert(value.address), + memoTextField = memoFieldConverter.convertOrNull(value.memo), network = cryptoCurrencyStatusProvider().currency.network.name, isPrimaryButtonEnabled = false, wallets = persistentListOf(), recent = persistentListOf(), ) } + + data class Data(val address: String, val memo: String? = null) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 5c0dfc787b..0344343e70 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -1,18 +1,12 @@ package com.tangem.features.send.impl.presentation.ui -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandHorizontally -import androidx.compose.animation.shrinkHorizontally +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,161 +16,185 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.R -import com.tangem.core.ui.components.* -import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.extensions.shareText import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType @Composable internal fun SendNavigationButtons(uiState: SendUiState, currentState: SendUiCurrentScreen) { - Row( + val isSuccess = uiState.sendState.isSuccess + val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess + val isSentState = currentState.type == SendUiStateType.Send && isSuccess + Column( modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing12), + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), ) { - SendSecondaryNavigationButton( - uiState = uiState, - currentState = currentState, + SendingText(uiState = uiState, isVisible = isSendingState) + SendDoneButtons( + txUrl = uiState.sendState.txUrl, + onExploreClick = uiState.clickIntents::onExploreClick, + onShareClick = uiState.clickIntents::onShareClick, + isVisible = isSentState, ) - SendPrimaryNavigationButton( + SendNavigationButton( uiState = uiState, currentState = currentState, - modifier = Modifier - .weight(1f) - .padding(horizontal = TangemTheme.dimens.spacing16), + modifier = Modifier, ) } } @Composable -private fun SendSecondaryNavigationButton(uiState: SendUiState, currentState: SendUiCurrentScreen) { - val isEditingDisabled = uiState.isEditingDisabled - val isFromConfirmation = currentState.isFromConfirmation - val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee - AnimatedVisibility( - visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, - enter = expandHorizontally(expandFrom = Alignment.End), - exit = shrinkHorizontally(shrinkTowards = Alignment.End), - ) { - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing16) - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.button.secondary) - .clickable { - uiState.clickIntents.onPrevClick() - } - .padding(TangemTheme.dimens.spacing12), - painter = painterResource(R.drawable.ic_back_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } -} - -@Composable -private fun SendPrimaryNavigationButton( +private fun SendNavigationButton( uiState: SendUiState, currentState: SendUiCurrentScreen, modifier: Modifier = Modifier, ) { + val hapticFeedback = LocalHapticFeedback.current + val isEditingDisabled = uiState.isEditingDisabled val isSuccess = uiState.sendState.isSuccess - val isSending = uiState.sendState.isSending - val txUrl = uiState.sendState.txUrl + + val isFromConfirmation = currentState.isFromConfirmation + val isCorrectScreen = currentState.type == SendUiStateType.Amount || currentState.type == SendUiStateType.Fee + val isSendingState = currentState.type == SendUiStateType.Send && !isSuccess val (buttonTextId, buttonClick) = getButtonData( currentState = currentState, isSuccess = isSuccess, uiState = uiState, ) - - val isButtonEnabled = isButtonEnabled( - currentState = currentState, - uiState = uiState, - ) - - AnimatedContent( - targetState = buttonTextId, - label = "Update send screen state", + val isButtonEnabled = isButtonEnabled(currentState, uiState) + val buttonIcon = if (isSendingState) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + } + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), modifier = modifier, - ) { textId -> - when { - currentState.type == SendUiStateType.Send && !isSuccess -> { - val hapticFeedback = rememberHapticFeedback(state = currentState, onAction = buttonClick) - PrimaryButtonIconEnd( - text = stringResource(textId), - iconResId = R.drawable.ic_tangem_24, - enabled = isButtonEnabled, - onClick = hapticFeedback, - showProgress = isSending, - ) - } - currentState.type == SendUiStateType.Send && isSuccess -> { - PrimaryButtonsDone( - textRes = textId, - txUrl = txUrl, - onExploreClick = uiState.clickIntents::onExploreClick, - onShareClick = uiState.clickIntents::onShareClick, - onDoneClick = buttonClick, - modifier = Modifier, - ) - } - else -> { - PrimaryButton( - text = stringResource(textId), - enabled = isButtonEnabled, - onClick = buttonClick, - ) - } + ) { + AnimatedVisibility( + visible = !isEditingDisabled && isCorrectScreen && !isFromConfirmation, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + } + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled, + onClick = { + if (isSendingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + buttonClick() + }, + showProgress = uiState.sendState.isSending, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } +} + +@Composable +private fun SendingText(uiState: SendUiState, isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + visible = isVisible, + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sending state text", + ) { + val amountState = uiState.amountState + val feeState = uiState.feeState + val fiatRate = feeState?.rate + val fiatAmount = amountState?.amountTextField?.fiatAmount + val feeFiat = feeState?.fee?.amount?.value?.multiply(fiatRate) + val sendingFiat = feeFiat?.let { fiatAmount?.value?.plus(it) } + + if (feeFiat != null && sendingFiat != null) { + val sendingValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = sendingFiat, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + val feeValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeFiat, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + Text( + text = stringResource(id = R.string.send_summary_transaction_description, sendingValue, feeValue), + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + ) } } } @Composable -private fun PrimaryButtonsDone( - @StringRes textRes: Int, +private fun SendDoneButtons( txUrl: String, onExploreClick: () -> Unit, onShareClick: () -> Unit, - onDoneClick: () -> Unit, + isVisible: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current val context = LocalContext.current - Column(modifier = modifier) { - if (txUrl.isNotBlank()) { - Row { - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_explore), - iconResId = R.drawable.ic_web_24, - onClick = onExploreClick, - modifier = Modifier.weight(1f), - ) - SpacerW12() - SecondaryButtonIconStart( - text = stringResource(id = R.string.common_share), - iconResId = R.drawable.ic_share_24, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - context.shareText(txUrl) - onShareClick() - }, - modifier = Modifier.weight(1f), - ) - } - SpacerH12() + AnimatedVisibility( + visible = isVisible && txUrl.isNotBlank(), + modifier = modifier, + enter = slideInVertically().plus(fadeIn()), + exit = slideOutVertically().plus(fadeOut()), + label = "Animate show sent state buttons", + ) { + Row(modifier = Modifier.padding(TangemTheme.dimens.spacing12)) { + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_explore), + iconResId = R.drawable.ic_web_24, + onClick = onExploreClick, + modifier = Modifier.weight(1f), + ) + SpacerW12() + SecondaryButtonIconStart( + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(txUrl) + onShareClick() + }, + modifier = Modifier.weight(1f), + ) } - PrimaryButton( - text = stringResource(id = textRes), - enabled = true, - onClick = onDoneClick, - modifier = Modifier.fillMaxWidth(), - ) } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 7cd9b84daf..729c3c9a24 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -1,20 +1,21 @@ package com.tangem.features.send.impl.presentation.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen @@ -40,12 +41,22 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow R.string.send_amount_label - SendUiStateType.Recipient -> R.string.send_recipient_label - SendUiStateType.Fee -> R.string.common_fee_selector_title - SendUiStateType.Send -> if (!uiState.sendState.isSuccess) R.string.send_confirm_label else null + SendUiStateType.Amount -> resourceReference(R.string.send_amount_label) + SendUiStateType.Recipient -> resourceReference(R.string.send_recipient_label) + SendUiStateType.Fee -> resourceReference(R.string.common_fee_selector_title) + SendUiStateType.Send -> if (!uiState.sendState.isSuccess) { + resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencySymbol)) + } else { + null + } else -> null } + val isSending = currentState.value.type == SendUiStateType.Send && !uiState.sendState.isSuccess + val subtitleRes = if (isSending) { + uiState.amountState?.walletName + } else { + null + } val iconRes = if (currentState.value.type == SendUiStateType.Recipient) { R.drawable.ic_qrcode_scan_24 } else { @@ -53,12 +64,14 @@ internal fun SendScreen(uiState: SendUiState, currentStateFlow: StateFlow, - modifier: Modifier = Modifier, -) { - notifications( - configs = configs.filter { - it is SendFeeNotification.Error.ExceedsBalance || - it is SendFeeNotification.Warning.NetworkFeeUnreachable - }.toImmutableList(), - modifier = modifier, - ) -} - -private fun LazyListScope.middleNotifications( - configs: ImmutableList, - modifier: Modifier = Modifier, -) { - notifications( - configs = configs.filterIsInstance().toImmutableList(), - modifier = modifier, - ) -} - @OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.notifications( configs: ImmutableList, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt new file mode 100644 index 0000000000..1213f43797 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt @@ -0,0 +1,78 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.ResizableText +import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates + +@Composable +internal fun AmountBlock( + amountState: SendStates.AmountState, + isSuccess: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, +) { + val amount = amountState.amountTextField + + val cryptoAmount = getAmountWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val fiatAmount = getAmountWithSymbol(amount.fiatValue, amount.fiatAmount.currencySymbol) + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + val (firstAmount, secondAmount) = if (amount.isFiatValue) { + fiatAmount to cryptoAmount + } else { + cryptoAmount to fiatAmount + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .padding( + vertical = TangemTheme.dimens.spacing14, + horizontal = TangemTheme.dimens.spacing16, + ), + ) { + TokenIcon(state = amountState.tokenIconState) + ResizableText( + text = firstAmount, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing16), + ) + Text( + text = secondAmount, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing4), + ) + } +} + +private fun getAmountWithSymbol(amount: String, symbol: String): String { + return "$amount $symbol" +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt new file mode 100644 index 0000000000..13ff8cab42 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -0,0 +1,96 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.send.impl.R +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState +import com.tangem.features.send.impl.presentation.state.fee.FeeType + +@Composable +internal fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { + val fee = feeState.fee ?: return + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return + val (title, icon) = when (feeSelectorState.selectedFee) { + FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24 + FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 + FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 + } + val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = fee.amount.value, + cryptoCurrency = fee.amount.currencySymbol, + decimals = fee.amount.decimals, + ) + val feeFiatValue = BigDecimalFormatter.formatFiatAmount( + fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, + fiatCurrencyCode = feeState.appCurrency.code, + fiatCurrencySymbol = feeState.appCurrency.symbol, + ) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(enabled = !isSuccess) { onClick() } + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Row( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(icon), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + Text( + text = stringResource(title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing8), + ) + EllipsisText( + text = feeCryptoValue, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.End, + ellipsis = TextEllipsis.OffsetEnd(fee.amount.currencySymbol.length), + modifier = Modifier + .weight(1f) + .padding(start = TangemTheme.dimens.spacing4), + ) + Text( + text = "($feeFiatValue)", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt new file mode 100644 index 0000000000..7169e38f49 --- /dev/null +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/RecipientBlock.kt @@ -0,0 +1,91 @@ +package com.tangem.features.send.impl.presentation.ui.send + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.features.send.impl.presentation.state.fields.SendTextField + +@Composable +internal fun RecipientBlock( + recipientState: SendStates.RecipientState, + isSuccess: Boolean, + isEditingDisabled: Boolean, + onClick: () -> Unit, +) { + val backgroundColor = if (isEditingDisabled) { + TangemTheme.colors.button.disabled + } else { + TangemTheme.colors.background.action + } + + Column( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(backgroundColor) + .clickable(enabled = !isSuccess && !isEditingDisabled, onClick = onClick) + .padding(TangemTheme.dimens.spacing12), + ) { + AddressBlock(recipientState.addressTextField) + MemoBlock(recipientState.memoTextField) + } +} + +@Composable +private fun AddressBlock(address: SendTextField.RecipientAddress) { + Text( + text = address.label.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + IdentIcon( + address = address.value, + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius18)) + .background(TangemTheme.colors.background.tertiary), + ) + Text( + text = address.value, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun MemoBlock(memo: SendTextField.RecipientMemo?) { + val showMemo = memo != null && memo.value.isNotBlank() + if (showMemo) { + HorizontalDivider( + color = TangemTheme.colors.stroke.primary, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + ) + Text( + text = memo?.label?.resolveReference().orEmpty(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = memo?.value.orEmpty(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 6d335ee554..6cd08ba079 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -1,235 +1,121 @@ package com.tangem.features.send.impl.presentation.ui.send -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle -import com.tangem.common.Strings -import com.tangem.core.ui.components.inputrow.InputRowDefault -import com.tangem.core.ui.components.inputrow.InputRowImage -import com.tangem.core.ui.components.inputrow.InputRowRecipientDefault import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.TransactionDoneTitle -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount -import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount -import com.tangem.domain.tokens.model.AmountType import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.SendNotification -import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.SendUiState import kotlinx.collections.immutable.ImmutableList +private const val TAP_HELP_KEY = "TAP_HELP_KEY" +private const val BLOCKS_KEY = "BLOCKS_KEY" + @Suppress("LongMethod") @Composable internal fun SendContent(uiState: SendUiState) { - val amountState = uiState.amountState ?: return - val recipientState = uiState.recipientState ?: return - val feeState = uiState.feeState ?: return val sendState = uiState.sendState - - val isSuccess = sendState.isSuccess - val timestamp = sendState.transactionDate - LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = TangemTheme.dimens.spacing16), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - item { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) { - AnimatedVisibility(visible = isSuccess) { - TransactionDoneTitle( - titleRes = R.string.sent_transaction_sent_title, - date = timestamp, - ) - } - AnimatedVisibility(visible = !isSuccess) { - FromWallet( - walletName = amountState.walletName, - walletBalance = amountState.walletBalance.resolveReference(), - isBalanceHidden = uiState.isBalanceHidden, - ) - } - RecipientBlock( - recipientState = recipientState, - isSuccess = isSuccess, - isEditingDisabled = uiState.isEditingDisabled, - onClick = uiState.clickIntents::showRecipient, - ) - AmountBlock( - amountState = amountState, - isSuccess = isSuccess, - isEditingDisabled = uiState.isEditingDisabled, - onClick = uiState.clickIntents::showAmount, - ) - FeeBlock( - feeState = feeState, - isSuccess = isSuccess, - onClick = uiState.clickIntents::showFee, - ) - } - } + blocks(uiState) + tapHelp(isDisplay = sendState.notifications.isEmpty() && !uiState.sendState.isSuccess) notifications(sendState.notifications) } } -@Composable -private fun FromWallet(walletName: String, walletBalance: String, isBalanceHidden: Boolean) { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.button.disabled) - .padding(TangemTheme.dimens.spacing12), - ) { - Text( - text = buildAnnotatedString { - append(stringResource(R.string.send_from_wallet_android)) - append(" ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append(walletName) - } - }, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - ) - val balance = if (isBalanceHidden) Strings.STARS else walletBalance - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", - ) { - Text( - text = it, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing8, - ), +private fun LazyListScope.blocks(uiState: SendUiState) { + val amountState = uiState.amountState ?: return + val recipientState = uiState.recipientState ?: return + val feeState = uiState.feeState ?: return + val sendState = uiState.sendState + val isSuccess = sendState.isSuccess + val timestamp = sendState.transactionDate + + item(key = BLOCKS_KEY) { + Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12)) { + AnimatedVisibility( + visible = isSuccess, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + ) { + TransactionDoneTitle( + titleRes = R.string.sent_transaction_sent_title, + date = timestamp, + ) + } + RecipientBlock( + recipientState = recipientState, + isSuccess = isSuccess, + isEditingDisabled = uiState.isEditingDisabled, + onClick = uiState.clickIntents::showRecipient, + ) + AmountBlock( + amountState = amountState, + isSuccess = isSuccess, + isEditingDisabled = uiState.isEditingDisabled, + onClick = uiState.clickIntents::showAmount, + ) + FeeBlock( + feeState = feeState, + isSuccess = isSuccess, + onClick = uiState.clickIntents::showFee, ) } } } -@Composable -private fun AmountBlock( - amountState: SendStates.AmountState, - isSuccess: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { - val amount = amountState.amountTextField - - val cryptoAmount = formatCryptoAmount( - cryptoAmount = amount.cryptoAmount.value, - cryptoCurrency = amount.cryptoAmount.currencySymbol, - decimals = amount.cryptoAmount.decimals, - ) - val fiatAmount = formatFiatAmount( - fiatAmount = amount.fiatAmount.value, - fiatCurrencyCode = (amount.fiatAmount.type as AmountType.FiatType).code, - fiatCurrencySymbol = amount.fiatAmount.currencySymbol, - ) - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - InputRowImage( - title = TextReference.Res(R.string.send_amount_label), - subtitle = TextReference.Str(cryptoAmount), - caption = TextReference.Str(fiatAmount), - tokenIconState = amountState.tokenIconState, - showNetworkIcon = true, - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() }, - ) -} - -@Composable -private fun RecipientBlock( - recipientState: SendStates.RecipientState, - isSuccess: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { - val address = recipientState.addressTextField - val memo = recipientState.memoTextField - val backgroundColor = if (isEditingDisabled) { - TangemTheme.colors.button.disabled - } else { - TangemTheme.colors.background.action - } - - Column( - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(backgroundColor) - .clickable(enabled = !isSuccess && !isEditingDisabled) { onClick() }, - ) { - val showMemo = memo != null && memo.value.isNotBlank() - InputRowRecipientDefault( - title = TextReference.Res(R.string.send_recipient), - value = address.value, - showDivider = showMemo, - ) - if (showMemo) { - InputRowDefault( - title = TextReference.Res(R.string.send_extras_hint_memo), - text = TextReference.Str(memo?.value.orEmpty()), - ) +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.tapHelp(isDisplay: Boolean, modifier: Modifier = Modifier) { + if (isDisplay) { + item(key = TAP_HELP_KEY) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .animateItemPlacement(), + ) { + val background = TangemTheme.colors.button.secondary + Icon( + painter = painterResource(id = R.drawable.send_hint_shape_12), + tint = TangemTheme.colors.button.secondary, + contentDescription = null, + modifier = Modifier, + ) + Text( + text = stringResource(id = R.string.send_summary_tap_hint), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(background) + .padding( + horizontal = TangemTheme.dimens.spacing14, + vertical = TangemTheme.dimens.spacing12, + ), + ) + } } } } -@Composable -private fun FeeBlock(feeState: SendStates.FeeState, isSuccess: Boolean, onClick: () -> Unit) { - val fee = feeState.fee ?: return - val feeCryptoValue = formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) - val feeFiatValue = formatFiatAmount( - fiatAmount = feeState.rate?.let { fee.amount.value?.multiply(it) }, - fiatCurrencyCode = feeState.appCurrency.code, - fiatCurrencySymbol = feeState.appCurrency.symbol, - ) - InputRowDefault( - title = resourceReference(R.string.common_network_fee_title), - text = resourceReference( - id = R.string.send_wallet_balance_format, - wrappedList(feeCryptoValue, feeFiatValue), - ), - modifier = Modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(enabled = !isSuccess) { onClick() }, - ) -} - @OptIn(ExperimentalFoundationApi::class) internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 621edfae39..073fb98d89 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -12,9 +12,12 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.repository.CurrencyChecksRepository @@ -30,7 +33,10 @@ import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.wallets.usecase.ValidateWalletMemoUseCase import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -66,17 +72,19 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, - private val parseSharedAddressUseCase: ParseSharedAddressUseCase, private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val parseQrCodeUseCase: ParseQrCodeUseCase, currencyChecksRepository: CurrencyChecksRepository, isFeeApproximateUseCase: IsFeeApproximateUseCase, getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -95,6 +103,7 @@ internal class SendViewModel @Inject constructor( private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY] private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY] private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY] + private val memo: String? = savedStateHandle[SendRouter.TAG_KEY] private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -108,6 +117,7 @@ internal class SendViewModel @Inject constructor( userWalletProvider = Provider { userWallet }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, validateWalletMemoUseCase = validateWalletMemoUseCase, getExplorerTransactionUrlUseCase = getExplorerTransactionUrlUseCase, ) @@ -120,7 +130,7 @@ internal class SendViewModel @Inject constructor( private val feeStateFactory = FeeStateFactory( clickIntents = this, currentStateProvider = Provider { uiState }, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isFeeApproximateUseCase = isFeeApproximateUseCase, ) @@ -165,12 +175,14 @@ internal class SendViewModel @Inject constructor( private var isAmountSubtractAvailable: Boolean = false private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var feeCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() private var recipientsJobHolder = JobHolder() private var feeJobHolder = JobHolder() private var addressValidationJobHolder = JobHolder() + private var memoValidationJobHolder = JobHolder() private var sendNotificationsJobHolder = JobHolder() private var feeNotificationsJobHolder = JobHolder() private var qrScannerJobHolder = JobHolder() @@ -204,7 +216,13 @@ internal class SendViewModel @Inject constructor( ifRight = { wallet -> userWallet = wallet checkIfSubtractAvailable() - getCurrenciesStatusUpdates(wallet) + + val isSingleWalletWithToken = wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + val isMultiCurrency = wallet.isMultiCurrency + getCurrenciesStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ) }, ifLeft = { uiState = eventStateFactory.getGenericErrorState( @@ -227,31 +245,38 @@ internal class SendViewModel @Inject constructor( .saveIn(balanceHidingJobHolder) } - private fun getCurrenciesStatusUpdates(wallet: UserWallet) { - val isSingleWallet = wallet.scanResponse.walletData?.token != null && !wallet.isMultiCurrency - + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { if (cryptoCurrency is CryptoCurrency.Coin) { - getCurrencyStatusUpdates(isSingleWallet = isSingleWallet) - .onEach { currencyStatus -> - currencyStatus.onRight { - onDataLoaded( - currencyStatus = it, - coinCurrencyStatus = it, - ) - } + getCurrencyStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ).onEach { currencyStatus -> + currencyStatus.onRight { + onDataLoaded( + currencyStatus = it, + coinCurrencyStatus = it, + feeCurrencyStatus = getFeeCurrencyStatusSync(it, isMultiCurrency), + ) } + } .flowOn(dispatchers.main) .launchIn(viewModelScope) .saveIn(balanceJobHolder) } else { combine( - flow = getCoinCurrencyStatusUpdates(isSingleWallet = isSingleWallet), - flow2 = getCurrencyStatusUpdates(isSingleWallet = isSingleWallet), - ) { coinStatus, currencyStatus -> - if (coinStatus.isRight() && currencyStatus.isRight()) { + flow = getCoinCurrencyStatusUpdates(isSingleWalletWithToken), + flow2 = getCurrencyStatusUpdates( + isSingleWalletWithToken = isSingleWalletWithToken, + isMultiCurrency = isMultiCurrency, + ), + ) { maybeCoinStatus, maybeCurrencyStatus -> + if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { + val currencyStatus = maybeCurrencyStatus.getOrElse { error("Currency status is unreachable") } + val coinStatus = maybeCoinStatus.getOrElse { error("Coin status is unreachable") } onDataLoaded( - currencyStatus = currencyStatus.getOrElse { error("Currency status is unreachable") }, - coinCurrencyStatus = coinStatus.getOrElse { error("Coin status is unreachable") }, + currencyStatus = currencyStatus, + coinCurrencyStatus = coinStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), ) } } @@ -261,18 +286,41 @@ internal class SendViewModel @Inject constructor( } } - private fun getCoinCurrencyStatusUpdates(isSingleWallet: Boolean) = getNetworkCoinStatusUseCase( + private fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase( userWalletId = userWalletId, networkId = cryptoCurrency.network.id, derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWallet, + isSingleWalletWithTokens = isSingleWalletWithToken, ).conflate().distinctUntilChanged() - private fun getCurrencyStatusUpdates(isSingleWallet: Boolean) = getCurrencyStatusUpdatesUseCase( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = isSingleWallet, - ).conflate().distinctUntilChanged() + private fun getCurrencyStatusUpdates( + isSingleWalletWithToken: Boolean, + isMultiCurrency: Boolean, + ): Flow> { + return if (isMultiCurrency) { + getCurrencyStatusUpdatesUseCase( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + isSingleWalletWithTokens = isSingleWalletWithToken, + ).conflate().distinctUntilChanged() + } else { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + } + } + + private suspend fun getFeeCurrencyStatusSync( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isMultiCurrency: Boolean, + ): CryptoCurrencyStatus { + return if (isMultiCurrency) { + getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: error("Fee currency is unreachable") + } else { + cryptoCurrencyStatus + } + } private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() @@ -286,19 +334,31 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, coinCurrencyStatus: CryptoCurrencyStatus) { + private fun onDataLoaded( + currencyStatus: CryptoCurrencyStatus, + coinCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus, + ) { cryptoCurrencyStatus = currencyStatus coinCryptoCurrencyStatus = coinCurrencyStatus + feeCryptoCurrencyStatus = feeCurrencyStatus - if (transactionId != null && amount != null && destinationAddress != null) { - uiState = stateFactory.getReadyState(amount, destinationAddress) - stateRouter.showFee() - } else { - getWalletsAndRecent() - uiState = stateFactory.getReadyState() - stateRouter.showRecipient() + when { + uiState.sendState.isSuccess -> { + stateRouter.showSend() + } + transactionId != null && amount != null && destinationAddress != null -> { + uiState = stateFactory.getReadyState(amount, destinationAddress, memo) + stateRouter.showFee() + updateNotifications() + } + else -> { + getWalletsAndRecent() + uiState = stateFactory.getReadyState() + stateRouter.showRecipient() + updateNotifications() + } } - updateNotifications() } private fun getWalletsAndRecent() { @@ -406,7 +466,6 @@ internal class SendViewModel @Inject constructor( ) return } else { - uiState = stateFactory.onSubtractSelect(false) analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(false)) } if (checkIfFeeTooLow(uiState)) { @@ -454,13 +513,14 @@ internal class SendViewModel @Inject constructor( // region recipient state clicks fun onRecipientAddressScanned(address: String) { viewModelScope.launch(dispatchers.main) { - parseSharedAddressUseCase(address, cryptoCurrency.network).fold( + parseQrCodeUseCase(address, cryptoCurrency).fold( ifRight = { parsedCode -> onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) parsedCode.amount?.let { onAmountValueChange(it.toPlainString()) } parsedCode.memo?.let { onRecipientMemoValueChange(it) } }, ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) Timber.w(it) }, ) @@ -475,6 +535,7 @@ internal class SendViewModel @Inject constructor( val isValidAddress = validateAddress(value) uiState = stateFactory.getOnRecipientAddressValidState(value, isValidAddress) type?.let { analyticsEventHandler.send(SendAnalyticEvents.AddressEntered(it, isValidAddress)) } + autoNextFromRecipient(type, isValidAddress) } }.saveIn(addressValidationJobHolder) } @@ -487,7 +548,7 @@ internal class SendViewModel @Inject constructor( val isValidAddress = validateAddress(uiState.recipientState?.addressTextField?.value.orEmpty()) uiState = stateFactory.getOnRecipientMemoValidState(value, isValidAddress) } - }.saveIn(addressValidationJobHolder) + }.saveIn(memoValidationJobHolder) } private suspend fun validateAddress(value: String): Boolean { @@ -519,6 +580,12 @@ internal class SendViewModel @Inject constructor( ), ) } + + private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean) { + val isRecent = type == EnterAddressSource.RecentAddress + val isAddressOnly = uiState.recipientState?.memoTextField == null + if (isRecent && isAddressOnly && isValidAddress) onNextClick() + } // endregion // region fee @@ -538,7 +605,7 @@ internal class SendViewModel @Inject constructor( } override fun onSubtractSelect() { - uiState = stateFactory.onSubtractSelect(true) + uiState = stateFactory.onSubtractSelect(isAmountSubtractAvailable) stateRouter.showSend() analyticsEventHandler.send(SendAnalyticEvents.SubtractFromAmount(true)) } @@ -645,16 +712,10 @@ internal class SendViewModel @Inject constructor( val fee = feeState.fee ?: return val memo = uiState.recipientState?.memoTextField?.value val amountValue = uiState.amountState?.amountTextField?.cryptoAmount?.value ?: return - val amountToSend = if (uiState.sendState.isSubtract && isAmountSubtractAvailable) { - val feeValue = fee.amount.value ?: return - amountValue.minus(feeValue) - } else { - amountValue - } viewModelScope.launch(dispatchers.main) { createTransactionUseCase( - amount = amountToSend.convertToAmount(cryptoCurrency), + amount = amountValue.convertToAmount(cryptoCurrency), fee = fee, memo = memo, destination = recipient, diff --git a/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml b/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml new file mode 100644 index 0000000000..05cc9e9b5b --- /dev/null +++ b/features/send/impl/src/main/res/drawable/send_hint_shape_12.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index dca890c55e..ee0cf5d41d 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -18,9 +18,11 @@ internal class ErrorsDataConverter( return when (error.code) { 2010 -> DataError.BadRequest(code = error.code) + 2200 -> DataError.SwapsAreUnavailableNowError(code = error.code) 2210 -> DataError.ExchangeProviderNotFoundError(code = error.code) 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) + 2231 -> DataError.ExchangeProviderProviderInternalError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) 2250 -> tryParseExchangeTooSmallAmountError(error = error) 2251 -> tryParseExchangeTooBigAmountError(error = error) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 21c40c5588..12e5db5e37 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -8,12 +8,16 @@ sealed class DataError { data class BadRequest(override val code: Int) : DataError() + data class SwapsAreUnavailableNowError(override val code: Int) : DataError() + data class ExchangeProviderNotFoundError(override val code: Int) : DataError() data class ExchangeProviderNotActiveError(override val code: Int) : DataError() data class ExchangeProviderNotAvailableError(override val code: Int) : DataError() + data class ExchangeProviderProviderInternalError(override val code: Int) : DataError() + data class ExchangeNotPossibleError(override val code: Int) : DataError() data class ExchangeTooSmallAmountError(override val code: Int, val amount: SwapAmount) : DataError() @@ -40,7 +44,7 @@ sealed class DataError { data class InvalidPayoutAddressError(override val code: Int = 992) : DataError() - object UnknownError : DataError() { + data object UnknownError : DataError() { override val code: Int = -1 } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt index d7f7790479..318b5635de 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -7,4 +7,6 @@ sealed class Warning { data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning() data class MinAmountWarning(val dustValue: BigDecimal) : Warning() + + data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt new file mode 100644 index 0000000000..6ba68ddd31 --- /dev/null +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.swap.domain.models.ui + +import com.tangem.feature.swap.domain.models.DataError +import java.math.BigDecimal + +sealed class SwapTransactionState { + + data class TxSent( + val fromAmount: String? = null, + val fromAmountValue: BigDecimal? = null, + val toAmount: String? = null, + val toAmountValue: BigDecimal? = null, + val txHash: String, + val txExternalUrl: String? = null, + val timestamp: Long, + ) : SwapTransactionState() + + data object UserCancelled : SwapTransactionState() + + data object BlockchainError : SwapTransactionState() + + data object TangemSdkError : SwapTransactionState() + + data object NetworkError : SwapTransactionState() + + data object UnknownError : SwapTransactionState() + + data class ExpressError(val dataError: DataError) : SwapTransactionState() +} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt deleted file mode 100644 index 14259bc672..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.swap.domain.models.ui - -import java.math.BigDecimal - -sealed class TxState { - - data class TxSent( - val fromAmount: String? = null, - val fromAmountValue: BigDecimal? = null, - val toAmount: String? = null, - val toAmountValue: BigDecimal? = null, - val txHash: String, - val txExternalUrl: String? = null, - val timestamp: Long, - ) : TxState() - - object UserCancelled : TxState() - object BlockchainError : TxState() - object TangemSdkError : TxState() - object NetworkError : TxState() - object UnknownError : TxState() -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index d3ec1058b9..dafaff9a51 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -21,7 +21,7 @@ interface SwapInteractor { * @param permissionOptions data to give permissions */ @Throws(IllegalStateException::class) - suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState + suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): SwapTransactionState /** * Find best quote for given tokens to swap @@ -51,7 +51,7 @@ interface SwapInteractor { * @param currencyToGet [Currency] * @param amountToSwap amount to swap * @param fee for tx - * @return [TxState] + * @return [SwapTransactionState] */ @Suppress("LongParameterList") @Throws(IllegalStateException::class) @@ -63,7 +63,7 @@ interface SwapInteractor { amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, - ): TxState + ): SwapTransactionState suspend fun updateQuotesStateWithSelectedFee( state: SwapState.QuotesLoadedState, @@ -79,8 +79,6 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - fun isAvailableToSwap(networkId: String): Boolean - fun getSelectedWallet(): UserWallet? suspend fun selectInitialCurrencyToSwap( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 90148abfc1..6df1eadf9d 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -197,8 +197,10 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getPairs(initialCurrency, currenciesList) } - @Deprecated("used in old swap mechanism") - override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { + override suspend fun givePermissionToSwap( + networkId: String, + permissionOptions: PermissionOptions, + ): SwapTransactionState { val derivationPath = permissionOptions.fromToken.network.derivationPath.value val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { getApproveData( @@ -228,20 +230,19 @@ internal class SwapInteractorImpl @Inject constructor( return when (result) { is SendTxResult.Success -> { allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) - TxState.TxSent( + SwapTransactionState.TxSent( txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) } - SendTxResult.UserCancelledError -> TxState.UserCancelled - is SendTxResult.BlockchainSdkError -> TxState.BlockchainError - is SendTxResult.TangemSdkError -> TxState.TangemSdkError - is SendTxResult.NetworkError -> TxState.NetworkError - is SendTxResult.UnknownError -> TxState.UnknownError + SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTxResult.NetworkError -> SwapTransactionState.NetworkError + is SendTxResult.UnknownError -> SwapTransactionState.UnknownError } } - @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, @@ -376,6 +377,7 @@ internal class SwapInteractorImpl @Inject constructor( val warnings = mutableListOf() manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken) manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) + manageReduceAmountWarning(warnings, fromTokenStatus, amount) return warnings } @@ -424,6 +426,17 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun manageReduceAmountWarning( + warnings: MutableList, + fromTokenStatus: CryptoCurrencyStatus, + amount: SwapAmount, + ) { + val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id + if (isTezos && amount.value == fromTokenStatus.value.amount) { + warnings.add(Warning.ReduceAmountWarning(TEZOS_FEE_THRESHOLD)) + } + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -432,7 +445,7 @@ internal class SwapInteractorImpl @Inject constructor( amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee, - ): TxState { + ): SwapTransactionState { return when (swapProvider.type) { ExchangeProviderType.CEX -> { val amountDecimal = toBigDecimalOrNull(amountToSwap) @@ -509,7 +522,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet: CryptoCurrency, amountToSwap: String, fee: TxFee, - ): TxState { + ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSend.decimals) val derivationPath = currencyToSend.network.derivationPath.value @@ -533,7 +546,7 @@ internal class SwapInteractorImpl @Inject constructor( return when (result) { is SendTxResult.Success -> { storeLastCryptoCurrencyId(currencyToGet) - TxState.TxSent( + SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, currencyToSend.symbol, @@ -548,11 +561,11 @@ internal class SwapInteractorImpl @Inject constructor( timestamp = System.currentTimeMillis(), ) } - SendTxResult.UserCancelledError -> TxState.UserCancelled - is SendTxResult.BlockchainSdkError -> TxState.BlockchainError - is SendTxResult.TangemSdkError -> TxState.TangemSdkError - is SendTxResult.NetworkError -> TxState.NetworkError - is SendTxResult.UnknownError -> TxState.UnknownError + SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTxResult.NetworkError -> SwapTransactionState.NetworkError + is SendTxResult.UnknownError -> SwapTransactionState.UnknownError } } @@ -564,7 +577,7 @@ internal class SwapInteractorImpl @Inject constructor( txFee: TxFee, swapProvider: SwapProvider, userWalletId: UserWalletId, - ): TxState { + ): SwapTransactionState { val exchangeData = repository.getExchangeData( fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, @@ -576,15 +589,17 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", - ).getOrNull() + ).getOrElse { return SwapTransactionState.ExpressError(it) } + + val exchangeDataCex = + exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val exchangeDataCex = exchangeData?.transaction as? ExpressTransactionModel.CEX ?: return TxState.UnknownError val txExtras = transactionManager.getMemoExtras( currencyToSend.currency.network.backendId, exchangeDataCex.txExtraId, ) if (txExtras == null && exchangeDataCex.txExtraId != null) { - return TxState.UnknownError + return SwapTransactionState.UnknownError } val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), @@ -612,11 +627,11 @@ internal class SwapInteractorImpl @Inject constructor( return result.fold( ifLeft = { when (it) { - SendTransactionError.UserCancelledError -> TxState.UserCancelled - is SendTransactionError.BlockchainSdkError -> TxState.BlockchainError - is SendTransactionError.TangemSdkError -> TxState.TangemSdkError - is SendTransactionError.NetworkError -> TxState.NetworkError - else -> TxState.UnknownError + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + else -> SwapTransactionState.UnknownError } }, ifRight = { @@ -633,7 +648,7 @@ internal class SwapInteractorImpl @Inject constructor( txExternalId = txCexModel?.externalTxId.orEmpty(), ) storeLastCryptoCurrencyId(currencyToGet.currency) - TxState.TxSent( + SwapTransactionState.TxSent( fromAmount = amountFormatter.formatSwapAmountToUI( amount, currencyToSend.currency.symbol, @@ -729,16 +744,10 @@ internal class SwapInteractorImpl @Inject constructor( ) } - @Deprecated("used in old swap mechanism") override fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount { return SwapAmount(token.value.amount ?: BigDecimal.ZERO, token.currency.decimals) } - @Deprecated("used in old swap mechanism") - override fun isAvailableToSwap(networkId: String): Boolean { - return ONE_INCH_SUPPORTED_NETWORKS.contains(networkId) - } - override suspend fun selectInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, @@ -1584,16 +1593,6 @@ internal class SwapInteractorImpl @Inject constructor( private const val INCREASE_GAS_LIMIT_BY = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - - private val ONE_INCH_SUPPORTED_NETWORKS = listOf( - "ethereum", - "binance-smart-chain", - "polygon-pos", - "optimistic-ethereum", - "arbitrum-one", - "xdai", - "avalanche", - "fantom", - ) + private val TEZOS_FEE_THRESHOLD = BigDecimal("0.01") } } \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8986bf7e0d..c78375b5e4 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -67,4 +67,5 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 32b1388ef6..e5db542bba 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -20,6 +20,7 @@ data class SwapStateHolder( val alert: SwapWarning.GenericWarning? = null, val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, + val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01 val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, @@ -108,7 +109,6 @@ sealed interface SwapWarning { val title: TextReference? = null, val message: TextReference? = null, val type: GenericWarningType = GenericWarningType.OTHER, - val shouldWrapMessage: Boolean = false, val onClick: () -> Unit, ) : SwapWarning data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning @@ -116,6 +116,8 @@ sealed interface SwapWarning { data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning + data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning + data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index e7641eca69..bd84a05cac 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee data class UiActions( @@ -13,6 +14,8 @@ data class UiActions( val onChangeCardsClicked: () -> Unit, val onBackClicked: () -> Unit, val onMaxAmountSelected: () -> Unit, + val onReduceAmount: (SwapAmount) -> Unit, + val onReduceAmountIgnoreClick: () -> Unit, val openPermissionBottomSheet: () -> Unit, val onChangeApproveType: (ApproveType) -> Unit, // region new actions diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 1761af22d7..eaf4ddfa6b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -88,6 +88,7 @@ internal class StateBuilder( onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, + reduceAmountIgnore = false, priceImpact = PriceImpact.Empty(), ) } @@ -213,7 +214,11 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = getWarningsForSuccessState(quoteModel, fromToken) + val warnings = getWarningsForSuccessState( + quoteModel = quoteModel, + fromToken = fromToken, + ignoreAmountReduce = uiStateHolder.reduceAmountIgnore, + ) val feeState = createFeeState(quoteModel.txFee, selectedFeeType) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus @@ -311,41 +316,28 @@ internal class StateBuilder( private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + ignoreAmountReduce: Boolean, ): List { val warnings = mutableListOf() - addDomainWarnings(quoteModel, warnings) - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && - quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest - ) { - warnings.add( - SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol), - ), - ) - } - when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig(), - ), - ) - else -> Unit - } - addUnableCoverFeeWarning(quoteModel, fromToken, warnings) - // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (!quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included - ) { - warnings.add(SwapWarning.InsufficientFunds) - } + maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce) + maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) + maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken) + maybeAddNetworkFeeCoverageWarning(quoteModel, warnings) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) + maybeAddInsufficientFundsWarning(quoteModel, warnings) + maybeAddTransactionInProgressWarning(quoteModel, warnings) + return warnings + } + private fun maybeAddTransactionInProgressWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { warnings.add( SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.swapping_pending_transaction_title), - description = resourceReference(R.string.swapping_pending_transaction_subtitle), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { @@ -361,10 +353,13 @@ internal class StateBuilder( ), ) } - return warnings } - private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { + private fun maybeAddDomainWarnings( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ignoreAmountReduce: Boolean, + ) { quoteModel.warnings.forEach { when (it) { is Warning.ExistentialDepositWarning -> { @@ -390,7 +385,7 @@ internal class StateBuilder( NotificationConfig( title = resourceReference(R.string.send_notification_invalid_amount_title), subtitle = resourceReference( - R.string.send_notification_invalid_minimum_amount_text, + R.string.warning_express_dust_message, wrappedList( it.dustValue.toPlainString(), it.dustValue.toPlainString(), @@ -401,11 +396,84 @@ internal class StateBuilder( ), ) } + is Warning.ReduceAmountWarning -> { + if (!ignoreAmountReduce) { + warnings.add( + SwapWarning.ReduceAmount( + notificationConfig = createReduceAmountNotificationConfig( + amount = it.tezosFeeThreshold.toPlainString(), + onConfirmClick = { + val fromAmount = quoteModel.fromTokenInfo.tokenAmount + val patchedAmount = fromAmount.copy( + value = fromAmount.value - it.tezosFeeThreshold, + ) + actions.onReduceAmount(patchedAmount) + }, + onDismissClick = actions.onReduceAmountIgnoreClick, + ), + ), + ) + } + } } } } - private fun addUnableCoverFeeWarning( + private fun maybeAddNeedReserveToCreateAccountWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + warnings.add( + SwapWarning.NeedReserveToCreateAccount( + notificationConfig = createActivateAccountNotificationConfig( + status.amountToCreateAccount, + quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.name, + ), + ), + ) + } + } + } + + private fun maybeAddPermissionNeededWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + fromToken: CryptoCurrency, + ) { + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + warnings.add( + SwapWarning.PermissionNeeded( + createPermissionNotificationConfig(fromToken.symbol), + ), + ) + } + } + + private fun maybeAddNetworkFeeCoverageWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig(), + ), + ) + else -> Unit + } + } + + private fun maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, warnings: MutableList, @@ -428,7 +496,29 @@ internal class StateBuilder( } } + private fun maybeAddInsufficientFundsWarning( + quoteModel: SwapState.QuotesLoadedState, + warnings: MutableList, + ) { + // check isBalanceEnough, but for dex includeFeeInAmount always Excluded + if (!quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included + ) { + warnings.add(SwapWarning.InsufficientFunds) + } + } + private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + + if (amount < amountToCreateAccount) { + return false + } + } + val preparedSwapConfigState = quoteModel.preparedSwapConfigState // check has has outgoing transaction if (preparedSwapConfigState.hasOutgoingTransaction) return false @@ -587,7 +677,7 @@ internal class StateBuilder( subtitle = if (dataError is DataError.UnknownError) { resourceReference(R.string.common_unknown_error) } else { - resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) + resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString())) }, iconResId = R.drawable.img_attention_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( @@ -774,7 +864,7 @@ internal class StateBuilder( SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)), + subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = TextReference.Res(R.string.warning_button_refresh), @@ -823,8 +913,8 @@ internal class StateBuilder( warnings.add( 0, SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.swapping_pending_transaction_title), - description = resourceReference(R.string.swapping_pending_transaction_subtitle), + title = resourceReference(R.string.warning_express_approval_in_progress_title), + description = resourceReference(R.string.warning_express_approval_in_progress_message), ), ) return uiState.copy( @@ -836,7 +926,7 @@ internal class StateBuilder( @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, - txState: TxState.TxSent, + swapTransactionState: SwapTransactionState.TxSent, dataState: SwapProcessDataState, onExploreClick: () -> Unit, onStatusClick: () -> Unit, @@ -845,8 +935,8 @@ internal class StateBuilder( val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) - val fromAmount = txState.fromAmountValue ?: BigDecimal.ZERO - val toAmount = txState.toAmountValue ?: BigDecimal.ZERO + val fromAmount = swapTransactionState.fromAmountValue ?: BigDecimal.ZERO + val toAmount = swapTransactionState.toAmountValue ?: BigDecimal.ZERO val providerState = uiState.providerState as ProviderState.Content val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) @@ -854,7 +944,7 @@ internal class StateBuilder( return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = txState.timestamp, + timestamp = swapTransactionState.timestamp, txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), @@ -862,8 +952,8 @@ internal class StateBuilder( providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), - fromTokenAmount = stringReference(txState.fromAmount.orEmpty()), - toTokenAmount = stringReference(txState.toAmount.orEmpty()), + fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), + toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), toTokenFiatAmount = stringReference(toFiatAmount), fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), @@ -874,17 +964,50 @@ internal class StateBuilder( ) } - fun createErrorTransaction(uiState: SwapStateHolder, txState: TxState, onAlertClick: () -> Unit): SwapStateHolder { + fun createErrorTransaction( + uiState: SwapStateHolder, + swapTransactionState: SwapTransactionState, + onAlertClick: () -> Unit, + ): SwapStateHolder { return uiState.copy( alert = SwapWarning.GenericWarning( - message = null, + message = if (swapTransactionState is SwapTransactionState.ExpressError) { + getAlertErrorMessage(swapTransactionState.dataError) + } else { + null + }, onClick = onAlertClick, - type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER, + type = if (swapTransactionState is SwapTransactionState.NetworkError) { + GenericWarningType.NETWORK + } else { + GenericWarningType.OTHER + }, ), changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } + private fun getAlertErrorMessage(dataError: DataError): TextReference? { + return when (dataError) { + is DataError.SwapsAreUnavailableNowError -> resourceReference( + id = R.string.express_error_swap_unavailable, + formatArgs = wrappedList(dataError.code), + ) + is DataError.ExchangeNotPossibleError -> resourceReference( + id = R.string.express_error_provider_unavailable, + formatArgs = wrappedList(dataError.code), + ) + is DataError.ExchangeProviderNotActiveError, + is DataError.ExchangeProviderNotAvailableError, + is DataError.ExchangeProviderProviderInternalError, + -> resourceReference( + id = R.string.express_error_swap_pair_unavailable, + formatArgs = wrappedList(dataError.code), + ) + else -> null + } + } + fun createAlert( uiState: SwapStateHolder, isPriceImpact: Boolean, @@ -928,17 +1051,11 @@ internal class StateBuilder( fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null) - fun addWarning( - uiState: SwapStateHolder, - message: TextReference?, - shouldWrapMessage: Boolean = false, - onClick: () -> Unit, - ): SwapStateHolder { + fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList() renewWarnings.add( SwapWarning.GenericWarning( message = message, - shouldWrapMessage = shouldWrapMessage, onClick = onClick, ), ) @@ -1217,6 +1334,35 @@ internal class StateBuilder( ) } + private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig { + return NotificationConfig( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$amount $token"), + ), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + iconResId = R.drawable.img_attention_20, + ) + } + + private fun createReduceAmountNotificationConfig( + amount: String, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit, + ): NotificationConfig { + return NotificationConfig( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), + onPrimaryClick = onConfirmClick, + secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore), + onSecondaryClick = onDismissClick, + ), + ) + } + private fun createUnableToCoverFeeNotificationConfig( fromToken: CryptoCurrency, feeCurrency: CryptoCurrency?, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 6c31dea226..fa48e3f6fb 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -337,14 +337,9 @@ private fun SwapWarnings(warnings: List) { ) } is SwapWarning.GenericWarning -> { - val message = warning.message?.let { - if (warning.shouldWrapMessage) { - String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference()) - } else { - it.resolveReference() - } - } ?: stringResource(id = R.string.common_unknown_error) - RefreshableWaringCard( + val message = warning.message?.resolveReference() + ?: stringResource(id = R.string.common_unknown_error) + RefreshableWarningCard( title = stringResource(id = R.string.common_warning), description = message, onClick = warning.onClick, @@ -377,6 +372,16 @@ private fun SwapWarnings(warnings: List) { iconTint = TangemTheme.colors.icon.accent, ) } + is SwapWarning.NeedReserveToCreateAccount -> { + Notification( + config = warning.notificationConfig, + ) + } + is SwapWarning.ReduceAmount -> { + Notification( + config = warning.notificationConfig, + ) + } is SwapWarning.TransactionInProgressWarning -> { CardWithIcon( title = warning.title.resolveReference(), @@ -499,6 +504,7 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + reduceAmountIgnore = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index d9a604c6d4..bd77a3f66e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -283,7 +283,7 @@ private fun Content( if (amountEquivalent != null) { if (type is TransactionCardType.ReadOnly) { - Row { + Row(modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20)) { if (priceImpact is PriceImpact.Value) { Text( text = makePriceImpactBalanceWarning( @@ -293,7 +293,6 @@ private fun Content( color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size20) .align(Alignment.CenterVertically), ) } else { @@ -302,7 +301,6 @@ private fun Content( text = it, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), ) } } @@ -564,7 +562,7 @@ private fun TransactionCardPreview() { @Suppress("MagicNumber") private fun TransactionCardPreviewWithPriceImpact() { TransactionCard( - type = TransactionCardType.ReadOnly(), + type = TransactionCardType.ReadOnly(showWarning = true), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index d30242c9f2..d9598de96f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* @@ -502,7 +503,7 @@ internal class SwapViewModel @Inject constructor( ) }.onSuccess { when (it) { - is TxState.TxSent -> { + is SwapTransactionState.TxSent -> { sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) val url = blockchainInteractor.getExplorerTransactionLink( networkId = fromCurrency.currency.network.backendId, @@ -511,7 +512,7 @@ internal class SwapViewModel @Inject constructor( updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, - txState = it, + swapTransactionState = it, dataState = dataState, txUrl = url, onExploreClick = { @@ -536,7 +537,7 @@ internal class SwapViewModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } - is TxState.UserCancelled -> { + is SwapTransactionState.UserCancelled -> { startLoadingQuotesFromLastState() } else -> { @@ -603,14 +604,14 @@ internal class SwapViewModel @Inject constructor( ) }.onSuccess { when (it) { - is TxState.TxSent -> { + is SwapTransactionState.TxSent -> { sendApproveSuccessEvent(fromToken, feeForPermission.feeType, approveType) updateWalletBalance() uiState = stateBuilder.loadingPermissionState(uiState) uiState = stateBuilder.dismissBottomSheet(uiState) startLoadingQuotesFromLastState(isSilent = true) } - is TxState.UserCancelled -> Unit + is SwapTransactionState.UserCancelled -> Unit else -> { uiState = stateBuilder.createErrorTransaction(uiState, it) { uiState = stateBuilder.clearAlert(uiState) @@ -812,6 +813,10 @@ internal class SwapViewModel @Inject constructor( } } + private fun onReduceAmountClicked(newAmount: SwapAmount) { + onAmountChanged(newAmount.formatToUIRepresentation()) + } + @Suppress("UnusedPrivateMember") private fun onAmountSelected(selected: Boolean) { if (selected) { @@ -874,7 +879,14 @@ internal class SwapViewModel @Inject constructor( } onSearchEntered("") }, - onMaxAmountSelected = { onMaxAmountClicked() }, + onMaxAmountSelected = ::onMaxAmountClicked, + onReduceAmount = ::onReduceAmountClicked, + onReduceAmountIgnoreClick = { + uiState = uiState.copy( + reduceAmountIgnore = true, + warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount }, + ) + }, openPermissionBottomSheet = { singleTaskScheduler.cancelTask() analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked) @@ -1152,11 +1164,11 @@ internal class SwapViewModel @Inject constructor( ) } - companion object { - private const val loggingTag = "SwapViewModel" - private const val INITIAL_AMOUNT = "" - private const val UPDATE_DELAY = 10000L - private const val DEBOUNCE_AMOUNT_DELAY = 1000L - private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L + private companion object { + const val loggingTag = "SwapViewModel" + const val INITIAL_AMOUNT = "" + const val UPDATE_DELAY = 10000L + const val DEBOUNCE_AMOUNT_DELAY = 1000L + const val UPDATE_BALANCE_DELAY_MILLIS = 11000L } } \ No newline at end of file diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt new file mode 100644 index 0000000000..d64095a963 --- /dev/null +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/featuretoggles/TokenDetailsFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tokendetails.featuretoggles + +interface TokenDetailsFeatureToggles { + fun isGenerateXPubEnabled(): Boolean +} \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 81f6868424..6db0120517 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -53,10 +53,14 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) + implementation(projects.core.featuretoggles) + + implementation(projects.libs.crypto) /** Domain modules */ implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) implementation(projects.domain.models) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt new file mode 100644 index 0000000000..21b9d911cb --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsFeatureTogglesModule.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tokendetails.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.feature.tokendetails.featuretoggles.DefaultTokenDetailsFeatureToggles +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokenDetailsFeatureTogglesModule { + + @Provides + @Singleton + fun provideTokenDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokenDetailsFeatureToggles { + return DefaultTokenDetailsFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt new file mode 100644 index 0000000000..1df3fcf7be --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/featuretoggles/DefaultTokenDetailsFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tokendetails.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles + +internal class DefaultTokenDetailsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TokenDetailsFeatureToggles { + override fun isGenerateXPubEnabled() = featureTogglesManager.isFeatureEnabled(name = "GENERATE_XPUB_ENABLED") +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 00693c486c..cadf3b9549 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -24,6 +24,10 @@ internal class DefaultTokenDetailsRouter( reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) } + override fun share(text: String) { + reduxNavController.navigate(NavigationAction.Share(text)) + } + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { reduxNavController.navigate( action = NavigationAction.NavigateTo( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index 0c870f833a..56eead22a5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -12,5 +12,8 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter { /** Open website by [url] */ fun openUrl(url: String) + /** Share [text] */ + fun share(text: String) + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 669c2e24f6..a33cc576e3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -165,7 +165,7 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { object TopUpWithoutReserve : Informational( title = resourceReference(id = R.string.warning_no_account_title), - subtitle = resourceReference(id = R.string.no_account_bnb), + subtitle = resourceReference(id = R.string.no_account_send_to_create), ) class HasPendingTransactions(val coinSymbol: String) : Informational( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 30d410450e..dbc7ff632b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -5,20 +5,25 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, + private val featureToggles: TokenDetailsFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -27,7 +32,7 @@ internal class TokenDetailsSkeletonStateConverter( return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, - tokenDetailsAppBarMenuConfig = createMenu(), + tokenDetailsAppBarMenuConfig = createMenu(value), ), tokenInfoBlockState = TokenInfoBlockState( name = value.name, @@ -60,14 +65,21 @@ internal class TokenDetailsSkeletonStateConverter( ) } - private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( - items = persistentListOf( + private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + items = buildList { + if (featureToggles.isGenerateXPubEnabled() && isBitcoin(cryptoCurrency.network.id.value)) { + TokenDetailsAppBarMenuConfig.MenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColorProvider = { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onGenerateExtendedKey, + ).let(::add) + } TokenDetailsAppBarMenuConfig.MenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, - ), - ), + ).let(::add) + }.toImmutableList(), ) private fun createButtons(): ImmutableList { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index c0e14ea621..06b0c838c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -11,6 +11,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver @@ -32,9 +33,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.t import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -44,12 +45,16 @@ internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, + private val featureToggles: TokenDetailsFeatureToggles, symbol: String, decimals: Int, ) { private val skeletonStateConverter by lazy { - TokenDetailsSkeletonStateConverter(clickIntents = clickIntents) + TokenDetailsSkeletonStateConverter( + clickIntents = clickIntents, + featureToggles = featureToggles, + ) } private val notificationConverter by lazy { @@ -282,12 +287,12 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithUpdatedMenu(cardTypesResolver: CardTypesResolver): TokenDetailsState { + fun getStateWithUpdatedMenu(cardTypesResolver: CardTypesResolver, isBitcoin: Boolean): TokenDetailsState { return with(currentStateProvider()) { copy( topAppBarConfig = topAppBarConfig.copy( tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig - ?.updateMenu(cardTypesResolver), + ?.updateMenu(cardTypesResolver, isBitcoin), ), ) } @@ -295,17 +300,25 @@ internal class TokenDetailsStateFactory( private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, + isBitcoin: Boolean, ): TokenDetailsAppBarMenuConfig? { if (cardTypesResolver.isSingleWalletWithToken()) return null - + val showGenerateExtendedKey = featureToggles.isGenerateXPubEnabled() && isBitcoin return copy( - items = persistentListOf( + items = buildList { + if (showGenerateExtendedKey) { + TokenDetailsAppBarMenuConfig.MenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColorProvider = { TangemTheme.colors.text.primary1 }, + onClick = clickIntents::onGenerateExtendedKey, + ).let(::add) + } TokenDetailsAppBarMenuConfig.MenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, - ), - ), + ).let(::add) + }.toImmutableList(), ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt index c08898d2dc..173e79dc30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeProvider.kt @@ -67,7 +67,7 @@ internal fun ExchangeProvider( ) Text( modifier = Modifier.align(Alignment.CenterVertically), - text = "ID: $providerTxId", + text = stringResource(R.string.express_transaction_id, providerTxId), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index d049e3bf31..712afdeb80 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -47,4 +47,6 @@ interface TokenDetailsClickIntents { fun onSwapPromoDismiss() fun onSwapPromoClick() + + fun onGenerateExtendedKey() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index c5d482b009..f4c38aca5c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -18,6 +18,7 @@ import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.card.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder @@ -50,8 +51,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTrans import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import dagger.hilt.android.lifecycle.HiltViewModel @@ -85,6 +88,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, + private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -92,6 +96,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, + featureToggles: TokenDetailsFeatureToggles, deepLinksRegistry: DeepLinksRegistry, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -121,6 +126,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, + featureToggles = featureToggles, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -174,9 +180,10 @@ internal class TokenDetailsViewModel @Inject constructor( status = cryptoCurrencyStatus ?: return, transactionInfo = data.let { TransactionInfo( - amount = it.baseCurrencyAmount, transactionId = it.transactionId, destinationAddress = it.depositWalletAddress, + amount = it.baseCurrencyAmount, + tag = it.depositWalletAddressTag, ) }, ) @@ -358,7 +365,10 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { val wallet = getUserWalletUseCase(userWalletId).getOrElse { return@launch } - uiState = stateFactory.getStateWithUpdatedMenu(wallet.scanResponse.cardTypesResolver) + uiState = stateFactory.getStateWithUpdatedMenu( + cardTypesResolver = wallet.scanResponse.cardTypesResolver, + isBitcoin = isBitcoin(cryptoCurrency.network.id.value), + ) } } @@ -495,6 +505,24 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onGenerateExtendedKey() { + viewModelScope.launch(dispatchers.main) { + val extendedKey = getExtendedPublicKeyForCurrencyUseCase( + userWalletId, + cryptoCurrency.network.derivationPath, + ).fold( + ifLeft = { + Timber.e(it.cause?.localizedMessage.orEmpty()) + "" + }, + ifRight = { it }, + ) + if (extendedKey.isNotBlank()) { + router.share(extendedKey) + } + } + } + override fun onSellClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonSell(cryptoCurrency.symbol)) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index db4d102187..6b46d5c5dc 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -56,6 +56,8 @@ dependencies { implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) + implementation(projects.libs.crypto) + /** Domain modules */ implementation(projects.domain.card) implementation(projects.domain.demo) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt index 0248645642..59e2022b58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -84,6 +84,7 @@ internal class WalletDeepLinksHandler @Inject constructor( amount = it.baseCurrencyAmount, destinationAddress = it.depositWalletAddress, transactionId = it.transactionId, + tag = it.depositWalletAddressTag, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 088622ed27..495f7edd82 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -23,7 +23,7 @@ sealed class WalletScreenAnalyticsEvent { override val oneTimeEventId: String = id + userWalletId.stringValue } - object WalletOpened : Basic(event = "Wallet Opened") + data object WalletOpened : Basic(event = "Wallet Opened") class CardWasScanned(source: AnalyticsParam.ScannedFrom) : Basic( event = "Card Was Scanned", @@ -53,8 +53,8 @@ sealed class WalletScreenAnalyticsEvent { params: Map = mapOf(), ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { - object ScreenOpened : MainScreen(event = "Screen opened") - object WalletSwipe : MainScreen(event = "Wallet Swipe") + data object ScreenOpened : MainScreen(event = "Screen opened") + data object WalletSwipe : MainScreen(event = "Wallet Swipe") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", @@ -66,37 +66,39 @@ sealed class WalletScreenAnalyticsEvent { params = mapOf("Result" to result.value), ) - object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") - object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") - object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") - object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") + data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") + data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") + data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") + data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") - object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable") + data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable") - object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") + data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") - object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") + data object CardSignedTransactions : MainScreen(event = "Notice - Card Signed Transactions") - object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") + data object HowDoYouLikeTangem : MainScreen(event = "Notice - How Do You Like Tangem") - object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") + data object ProductSampleCard : MainScreen(event = "Notice - Product Sample Card") - object TestnetCard : MainScreen(event = "Notice - Testnet Card") + data object TestnetCard : MainScreen(event = "Notice - Testnet Card") - object DemoCard : MainScreen(event = "Notice - Demo Card") + data object DemoCard : MainScreen(event = "Notice - Demo Card") - object DevelopmentCard : MainScreen(event = "Notice - Development Card") + data object DevelopmentCard : MainScreen(event = "Notice - Development Card") - object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") + data object WalletUnlock : MainScreen(event = "Notice - Wallet Unlock") - object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") + data object BackupYourWallet : MainScreen(event = "Notice - Backup Your Wallet") - object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") + data object BackupError : MainScreen(event = "Notice - Backup Error") - object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") + data object UnlockAllWithBiometrics : MainScreen(event = "Button - Unlock All With Biometrics") - object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + data object UnlockWithCardScan : MainScreen(event = "Button - Unlock With Card Scan") - object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") + data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped") + + data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 899dd5a09a..e80fc87a24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -43,6 +43,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem + is WalletNotification.Critical.BackupError -> MainScreen.BackupError is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt new file mode 100644 index 0000000000..caa4c9fd87 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.common.configs.CardConfig +import com.tangem.domain.models.scan.CardDTO +import javax.inject.Inject + +class BackupValidator @Inject constructor() { + + fun isValid(cardDTO: CardDTO): Boolean { + return validateBackupStatus(cardDTO) && validateCurves(cardDTO) + } + + private fun validateCurves(cardDTO: CardDTO): Boolean { + val config = CardConfig.createConfig(cardDTO) + // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, + // we cannot determine whether this curve is missing due to an error or because the user + // did not want to recreate the wallet. + val expectedCurves = config.mandatoryCurves + .filterNot { it == EllipticCurve.Bls12381G2Aug } + val curves = cardDTO.wallets.map { it.curve } + for (expectedCurve in expectedCurves) { + val cardCurvesCount = curves.count { it == expectedCurve } + + // missing curve + if (cardCurvesCount == 0) { + return false + } + + // duplicated curve + if (cardCurvesCount > 1) { + return false + } + } + return true + } + + private fun validateBackupStatus(cardDTO: CardDTO): Boolean { + val backupStatus = cardDTO.backupStatus + return backupStatus != null && backupStatus !is CardDTO.BackupStatus.CardLinked + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 54bfd1bb5a..7432cbb84e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -37,6 +37,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val promoRepository: PromoRepository, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, ) { private var readyForRateAppNotification = false @@ -57,7 +58,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( buildList { addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) - addCriticalNotifications(cardTypesResolver) + addCriticalNotifications(userWallet) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -85,7 +86,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + private fun MutableList.addCriticalNotifications(userWallet: UserWallet) { + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotification.Critical.BackupError, + condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + addIf( element = WalletNotification.Critical.DevCard, condition = !cardTypesResolver.isReleaseFirmwareType(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt index bfabae7770..1dd961ab14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/BalancesAndLimitsBottomSheetConfig.kt @@ -19,7 +19,7 @@ internal data class BalancesAndLimitsBottomSheetConfig( data class Limit( val availableBy: String, - val inStore: String, + val total: String, val other: String, val singleTransaction: String, val onInfoClick: () -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 2b1df72669..b8aecf2d9f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -48,19 +48,32 @@ internal sealed interface WalletAlertState { override val isWarningConfirmButton: Boolean = true } - object WrongCardIsScanned : Basic() { + data class VisaLimitsInfo( + val totalLimit: String, + val otherLimit: String, + ) : Basic() { + override val title: TextReference? = null + override val message: TextReference = stringReference( + value = "Limits are needed to control costs, improve security, manage risk. " + + "You can spend $totalLimit during the week for card payments in shops and " + + "$otherLimit for other transactions, e. g. subscriptions or debts.", + ) + override val onConfirmClick: (() -> Unit)? = null + } + + data object WrongCardIsScanned : Basic() { override val title: TextReference = resourceReference(R.string.common_warning) override val message: TextReference = resourceReference(R.string.error_wrong_wallet_tapped) override val onConfirmClick: (() -> Unit)? = null } - object RescanWallets : Basic() { + data object RescanWallets : Basic() { override val title: TextReference = resourceReference(R.string.common_attention) override val message: TextReference = resourceReference(R.string.key_invalidated_warning_description) override val onConfirmClick: (() -> Unit)? = null } - object VisaBalancesInfo : Basic() { + data object VisaBalancesInfo : Basic() { override val title: TextReference? = null override val message: TextReference = stringReference( value = "Available balance is actual funds available, considering pending transactions, " + @@ -68,14 +81,4 @@ internal sealed interface WalletAlertState { ) override val onConfirmClick: (() -> Unit)? = null } - - object VisaLimitsInfo : Basic() { - override val title: TextReference? = null - override val message: TextReference = stringReference( - value = "Limits are needed to control costs, improve security, manage risk. " + - "You can spend 1 000 USDT during the week for card payments in shops and " + - "100 USDT for other transactions, e. g. subscriptions or debts.", - ) - override val onConfirmClick: (() -> Unit)? = null - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt index d6ec5fcc93..adfede4a1c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt @@ -36,7 +36,10 @@ sealed class WalletBottomSheetConfig( ), iconResId = R.drawable.ic_locked_24, primaryButtonConfig = ButtonConfig( - text = resourceReference(id = R.string.user_wallet_list_unlock_all), + text = resourceReference( + id = R.string.user_wallet_list_unlock_all_with, + formatArgs = wrappedList(resourceReference(R.string.common_biometrics)), + ), onClick = onUnlockClick, ), secondaryButtonConfig = ButtonConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e7d25584d5..9f7f5ea139 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -36,6 +36,11 @@ sealed class WalletNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.warning_failed_to_verify_card_title), subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), ) + + data object BackupError : Critical( + title = resourceReference(R.string.warning_backup_errors_title), + subtitle = resourceReference(R.string.warning_backup_errors_message), + ) } sealed class Warning( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index f43ec73629..146e196a65 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -21,6 +21,9 @@ internal class BalancesAndLimitsBottomSheetConverter( decimals = value.decimals, ) + val otpLimit = value.limits.remainingOtp.let(::formatAmount) + val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount) + return BalancesAndLimitsBottomSheetConfig( balance = BalancesAndLimitsBottomSheetConfig.Balance( totalBalance = value.balances.total.let(::formatAmount), @@ -33,10 +36,10 @@ internal class BalancesAndLimitsBottomSheetConverter( ), limit = BalancesAndLimitsBottomSheetConfig.Limit( availableBy = DateTimeFormatters.formatDate(date = value.limits.expirationDate), - inStore = value.limits.remainingOtp.let(::formatAmount), - other = value.limits.remainingNoOtp.let(::formatAmount), + total = otpLimit, + other = noOtpLimit, singleTransaction = value.limits.singleTransaction.let(::formatAmount), - onInfoClick = this::showLimitInfo, + onInfoClick = { showLimitInfo(otpLimit, noOtpLimit) }, ), ) } @@ -45,7 +48,7 @@ internal class BalancesAndLimitsBottomSheetConverter( eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaBalancesInfo)) } - private fun showLimitInfo() { - eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo)) + private fun showLimitInfo(totalLimit: String, otherLimit: String) { + eventSender.send(WalletEvent.ShowAlert(WalletAlertState.VisaLimitsInfo(totalLimit, otherLimit))) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index f431a74a5e..6e8158da13 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -27,14 +28,14 @@ internal class VisaTxDetailsBottomSheetConverter( private fun createTransaction(details: VisaTxDetails): VisaTxDetailsBottomSheetConfig.Transaction { return VisaTxDetailsBottomSheetConfig.Transaction( id = details.id, - type = details.type, - status = details.status, + type = details.type.capitalize(), + status = details.status.capitalize(), blockchainAmount = formatNetworkAmount(details.blockchainAmount), blockchainFee = formatNetworkAmount(details.blockchainFee), transactionAmount = formatFiatAmount(details.transactionAmount, details.fiatCurrency), transactionCurrencyCode = details.transactionCurrencyCode.toString(), - merchantName = details.merchantName ?: UNKNOWN, - merchantCity = details.merchantCity ?: UNKNOWN, + merchantName = details.merchantName?.capitalize() ?: UNKNOWN, + merchantCity = details.merchantCity?.capitalize() ?: UNKNOWN, merchantCountryCode = details.merchantCountryCode ?: UNKNOWN, merchantCategoryCode = details.merchantCategoryCode ?: UNKNOWN, ) @@ -46,16 +47,16 @@ internal class VisaTxDetailsBottomSheetConverter( return VisaTxDetailsBottomSheetConfig.Request( id = request.id, - type = request.requestType, - status = request.requestStatus, + type = request.requestType.capitalize(), + status = request.requestStatus.capitalize(), blockchainAmount = formatNetworkAmount(request.blockchainAmount), blockchainFee = formatNetworkAmount(request.blockchainFee), transactionAmount = formatFiatAmount(request.transactionAmount, request.fiatCurrency), currencyCode = request.billingCurrencyCode.toString(), errorCode = request.errorCode, - date = DateTimeFormatters.formatDate(DateTimeFormatters.dateTimeFormatter, date = localDate), + date = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.dateTimeFormatter), txHash = request.txHash ?: UNKNOWN, - txStatus = request.txStatus ?: UNKNOWN, + txStatus = request.txStatus?.capitalize() ?: UNKNOWN, onExploreClick = if (exploreUrl != null) { { clickIntents.onExploreClick(exploreUrl) } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index bddc7ec7aa..5c1b0a20c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters @@ -18,8 +19,8 @@ internal class VisaTxHistoryItemStateConverter( override fun convert(value: VisaTxHistoryItem): TransactionState { val localDate = value.date.withZone(DateTimeZone.getDefault()) - val time = DateTimeFormatters.formatTime(time = localDate) - val subtitle = "$time • ${value.status}" + val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter) + val subtitle = "$time • ${value.status.capitalize()}" return TransactionState.Content( txHash = value.id, @@ -37,7 +38,7 @@ internal class VisaTxHistoryItemStateConverter( status = TransactionState.Content.Status.Confirmed, direction = TransactionState.Content.Direction.INCOMING, iconRes = R.drawable.ic_arrow_up_24, - title = stringReference(value = value.merchantName ?: "Unknown merchant"), + title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"), subtitle = stringReference(subtitle), timestamp = localDate.millis, onClick = { clickIntents.onVisaTransactionClick(value.id) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt index 552dbba1a5..9f6c2b8eb2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt @@ -42,6 +42,7 @@ internal class VisaWalletSubscriber( setLoadedCurrencyState(maybeCurrency) val currency = maybeCurrency.getOrElse { + Timber.e(it, "Failed to load VISA currency") setFailedTxHistoryState(it) return@flow } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt index 6fe4af13e5..332d7e3209 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBlock.kt @@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,7 +63,6 @@ private fun BalancesAndLimitsBlock(state: BalancesAndLimitsBlockState, modifier: ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private inline fun ContentContainer( enabled: Boolean, @@ -147,7 +149,7 @@ private fun AvailableLimit(availableBalance: String, limitDays: Int, modifier: M color = TangemTheme.colors.text.primary1, ) Text( - text = "available $limitDays-day limit", + text = "available for $limitDays day(s)", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt index c2e6a39b4d..b4a5b96ffc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/BalancesAndLimitsBottomSheet.kt @@ -94,8 +94,8 @@ private fun LimitsBlock(limits: BalancesAndLimitsBottomSheetConfig.Limit, modifi title = stringReference("Limits"), content = { BlockItem( - title = stringReference("In-store (otp)"), - value = limits.inStore, + title = stringReference("Total"), + value = limits.total, ) BlockItem( title = stringReference("Other (no-otp)"), @@ -193,7 +193,7 @@ private class BalancesAndLimitsBottomSheetParameterProvider : ), limit = BalancesAndLimitsBottomSheetConfig.Limit( availableBy = "Nov, 11 USDT", - inStore = "563.00 USDT", + total = "563.00 USDT", other = "100.00 USDT", singleTransaction = "100.00 USDT", onInfoClick = {}, diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 64ca08576f..a8f85f8c06 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -61,7 +61,6 @@ okhttp = "4.9.3" rekotlin = "1.0.4" retrofit = "2.8.1" retrofitMoshiConverter = "2.6.0" -shopifyBuySdk = "12.0.0" spongycastleCryptoCore = "1.58.0.0" timber = "4.7.1" viewBindingDelegate = "1.5.9" @@ -82,12 +81,13 @@ listenableFuture = "1.0" swipeRefreshLayout = "1.1.0" spr-client = "3.6.2" web3j = "4.10.1" +leakcanary = "2.13" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.8-536" +tangemBlockchainSdk = "develop-547" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.8-338" +tangemCardSdk = "develop-339" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem @@ -223,7 +223,6 @@ moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } -shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } @@ -249,4 +248,5 @@ camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "and camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "androidXCamera" } camera-view = { module = "androidx.camera:camera-view", version.ref = "androidXCamera" } web3j-core = { module = "org.web3j:core", version.ref = "web3j" } +leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } # endregion Other diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 1e7a2e6a6d..a6e7acf14d 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -26,4 +26,10 @@ object BlockchainUtils { null } } + + /** If current [networkId] is Bitcoin */ + fun isBitcoin(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet + } } \ No newline at end of file diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java b/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java index 7b92736361..fb6048f082 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/ERC20.java @@ -2,10 +2,7 @@ package com.tangem.lib.visa; import org.web3j.abi.EventEncoder; import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Event; -import org.web3j.abi.datatypes.Function; -import org.web3j.abi.datatypes.Utf8String; +import org.web3j.abi.datatypes.*; import org.web3j.abi.datatypes.generated.Uint256; import org.web3j.abi.datatypes.generated.Uint8; import org.web3j.crypto.Credentials; @@ -32,10 +29,10 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class ERC20 extends Contract { @@ -113,6 +110,16 @@ class ERC20 extends Contract { return typedResponse; } + @Deprecated + public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + public static List getTransferEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(TRANSFER_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -127,16 +134,6 @@ class ERC20 extends Contract { return responses; } - @Deprecated - public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC20(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - @Deprecated - public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new ERC20(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - public static TransferEventResponse getTransferEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSFER_EVENT, log); TransferEventResponse typedResponse = new TransferEventResponse(); @@ -147,14 +144,6 @@ class ERC20 extends Contract { return typedResponse; } - public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new ERC20(contractAddress, web3j, credentials, contractGasProvider); - } - - public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider); - } - public Flowable transferEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getTransferEventFromLog(log)); } @@ -165,6 +154,14 @@ class ERC20 extends Contract { return transferEventFlowable(filter); } + public static ERC20 load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new ERC20(contractAddress, web3j, credentials, contractGasProvider); + } + + public static ERC20 load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new ERC20(contractAddress, web3j, transactionManager, contractGasProvider); + } + public Flowable approvalEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getApprovalEventFromLog(log)); } diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java index f712f5cb24..b612de84bc 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemBridgeProcessor.java @@ -3,10 +3,7 @@ package com.tangem.lib.visa; import org.web3j.abi.EventEncoder; import org.web3j.abi.FunctionEncoder; import org.web3j.abi.TypeReference; -import org.web3j.abi.datatypes.Address; -import org.web3j.abi.datatypes.Bool; -import org.web3j.abi.datatypes.Event; -import org.web3j.abi.datatypes.Function; +import org.web3j.abi.datatypes.*; import org.web3j.abi.datatypes.generated.Bytes32; import org.web3j.abi.datatypes.generated.Bytes4; import org.web3j.abi.datatypes.generated.Uint256; @@ -35,14 +32,14 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class TangemBridgeProcessor extends Contract { - public static final String BINARY = "608060405262278d0060085534801562000017575f80fd5b5060405162002457380380620024578339810160408190526200003a91620001fa565b600580546001600160a01b038088166001600160a01b031992831617909255600680548784169083161790556002805486841690831617905560038054928516929091169190911790556009819055620000955f33620000a1565b5050505050506200025d565b5f80620000af8484620000dc565b90508015620000d3575f848152600160205260409020620000d1908462000187565b505b90505b92915050565b5f828152602081815260408083206001600160a01b038516845290915281205460ff166200017f575f838152602081815260408083206001600160a01b03861684529091529020805460ff19166001179055620001363390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001620000d6565b505f620000d6565b5f620000d3836001600160a01b0384165f8181526001830160205260408120546200017f57508154600181810184555f848152602080822090930184905584548482528286019093526040902091909155620000d6565b80516001600160a01b0381168114620001f5575f80fd5b919050565b5f805f805f60a086880312156200020f575f80fd5b6200021a86620001de565b94506200022a60208701620001de565b93506200023a60408701620001de565b92506200024a60608701620001de565b9150608086015190509295509295909350565b6121ec806200026b5f395ff3fe608060405234801561000f575f80fd5b506004361061024a575f3560e01c806391792d5b11610140578063c904ea39116100bf578063d6d32b2a11610084578063d6d32b2a14610551578063da90f3a014610564578063e24fd1f114610577578063e452f20d1461058a578063e96ede8e1461059d578063fe2208aa146105b0575f80fd5b8063c904ea39146104fc578063ca15c8731461050f578063cb37f3b214610522578063d47ae89c14610535578063d547741f1461053e575f80fd5b8063a217fddf11610105578063a217fddf14610495578063b6878ac91461049c578063b77c6d35146104af578063be4d1851146104c2578063c6b273e4146104d5575f80fd5b806391792d5b1461041757806391d14854146104205780639353ce4c1461043357806396bc563d1461045a578063978975591461046e575f80fd5b80634413e098116101cc57806373d3467c1161019157806373d3467c146103b85780637f2fadb0146103cb5780638072a022146103de57806383af133d146103f15780639010d07c14610404575f80fd5b80634413e098146103445780635673795114610357578063601c60651461036b57806365ebf99a1461037e57806370e4306e14610391575f80fd5b80632f2ff15d116102125780632f2ff15d146102cc5780633013ce29146102df57806336568abe1461030a57806337de81061461031d5780633d409a8614610330575f80fd5b806301ffc9a71461024e5780630f1071be1461027657806311dce7711461028d578063248a9ca3146102a25780632cc32641146102c4575b5f80fd5b61026161025c366004611c84565b6105c3565b60405190151581526020015b60405180910390f35b61027f60085481565b60405190815260200161026d565b6102a061029b366004611cbf565b6105ed565b005b61027f6102b0366004611cda565b5f9081526020819052604090206001015490565b6102a061065a565b6102a06102da366004611cf1565b6106d2565b6003546102f2906001600160a01b031681565b6040516001600160a01b03909116815260200161026d565b6102a0610318366004611cf1565b6106fc565b6102a061032b366004611cda565b610734565b61027f5f8051602061216e83398151915281565b6102a0610352366004611d1f565b610780565b61027f5f805160206120f983398151915281565b6102a0610379366004611d49565b610847565b6102a061038c366004611cbf565b610a6a565b61027f7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c81565b6102a06103c6366004611d88565b610acf565b6102a06103d9366004611d1f565b610bdc565b6102a06103ec366004611cda565b610cb1565b6102a06103ff366004611d1f565b610d3b565b6102f2610412366004611e06565b610df5565b61027f60075481565b61026161042e366004611cf1565b610e13565b61027f7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b8325681565b61027f5f8051602061204983398151915281565b61027f7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a8365681565b61027f5f81565b6102a06104aa366004611e26565b610e3b565b6002546102f2906001600160a01b031681565b6006546102f2906001600160a01b031681565b61027f7f56d36dba8e2d5c88fc1038bd95811d491e67c9a66c6848c9d4c9309b9587e06481565b6102a061050a366004611e5e565b610f3f565b61027f61051d366004611cda565b611030565b6005546102f2906001600160a01b031681565b61027f60095481565b6102a061054c366004611cf1565b611046565b6102a061055f366004611cda565b61106a565b6102a0610572366004611d1f565b6110f3565b6102a0610585366004611ea5565b611357565b6102f2610598366004611cbf565b611495565b6102a06105ab366004611d49565b6114f3565b6102a06105be366004611d49565b6115d6565b5f6001600160e01b03198216635a05180f60e01b14806105e757506105e7826116a1565b92915050565b5f80516020612049833981519152610604816116d5565b600680546001600160a01b0319166001600160a01b0384169081179091556040519081527f68c7b435e0ad7bfb2140fab0735300efbda206f62906049e0b35763cef67cc5e906020015b60405180910390a15050565b600a543390610668826116e2565b5f811160405180606001604052806029815260200161218e60299139906106ab5760405162461bcd60e51b81526004016106a29190611ef3565b60405180910390fd5b505f600a556006546003546106ce916001600160a01b0391821691168484611785565b5050565b5f828152602081905260409020600101546106ec816116d5565b6106f683836117df565b50505050565b6001600160a01b03811633146107255760405163334bd91960e11b815260040160405180910390fd5b61072f8282611812565b505050565b5f8051602061204983398151915261074b816116d5565b60078290556040518281527fb5aa183eb20407e22587bcd13d5c82a85835a4bd60e2a13d7c7efee3c2e9ed489060200161064e565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a836566107aa816116d5565b60405163b9603bdf60e01b8152600481018390526001600160a01b0384169063b9603bdf906024015f604051808303815f87803b1580156107e9575f80fd5b505af11580156107fb573d5f803e3d5ffd5b50505050826001600160a01b03167fc8818db5b3e4e986e2f21e002090b1513e17c05a7a326c3737af624bf6ef78c48360405161083a91815260200190565b60405180910390a2505050565b5f805160206120f983398151915261085e816116d5565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f00000000006020820152826108b05760405162461bcd60e51b81526004016106a29190611ef3565b505f6108bb85611495565b6003546040516370a0823160e01b81526001600160a01b0380841660048301529293505f92909116906370a0823190602401602060405180830381865afa158015610908573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061092c9190611f25565b9050838110156040518060400160405280601f81526020017f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e647300815250906109865760405162461bcd60e51b81526004016106a29190611ef3565b50600a84905560405163f89db45760e01b8152600481018690526001600160a01b0383169063f89db457906024015f604051808303815f87803b1580156109cb575f80fd5b505af11580156109dd573d5f803e3d5ffd5b50505050600a545f146040518060600160405280602e8152602001612119602e913990610a1d5760405162461bcd60e51b81526004016106a29190611ef3565b5084866001600160a01b03167fa5411d116afcb8415f0019792cccf595f94852ba82886dacbf756d9c3ce85f3f86604051610a5a91815260200190565b60405180910390a3505050505050565b5f80516020612049833981519152610a81816116d5565b600580546001600160a01b0319166001600160a01b0384169081179091556040519081527fccbdeb71dc680b2f0fd85f93e7ef0f70eda32a12b918c9715be07bb646360e599060200161064e565b5f8051602061216e833981519152610ae6816116d5565b5f610af088611495565b90505f60075487610b019190611f50565b6040516334284dfb60e01b8152600481018a9052602481018290526001600160801b03198816604482015261ffff8716606482015285151560848201529091506001600160a01b038316906334284dfb9060a4015f604051808303815f87803b158015610b6c575f80fd5b505af1158015610b7e573d5f803e3d5ffd5b5050505087896001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a89600754604051610bc9929190918252602082015260400190565b60405180910390a3505050505050505050565b7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b83256610c06816116d5565b5f610c1084611495565b604051636df6c7d560e11b8152600481018590529091506001600160a01b0382169063dbed8faa906024015f604051808303815f87803b158015610c52575f80fd5b505af1158015610c64573d5f803e3d5ffd5b50505050836001600160a01b03167f466723bb873015d5baf515fce6b0c8df2cb154adfd8badb6bc77a620a6ef676184604051610ca391815260200190565b60405180910390a250505050565b5f80516020612049833981519152610cc8816116d5565b6283d60082106040518060600160405280602a815260200161201f602a913990610d055760405162461bcd60e51b81526004016106a29190611ef3565b5060088290556040518281527fdc10143650bb79cd7a92cdf792545dcc2c3b0a719bebb3d83a8a21bb8fbfc3d69060200161064e565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a83656610d65816116d5565b6040516306b2a38360e41b8152600481018390526001600160a01b03841690636b2a3830906024015f604051808303815f87803b158015610da4575f80fd5b505af1158015610db6573d5f803e3d5ffd5b50505050826001600160a01b03167fcc1685e553848099cad0e11271e780371c44b5ed92a54552fbedf9f1b6b9f2478360405161083a91815260200190565b5f828152600160205260408120610e0c908361183d565b9392505050565b5f918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c610e65816116d5565b5f610e6f86611495565b90505f60075484610e809190611f50565b6040516304e7595f60e21b81526004810188905260248101879052604481018290529091506001600160a01b0383169063139d657c906064015f604051808303815f87803b158015610ed0575f80fd5b505af1158015610ee2573d5f803e3d5ffd5b505050508486886001600160a01b03167fcb06dfb173e98cd60a8931644d288167e193c33d81c1019b9f9aab36b13fbc0387600754604051610f2e929190918252602082015260400190565b60405180910390a450505050505050565b5f8051602061216e833981519152610f56816116d5565b5f610f6086611495565b90505f60075485610f719190611f50565b6040516320199e7960e21b8152600481018890526024810182905285151560448201529091506001600160a01b0383169063806679e4906064015f604051808303815f87803b158015610fc2575f80fd5b505af1158015610fd4573d5f803e3d5ffd5b5050505085876001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a8760075460405161101f929190918252602082015260400190565b60405180910390a350505050505050565b5f8181526001602052604081206105e790611848565b5f82815260208190526040902060010154611060816116d5565b6106f68383611812565b5f80516020612049833981519152611081816116d5565b610e10821060405180606001604052806027815260200161214760279139906110bd5760405162461bcd60e51b81526004016106a29190611ef3565b5060098290556040518281527f7f63f876249f25f7856e620802edd09af38a83c9c6040fcdb3717915403083119060200161064e565b5f805160206120f983398151915261110a816116d5565b5f61111484611495565b604051631902cad960e31b8152600481018590529091505f906001600160a01b0383169063c81656c890602401602060405180830381865afa15801561115c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906111809190611f25565b6003546040516370a0823160e01b81526001600160a01b0385811660048301529293505f92909116906370a0823190602401602060405180830381865afa1580156111cd573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906111f19190611f25565b90505f82116040518060600160405280602781526020016120d2602791399061122d5760405162461bcd60e51b81526004016106a29190611ef3565b5060408051808201909152601f81527f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473006020820152828210156112835760405162461bcd60e51b81526004016106a29190611ef3565b50600a82905560405163e3dbffd560e01b8152600481018690526001600160a01b0384169063e3dbffd5906024015f604051808303815f87803b1580156112c8575f80fd5b505af11580156112da573d5f803e3d5ffd5b50505050600a545f146040518060600160405280602e8152602001612119602e91399061131a5760405162461bcd60e51b81526004016106a29190611ef3565b5084866001600160a01b03167fdfab01fc691e8b69fca482c65142d1bcfeafeb9eb6e9fd5220867d7196d9476e84604051610a5a91815260200190565b7f56d36dba8e2d5c88fc1038bd95811d491e67c9a66c6848c9d4c9309b9587e064611381816116d5565b61138a826116e2565b306001600160a01b0316826001600160a01b031663ce1b1d436040518163ffffffff1660e01b8152600401602060405180830381865afa1580156113d0573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906113f49190611f63565b6001600160a01b0316146040518060600160405280603281526020016120a060329139906114355760405162461bcd60e51b81526004016106a29190611ef3565b506001600160a01b038381165f8181526004602090815260409182902080546001600160a01b0319169487169485179055905192835290917f0c1464e8c817204497a2076f3b466fe64093e83033c83303b0cb44001cd9b6b5910161083a565b6001600160a01b038082165f90815260046020908152604080832054815160608101909252602f8083529394169283151592611ff090830139906114ec5760405162461bcd60e51b81526004016106a29190611ef3565b5092915050565b5f8051602061216e83398151915261150a816116d5565b5f61151485611495565b90505f831561152557600754611527565b5f5b90505f6115348286611f50565b60405163e4d7ddb960e01b815260048101889052602481018290529091506001600160a01b0384169063e4d7ddb9906044015f604051808303815f87803b15801561157d575f80fd5b505af115801561158f573d5f803e3d5ffd5b505060408051888152602081018690528993506001600160a01b038b1692507f8ba3ad1bd4ff5c3d75861d56df66b7942244d700f4d4cb67df693bac78e805a2910161101f565b5f805160206120f98339815191526115ed816116d5565b5f6115f785611495565b60405163456575e560e01b815260048101869052602481018590529091506001600160a01b0382169063456575e5906044015f604051808303815f87803b158015611640575f80fd5b505af1158015611652573d5f803e3d5ffd5b5050505083856001600160a01b03167f6f60b80b8dcecc3741c03485554a9a35ced96ebba66405fd324caf12a16261da8560405161169291815260200190565b60405180910390a35050505050565b5f6001600160e01b03198216637965db0b60e01b14806105e757506301ffc9a760e01b6001600160e01b03198316146105e7565b6116df8133611851565b50565b6002546040516385bb392360e01b81526001600160a01b038381166004830152909116906385bb392390602401602060405180830381865afa15801561172a573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061174e9190611f7e565b60405180606001604052806037815260200161206960379139906106ce5760405162461bcd60e51b81526004016106a29190611ef3565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526106f690859061188a565b5f806117eb84846118eb565b90508015610e0c575f84815260016020526040902061180a908461197a565b509392505050565b5f8061181e848461198e565b90508015610e0c575f84815260016020526040902061180a90846119f7565b5f610e0c8383611a0b565b5f6105e7825490565b61185b8282610e13565b6106ce5760405163e2517d3f60e01b81526001600160a01b0382166004820152602481018390526044016106a2565b5f61189e6001600160a01b03841683611a31565b905080515f141580156118c25750808060200190518101906118c09190611f7e565b155b1561072f57604051635274afe760e01b81526001600160a01b03841660048201526024016106a2565b5f6118f68383610e13565b611973575f838152602081815260408083206001600160a01b03861684529091529020805460ff1916600117905561192b3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a45060016105e7565b505f6105e7565b5f610e0c836001600160a01b038416611a3e565b5f6119998383610e13565b15611973575f838152602081815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a45060016105e7565b5f610e0c836001600160a01b038416611a83565b5f825f018281548110611a2057611a20611f99565b905f5260205f200154905092915050565b6060610e0c83835f611b66565b5f81815260018301602052604081205461197357508154600181810184555f8481526020808220909301849055845484825282860190935260409020919091556105e7565b5f8181526001830160205260408120548015611b5d575f611aa5600183611fad565b85549091505f90611ab890600190611fad565b9050808214611b17575f865f018281548110611ad657611ad6611f99565b905f5260205f200154905080875f018481548110611af657611af6611f99565b5f918252602080832090910192909255918252600188019052604090208390555b8554869080611b2857611b28611fc0565b600190038181905f5260205f20015f90559055856001015f8681526020019081526020015f205f9055600193505050506105e7565b5f9150506105e7565b606081471015611b8b5760405163cd78605960e01b81523060048201526024016106a2565b5f80856001600160a01b03168486604051611ba69190611fd4565b5f6040518083038185875af1925050503d805f8114611be0576040519150601f19603f3d011682016040523d82523d5f602084013e611be5565b606091505b5091509150611bf5868383611bff565b9695505050505050565b606082611c1457611c0f82611c5b565b610e0c565b8151158015611c2b57506001600160a01b0384163b155b15611c5457604051639996b31560e01b81526001600160a01b03851660048201526024016106a2565b5080610e0c565b805115611c6b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b5f60208284031215611c94575f80fd5b81356001600160e01b031981168114610e0c575f80fd5b6001600160a01b03811681146116df575f80fd5b5f60208284031215611ccf575f80fd5b8135610e0c81611cab565b5f60208284031215611cea575f80fd5b5035919050565b5f8060408385031215611d02575f80fd5b823591506020830135611d1481611cab565b809150509250929050565b5f8060408385031215611d30575f80fd5b8235611d3b81611cab565b946020939093013593505050565b5f805f60608486031215611d5b575f80fd5b8335611d6681611cab565b95602085013595506040909401359392505050565b80151581146116df575f80fd5b5f805f805f8060c08789031215611d9d575f80fd5b8635611da881611cab565b9550602087013594506040870135935060608701356001600160801b031981168114611dd2575f80fd5b9250608087013561ffff81168114611de8575f80fd5b915060a0870135611df881611d7b565b809150509295509295509295565b5f8060408385031215611e17575f80fd5b50508035926020909101359150565b5f805f8060808587031215611e39575f80fd5b8435611e4481611cab565b966020860135965060408601359560600135945092505050565b5f805f8060808587031215611e71575f80fd5b8435611e7c81611cab565b935060208501359250604085013591506060850135611e9a81611d7b565b939692955090935050565b5f8060408385031215611eb6575f80fd5b8235611ec181611cab565b91506020830135611d1481611cab565b5f5b83811015611eeb578181015183820152602001611ed3565b50505f910152565b602081525f8251806020840152611f11816040850160208701611ed1565b601f01601f19169190910160400192915050565b5f60208284031215611f35575f80fd5b5051919050565b634e487b7160e01b5f52601160045260245ffd5b808201808211156105e7576105e7611f3c565b5f60208284031215611f73575f80fd5b8151610e0c81611cab565b5f60208284031215611f8e575f80fd5b8151610e0c81611d7b565b634e487b7160e01b5f52603260045260245ffd5b818103818111156105e7576105e7611f3c565b634e487b7160e01b5f52603160045260245ffd5b5f8251611fe5818460208701611ed1565b919091019291505056fe353230327c50726f636573736f723a206e6f207061796d656e74206163636f756e7420666f72207468652063617264353231307c50726f636573736f723a20736574746c656d656e7420706572696f6420746f6f206c6f6e67fa658e47c75a6eb0e27156b2f72e1b295c98b53e8a1d238d7ca6ea6345b34880353230307c50726f636573736f723a207061796d656e74206163636f756e74206e6f74206465706c6f79656420627920666163746f7279353230317c50726f636573736f723a207061796d656e74206163636f756e742070726f636573736f72206d69736d61746368353235307c50726f636573736f723a20726566756e64207265636f7264206e6f7420666f756e64d77cc12a543481a2b3ef8fd055979569715a10db9879cd9e664395eca3a54dff353235327c50726f636573736f723a20726566756e6420746f2070726f63657373207761736e2774207265736574353231317c50726f636573736f723a2073656375726974792064656c617920746f6f206c6f6e67a567244cad934c87b7f7e15b15dd0e75f95a76afd1916f8eab4a391410d6f278353235317c50726f636573736f723a20726566756e6420746f2070726f63657373206973207a65726fa2646970667358221220abcbacdb9048a2744f99cdd5031db80dd0390bf503ccade142542ce178f0668364736f6c63430008160033"; + public static final String BINARY = "608060405262278d006008553480156200001857600080fd5b506040516200272f3803806200272f8339810160408190526200003b9162000214565b60028054600580546001600160a01b03199081166001600160a01b038a8116919091179092556006805482168984161790556001600160a81b03199092166101008783160217909255600380549091169184169190911790556009819055620000a6600033620000b2565b5050505050506200027b565b600080620000c18484620000ef565b90508015620000e6576000848152600160205260409020620000e490846200019d565b505b90505b92915050565b6000828152602081815260408083206001600160a01b038516845290915281205460ff1662000194576000838152602081815260408083206001600160a01b03861684529091529020805460ff191660011790556200014b3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a4506001620000e9565b506000620000e9565b6000620000e6836001600160a01b03841660008181526001830160205260408120546200019457508154600181810184556000848152602080822090930184905584548482528286019093526040902091909155620000e9565b80516001600160a01b03811681146200020f57600080fd5b919050565b600080600080600060a086880312156200022d57600080fd5b6200023886620001f7565b94506200024860208701620001f7565b93506200025860408701620001f7565b92506200026860608701620001f7565b9150608086015190509295509295909350565b6124a4806200028b6000396000f3fe608060405234801561001057600080fd5b506004361061028a5760003560e01c806383af133d1161015c578063c904ea39116100ce578063da90f3a011610087578063da90f3a0146105f1578063e24fd1f114610604578063e452f20d14610617578063e63ab1e91461062a578063e96ede8e14610651578063fe2208aa1461066457600080fd5b8063c904ea3914610589578063ca15c8731461059c578063cb37f3b2146105af578063d47ae89c146105c2578063d547741f146105cb578063d6d32b2a146105de57600080fd5b80639353ce4c116101205780639353ce4c146104f357806396bc563d1461051a578063978975591461052f578063a217fddf14610556578063b77c6d351461055e578063be4d18511461057657600080fd5b806383af133d146104a95780638456cb59146104bc5780639010d07c146104c457806391792d5b146104d757806391d14854146104e057600080fd5b80633f4ba83a11610200578063601c6065116101b9578063601c60651461042357806365ebf99a1461043657806370e4306e1461044957806373d3467c146104705780637f2fadb0146104835780638072a0221461049657600080fd5b80633f4ba83a146103ae5780634413e098146103b65780634b9bb0b4146103c95780634e6d8a73146103f057806356737951146104035780635c975abb1461041857600080fd5b80632f2ff15d116102525780632f2ff15d1461030e5780633013ce291461032157806336568abe1461034c57806337de81061461035f5780633d409a86146103725780633e1dbe1f1461038757600080fd5b806301ffc9a71461028f5780630f1071be146102b757806311dce771146102ce578063248a9ca3146102e35780632cc3264114610306575b600080fd5b6102a261029d366004611f47565b610677565b60405190151581526020015b60405180910390f35b6102c060085481565b6040519081526020016102ae565b6102e16102dc366004611f86565b6106a2565b005b6102c06102f1366004611fa3565b60009081526020819052604090206001015490565b6102e1610710565b6102e161031c366004611fbc565b61078a565b600354610334906001600160a01b031681565b6040516001600160a01b0390911681526020016102ae565b6102e161035a366004611fbc565b6107b5565b6102e161036d366004611fa3565b6107ed565b6102c060008051602061242683398151915281565b6102c07f80578ee961742bd59ed4d7f52c856d4014970acf79709a07f09c8c279d66cbb481565b6102e161084c565b6102e16103c4366004611fec565b610881565b6102c07fc62d23620f0c241b20cdec293f57ff9c0cc09ac7c55352efe3363184cd93032281565b6102e16103fe366004612018565b61094d565b6102c06000805160206123b183398151915281565b60025460ff166102a2565b6102e1610431366004612018565b610a57565b6102e1610444366004611f86565b610c7f565b6102c07f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c81565b6102e161047e36600461205b565b610ce5565b6102e1610491366004611fec565b610e02565b6102e16104a4366004611fa3565b610ee5565b6102e16104b7366004611fec565b610f70565b6102e161102f565b6103346104d23660046120df565b611061565b6102c060075481565b6102a26104ee366004611fbc565b611080565b6102c07f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b8325681565b6102c060008051602061230183398151915281565b6102c07f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a8365681565b6102c0600081565b6002546103349061010090046001600160a01b031681565b600654610334906001600160a01b031681565b6102e1610597366004612101565b6110a9565b6102c06105aa366004611fa3565b6111aa565b600554610334906001600160a01b031681565b6102c060095481565b6102e16105d9366004611fbc565b6111c1565b6102e16105ec366004611fa3565b6111e6565b6102e16105ff366004611fec565b611270565b6102e161061236600461214b565b6114ed565b610334610625366004611f86565b61162e565b6102c07f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a81565b6102e161065f366004612018565b61168d565b6102e1610672366004612018565b611782565b60006001600160e01b03198216635a05180f60e01b148061069c575061069c8261185c565b92915050565b6000805160206123018339815191526106ba81611891565b600680546001600160a01b0319166001600160a01b0384169081179091556040519081527f68c7b435e0ad7bfb2140fab0735300efbda206f62906049e0b35763cef67cc5e906020015b60405180910390a15050565b600a54339061071e8261189b565b6000811160405180606001604052806029815260200161244660299139906107625760405162461bcd60e51b8152600401610759919061219d565b60405180910390fd5b506000600a55600654600354610786916001600160a01b0391821691168484611946565b5050565b6000828152602081905260409020600101546107a581611891565b6107af83836119a0565b50505050565b6001600160a01b03811633146107de5760405163334bd91960e11b815260040160405180910390fd5b6107e882826119d5565b505050565b7f80578ee961742bd59ed4d7f52c856d4014970acf79709a07f09c8c279d66cbb461081781611891565b60078290556040518281527fb5aa183eb20407e22587bcd13d5c82a85835a4bd60e2a13d7c7efee3c2e9ed4890602001610704565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a61087681611891565b61087e611a02565b50565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a836566108ab81611891565b60405163b9603bdf60e01b8152600481018390526001600160a01b0384169063b9603bdf90602401600060405180830381600087803b1580156108ed57600080fd5b505af1158015610901573d6000803e3d6000fd5b50505050826001600160a01b03167fc8818db5b3e4e986e2f21e002090b1513e17c05a7a326c3737af624bf6ef78c48360405161094091815260200190565b60405180910390a2505050565b610955611a54565b7f6ac2c8b5322a929d300af17766189693ff6db6939ffe690430ad4720baf47d1c61097f81611891565b600061098a8561162e565b905060006007548461099c91906121e6565b604051630381b4dd60e01b815260048101879052602481018290529091506001600160a01b03831690630381b4dd90604401600060405180830381600087803b1580156109e857600080fd5b505af11580156109fc573d6000803e3d6000fd5b5050505084866001600160a01b03167fa77a4ae2cfa0139350d70cd52434d01ae72e9ec897ae89be03580f1ff00dd3d086600754604051610a47929190918252602082015260400190565b60405180910390a3505050505050565b610a5f611a54565b6000805160206123b1833981519152610a7781611891565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f0000000000602082015282610ac95760405162461bcd60e51b8152600401610759919061219d565b506000610ad58561162e565b6003546006546040516370a0823160e01b81526001600160a01b0391821660048201529293506000929116906370a0823190602401602060405180830381865afa158015610b27573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610b4b91906121f9565b9050838110156040518060400160405280601f81526020017f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e64730081525090610ba55760405162461bcd60e51b8152600401610759919061219d565b50600a84905560405163f89db45760e01b8152600481018690526001600160a01b0383169063f89db45790602401600060405180830381600087803b158015610bed57600080fd5b505af1158015610c01573d6000803e3d6000fd5b50505050600a546000146040518060600160405280602e81526020016123d1602e913990610c425760405162461bcd60e51b8152600401610759919061219d565b5084866001600160a01b03167fa5411d116afcb8415f0019792cccf595f94852ba82886dacbf756d9c3ce85f3f86604051610a4791815260200190565b600080516020612301833981519152610c9781611891565b600580546001600160a01b0319166001600160a01b0384169081179091556040519081527fccbdeb71dc680b2f0fd85f93e7ef0f70eda32a12b918c9715be07bb646360e5990602001610704565b610ced611a54565b600080516020612426833981519152610d0581611891565b6000610d108861162e565b9050600060075487610d2291906121e6565b6040516334284dfb60e01b8152600481018a9052602481018290526001600160801b03198816604482015261ffff8716606482015285151560848201529091506001600160a01b038316906334284dfb9060a401600060405180830381600087803b158015610d9057600080fd5b505af1158015610da4573d6000803e3d6000fd5b5050505087896001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a89600754604051610def929190918252602082015260400190565b60405180910390a3505050505050505050565b610e0a611a54565b7f79af5a85184b45263c1b3721b9c8eb1becd98e55ade5a61da0fed18b90b83256610e3481611891565b6000610e3f8461162e565b604051636df6c7d560e11b8152600481018590529091506001600160a01b0382169063dbed8faa90602401600060405180830381600087803b158015610e8457600080fd5b505af1158015610e98573d6000803e3d6000fd5b50505050836001600160a01b03167f466723bb873015d5baf515fce6b0c8df2cb154adfd8badb6bc77a620a6ef676184604051610ed791815260200190565b60405180910390a250505050565b600080516020612301833981519152610efd81611891565b6283d60082106040518060600160405280602a81526020016122d7602a913990610f3a5760405162461bcd60e51b8152600401610759919061219d565b5060088290556040518281527fdc10143650bb79cd7a92cdf792545dcc2c3b0a719bebb3d83a8a21bb8fbfc3d690602001610704565b7f581be84b4822abe392a3bcac0d4d656e4cd57faf2e321a32108215cdd4a83656610f9a81611891565b6040516306b2a38360e41b8152600481018390526001600160a01b03841690636b2a383090602401600060405180830381600087803b158015610fdc57600080fd5b505af1158015610ff0573d6000803e3d6000fd5b50505050826001600160a01b03167fcc1685e553848099cad0e11271e780371c44b5ed92a54552fbedf9f1b6b9f2478360405161094091815260200190565b7f65d7a28e3265b37a6474929f336521b332c1681b933f6cb9f3376673440d862a61105981611891565b61087e611a7a565b60008281526001602052604081206110799083611ab7565b9392505050565b6000918252602082815260408084206001600160a01b0393909316845291905290205460ff1690565b6110b1611a54565b6000805160206124268339815191526110c981611891565b60006110d48661162e565b90506000600754856110e691906121e6565b6040516320199e7960e21b8152600481018890526024810182905285151560448201529091506001600160a01b0383169063806679e490606401600060405180830381600087803b15801561113a57600080fd5b505af115801561114e573d6000803e3d6000fd5b5050505085876001600160a01b03167f92c1dbadbb7b791052341ce7c185ca37861e432438355dc3e1642b7c1287609a87600754604051611199929190918252602082015260400190565b60405180910390a350505050505050565b600081815260016020526040812061069c90611ac3565b6000828152602081905260409020600101546111dc81611891565b6107af83836119d5565b6000805160206123018339815191526111fe81611891565b610e1082106040518060600160405280602781526020016123ff602791399061123a5760405162461bcd60e51b8152600401610759919061219d565b5060098290556040518281527f7f63f876249f25f7856e620802edd09af38a83c9c6040fcdb37179154030831190602001610704565b611278611a54565b6000805160206123b183398151915261129081611891565b600061129b8461162e565b604051631902cad960e31b8152600481018590529091506000906001600160a01b0383169063c81656c890602401602060405180830381865afa1580156112e6573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061130a91906121f9565b6003546006546040516370a0823160e01b81526001600160a01b0391821660048201529293506000929116906370a0823190602401602060405180830381865afa15801561135c573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061138091906121f9565b90506000821160405180606001604052806027815260200161238a60279139906113bd5760405162461bcd60e51b8152600401610759919061219d565b5060408051808201909152601f81527f353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473006020820152828210156114135760405162461bcd60e51b8152600401610759919061219d565b50600a82905560405163e3dbffd560e01b8152600481018690526001600160a01b0384169063e3dbffd590602401600060405180830381600087803b15801561145b57600080fd5b505af115801561146f573d6000803e3d6000fd5b50505050600a546000146040518060600160405280602e81526020016123d1602e9139906114b05760405162461bcd60e51b8152600401610759919061219d565b5084866001600160a01b03167fdfab01fc691e8b69fca482c65142d1bcfeafeb9eb6e9fd5220867d7196d9476e84604051610a4791815260200190565b7fc62d23620f0c241b20cdec293f57ff9c0cc09ac7c55352efe3363184cd93032261151781611891565b6115208261189b565b306001600160a01b0316826001600160a01b031663ce1b1d436040518163ffffffff1660e01b8152600401602060405180830381865afa158015611568573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061158c9190612212565b6001600160a01b03161460405180606001604052806032815260200161235860329139906115cd5760405162461bcd60e51b8152600401610759919061219d565b506001600160a01b0383811660008181526004602090815260409182902080546001600160a01b0319169487169485179055905192835290917f0c1464e8c817204497a2076f3b466fe64093e83033c83303b0cb44001cd9b6b59101610940565b6001600160a01b03808216600090815260046020908152604080832054815160608101909252602f80835293941692831515926122a890830139906116865760405162461bcd60e51b8152600401610759919061219d565b5092915050565b611695611a54565b6000805160206124268339815191526116ad81611891565b60006116b88561162e565b9050600083156116ca576007546116cd565b60005b905060006116db82866121e6565b60405163e4d7ddb960e01b815260048101889052602481018290529091506001600160a01b0384169063e4d7ddb990604401600060405180830381600087803b15801561172757600080fd5b505af115801561173b573d6000803e3d6000fd5b505060408051888152602081018690528993506001600160a01b038b1692507f8ba3ad1bd4ff5c3d75861d56df66b7942244d700f4d4cb67df693bac78e805a29101611199565b61178a611a54565b6000805160206123b18339815191526117a281611891565b60006117ad8561162e565b60405163456575e560e01b815260048101869052602481018590529091506001600160a01b0382169063456575e590604401600060405180830381600087803b1580156117f957600080fd5b505af115801561180d573d6000803e3d6000fd5b5050505083856001600160a01b03167f6f60b80b8dcecc3741c03485554a9a35ced96ebba66405fd324caf12a16261da8560405161184d91815260200190565b60405180910390a35050505050565b60006001600160e01b03198216637965db0b60e01b148061069c57506301ffc9a760e01b6001600160e01b031983161461069c565b61087e8133611acd565b6002546040516385bb392360e01b81526001600160a01b038381166004830152610100909204909116906385bb392390602401602060405180830381865afa1580156118eb573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061190f919061222f565b60405180606001604052806037815260200161232160379139906107865760405162461bcd60e51b8152600401610759919061219d565b604080516001600160a01b0385811660248301528416604482015260648082018490528251808303909101815260849091019091526020810180516001600160e01b03166323b872dd60e01b1790526107af908590611b06565b6000806119ad8484611b69565b905080156110795760008481526001602052604090206119cd9084611bfb565b509392505050565b6000806119e28484611c10565b905080156110795760008481526001602052604090206119cd9084611c7b565b611a0a611c90565b6002805460ff191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b60025460ff1615611a785760405163d93c066560e01b815260040160405180910390fd5b565b611a82611a54565b6002805460ff191660011790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258611a373390565b60006110798383611cb3565b600061069c825490565b611ad78282611080565b6107865760405163e2517d3f60e01b81526001600160a01b038216600482015260248101839052604401610759565b6000611b1b6001600160a01b03841683611cdd565b90508051600014158015611b40575080806020019051810190611b3e919061222f565b155b156107e857604051635274afe760e01b81526001600160a01b0384166004820152602401610759565b6000611b758383611080565b611bf3576000838152602081815260408083206001600160a01b03861684529091529020805460ff19166001179055611bab3390565b6001600160a01b0316826001600160a01b0316847f2f8788117e7eff1d82e926ec794901d17c78024a50270940304540a733656f0d60405160405180910390a450600161069c565b50600061069c565b6000611079836001600160a01b038416611ceb565b6000611c1c8383611080565b15611bf3576000838152602081815260408083206001600160a01b0386168085529252808320805460ff1916905551339286917ff6391f5c32d9c69d2a47ea670b442974b53935d1edc7fd64eb21e047a839171b9190a450600161069c565b6000611079836001600160a01b038416611d32565b60025460ff16611a7857604051638dfc202b60e01b815260040160405180910390fd5b6000826000018281548110611cca57611cca61224c565b9060005260206000200154905092915050565b606061107983836000611e25565b6000818152600183016020526040812054611bf35750815460018181018455600084815260208082209093018490558454848252828601909352604090209190915561069c565b60008181526001830160205260408120548015611e1b576000611d56600183612262565b8554909150600090611d6a90600190612262565b9050808214611dcf576000866000018281548110611d8a57611d8a61224c565b9060005260206000200154905080876000018481548110611dad57611dad61224c565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080611de057611de0612275565b60019003818190600052602060002001600090559055856001016000868152602001908152602001600020600090556001935050505061069c565b600091505061069c565b606081471015611e4a5760405163cd78605960e01b8152306004820152602401610759565b600080856001600160a01b03168486604051611e66919061228b565b60006040518083038185875af1925050503d8060008114611ea3576040519150601f19603f3d011682016040523d82523d6000602084013e611ea8565b606091505b5091509150611eb8868383611ec2565b9695505050505050565b606082611ed757611ed282611f1e565b611079565b8151158015611eee57506001600160a01b0384163b155b15611f1757604051639996b31560e01b81526001600160a01b0385166004820152602401610759565b5080611079565b805115611f2e5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b600060208284031215611f5957600080fd5b81356001600160e01b03198116811461107957600080fd5b6001600160a01b038116811461087e57600080fd5b600060208284031215611f9857600080fd5b813561107981611f71565b600060208284031215611fb557600080fd5b5035919050565b60008060408385031215611fcf57600080fd5b823591506020830135611fe181611f71565b809150509250929050565b60008060408385031215611fff57600080fd5b823561200a81611f71565b946020939093013593505050565b60008060006060848603121561202d57600080fd5b833561203881611f71565b95602085013595506040909401359392505050565b801515811461087e57600080fd5b60008060008060008060c0878903121561207457600080fd5b863561207f81611f71565b9550602087013594506040870135935060608701356001600160801b0319811681146120aa57600080fd5b9250608087013561ffff811681146120c157600080fd5b915060a08701356120d18161204d565b809150509295509295509295565b600080604083850312156120f257600080fd5b50508035926020909101359150565b6000806000806080858703121561211757600080fd5b843561212281611f71565b9350602085013592506040850135915060608501356121408161204d565b939692955090935050565b6000806040838503121561215e57600080fd5b823561216981611f71565b91506020830135611fe181611f71565b60005b8381101561219457818101518382015260200161217c565b50506000910152565b60208152600082518060208401526121bc816040850160208701612179565b601f01601f19169190910160400192915050565b634e487b7160e01b600052601160045260246000fd5b8082018082111561069c5761069c6121d0565b60006020828403121561220b57600080fd5b5051919050565b60006020828403121561222457600080fd5b815161107981611f71565b60006020828403121561224157600080fd5b81516110798161204d565b634e487b7160e01b600052603260045260246000fd5b8181038181111561069c5761069c6121d0565b634e487b7160e01b600052603160045260246000fd5b6000825161229d818460208701612179565b919091019291505056fe353230327c50726f636573736f723a206e6f207061796d656e74206163636f756e7420666f72207468652063617264353231307c50726f636573736f723a20736574746c656d656e7420706572696f6420746f6f206c6f6e67fa658e47c75a6eb0e27156b2f72e1b295c98b53e8a1d238d7ca6ea6345b34880353230307c50726f636573736f723a207061796d656e74206163636f756e74206e6f74206465706c6f79656420627920666163746f7279353230317c50726f636573736f723a207061796d656e74206163636f756e742070726f636573736f72206d69736d61746368353235307c50726f636573736f723a20726566756e64207265636f7264206e6f7420666f756e64d77cc12a543481a2b3ef8fd055979569715a10db9879cd9e664395eca3a54dff353235327c50726f636573736f723a20726566756e6420746f2070726f63657373207761736e2774207265736574353231317c50726f636573736f723a2073656375726974792064656c617920746f6f206c6f6e67a567244cad934c87b7f7e15b15dd0e75f95a76afd1916f8eab4a391410d6f278353235317c50726f636573736f723a20726566756e6420746f2070726f63657373206973207a65726fa2646970667358221220098c0e97a9ad9d3bb5cd4459c44925a6fb83295823a48d337beb3c254c7b728b64736f6c63430008160033"; public static final String FUNC_AUTHORIZATION_PROCESSOR_ROLE = "AUTHORIZATION_PROCESSOR_ROLE"; @@ -52,7 +49,11 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_DEFAULT_ADMIN_ROLE = "DEFAULT_ADMIN_ROLE"; - public static final String FUNC_PAYMENT_ACCOUNT_SETTER = "PAYMENT_ACCOUNT_SETTER"; + public static final String FUNC_FIXED_FEE_SETTER_ROLE = "FIXED_FEE_SETTER_ROLE"; + + public static final String FUNC_PAUSER_ROLE = "PAUSER_ROLE"; + + public static final String FUNC_PAYMENT_ACCOUNT_SETTER_ROLE = "PAYMENT_ACCOUNT_SETTER_ROLE"; public static final String FUNC_PROPERTY_SETTER_ROLE = "PROPERTY_SETTER_ROLE"; @@ -76,6 +77,10 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_INCREASEVERIFIEDBALANCEFOR = "increaseVerifiedBalanceFor"; + public static final String FUNC_PAUSE = "pause"; + + public static final String FUNC_PAUSED = "paused"; + public static final String FUNC_PAYMENTACCOUNTFACTORY = "paymentAccountFactory"; public static final String FUNC_PAYMENTRECEIVER = "paymentReceiver"; @@ -124,6 +129,8 @@ class TangemBridgeProcessor extends Contract { public static final String FUNC_SUPPORTSINTERFACE = "supportsInterface"; + public static final String FUNC_UNPAUSE = "unpause"; + public static final String FUNC_WRITEOFFDEBT = "writeOffDebt"; public static final Event AUTHORIZATIONCHANGEPROCESSED_EVENT = new Event("AuthorizationChangeProcessed", @@ -149,6 +156,10 @@ class TangemBridgeProcessor extends Contract { List.of(new TypeReference() { })); + public static final Event PAUSED_EVENT = new Event("Paused", + List.of(new TypeReference

() { + })); + public static final Event PAYMENTACCOUNTSET_EVENT = new Event("PaymentAccountSet", Arrays.asList(new TypeReference
(true) { }, new TypeReference
() { @@ -209,11 +220,14 @@ class TangemBridgeProcessor extends Contract { public static final Event SETTLEMENTPROCESSED_EVENT = new Event("SettlementProcessed", Arrays.asList(new TypeReference
(true) { }, new TypeReference(true) { - }, new TypeReference(true) { }, new TypeReference() { }, new TypeReference() { })); + public static final Event UNPAUSED_EVENT = new Event("Unpaused", + List.of(new TypeReference
() { + })); + public static final Event VERIFIEDBALANCEINCREASEDFOR_EVENT = new Event("VerifiedBalanceIncreasedFor", Arrays.asList(new TypeReference
(true) { }, new TypeReference() { @@ -268,6 +282,26 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } + public static AuthorizationProcessedEventResponse getAuthorizationProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, log); + AuthorizationProcessedEventResponse typedResponse = new AuthorizationProcessedEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static DebtWriteOffProcessedEventResponse getDebtWriteOffProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, log); + DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + public static List getAuthorizationProcessedEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -283,126 +317,6 @@ class TangemBridgeProcessor extends Contract { return responses; } - public static List getDebtWriteOffProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getFixedFeeSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.fixedFee = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static AuthorizationProcessedEventResponse getAuthorizationProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(AUTHORIZATIONPROCESSED_EVENT, log); - AuthorizationProcessedEventResponse typedResponse = new AuthorizationProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getPaymentAccountSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PaymentAccountSetEventResponse typedResponse = new PaymentAccountSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPaymentReceiverSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentReceiver = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPendingRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtWriteOffProcessedEventResponse getDebtWriteOffProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, log); - DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - return typedResponse; - } - - public static PendingRefundProcessedEventResponse getPendingRefundProcessedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, log); - PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); - typedResponse.log = log; - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - return typedResponse; - } - - public static List getRefundAccountSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.refundAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundProcessedEventResponse typedResponse = new RefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static FixedFeeSetEventResponse getFixedFeeSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, log); FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); @@ -411,43 +325,34 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getRoleAdminChangedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getPausedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAUSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); + PausedEventResponse typedResponse = new PausedEventResponse(); typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.previousAdminRole = (byte[]) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.newAdminRole = (byte[]) eventValues.getIndexedValues().get(2).getValue(); + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; } - public static List getRoleGrantedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEGRANTED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RoleGrantedEventResponse typedResponse = new RoleGrantedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; + public static PausedEventResponse getPausedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAUSED_EVENT, log); + PausedEventResponse typedResponse = new PausedEventResponse(); + typedResponse.log = log; + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; } - public static List getRoleRevokedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(ROLEREVOKED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getDebtWriteOffProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTWRITEOFFPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - RoleRevokedEventResponse typedResponse = new RoleRevokedEventResponse(); + DebtWriteOffProcessedEventResponse typedResponse = new DebtWriteOffProcessedEventResponse(); typedResponse.log = eventValues.getLog(); - typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); responses.add(typedResponse); } return responses; @@ -462,44 +367,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getSaveRefundProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SAVEREFUNDPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SaveRefundProcessedEventResponse typedResponse = new SaveRefundProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getSecurityDelaySetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SECURITYDELAYSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SecurityDelaySetEventResponse typedResponse = new SecurityDelaySetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.securityDelay = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getSettlementPeriodSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SETTLEMENTPERIODSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SettlementPeriodSetEventResponse typedResponse = new SettlementPeriodSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.settlementPeriod = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static PaymentReceiverSetEventResponse getPaymentReceiverSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, log); PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); @@ -508,66 +375,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static List getSettlementProcessedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceIncreasedForEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceSetForEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - @Deprecated - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemBridgeProcessor(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - - @Deprecated - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new TangemBridgeProcessor(contractAddress, web3j, credentials, contractGasProvider); - } - - public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, contractGasProvider); - } - public static RefundAccountSetEventResponse getRefundAccountSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, log); RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); @@ -576,32 +383,16 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); - } - - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); - } - - @Deprecated - public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + public static List getFixedFeeSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(FIXEDFEESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + FixedFeeSetEventResponse typedResponse = new FixedFeeSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.fixedFee = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } public static RefundProcessedEventResponse getRefundProcessedEventFromLog(Log log) { @@ -614,16 +405,6 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { - String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), - new Address(160, refundAccount_), - new Address(160, paymentAccountFactory_), - new Address(160, paymentToken_), - new Uint256(securityDelay_))); - return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); - } - public static RoleAdminChangedEventResponse getRoleAdminChangedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, log); RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); @@ -680,18 +461,62 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } + public static List getPaymentAccountSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountSetEventResponse typedResponse = new PaymentAccountSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static SettlementProcessedEventResponse getSettlementProcessedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, log); SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); typedResponse.log = log; typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(2).getValue(); typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); return typedResponse; } + public static List getUnpausedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UNPAUSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UnpausedEventResponse typedResponse = new UnpausedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static UnpausedEventResponse getUnpausedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(UNPAUSED_EVENT, log); + UnpausedEventResponse typedResponse = new UnpausedEventResponse(); + typedResponse.log = log; + typedResponse.account = (String) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public static List getPaymentReceiverSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTRECEIVERSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentReceiverSetEventResponse typedResponse = new PaymentReceiverSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentReceiver = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static VerifiedBalanceIncreasedForEventResponse getVerifiedBalanceIncreasedForEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, log); VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); @@ -701,15 +526,112 @@ class TangemBridgeProcessor extends Contract { return typedResponse; } - public static VerifiedBalanceSetForEventResponse getVerifiedBalanceSetForEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, log); - VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + @Deprecated + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemBridgeProcessor(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static List getPendingRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PendingRefundProcessedEventResponse getPendingRefundProcessedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDPROCESSED_EVENT, log); + PendingRefundProcessedEventResponse typedResponse = new PendingRefundProcessedEventResponse(); typedResponse.log = log; - typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemBridgeProcessor(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemBridgeProcessor load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemBridgeProcessor(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static List getRefundAccountSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDACCOUNTSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundAccountSetEventResponse typedResponse = new RefundAccountSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.refundAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + public static List getRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundProcessedEventResponse typedResponse = new RefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String paymentReceiver_, String refundAccount_, String paymentAccountFactory_, String paymentToken_, BigInteger securityDelay_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(Arrays.asList(new Address(160, paymentReceiver_), + new Address(160, refundAccount_), + new Address(160, paymentAccountFactory_), + new Address(160, paymentToken_), + new Uint256(securityDelay_))); + return deployRemoteCall(TangemBridgeProcessor.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); + } + public Flowable authorizationChangeProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getAuthorizationChangeProcessedEventFromLog(log)); } @@ -720,6 +642,20 @@ class TangemBridgeProcessor extends Contract { return authorizationChangeProcessedEventFlowable(filter); } + public static List getRoleAdminChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEADMINCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleAdminChangedEventResponse typedResponse = new RoleAdminChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.previousAdminRole = (byte[]) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.newAdminRole = (byte[]) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable authorizationProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getAuthorizationProcessedEventFromLog(log)); } @@ -734,6 +670,20 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getDebtWriteOffProcessedEventFromLog(log)); } + public static List getRoleGrantedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEGRANTED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleGrantedEventResponse typedResponse = new RoleGrantedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtWriteOffProcessedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(DEBTWRITEOFFPROCESSED_EVENT)); @@ -750,10 +700,48 @@ class TangemBridgeProcessor extends Contract { return fixedFeeSetEventFlowable(filter); } + public static List getRoleRevokedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(ROLEREVOKED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RoleRevokedEventResponse typedResponse = new RoleRevokedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.role = (byte[]) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.account = (String) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.sender = (String) eventValues.getIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public Flowable pausedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPausedEventFromLog(log)); + } + + public Flowable pausedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAUSED_EVENT)); + return pausedEventFlowable(filter); + } + public Flowable paymentAccountSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountSetEventFromLog(log)); } + public static List getSaveRefundProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SAVEREFUNDPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SaveRefundProcessedEventResponse typedResponse = new SaveRefundProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable paymentAccountSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTSET_EVENT)); @@ -770,6 +758,18 @@ class TangemBridgeProcessor extends Contract { return paymentReceiverSetEventFlowable(filter); } + public static List getSecurityDelaySetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SECURITYDELAYSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SecurityDelaySetEventResponse typedResponse = new SecurityDelaySetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.securityDelay = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable pendingRefundProcessedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPendingRefundProcessedEventFromLog(log)); } @@ -784,6 +784,18 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getRefundAccountSetEventFromLog(log)); } + public static List getSettlementPeriodSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SETTLEMENTPERIODSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SettlementPeriodSetEventResponse typedResponse = new SettlementPeriodSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.settlementPeriod = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable refundAccountSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(REFUNDACCOUNTSET_EVENT)); @@ -800,6 +812,21 @@ class TangemBridgeProcessor extends Contract { return refundProcessedEventFlowable(filter); } + public static List getSettlementProcessedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(SETTLEMENTPROCESSED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + SettlementProcessedEventResponse typedResponse = new SettlementProcessedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.card = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.fee = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable roleAdminChangedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getRoleAdminChangedEventFromLog(log)); } @@ -834,22 +861,25 @@ class TangemBridgeProcessor extends Contract { return web3j.ethLogFlowable(filter).map(log -> getSaveRefundProcessedEventFromLog(log)); } + public static List getVerifiedBalanceIncreasedForEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASEDFOR_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceIncreasedForEventResponse typedResponse = new VerifiedBalanceIncreasedForEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable saveRefundProcessedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(SAVEREFUNDPROCESSED_EVENT)); return saveRefundProcessedEventFlowable(filter); } - public Flowable verifiedBalanceSetForEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceSetForEventFromLog(log)); - } - - public Flowable verifiedBalanceSetForEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { - EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(VERIFIEDBALANCESETFOR_EVENT)); - return verifiedBalanceSetForEventFlowable(filter); - } - public Flowable securityDelaySetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getSecurityDelaySetEventFromLog(log)); } @@ -860,6 +890,38 @@ class TangemBridgeProcessor extends Contract { return securityDelaySetEventFlowable(filter); } + public static List getVerifiedBalanceSetForEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static VerifiedBalanceSetForEventResponse getVerifiedBalanceSetForEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCESETFOR_EVENT, log); + VerifiedBalanceSetForEventResponse typedResponse = new VerifiedBalanceSetForEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + return typedResponse; + } + + public Flowable verifiedBalanceSetForEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceSetForEventFromLog(log)); + } + + public Flowable verifiedBalanceSetForEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(VERIFIEDBALANCESETFOR_EVENT)); + return verifiedBalanceSetForEventFlowable(filter); + } + public Flowable settlementPeriodSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getSettlementPeriodSetEventFromLog(log)); } @@ -880,6 +942,16 @@ class TangemBridgeProcessor extends Contract { return settlementProcessedEventFlowable(filter); } + public Flowable unpausedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getUnpausedEventFromLog(log)); + } + + public Flowable unpausedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(UNPAUSED_EVENT)); + return unpausedEventFlowable(filter); + } + public Flowable verifiedBalanceIncreasedForEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getVerifiedBalanceIncreasedForEventFromLog(log)); } @@ -922,8 +994,24 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallSingleValueReturn(function, byte[].class); } - public RemoteFunctionCall PAYMENT_ACCOUNT_SETTER() { - final Function function = new Function(FUNC_PAYMENT_ACCOUNT_SETTER, + public RemoteFunctionCall FIXED_FEE_SETTER_ROLE() { + final Function function = new Function(FUNC_FIXED_FEE_SETTER_ROLE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, byte[].class); + } + + public RemoteFunctionCall PAUSER_ROLE() { + final Function function = new Function(FUNC_PAUSER_ROLE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, byte[].class); + } + + public RemoteFunctionCall PAYMENT_ACCOUNT_SETTER_ROLE() { + final Function function = new Function(FUNC_PAYMENT_ACCOUNT_SETTER_ROLE, List.of(), List.of(new TypeReference() { })); @@ -1022,6 +1110,22 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallTransaction(function); } + public RemoteFunctionCall pause() { + final Function function = new Function( + FUNC_PAUSE, + List.of(), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall paused() { + final Function function = new Function(FUNC_PAUSED, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, Boolean.class); + } + public RemoteFunctionCall paymentAccountFactory() { final Function function = new Function(FUNC_PAYMENTACCOUNTFACTORY, List.of(), @@ -1107,12 +1211,11 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall processSettlement(String card, BigInteger transactionId, BigInteger settlementId, BigInteger amount) { + public RemoteFunctionCall processSettlement(String card, BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_PROCESSSETTLEMENT, Arrays.asList(new Address(160, card), new Uint256(transactionId), - new Uint256(settlementId), new Uint256(amount)), Collections.emptyList()); return executeRemoteCallTransaction(function); @@ -1236,6 +1339,14 @@ class TangemBridgeProcessor extends Contract { return executeRemoteCallSingleValueReturn(function, Boolean.class); } + public RemoteFunctionCall unpause() { + final Function function = new Function( + FUNC_UNPAUSE, + List.of(), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + public RemoteFunctionCall writeOffDebt(String card, BigInteger amount) { final Function function = new Function( FUNC_WRITEOFFDEBT, @@ -1275,6 +1386,10 @@ class TangemBridgeProcessor extends Contract { public BigInteger fixedFee; } + public static class PausedEventResponse extends BaseEventResponse { + public String account; + } + public static class PaymentAccountSetEventResponse extends BaseEventResponse { public String card; @@ -1350,13 +1465,15 @@ class TangemBridgeProcessor extends Contract { public BigInteger transactionId; - public BigInteger settlementId; - public BigInteger amount; public BigInteger fee; } + public static class UnpausedEventResponse extends BaseEventResponse { + public String account; + } + public static class VerifiedBalanceIncreasedForEventResponse extends BaseEventResponse { public String paymentAccount; diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java index 943509e28e..834d85f3c2 100644 --- a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccount.java @@ -15,6 +15,7 @@ import org.web3j.protocol.core.methods.response.BaseEventResponse; import org.web3j.protocol.core.methods.response.Log; import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.tuples.generated.Tuple3; +import org.web3j.tuples.generated.Tuple7; import org.web3j.tx.Contract; import org.web3j.tx.TransactionManager; import org.web3j.tx.gas.ContractGasProvider; @@ -32,14 +33,14 @@ import io.reactivex.Flowable; *

Auto generated code. *

Do not modify! *

Please use the web3j command line tools, - * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the * codegen module to update. * - *

Generated with web3j version 1.5.0. + *

Generated with web3j version 1.5.2. */ @SuppressWarnings("rawtypes") class TangemPaymentAccount extends Contract { - public static final String BINARY = "60c06040523060a05234801562000014575f80fd5b5060405162004c6938038062004c69833981016040819052620000379162000109565b6001600160a01b0381166080526200004e62000055565b5062000138565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620000a65760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001065780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b5f602082840312156200011a575f80fd5b81516001600160a01b038116811462000131575f80fd5b9392505050565b60805160a051614af3620001765f395f8181612b9501528181612bbe0152612e1b01525f8181610439015281816104bb01526125500152614af35ff3fe60806040526004361061027f575f3560e01c80638c3b2a9b11610155578063ce1b1d43116100be578063e37b815411610078578063e37b8154146108c2578063e3dbffd51461092e578063e4d7ddb91461094d578063e6e268f41461096c578063f89db45714610980578063f92ad2191461099f575f80fd5b8063ce1b1d4314610831578063d47ae89c14610850578063dbed8faa14610864578063dcab7e5014610883578063df7727b4146102aa578063e1a8eafd146108a3575f80fd5b8063b4d02d0d1161010f578063b4d02d0d14610776578063b9603bdf14610795578063bb542ef0146107b4578063be69191d146107d3578063c45a0155146107e7578063c81656c814610806575f80fd5b80638c3b2a9b146106125780638cf351e7146106315780639335dcb714610650578063a075b7c71461066f578063a789457d1461068e578063ad3cb1cc14610739575f80fd5b8063456575e5116101f75780636b4943a7116101b15780636b4943a7146104985780637da0a877146104ad578063806679e4146104df578063860aefcf146104fe57806386f15d09146105df5780638bbe11af146105f3575f80fd5b8063456575e5146103c357806346236eb9146103e25780634f1ef286146103f657806352d1902d14610409578063572b6c051461041d5780636b2a383014610479575f80fd5b806322611280116102485780632261128014610308578063229865d11461031c5780633013ce291461033b57806334284dfb1461037157806335ba9af8146103905780633c1a5012146103a4575f80fd5b806274f3561461028357806301e948ff146102aa5780630f1071be146102be578063139d657c146102d257806319cdeb6f146102f3575b5f80fd5b34801561028e575f80fd5b506102976109be565b6040519081526020015b60405180910390f35b3480156102b5575f80fd5b506102976109d6565b3480156102c9575f80fd5b506102976109e5565b3480156102dd575f80fd5b506102f16102ec3660046140bc565b610a66565b005b3480156102fe575f80fd5b5061029760055481565b348015610313575f80fd5b506102f1610d5b565b348015610327575f80fd5b506102f16103363660046140e5565b610de8565b348015610346575f80fd5b505f54610359906001600160a01b031681565b6040516001600160a01b0390911681526020016102a1565b34801561037c575f80fd5b506102f161038b366004614136565b610e7f565b34801561039b575f80fd5b50610297610f45565b3480156103af575f80fd5b506102f16103be3660046141a1565b610fd4565b3480156103ce575f80fd5b506102f16103dd3660046141bc565b611060565b3480156103ed575f80fd5b50601b54610297565b6102f16104043660046141f0565b611100565b348015610414575f80fd5b5061029761111f565b348015610428575f80fd5b506104696104373660046141a1565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0390811691161490565b60405190151581526020016102a1565b348015610484575f80fd5b506102f16104933660046140e5565b61113a565b3480156104a3575f80fd5b5061029760065481565b3480156104b8575f80fd5b507f0000000000000000000000000000000000000000000000000000000000000000610359565b3480156104ea575f80fd5b506102f16104f93660046142ae565b611287565b348015610509575f80fd5b506040805160a08082018352600c54825282518084018452600d548152600e546020828101919091528084019190915283518085018552600f5481526010548183015283850152835180820185526011548152606080850191909152601254608080860191909152855193840186526013548452855180870187526014548152601554818501528484015285518087018752601654815260175481850152848701528551928301909552601854825282015260195492810192909252601a546105d0929083565b6040516102a19392919061432c565b3480156105ea575f80fd5b5061046961132a565b3480156105fe575f80fd5b506102f161060d3660046140e5565b611351565b34801561061d575f80fd5b506102f161062c366004614357565b611468565b34801561063c575f80fd5b506102f161064b366004614388565b611583565b34801561065b575f80fd5b50600354610359906001600160a01b031681565b34801561067a575f80fd5b506102f16106893660046143ca565b611710565b348015610699575f80fd5b506040805180820182526007546001600160a01b039081168252825180840184526008546001600160801b0319608082811b8216845261ffff600160801b938490048116602086810191909152808801959095528751808901895260095490961686528751808901909852600a549182901b9092168752919091041684820152810192909252600b5461072a929083565b6040516102a19392919061442a565b348015610744575f80fd5b50610769604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516102a19190614475565b348015610781575f80fd5b506102f16107903660046140e5565b611801565b3480156107a0575f80fd5b506102f16107af3660046140e5565b611901565b3480156107bf575f80fd5b506102f16107ce3660046141a1565b611a37565b3480156107de575f80fd5b50610469611b08565b3480156107f2575f80fd5b50600154610359906001600160a01b031681565b348015610811575f80fd5b506102976108203660046140e5565b5f908152601c602052604090205490565b34801561083c575f80fd5b50600254610359906001600160a01b031681565b34801561085b575f80fd5b50610297611b2d565b34801561086f575f80fd5b506102f161087e3660046140e5565b611bad565b34801561088e575f80fd5b505f8051602061479083398151915254610297565b3480156108ae575f80fd5b506102f16108bd3660046141a1565b611c0c565b3480156108cd575f80fd5b506109096108dc3660046140e5565b60046020525f9081526040902080546001909101546001600160801b03811690600160801b900460ff1683565b604080519384526001600160801b0390921660208401521515908201526060016102a1565b348015610939575f80fd5b506102f16109483660046140e5565b611da8565b348015610958575f80fd5b506102f16109673660046141bc565b611f82565b348015610977575f80fd5b5061029761212a565b34801561098b575f80fd5b506102f161099a3660046140e5565b61213f565b3480156109aa575f80fd5b506102f16109b93660046144a7565b61230d565b5f805f805160206147708339815191525b5492915050565b5f6109e05f61247c565b905090565b5f6109e060025f9054906101000a90046001600160a01b03166001600160a01b0316630f1071be6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610a39573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610a5d91906144e7565b6283d600612534565b6002546001600160a01b0316610a7a61254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990610ac45760405162461bcd60e51b8152600401610abb9190614475565b60405180910390fd5b50610ace8161259e565b5f8381526004602052604081205490818311610b00575081808203828214610afa57610afa86826125f0565b50610b7f565b50805f610b0d600c612634565b80549091505f81851015610b4157610b30858303610b2b855f61264e565b612534565b9050610b3e81610b2b6109d6565b90505b8015610b67575f610b5482878903612534565b948501949050610b6584825f6126b9565b505b838603868514610b7a57610b7a816126e0565b505050505b816006541015604051806060016040528060298152602001614a186029913990610bbc5760405162461bcd60e51b8152600401610abb9190614475565b506006805483900390555f85815260046020818152604080842093845560019390930180546001600160881b0319169055600254835163659bf9d960e11b81529351610c5d946001600160a01b039092169363cb37f3b29383820193909291908290030181865afa158015610c33573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610c5791906144fe565b8261273f565b6040805184815260208101839052859187917f3e565e38537fb4064a62ad5c266c37a9798b673d92066b5bbbd2269c70078fd1910160405180910390a35f546040516370a0823160e01b81523060048201527f273f30c859762d267fd40559825ed2fa0f467908c92843dfd0184b9d577dc082916001600160a01b0316906370a0823190602401602060405180830381865afa158015610cff573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610d2391906144e7565b600654610d2e6109be565b601b5460408051948552602085019390935291830152606082015260800160405180910390a15050505050565b6003546001600160a01b0316610d6f61254d565b6001600160a01b03161480610da65750610d89600761284a565b546001600160a01b0316610d9b61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990610ddd5760405162461bcd60e51b8152600401610abb9190614475565b50610de6612864565b565b6003546001600160a01b0316610dfc61254d565b6001600160a01b03161480610e335750610e16600761284a565b546001600160a01b0316610e2861254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990610e6a5760405162461bcd60e51b8152600401610abb9190614475565b50610e7c81610e77611b2d565b6128b2565b50565b6002546001600160a01b0316610e9361254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990610ed45760405162461bcd60e51b8152600401610abb9190614475565b506001610ee2848484612963565b610eee868684846129bf565b604080518681526001600160801b03198616602082015261ffff851681830152905187917faab1db26b8ce0ed14d67fd3d8eab92617f4caf4134148122ad6f2a4e20068df7919081900360600190a2505050505050565b5f80546040516370a0823160e01b815230600482015282916001600160a01b0316906370a0823190602401602060405180830381865afa158015610f8b573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190610faf91906144e7565b9050600654811015610fc1575f610fce565b600654610fce908261452d565b91505090565b6003546001600160a01b0316610fe861254d565b6001600160a01b0316148061101f5750611002600761284a565b546001600160a01b031661101461254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906110565760405162461bcd60e51b8152600401610abb9190614475565b50610e7c81612a24565b6002546001600160a01b031661107461254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906110b55760405162461bcd60e51b8152600401610abb9190614475565b506110c2601b8383612b0f565b817fea92e836c1b642a333bb58b44d5f13c5a066c5b0f35b8d6da5032af605c9e031826040516110f491815260200190565b60405180910390a25050565b611108612b8a565b61111182612c2e565b61111b8282612d54565b5050565b5f611128612e10565b505f8051602061484383398151915290565b6002546001600160a01b031661114e61254d565b6001600160a01b0316146040518060600160405280603381526020016145b9603391399061118f5760405162461bcd60e51b8152600401610abb9190614475565b505f8160055461119f9190614540565b5f546040516370a0823160e01b81523060048201529192506001600160a01b0316906370a0823190602401602060405180830381865afa1580156111e5573d5f803e3d5ffd5b505050506040513d601f19601f8201168201806040525081019061120991906144e7565b81111560405180608001604052806048815260200161468d60489139906112435760405162461bcd60e51b8152600401610abb9190614475565b50600581905560408051838152602081018390527fd6004694c2f543b69302481de26731f63c1d41062795b5366d8ca20fd3e3d6d391015b60405180910390a15050565b6002546001600160a01b031661129b61254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906112dc5760405162461bcd60e51b8152600401610abb9190614475565b505f6112ea848484846129bf565b837f5e1266400272ee17509f7d7edb190c0fa83572f90677ee6a3bbfac22bf1f679d8460405161131c91815260200190565b60405180910390a250505050565b5f80516020614790833981519152545f905f80516020614730833981519152901515610fce565b5f818152600460205260409020600181015490546001600160801b03909116906113796109e5565b6113839083614540565b42116040518060600160405280602b81526020016145ec602b9139906113bc5760405162461bcd60e51b8152600401610abb9190614475565b50806006541015604051806060016040528060298152602001614a1860299139906113fa5760405162461bcd60e51b8152600401610abb9190614475565b506006805482900390555f8381526004602052604080822091825560019190910180546001600160881b03191690555183907f056bed1bad5d90aa1df671c8d28b1bbb82ff67fd545f804e63cd261140b11f7b9061145b9084815260200190565b60405180910390a2505050565b6003546001600160a01b031661147c61254d565b6001600160a01b031614806114b35750611496600761284a565b546001600160a01b03166114a861254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906114ea5760405162461bcd60e51b8152600401610abb9190614475565b506040805180820182526009546001600160a01b0316815281518083019092526001600160801b03198416825261ffff831660208381019190915281019190915261154090611537611b2d565b60079190612e59565b604080516001600160801b03198416815261ffff831660208201527fc50d2cc66f62a75104edeabfbcedb2a37ebd1cb8ce0b873ec3afa504e8bb1480910161127b565b6003546001600160a01b031661159761254d565b6001600160a01b031614806115ce57506115b1600761284a565b546001600160a01b03166115c361254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906116055760405162461bcd60e51b8152600401610abb9190614475565b50600b541561165e576116596040518060400160405280856001600160a01b031681526020016040518060400160405280866001600160801b03191681526020018561ffff16815250815250611537611b2d565b6116b1565b6116b16040518060400160405280856001600160a01b031681526020016040518060400160405280866001600160801b03191681526020018561ffff16815250815250600761300090919063ffffffff16565b604080516001600160a01b03851681526001600160801b03198416602082015261ffff8316918101919091527fae81d31085749debb22edb9a09f263e50cf6047cfc57ec741c07ce62b1121b9a906060015b60405180910390a1505050565b6003546001600160a01b031661172461254d565b6001600160a01b0316148061175b575061173e600761284a565b546001600160a01b031661175061254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906117925760405162461bcd60e51b8152600401610abb9190614475565b506117b36117a2858585856130a1565b6117aa611b2d565b600c9190613102565b6040805185815260208101859052908101839052606081018290527ffc3ccf3ca5a38fad8786ed39386de19cb574b06205422992c583e4753aa6268f9060800160405180910390a150505050565b6003546001600160a01b031661181561254d565b6001600160a01b0316148061184c575061182f600761284a565b546001600160a01b031661184161254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f9139906118835760405162461bcd60e51b8152600401610abb9190614475565b50610e7c8160025f9054906101000a90046001600160a01b03166001600160a01b031663cb37f3b26040518163ffffffff1660e01b8152600401602060405180830381865afa1580156118d8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906118fc91906144fe565b6131f0565b6002546001600160a01b031661191561254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906119565760405162461bcd60e51b8152600401610abb9190614475565b505f546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa15801561199c573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906119c091906144e7565b81111560405180608001604052806048815260200161468d60489139906119fa5760405162461bcd60e51b8152600401610abb9190614475565b5060058190556040518181527f321b800465e47508430424e6c089344256e17f9a5b0b43c751dffbdfd3930263906020015b60405180910390a150565b6003546001600160a01b0316611a4b61254d565b6001600160a01b03161480611a825750611a65600761284a565b546001600160a01b0316611a7761254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990611ab95760405162461bcd60e51b8152600401610abb9190614475565b50600380546001600160a01b0319166001600160a01b0383169081179091556040519081527f0435c4db9db56b8cdd5d58a213f47d6fed6c2b35c81bbd6baf87547848f4c88b90602001611a2c565b5f5f80516020614730833981519152610fce5f805160206147908339815191526133a4565b5f6109e060025f9054906101000a90046001600160a01b03166001600160a01b031663d47ae89c6040518163ffffffff1660e01b8152600401602060405180830381865afa158015611b81573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611ba591906144e7565b610e10612534565b6002546001600160a01b0316611bc161254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611c025760405162461bcd60e51b8152600401610abb9190614475565b50610e7c8161340b565b6003546001600160a01b0316611c2061254d565b6001600160a01b03161480611c575750611c3a600761284a565b546001600160a01b0316611c4c61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990611c8e5760405162461bcd60e51b8152600401610abb9190614475565b50600254604080516060810190915260238082526001600160a01b039092161591614641602083013990611cd55760405162461bcd60e51b8152600401610abb9190614475565b50600280546001600160a01b0319166001600160a01b03831690811790915560408051633013ce2960e01b81529051633013ce29916004808201926020929091908290030181865afa158015611d2d573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611d5191906144fe565b5f80546001600160a01b0319166001600160a01b0392831690811790915560408051928416835260208301919091527fb84a8bf26086331c9421c7ce6650da3b0ef748bd335c3f00ada56f481e4321939101611a2c565b6002546001600160a01b0316611dbc61254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611dfd5760405162461bcd60e51b8152600401610abb9190614475565b505f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015611e44573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611e6891906144e7565b905060025f9054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b81526004015f604051808303815f87803b158015611eb6575f80fd5b505af1158015611ec8573d5f803e3d5ffd5b50505f80546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa158015611f14573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190611f3891906144e7565b611f42919061452d565b9050611f50601b84836134e9565b827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c08260405161145b91815260200190565b6002546001600160a01b0316611f9661254d565b6001600160a01b0316146040518060600160405280603381526020016145b96033913990611fd75760405162461bcd60e51b8152600401610abb9190614475565b505f60045f8481526020019081526020015f205f015490505f811160405180606001604052806023815260200161470d60239139906120295760405162461bcd60e51b8152600401610abb9190614475565b508082106040518060600160405280603b8152602001614929603b9139906120645760405162461bcd60e51b8152600401610abb9190614475565b5081810361207284826125f0565b806006541015604051806060016040528060298152602001614a1860299139906120af5760405162461bcd60e51b8152600401610abb9190614475565b506006805482900390555f8390036120e7575f84815260046020526040812090815560010180546001600160881b03191690556120f8565b5f8481526004602052604090208390555b837f8274e5392b3ab6bba68ee0567f4ede55e361239b8d0122fa5d3b5dc36497b91d8460405161131c91815260200190565b5f805f805160206147308339815191526109cf565b6002546001600160a01b031661215361254d565b6001600160a01b0316146040518060600160405280603381526020016145b960339139906121945760405162461bcd60e51b8152600401610abb9190614475565b505f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa1580156121db573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906121ff91906144e7565b905060025f9054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b81526004015f604051808303815f87803b15801561224d575f80fd5b505af115801561225f573d5f803e3d5ffd5b50505f80546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa1580156122ab573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906122cf91906144e7565b6122d9919061452d565b9050827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c08260405161145b91815260200190565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff165f811580156123525750825b90505f8267ffffffffffffffff16600114801561236e5750303b155b90508115801561237c575080155b1561239a5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff1916600117855583156123c457845460ff60401b1916600160401b1785555b6123cc6135ce565b6123d46135ce565b6123dc6135ce565b6123e461254d565b600180546001600160a01b03199081166001600160a01b039384161790915560038054909116918c1691909117905561242a6124228a8a8a8a6130a1565b600c906135d6565b831561247057845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50505050505050505050565b5f80546040516370a0823160e01b815230600482015282916124f4916001600160a01b03909116906370a0823190602401602060405180830381865afa1580156124c8573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906124ec91906144e7565b600554612534565b600654909150836125145761250761212a565b6125119082614540565b90505b80821015612522575f61252c565b61252c818361452d565b949350505050565b5f8183106125425781612544565b825b90505b92915050565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031633148015612587575060143610155b15612599575060131936013560601c90565b503390565b60408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f000000000060208201528161111b5760405162461bcd60e51b8152600401610abb9190614475565b5f828152600460205260409020600101546001600160801b03811690600160801b900460ff1661262e838383612626600c612634565b929190613662565b50505050565b5f4282600e015411156126475781612547565b5060070190565b5f61265b836005016133a4565b1561268c57811561267157506001820154612547565b600183015460038401546126859190612534565b9050612547565b811561269e5761268583600101613690565b6126856126ad84600101613690565b610b2b85600301613690565b6126c2836136b0565b6126cc83836136e0565b806126db576126db83836136ed565b505050565b5f805160206147708339815191528054829082905f90612701908490614540565b909155505080546040805184815260208101929092527ff3a2c404b3362c4238317c3dad1020c2e9e984ba8df38a4a1ff18d57e92a242a910161127b565b5f80546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612785573d5f803e3d5ffd5b505050506040513d601f19601f820116820180604052508101906127a991906144e7565b905080600554146040518060600160405280603381526020016149b760339139906127e75760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601f81525f8051602061475083398151915260208201528282101561282a5760405162461bcd60e51b8152600401610abb9190614475565b506005805483900390555f546126db906001600160a01b031684846136fa565b5f428260040154111561285d5781612547565b5060020190565b5f5f805160206147308339815191528181555f8051602061479083398151915282905560405190917f8892a543ee6d561b73cc067b2cde5a76c251a4e0b978d92390f1301db5d125ab91a150565b6128ba610f45565b8211156040518060400160405280601f81526020015f80516020614750833981519152815250906128fe5760405162461bcd60e51b8152600401610abb9190614475565b505f805160206147308339815191528281556129275f805160206147908339815191528361374c565b60018101546040805185815260208101929092527ffa4870dc2e2f95d11863e7e765a98e12fe92c3d1d2a703b4cf983f9c4b1052e69101611703565b600b54421015806129715750805b6040518060600160405280603581526020016148cd60359139906129a85760405162461bcd60e51b8152600401610abb9190614475565b506126db83836129b8600761284a565b91906137af565b60045f8581526020019081526020015f205f01545f1460405180606001604052806029815260200161498e6029913990612a0c5760405162461bcd60e51b8152600401610abb9190614475565b50612a188383836137ca565b61262e8484848461380b565b5f805160206147308339815191528054612a3c610f45565b8111156040518060400160405280601f81526020015f8051602061475083398151915281525090612a805760405162461bcd60e51b8152600401610abb9190614475565b50612a89611b08565b6040518060600160405280603881526020016146d56038913990612ac05760405162461bcd60e51b8152600401610abb9190614475565b505f8083556001830155612ad483826139a6565b826001600160a01b03167fa578b4c05763eb039caef37b2d7b949c22914c3838384c9c73cefb78c4a8ed0a8260405161145b91815260200190565b826001015f8381526020019081526020015f20545f146040518060600160405280602a8152602001614617602a913990612b5c5760405162461bcd60e51b8152600401610abb9190614475565b505f8281526001840160205260408120829055835482918591612b80908490614540565b9091555050505050565b306001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000161480612c1057507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0316612c045f80516020614843833981519152546001600160a01b031690565b6001600160a01b031614155b15610de65760405163703e46dd60e11b815260040160405180910390fd5b6003546001600160a01b0316612c4261254d565b6001600160a01b03161480612c795750612c5c600761284a565b546001600160a01b0316612c6e61254d565b6001600160a01b0316145b6040518060600160405280602f8152602001614a66602f913990612cb05760405162461bcd60e51b8152600401610abb9190614475565b506001546040516316da1fb760e11b81526001600160a01b03838116600483015290911690632db43f6e90602401602060405180830381865afa158015612cf9573d5f803e3d5ffd5b505050506040513d601f19601f82011682018060405250810190612d1d9190614553565b604051806060016040528060298152602001614a95602991399061111b5760405162461bcd60e51b8152600401610abb9190614475565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015612dae575060408051601f3d908101601f19168201909252612dab918101906144e7565b60015b612dd657604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610abb565b5f805160206148438339815191528114612e0657604051632a87526960e21b815260048101829052602401610abb565b6126db83836139dd565b306001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001614610de65760405163703e46dd60e11b815260040160405180910390fd5b602082015151600384015460801b6001600160801b03199081169116141580612e9c575060208083015101516003840154600160801b900461ffff908116911614155b80612eb95750815160028401546001600160a01b03908116911614155b6040518060600160405280602781526020016149026027913990612ef05760405162461bcd60e51b8152600401610abb9190614475565b506004830154421015612f695781516002840180546001600160a01b039092166001600160a01b03199092169190911790556020808301518051600386018054929093015161ffff16600160801b026001600160901b031990921660809190911c17179055612f5f8142614540565b6004840155505050565b60028301805484546001600160a01b03199081166001600160a01b0380841691909117875560038701805460018901805461ffff600160801b808504821681026001600160901b03199384166001600160801b03871617179093558a51909516959096169490941790955560208088015180519101519092169092029290931660809390931c92909217179055612f5f8142614540565b60048201541515156040518060600160405280602f815260200161458a602f91399061303f5760405162461bcd60e51b8152600401610abb9190614475565b5080516002830180546001600160a01b039092166001600160a01b03199092169190911790556020908101518051600384018054929093015161ffff16600160801b026001600160901b031990921660809190911c1717905542600490910155565b6130a961404a565b506040805160a081018252948552805180820182529384525f6020858101829052808701959095528151808301835293845283850152848101929092528151928301909152600182526060830191909152608082015290565b600e830154421015613166578151600784015560208083015180516008860155810151600985015560408301518051600a8601550151600b840155606082015151600c8401556080820151600d84015561315c8142614540565b600e840155505050565b600783018054845560088401805460018601556009850180546002870155600a860180546003880155600b870180546004890155600c8801805460058a0155600d8901805460068b015588519096556020808901518051909655948501519093556040870151805190925592015190915560608401515190556080830151905561315c8142614540565b5f805160206147708339815191525f6132076109d6565b825460408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f00000000006020820152919250908561325f5760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601881527f353430317c446562743a206e6f206465627420666f756e6400000000000000006020820152816132b25760405162461bcd60e51b8152600401610abb9190614475565b5060408051808201909152601d81527f353430327c446562743a206e6f2066756e647320617661696c61626c650000006020820152826133055760405162461bcd60e51b8152600401610abb9190614475565b50808511156040518060600160405280602581526020016147d560259139906133415760405162461bcd60e51b8152600401610abb9190614475565b505f61334d8387612534565b8454819003855590506133608582613a32565b83546040805183815260208101929092527f75aec94f6ad949cfd9728eb818639f1b0b882089b480409ab73245c06469eef4910160405180910390a1505050505050565b5f6133af8254151590565b6040518060400160405280601d81526020017f353534317c54696d6572733a2074696d6572206e6f7420616374697665000000815250906134035760405162461bcd60e51b8152600401610abb9190614475565b505054421190565b5f80516020614770833981519152805460408051808201909152601b81527f353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f000000000060208201528361346d5760405162461bcd60e51b8152600401610abb9190614475565b50808311156040518060600160405280602581526020016147d560259139906134a95760405162461bcd60e51b8152600401610abb9190614475565b5081548390038083556040805185815260208101929092527fb940258db5193d85cafbc591d0c29fad88d804ff8b9ab71c8f9545af2bb2c07c9101611703565b5f836001015f8481526020019081526020015f205490505f81116040518060600160405280602581526020016147b0602591399061353a5760405162461bcd60e51b8152600401610abb9190614475565b50808214604051806060016040528060258152602001614a4160259139906135755760405162461bcd60e51b8152600401610abb9190614475565b50835f01548111156040518060600160405280602a8152602001614964602a9139906135b45760405162461bcd60e51b8152600401610abb9190614475565b508354038355505f90815260019091016020526040812055565b610de6613a3c565b600e8201541515156040518060600160405280602f815260200161458a602f9139906136155760405162461bcd60e51b8152600401610abb9190614475565b508051600783015560208082015180516008850155810151600984015560408201518051600a8501550151600b830155606081015151600c83015560800151600d82015542600e90910155565b61366c8483613a85565b1561262e5761367e6001850184613ac0565b8061262e5761262e6003850184613ac0565b805460018201545f9190808210156136a8575f61252c565b900392915050565b6136bc816005016133a4565b15610e7c575f60028201555f60048201556006810154610e7c90600583019061374c565b61111b6001830182613b0f565b61111b6003830182613b0f565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b1790526126db908490613b2b565b60408051808201909152601781527f353534307c54696d6572733a207a65726f2064656c617900000000000000000060208201528161379e5760405162461bcd60e51b8152600401610abb9190614475565b506137a98142614540565b90915550565b82546126db9060018501906001600160a01b03168484613b8c565b601a54421015806137d85750815b156137f3576126db83826137ec600c612634565b9190613d12565b6137ff600c8483613d12565b6126db60138483613d12565b6138148361259e565b5f821561387d575f613826600161247c565b905084811061383757849150613877565b809150857f83067e3478aa8b638f7e34eeecb23875cc3671e615445e892cc22dcd4c8b8df382870360405161386e91815260200190565b60405180910390a25b506138ce565b6138856109d6565b8411156040518060400160405280601f81526020015f80516020614750833981519152815250906138c95760405162461bcd60e51b8152600401610abb9190614475565b508390505b60408051808201909152601f81527f353331337c4163636f756e743a206e6f2066756e647320746f20626c6f636b006020820152816139205760405162461bcd60e51b8152600401610abb9190614475565b50604080516060810182528281526001600160801b0342811660208084019182528615158486019081525f8b815260049092529481209351845590516001909301805494511515600160801b026001600160881b03199095169390921692909217929092179091556006805483929061399a908490614540565b90915550505050505050565b8060055410156139b6575f6139c4565b806005546139c4919061452d565b6005555f5461111b906001600160a01b031683836136fa565b6139e682613d73565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a2805115613a2a576126db8282613dd6565b61111b613e48565b61111b828261273f565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff16610de657604051631afcd79f60e31b815260040160405180910390fd5b5f60058301613a93816133a4565b15613aa1575f915050612547565b600684015481545f91613ab39161452d565b8410159250612547915050565b80826001015410156040518060600160405280602e81526020016149ea602e913990613aff5760405162461bcd60e51b8152600401610abb9190614475565b5060019091018054919091039055565b80826001015f828254613b229190614540565b90915550505050565b5f613b3f6001600160a01b03841683613e67565b905080515f14158015613b63575080806020019051810190613b619190614553565b155b156126db57604051635274afe760e01b81526001600160a01b0384166004820152602401610abb565b83546040805160608101909152602980825261ffff600160801b909304831692841683119190614664602083013990613bd85760405162461bcd60e51b8152600401610abb9190614475565b5082825b8261ffff168161ffff161015613c94576040516bffffffffffffffffffffffff19606088901b1660208201526001600160f01b031960f083901b1660348201526001600160801b03198316603682015260029060460160408051601f1981840301815290829052613c4c9161456e565b602060405180830381855afa158015613c67573d5f803e3d5ffd5b5050506040513d601f19601f82011682018060405250810190613c8a91906144e7565b9150600101613bdc565b5085546040805160608101909152602180825260809290921b6001600160801b03199081169084161491614822602083013990613ce45760405162461bcd60e51b8152600401610abb9190614475565b5050845461ffff909216600160801b026001600160901b031990921660809390931c92909217179092555050565b825f01548211156040518060600160405280603b8152602001614892603b913990613d505760405162461bcd60e51b8152600401610abb9190614475565b50613d5a836136b0565b613d648383613e74565b806126db576126db8383613ec3565b806001600160a01b03163b5f03613da857604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610abb565b5f8051602061484383398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f80846001600160a01b031684604051613df2919061456e565b5f60405180830381855af49150503d805f8114613e2a576040519150601f19603f3d011682016040523d82523d5f602084013e613e2f565b606091505b5091509150613e3f858383613f15565b95945050505050565b3415610de65760405163b398979f60e01b815260040160405180910390fd5b606061254483835f613f74565b613e81600183018261400d565b6040518060600160405280602881526020016147fa6028913990613eb85760405162461bcd60e51b8152600401610abb9190614475565b5061111b82826136e0565b613ed0600383018261400d565b6040518060600160405280602f8152602001614863602f913990613f075760405162461bcd60e51b8152600401610abb9190614475565b5061111b6003830182613b0f565b606082613f2a57613f2582614021565b613f6d565b8151158015613f4157506001600160a01b0384163b155b15613f6a57604051639996b31560e01b81526001600160a01b0385166004820152602401610abb565b50805b9392505050565b606081471015613f995760405163cd78605960e01b8152306004820152602401610abb565b5f80856001600160a01b03168486604051613fb4919061456e565b5f6040518083038185875af1925050503d805f8114613fee576040519150601f19603f3d011682016040523d82523d5f602084013e613ff3565b606091505b5091509150614003868383613f15565b9695505050505050565b5f61401783613690565b9091111592915050565b8051156140315780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b6040518060a001604052805f815260200161407660405180604001604052805f81526020015f81525090565b815260200161409660405180604001604052805f81526020015f81525090565b81526020016140b060405180602001604052805f81525090565b81526020015f81525090565b5f805f606084860312156140ce575f80fd5b505081359360208301359350604090920135919050565b5f602082840312156140f5575f80fd5b5035919050565b80356001600160801b031981168114614113575f80fd5b919050565b803561ffff81168114614113575f80fd5b8015158114610e7c575f80fd5b5f805f805f60a0868803121561414a575f80fd5b8535945060208601359350614161604087016140fc565b925061416f60608701614118565b9150608086013561417f81614129565b809150509295509295909350565b6001600160a01b0381168114610e7c575f80fd5b5f602082840312156141b1575f80fd5b8135613f6d8161418d565b5f80604083850312156141cd575f80fd5b50508035926020909101359150565b634e487b7160e01b5f52604160045260245ffd5b5f8060408385031215614201575f80fd5b823561420c8161418d565b9150602083013567ffffffffffffffff80821115614228575f80fd5b818501915085601f83011261423b575f80fd5b81358181111561424d5761424d6141dc565b604051601f8201601f19908116603f01168101908382118183101715614275576142756141dc565b8160405282815288602084870101111561428d575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f805f606084860312156142c0575f80fd5b833592506020840135915060408401356142d981614129565b809150509250925092565b8051825260208082015180518285015290810151604084015250604081015180516060840152602081015160808401525060608101515160a08301526080015160c090910152565b6101e0810161433b82866142e4565b61434860e08301856142e4565b826101c0830152949350505050565b5f8060408385031215614368575f80fd5b614371836140fc565b915061437f60208401614118565b90509250929050565b5f805f6060848603121561439a575f80fd5b83356143a58161418d565b92506143b3602085016140fc565b91506143c160408501614118565b90509250925092565b5f805f80608085870312156143dd575f80fd5b5050823594602084013594506040840135936060013592509050565b80516001600160a01b0316825260209081015180516001600160801b03191682840152015161ffff16604090910152565b60e0810161443882866143f9565b61444560608301856143f9565b8260c0830152949350505050565b5f5b8381101561446d578181015183820152602001614455565b50505f910152565b602081525f8251806020840152614493816040850160208701614453565b601f01601f19169190910160400192915050565b5f805f805f60a086880312156144bb575f80fd5b85356144c68161418d565b97602087013597506040870135966060810135965060800135945092505050565b5f602082840312156144f7575f80fd5b5051919050565b5f6020828403121561450e575f80fd5b8151613f6d8161418d565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561254757612547614519565b8082018082111561254757612547614519565b5f60208284031215614563575f80fd5b8151613f6d81614129565b5f825161457f818460208701614453565b919091019291505056fe353530307c44656c6179656453657474696e67733a2076616c756520616c726561647920696e697469616c697a6564353330327c4163636f756e743a206f6e6c792070726f636573736f722063616e2063616c6c20746869732066756e6374696f6e353334307c4163636f756e743a20736574746c656d656e7420706572696f64206973206e6f74206f766572353432307c526566756e64733a20726566756e64207265636f726420616c726561647920657869737473353336307c4163636f756e743a2070726f636573736f7220616c726561647920736574353531307c4f6e6554696d6550617373776f72643a20696e76616c6964204f545020636f756e746572353335307c4163636f756e743a20726573756c74696e672076657269666965642062616c616e6365206e6f742065717569616c20746f2062616c616e636520616e64206e6f742030353431307c44656c617965645769746864726177616c733a207769746864726177616c2074696d656c6f636b206e6f742065787072696564353332307c4163636f756e743a207472616e73616374696f6e206e6f7420666f756e6476d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435500353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473003486e95df892f06bd37fd38c44e2fee4c4efb6660a66e6eb20ed6d8a167eae0076d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435501353432317c526566756e64733a20726566756e64207265636f7264206e6f7420666f756e64353430337c446562743a20616d6f756e7420697320686967686572207468616e2064656274353532317c4163636f756e744c696d6974733a207370656e64206c696d6974206578636565646564353531317c4f6e6554696d6550617373776f72643a20696e76616c6964204f5450360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc353532337c4163636f756e744c696d6974733a206e6f206f7470207370656e64206c696d6974206578636565646564353532307c4163636f756e744c696d6974733a20616d6f756e7420657863656564732073696e676c65207472616e73616374696f6e206c696d6974353331327c4163636f756e743a2063617264206f72204f5450207374617465206368616e676520697320696e2070726f6772657373353530317c44656c6179656453657474696e67733a2076616c756520616c726561647920736574353332317c4163636f756e743a206e657720616d6f756e7420697320657175616c206f7220686967686572207468616e20617574686f72697a6564353432337c526566756e64733a2070656e64696e6720616d6f756e74206578636565647320746f74616c353331307c4163636f756e743a207472616e73616374696f6e20494420616c72656164792075736564353333307c4163636f756e743a2076657269666965642062616c616e6365206e6f7420657175616c20746f2062616c616e6365353533317c5370656e644c696d6974733a20696e73756666696369656e74207370656e7420746f2063616e63656c353330347c4163636f756e743a20696e73756666696369656e7420626c6f636b656420616d6f756e74353432327c526566756e64733a20696e76616c696420616d6f756e74207265636569766564353330317c4163636f756e743a206f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e353337307c4163636f756e743a20756e617574686f72697a656420696d706c656d656e746174696f6ea264697066735822122012a8bf0b550c0636d300948851d60c701346e82b5d29b544481d654d6f3ba1f164736f6c63430008160033"; + public static final String BINARY = "60c06040523060a0523480156200001557600080fd5b5060405162005aa938038062005aa983398101604081905262000038916200010a565b6001600160a01b0381166080526200004f62000056565b506200013c565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff1615620000a75760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b0390811614620001075780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b6000602082840312156200011d57600080fd5b81516001600160a01b03811681146200013557600080fd5b9392505050565b60805160a05161592b6200017e600039600081816132fe0152818161332701526135880152600081816104d301528181610578015261290d015261592b6000f3fe6080604052600436106102c85760003560e01c806386489ba911610175578063c470d703116100dc578063df7727b411610095578063e3dbffd51161006f578063e3dbffd514610a40578063e4d7ddb914610a60578063e6e268f414610a80578063f89db45714610a9557600080fd5b8063df7727b4146102f5578063e1a8eafd146109b2578063e37b8154146109d257600080fd5b8063c470d703146108da578063c81656c81461090e578063ce1b1d431461093b578063d47ae89c1461095b578063dbed8faa14610970578063dcab7e501461099057600080fd5b8063a789457d1161012e578063a789457d1461077b578063ad3cb1cc14610827578063b4d02d0d14610865578063b9603bdf14610885578063be69191d146108a5578063c45a0155146108ba57600080fd5b806386489ba9146106c657806386f15d09146106e65780638bbe11af146106fb5780638c3b2a9b1461071b5780638da5cb5b1461073b578063a075b7c71461075b57600080fd5b80633c1a5012116102345780636b2a3830116101ed5780637da0a877116101c75780637da0a87714610569578063806679e41461059c57806384b0196e146105bc578063860aefcf146105e457600080fd5b80636b2a3830146105135780636b4943a7146105335780637b1039991461054957600080fd5b80633c1a501214610439578063456575e51461045957806346236eb9146104795780634f1ef2861461048e57806352d1902d146104a1578063572b6c05146104b657600080fd5b806322611280116102865780632261128014610377578063229865d11461038c5780633013ce29146103ac578063309433ba146103e457806334284dfb1461040457806335ba9af81461042457600080fd5b806274f356146102cd57806301e948ff146102f55780630381b4dd1461030a5780630f1071be1461032c57806319cdeb6f146103415780631f852f4b14610357575b600080fd5b3480156102d957600080fd5b506102e2610ab5565b6040519081526020015b60405180910390f35b34801561030157600080fd5b506102e2610acf565b34801561031657600080fd5b5061032a610325366004614b0d565b610ae0565b005b34801561033857600080fd5b506102e2610dde565b34801561034d57600080fd5b506102e260065481565b34801561036357600080fd5b5061032a610372366004614b8d565b610e63565b34801561038357600080fd5b5061032a610fa7565b34801561039857600080fd5b5061032a6103a7366004614be2565b611034565b3480156103b857600080fd5b506000546103cc906001600160a01b031681565b6040516001600160a01b0390911681526020016102ec565b3480156103f057600080fd5b5061032a6103ff366004614c2a565b6110cb565b34801561041057600080fd5b5061032a61041f366004614cb0565b611337565b34801561043057600080fd5b506102e26113fd565b34801561044557600080fd5b5061032a610454366004614d0b565b611490565b34801561046557600080fd5b5061032a610474366004614b0d565b61151c565b34801561048557600080fd5b50601c546102e2565b61032a61049c366004614d3e565b6115bc565b3480156104ad57600080fd5b506102e26115db565b3480156104c257600080fd5b506105036104d1366004614d0b565b7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b0390811691161490565b60405190151581526020016102ec565b34801561051f57600080fd5b5061032a61052e366004614be2565b6115f8565b34801561053f57600080fd5b506102e260075481565b34801561055557600080fd5b506002546103cc906001600160a01b031681565b34801561057557600080fd5b507f00000000000000000000000000000000000000000000000000000000000000006103cc565b3480156105a857600080fd5b5061032a6105b7366004614e02565b611749565b3480156105c857600080fd5b506105d16117ed565b6040516102ec9796959493929190614e8b565b3480156105f057600080fd5b506040805160a08082018352600d54825282518084018452600e548152600f546020828101919091528084019190915283518085018552601054815260115481830152838501528351808201855260125481526060808501919091526013546080808601919091528551938401865260145484528551808701875260155481526016548185015284840152855180870187526017548152601854818501528487015285519283019095526019548252820152601a5492810192909252601b546106b7929083565b6040516102ec93929190614f6c565b3480156106d257600080fd5b5061032a6106e1366004614f97565b611899565b3480156106f257600080fd5b50610503611a66565b34801561070757600080fd5b5061032a610716366004614be2565b611a90565b34801561072757600080fd5b5061032a610736366004614ff0565b611ba9565b34801561074757600080fd5b506004546103cc906001600160a01b031681565b34801561076757600080fd5b5061032a610776366004615023565b611cf8565b34801561078757600080fd5b506040805180820182526008546001600160a01b039081168252825180840184526009546001600160801b0319608082811b8216845261ffff600160801b9384900481166020868101919091528088019590955287518089018952600a5490961686528751808901909852600b549182901b9092168752919091041684820152810192909252600c54610818929083565b6040516102ec93929190615086565b34801561083357600080fd5b50610858604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516102ec91906150af565b34801561087157600080fd5b5061032a610880366004614be2565b611de0565b34801561089157600080fd5b5061032a6108a0366004614be2565b611ee3565b3480156108b157600080fd5b5061050361201c565b3480156108c657600080fd5b506001546103cc906001600160a01b031681565b3480156108e657600080fd5b507f3fc394ec90f88dd7e18c46ffeef55f1b6719a2027f4ae9fbc4ab118271891200546102e2565b34801561091a57600080fd5b506102e2610929366004614be2565b6000908152601d602052604090205490565b34801561094757600080fd5b506003546103cc906001600160a01b031681565b34801561096757600080fd5b506102e2612044565b34801561097c57600080fd5b5061032a61098b366004614be2565b6120c8565b34801561099c57600080fd5b506000805160206154fc833981519152546102e2565b3480156109be57600080fd5b5061032a6109cd366004614d0b565b612127565b3480156109de57600080fd5b50610a1b6109ed366004614be2565b600560205260009081526040902080546001909101546001600160801b03811690600160801b900460ff1683565b604080519384526001600160801b0390921660208401521515908201526060016102ec565b348015610a4c57600080fd5b5061032a610a5b366004614be2565b6122c6565b348015610a6c57600080fd5b5061032a610a7b366004614b0d565b6124ac565b348015610a8c57600080fd5b506102e261265c565b348015610aa157600080fd5b5061032a610ab0366004614be2565b612673565b6000806000805160206154dc8339815191525b5492915050565b6000610adb600061284d565b905090565b6003546001600160a01b0316610af4612909565b6001600160a01b0316146040518060600160405280603381526020016153166033913990610b3e5760405162461bcd60e51b8152600401610b3591906150af565b60405180910390fd5b50610b488161295b565b60008281526005602052604081205490818311610b7b575081808203828214610b7557610b75858261299b565b50610bff565b50806000610b89600d6129e0565b8054909150600081851015610bbf57610bae858303610ba98560006129ff565b612a6b565b9050610bbc81610ba9610acf565b90505b8015610be7576000610bd382878903612a6b565b948501949050610be584826000612a83565b505b838603868514610bfa57610bfa81612aaa565b505050505b8160075410156040518060600160405280602981526020016158226029913990610c3c5760405162461bcd60e51b8152600401610b3591906150af565b50600780548390039055600084815260056020908152604080832092835560019290920180546001600160881b0319169055600354825163659bf9d960e11b81529251610cdf936001600160a01b039092169263cb37f3b292600480820193918290030181865afa158015610cb5573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610cd991906150c2565b82612b0b565b604080518481526020810183905285917fa06caae8c696e5deb33f685024044b74125cb932e11521aef47de748321657c4910160405180910390a26000546040516370a0823160e01b81523060048201527f273f30c859762d267fd40559825ed2fa0f467908c92843dfd0184b9d577dc082916001600160a01b0316906370a0823190602401602060405180830381865afa158015610d82573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610da691906150df565b600754610db1610ab5565b601c546040805194855260208501939093529183015260608201526080015b60405180910390a150505050565b6000610adb600360009054906101000a90046001600160a01b03166001600160a01b0316630f1071be6040518163ffffffff1660e01b8152600401602060405180830381865afa158015610e36573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610e5a91906150df565b6283d600612a6b565b6004546001600160a01b0316610e77612909565b6001600160a01b03161480610eae5750610e916008612c1b565b546001600160a01b0316610ea3612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990610ee55760405162461bcd60e51b8152600401610b3591906150af565b50610ef1838383612c36565b600480546001600160a01b038581166001600160a01b0319831681178455600254604051635cd2673760e01b815293831694840185905260248401919091521690635cd2673790604401600060405180830381600087803b158015610f5557600080fd5b505af1158015610f69573d6000803e3d6000fd5b50506040516001600160a01b03871681527f50146d0e3c60aa1d17a70635b05494f864e86144a2201275021014fbf08bafe292506020019050610dd0565b6004546001600160a01b0316610fbb612909565b6001600160a01b03161480610ff25750610fd56008612c1b565b546001600160a01b0316610fe7612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906110295760405162461bcd60e51b8152600401610b3591906150af565b50611032612d2a565b565b6004546001600160a01b0316611048612909565b6001600160a01b0316148061107f57506110626008612c1b565b546001600160a01b0316611074612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906110b65760405162461bcd60e51b8152600401610b3591906150af565b506110c8816110c3612044565b612d7b565b50565b6004546001600160a01b03166110df612909565b6001600160a01b0316148061111657506110f96008612c1b565b546001600160a01b031661110b612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f91399061114d5760405162461bcd60e51b8152600401610b3591906150af565b50611159858383612c36565b600c541561122857600a546040805180820182526001600160a01b03888116825282518084019093526001600160801b03198816835261ffff87166020848101919091528201929092529116906111bb906111b2612044565b60089190612e79565b600254604051630747fc3760e41b81526001600160a01b03838116600483015288811660248301529091169063747fc37090604401600060405180830381600087803b15801561120a57600080fd5b505af115801561121e573d6000803e3d6000fd5b50505050506112db565b61127b6040518060400160405280876001600160a01b031681526020016040518060400160405280886001600160801b03191681526020018761ffff16815250815250600861302090919063ffffffff16565b60025460405163346b888d60e11b81526001600160a01b038781166004830152909116906368d7111a90602401600060405180830381600087803b1580156112c257600080fd5b505af11580156112d6573d6000803e3d6000fd5b505050505b604080516001600160a01b03871681526001600160801b03198616602082015261ffff85168183015290517fae81d31085749debb22edb9a09f263e50cf6047cfc57ec741c07ce62b1121b9a9181900360600190a15050505050565b6003546001600160a01b031661134b612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061138c5760405162461bcd60e51b8152600401610b3591906150af565b50600161139a8484846130c1565b6113a68686848461311d565b604080518681526001600160801b03198616602082015261ffff851681830152905187917faab1db26b8ce0ed14d67fd3d8eab92617f4caf4134148122ad6f2a4e20068df7919081900360600190a2505050505050565b600080546040516370a0823160e01b815230600482015282916001600160a01b0316906370a0823190602401602060405180830381865afa158015611446573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061146a91906150df565b905060075481101561147d57600061148a565b60075461148a908261510e565b91505090565b6004546001600160a01b03166114a4612909565b6001600160a01b031614806114db57506114be6008612c1b565b546001600160a01b03166114d0612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906115125760405162461bcd60e51b8152600401610b3591906150af565b506110c881613186565b6003546001600160a01b0316611530612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906115715760405162461bcd60e51b8152600401610b3591906150af565b5061157e601c8383613274565b817fea92e836c1b642a333bb58b44d5f13c5a066c5b0f35b8d6da5032af605c9e031826040516115b091815260200190565b60405180910390a25050565b6115c46132f3565b6115cd82613398565b6115d782826134c0565b5050565b60006115e561357d565b506000805160206155cf83398151915290565b6003546001600160a01b031661160c612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061164d5760405162461bcd60e51b8152600401610b3591906150af565b5060008160065461165e9190615121565b6000546040516370a0823160e01b81523060048201529192506001600160a01b0316906370a0823190602401602060405180830381865afa1580156116a7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906116cb91906150df565b8111156040518060600160405280603781526020016153c760379139906117055760405162461bcd60e51b8152600401610b3591906150af565b50600681905560408051838152602081018390527fd6004694c2f543b69302481de26731f63c1d41062795b5366d8ca20fd3e3d6d391015b60405180910390a15050565b6003546001600160a01b031661175d612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061179e5760405162461bcd60e51b8152600401610b3591906150af565b5060006117ad8484848461311d565b837f5e1266400272ee17509f7d7edb190c0fa83572f90677ee6a3bbfac22bf1f679d846040516117df91815260200190565b60405180910390a250505050565b60006060808280808381600080516020615541833981519152805490915015801561181a57506001810154155b61185e5760405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b6044820152606401610b35565b6118666135c6565b61186e613689565b60408051600080825260208201909252600f60f81b9c939b5091995046985030975095509350915050565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a008054600160401b810460ff16159067ffffffffffffffff166000811580156118df5750825b905060008267ffffffffffffffff1660011480156118fc5750303b155b90508115801561190a575080155b156119285760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561195257845460ff60401b1916600160401b1785555b61195a6136c8565b6119626136c8565b6119b16040518060400160405280601481526020017315185b99d95b54185e5b595b9d1058d8dbdd5b9d60621b815250604051806040016040528060018152602001603160f81b8152506136d0565b6119b96136c8565b6119c1612909565b600180546001600160a01b03199081166001600160a01b03938416179091556004805482168e841617905560028054909116918c16919091179055611a13611a0b8a8a8a8a6136e2565b600d90613780565b8315611a5957845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b5050505050505050505050565b6000805160206154fc8339815191525460009060008051602061549c83398151915290151561148a565b6000818152600560205260409020600181015490546001600160801b0390911690611ab9610dde565b611ac39083615121565b42116040518060600160405280602b8152602001615349602b913990611afc5760405162461bcd60e51b8152600401610b3591906150af565b508060075410156040518060600160405280602981526020016158226029913990611b3a5760405162461bcd60e51b8152600401610b3591906150af565b5060078054829003905560008381526005602052604080822091825560019190910180546001600160881b03191690555183907f056bed1bad5d90aa1df671c8d28b1bbb82ff67fd545f804e63cd261140b11f7b90611b9c9084815260200190565b60405180910390a2505050565b6004546001600160a01b0316611bbd612909565b6001600160a01b03161480611bf45750611bd76008612c1b565b546001600160a01b0316611be9612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611c2b5760405162461bcd60e51b8152600401610b3591906150af565b50600c5415156040518060600160405280603181526020016157c66031913990611c685760405162461bcd60e51b8152600401610b3591906150af565b50604080518082018252600a546001600160a01b0316815281518083019092526001600160801b03198416825261ffff8316602083810191909152810191909152611cb5906111b2612044565b604080516001600160801b03198416815261ffff831660208201527fc50d2cc66f62a75104edeabfbcedb2a37ebd1cb8ce0b873ec3afa504e8bb1480910161173d565b6004546001600160a01b0316611d0c612909565b6001600160a01b03161480611d435750611d266008612c1b565b546001600160a01b0316611d38612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611d7a5760405162461bcd60e51b8152600401610b3591906150af565b50611d9b611d8a858585856136e2565b611d92612044565b600d919061380c565b6040805185815260208101859052908101839052606081018290527ffc3ccf3ca5a38fad8786ed39386de19cb574b06205422992c583e4753aa6268f90608001610dd0565b6004546001600160a01b0316611df4612909565b6001600160a01b03161480611e2b5750611e0e6008612c1b565b546001600160a01b0316611e20612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f913990611e625760405162461bcd60e51b8152600401610b3591906150af565b506110c881600360009054906101000a90046001600160a01b03166001600160a01b031663cb37f3b26040518163ffffffff1660e01b8152600401602060405180830381865afa158015611eba573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611ede91906150c2565b6138fa565b6003546001600160a01b0316611ef7612909565b6001600160a01b0316146040518060600160405280603381526020016153166033913990611f385760405162461bcd60e51b8152600401610b3591906150af565b506000546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015611f81573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611fa591906150df565b8111156040518060600160405280603781526020016153c76037913990611fdf5760405162461bcd60e51b8152600401610b3591906150af565b5060068190556040518181527f321b800465e47508430424e6c089344256e17f9a5b0b43c751dffbdfd3930263906020015b60405180910390a150565b600060008051602061549c83398151915261148a6000805160206154fc833981519152613a4c565b6000610adb600360009054906101000a90046001600160a01b03166001600160a01b031663d47ae89c6040518163ffffffff1660e01b8152600401602060405180830381865afa15801561209c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906120c091906150df565b610e10612a6b565b6003546001600160a01b03166120dc612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061211d5760405162461bcd60e51b8152600401610b3591906150af565b506110c881613ab4565b6004546001600160a01b031661213b612909565b6001600160a01b0316148061217257506121556008612c1b565b546001600160a01b0316612167612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f9139906121a95760405162461bcd60e51b8152600401610b3591906150af565b50600354604080516060810190915260238082526001600160a01b0390921615916153fe6020830139906121f05760405162461bcd60e51b8152600401610b3591906150af565b50600380546001600160a01b0319166001600160a01b03831690811790915560408051633013ce2960e01b81529051633013ce29916004808201926020929091908290030181865afa15801561224a573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061226e91906150c2565b600080546001600160a01b0319166001600160a01b0392831690811790915560408051928416835260208301919091527fb84a8bf26086331c9421c7ce6650da3b0ef748bd335c3f00ada56f481e4321939101612011565b6003546001600160a01b03166122da612909565b6001600160a01b031614604051806060016040528060338152602001615316603391399061231b5760405162461bcd60e51b8152600401610b3591906150af565b50600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612365573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061238991906150df565b9050600360009054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156123db57600080fd5b505af11580156123ef573d6000803e3d6000fd5b5050600080546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa15801561243e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061246291906150df565b61246c919061510e565b905061247a601c8483613b81565b827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c082604051611b9c91815260200190565b6003546001600160a01b03166124c0612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906125015760405162461bcd60e51b8152600401610b3591906150af565b506000600560008481526020019081526020016000206000015490506000811160405180606001604052806023815260200161547960239139906125585760405162461bcd60e51b8152600401610b3591906150af565b508082106040518060600160405280603b81526020016156d7603b9139906125935760405162461bcd60e51b8152600401610b3591906150af565b508181036125a1848261299b565b80600754101560405180606001604052806029815260200161582260299139906125de5760405162461bcd60e51b8152600401610b3591906150af565b50600780548290039055600083900361261857600084815260056020526040812090815560010180546001600160881b031916905561262a565b60008481526005602052604090208390555b837f8274e5392b3ab6bba68ee0567f4ede55e361239b8d0122fa5d3b5dc36497b91d846040516117df91815260200190565b60008060008051602061549c833981519152610ac8565b6003546001600160a01b0316612687612909565b6001600160a01b03161460405180606001604052806033815260200161531660339139906126c85760405162461bcd60e51b8152600401610b3591906150af565b50600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612712573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061273691906150df565b9050600360009054906101000a90046001600160a01b03166001600160a01b0316632cc326416040518163ffffffff1660e01b8152600401600060405180830381600087803b15801561278857600080fd5b505af115801561279c573d6000803e3d6000fd5b5050600080546040516370a0823160e01b81523060048201529193508492506001600160a01b0316906370a0823190602401602060405180830381865afa1580156127eb573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061280f91906150df565b612819919061510e565b9050827f2c6a6ee02f179bb5f13a4b7fdc9ccd1dd37e524b48ff0957a931348cc9ad75c082604051611b9c91815260200190565b600080546040516370a0823160e01b815230600482015282916128c8916001600160a01b03909116906370a0823190602401602060405180830381865afa15801561289c573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906128c091906150df565b600654612a6b565b600754909150836128e8576128db61265c565b6128e59082615121565b90505b808210156128f7576000612901565b612901818361510e565b949350505050565b60007f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031633148015612944575060143610155b15612956575060131936013560601c90565b503390565b60408051808201909152601b81526000805160206154218339815191526020820152816115d75760405162461bcd60e51b8152600401610b3591906150af565b6000828152600560205260409020600101546001600160801b03811690600160801b900460ff166129da8383836129d2600d6129e0565b929190613c6c565b50505050565b60004282600e015411156129f457816129f9565b816007015b92915050565b6000612a0d83600501613a4c565b15612a3e578115612a23575060018201546129f9565b60018301546003840154612a379190612a6b565b90506129f9565b8115612a5057612a3783600101613c9a565b612a37612a5f84600101613c9a565b610ba985600301613c9a565b6000818310612a7a5781612a7c565b825b9392505050565b612a8c83613cbc565b612a968383613cee565b80612aa557612aa58383613cfb565b505050565b6000805160206154dc833981519152805482908290600090612acd908490615121565b909155505080546040805184815260208101929092527ff3a2c404b3362c4238317c3dad1020c2e9e984ba8df38a4a1ff18d57e92a242a910161173d565b600080546040516370a0823160e01b81523060048201526001600160a01b03909116906370a0823190602401602060405180830381865afa158015612b54573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190612b7891906150df565b905080600654146040518060600160405280603381526020016157656033913990612bb65760405162461bcd60e51b8152600401610b3591906150af565b5060408051808201909152601f81526000805160206154bc833981519152602082015282821015612bfa5760405162461bcd60e51b8152600401610b3591906150af565b50600680548390039055600054612aa5906001600160a01b03168484613d08565b60004282600401541115612c2f57816129f9565b5060020190565b60408051602081019091527f3fc394ec90f88dd7e18c46ffeef55f1b6719a2027f4ae9fbc4ab1182718912008054825290600090612c7c90612c7790613d5a565b613db2565b90506000612cc08286868080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250613ddf92505050565b9050856001600160a01b0316816001600160a01b0316146040518060600160405280602b81526020016157f7602b913990612d0e5760405162461bcd60e51b8152600401610b3591906150af565b508254836000612d1d83615134565b9190505550505050505050565b600060008051602061549c8339815191528181556000805160206154fc83398151915282905560405190917f8892a543ee6d561b73cc067b2cde5a76c251a4e0b978d92390f1301db5d125ab91a150565b60408051808201909152601b8152600080516020615421833981519152602082015282612dbb5760405162461bcd60e51b8152600401610b3591906150af565b50612dc46113fd565b8211156040518060400160405280601f81526020016000805160206154bc83398151915281525090612e095760405162461bcd60e51b8152600401610b3591906150af565b5060008051602061549c833981519152828155612e346000805160206154fc83398151915283613e09565b60018101546040805185815260208101929092527ffa4870dc2e2f95d11863e7e765a98e12fe92c3d1d2a703b4cf983f9c4b1052e691015b60405180910390a1505050565b602082015151600384015460801b6001600160801b03199081169116141580612ebc575060208083015101516003840154600160801b900461ffff908116911614155b80612ed95750815160028401546001600160a01b03908116911614155b60405180606001604052806027815260200161568e6027913990612f105760405162461bcd60e51b8152600401610b3591906150af565b506004830154421015612f895781516002840180546001600160a01b039092166001600160a01b03199092169190911790556020808301518051600386018054929093015161ffff16600160801b026001600160901b031990921660809190911c17179055612f7f8142615121565b6004840155505050565b60028301805484546001600160a01b03199081166001600160a01b0380841691909117875560038701805460018901805461ffff600160801b808504821681026001600160901b03199384166001600160801b03871617179093558a51909516959096169490941790955560208088015180519101519092169092029290931660809390931c92909217179055612f7f8142615121565b60048201541515156040518060600160405280602f81526020016152e7602f91399061305f5760405162461bcd60e51b8152600401610b3591906150af565b5080516002830180546001600160a01b039092166001600160a01b03199092169190911790556020908101518051600384018054929093015161ffff16600160801b026001600160901b031990921660809190911c1717905542600490910155565b600c54421015806130cf5750805b60405180606001604052806035815260200161565960359139906131065760405162461bcd60e51b8152600401610b3591906150af565b50612aa583836131166008612c1b565b9190613e6c565b600560008581526020019081526020016000206000015460001460405180606001604052806029815260200161573c602991399061316e5760405162461bcd60e51b8152600401610b3591906150af565b5061317a838383613e87565b6129da84848484613ec8565b60008051602061549c833981519152805461319f6113fd565b8111156040518060400160405280601f81526020016000805160206154bc833981519152815250906131e45760405162461bcd60e51b8152600401610b3591906150af565b506131ed61201c565b60405180606001604052806038815260200161544160389139906132245760405162461bcd60e51b8152600401610b3591906150af565b50600080835560018301556132398382614067565b826001600160a01b03167fa578b4c05763eb039caef37b2d7b949c22914c3838384c9c73cefb78c4a8ed0a82604051611b9c91815260200190565b826001016000838152602001908152602001600020546000146040518060600160405280602a8152602001615374602a9139906132c45760405162461bcd60e51b8152600401610b3591906150af565b50600082815260018401602052604081208290558354829185916132e9908490615121565b9091555050505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061337a57507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661336e6000805160206155cf833981519152546001600160a01b031690565b6001600160a01b031614155b156110325760405163703e46dd60e11b815260040160405180910390fd5b6004546001600160a01b03166133ac612909565b6001600160a01b031614806133e357506133c66008612c1b565b546001600160a01b03166133d8612909565b6001600160a01b0316145b6040518060600160405280602f815260200161589e602f91399061341a5760405162461bcd60e51b8152600401610b3591906150af565b506001546040516316da1fb760e11b81526001600160a01b03838116600483015290911690632db43f6e90602401602060405180830381865afa158015613465573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190613489919061514d565b6040518060600160405280602981526020016158cd60299139906115d75760405162461bcd60e51b8152600401610b3591906150af565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa92505050801561351a575060408051601f3d908101601f19168201909252613517918101906150df565b60015b61354257604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610b35565b6000805160206155cf833981519152811461357357604051632a87526960e21b815260048101829052602401610b35565b612aa583836140a0565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146110325760405163703e46dd60e11b815260040160405180910390fd5b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1028054606091600080516020615541833981519152916136059061516a565b80601f01602080910402602001604051908101604052809291908181526020018280546136319061516a565b801561367e5780601f106136535761010080835404028352916020019161367e565b820191906000526020600020905b81548152906001019060200180831161366157829003601f168201915b505050505091505090565b7fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d1038054606091600080516020615541833981519152916136059061516a565b6110326140f6565b6136d86140f6565b6115d7828261413f565b6136ea614a94565b600082116040518060600160405280602e815260200161584b602e9139906137255760405162461bcd60e51b8152600401610b3591906150af565b50506040805160a0810182529485528051808201825293845260006020858101829052808701959095528151808301835293845283850152848101929092528151928301909152600182526060830191909152608082015290565b600e8201541515156040518060600160405280602f81526020016152e7602f9139906137bf5760405162461bcd60e51b8152600401610b3591906150af565b508051600783015560208082015180516008850155810151600984015560408201518051600a8501550151600b830155606081015151600c83015560800151600d82015542600e90910155565b600e830154421015613870578151600784015560208083015180516008860155810151600985015560408301518051600a8601550151600b840155606082015151600c8401556080820151600d8401556138668142615121565b600e840155505050565b600783018054845560088401805460018601556009850180546002870155600a860180546003880155600b870180546004890155600c8801805460058a0155600d8901805460068b01558851909655602080890151805190965594850151909355604087015180519092559201519091556060840151519055608083015190556138668142615121565b6000805160206154dc8339815191526000613913610acf565b825460408051808201909152601b8152600080516020615421833981519152602082015291925090856139595760405162461bcd60e51b8152600401610b3591906150af565b5060408051808201909152601d81527f353430327c446562743a206e6f2066756e647320617661696c61626c650000006020820152826139ac5760405162461bcd60e51b8152600401610b3591906150af565b508085111560405180606001604052806025815260200161556160259139906139e85760405162461bcd60e51b8152600401610b3591906150af565b5060006139f58387612a6b565b845481900385559050613a088582614151565b83546040805183815260208101929092527f75aec94f6ad949cfd9728eb818639f1b0b882089b480409ab73245c06469eef4910160405180910390a1505050505050565b6000613a588254151590565b6040518060400160405280601d81526020017f353534317c54696d6572733a2074696d6572206e6f742061637469766500000081525090613aac5760405162461bcd60e51b8152600401610b3591906150af565b505054421190565b6000805160206154dc833981519152805460408051808201909152601b8152600080516020615421833981519152602082015283613b055760405162461bcd60e51b8152600401610b3591906150af565b50808311156040518060600160405280602581526020016155616025913990613b415760405162461bcd60e51b8152600401610b3591906150af565b5081548390038083556040805185815260208101929092527fb940258db5193d85cafbc591d0c29fad88d804ff8b9ab71c8f9545af2bb2c07c9101612e6c565b60008360010160008481526020019081526020016000205490506000811160405180606001604052806025815260200161551c6025913990613bd65760405162461bcd60e51b8152600401610b3591906150af565b508082146040518060600160405280602581526020016158796025913990613c115760405162461bcd60e51b8152600401610b3591906150af565b5083600001548111156040518060600160405280602a8152602001615712602a913990613c515760405162461bcd60e51b8152600401610b3591906150af565b50835403835550600090815260019091016020526040812055565b613c76848361415b565b156129da57613c886001850184614199565b806129da576129da6003850184614199565b805460018201546000919080821015613cb4576000612901565b900392915050565b613cc881600501613a4c565b156110c857600060028201556000600482015560068101546110c8906005830190613e09565b6115d760018301826141e8565b6115d760038301826141e8565b604080516001600160a01b038416602482015260448082018490528251808303909101815260649091019091526020810180516001600160e01b031663a9059cbb60e01b179052612aa5908490614205565b60006040518060600160405280602281526020016156b56022913980516020918201208351604051613d959301918252602082015260400190565b604051602081830303815290604052805190602001209050919050565b60006129f9613dbf614268565b8360405161190160f01b8152600281019290925260228201526042902090565b600080600080613def8686614272565b925092509250613dff82826142bf565b5090949350505050565b60408051808201909152601781527f353534307c54696d6572733a207a65726f2064656c6179000000000000000000602082015281613e5b5760405162461bcd60e51b8152600401610b3591906150af565b50613e668142615121565b90915550565b8254612aa59060018501906001600160a01b03168484614378565b601b5442101580613e955750815b15613eb057612aa58382613ea9600d6129e0565b9190614500565b613ebc600d8483614500565b612aa560148483614500565b613ed18361295b565b60008215613f3c576000613ee5600161284d565b9050848110613ef657849150613f36565b809150857f83067e3478aa8b638f7e34eeecb23875cc3671e615445e892cc22dcd4c8b8df3828703604051613f2d91815260200190565b60405180910390a25b50613f8e565b613f44610acf565b8411156040518060400160405280601f81526020016000805160206154bc83398151915281525090613f895760405162461bcd60e51b8152600401610b3591906150af565b508390505b60408051808201909152601f81527f353331337c4163636f756e743a206e6f2066756e647320746f20626c6f636b00602082015281613fe05760405162461bcd60e51b8152600401610b3591906150af565b50604080516060810182528281526001600160801b03428116602080840191825286151584860190815260008b815260059092529481209351845590516001909301805494511515600160801b026001600160881b03199095169390921692909217929092179091556007805483929061405b908490615121565b90915550505050505050565b806006541015614078576000614086565b80600654614086919061510e565b6006556000546115d7906001600160a01b03168383613d08565b6140a982614562565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b90600090a28051156140ee57612aa582826145c7565b6115d761463d565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0054600160401b900460ff1661103257604051631afcd79f60e31b815260040160405180910390fd5b6141476140f6565b6115d7828261465c565b6115d78282612b0b565b60006005830161416a81613a4c565b156141795760009150506129f9565b6006840154815460009161418c9161510e565b84101592506129f9915050565b80826001015410156040518060600160405280602e8152602001615798602e9139906141d85760405162461bcd60e51b8152600401610b3591906150af565b5060019091018054919091039055565b808260010160008282546141fc9190615121565b90915550505050565b600061421a6001600160a01b038416836146bd565b9050805160001415801561423f57508080602001905181019061423d919061514d565b155b15612aa557604051635274afe760e01b81526001600160a01b0384166004820152602401610b35565b6000610adb6146cb565b600080600083516041036142ac5760208401516040850151606086015160001a61429e8882858561473f565b9550955095505050506142b8565b50508151600091506002905b9250925092565b60008260038111156142d3576142d36151a4565b036142dc575050565b60018260038111156142f0576142f06151a4565b0361430e5760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115614322576143226151a4565b036143435760405163fce698f760e01b815260048101829052602401610b35565b6003826003811115614357576143576151a4565b036115d7576040516335e2f38360e21b815260048101829052602401610b35565b83546040805160608101909152602980825261ffff600160801b90930483169284168311919061539e6020830139906143c45760405162461bcd60e51b8152600401610b3591906150af565b5082825b8261ffff168161ffff161015614482576040516bffffffffffffffffffffffff19606088901b1660208201526001600160f01b031960f083901b1660348201526001600160801b03198316603682015260029060460160408051601f1981840301815290829052614438916151ba565b602060405180830381855afa158015614455573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061447891906150df565b91506001016143c8565b5085546040805160608101909152602180825260809290921b6001600160801b031990811690841614916155ae6020830139906144d25760405162461bcd60e51b8152600401610b3591906150af565b5050845461ffff909216600160801b026001600160901b031990921660809390931c92909217179092555050565b82600001548211156040518060600160405280603b815260200161561e603b91399061453f5760405162461bcd60e51b8152600401610b3591906150af565b5061454983613cbc565b614553838361480e565b80612aa557612aa5838361485d565b806001600160a01b03163b60000361459857604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610b35565b6000805160206155cf83398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b6060600080846001600160a01b0316846040516145e491906151ba565b600060405180830381855af49150503d806000811461461f576040519150601f19603f3d011682016040523d82523d6000602084013e614624565b606091505b50915091506146348583836148af565b95945050505050565b34156110325760405163b398979f60e01b815260040160405180910390fd5b6146646140f6565b6000805160206155418339815191527fa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d10261469e8482615226565b50600381016146ad8382615226565b5060008082556001909101555050565b6060612a7c8383600061490b565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f6146f66149a8565b6146fe614a12565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b600080807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a084111561477a5750600091506003905082614804565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156147ce573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381166147fa57506000925060019150829050614804565b9250600091508190505b9450945094915050565b61481b6001830182614a56565b60405180606001604052806028815260200161558660289139906148525760405162461bcd60e51b8152600401610b3591906150af565b506115d78282613cee565b61486a6003830182614a56565b6040518060600160405280602f81526020016155ef602f9139906148a15760405162461bcd60e51b8152600401610b3591906150af565b506115d760038301826141e8565b6060826148c4576148bf82614a6b565b612a7c565b81511580156148db57506001600160a01b0384163b155b1561490457604051639996b31560e01b81526001600160a01b0385166004820152602401610b35565b5092915050565b6060814710156149305760405163cd78605960e01b8152306004820152602401610b35565b600080856001600160a01b0316848660405161494c91906151ba565b60006040518083038185875af1925050503d8060008114614989576040519150601f19603f3d011682016040523d82523d6000602084013e61498e565b606091505b509150915061499e8683836148af565b9695505050505050565b6000600080516020615541833981519152816149c26135c6565b8051909150156149da57805160209091012092915050565b815480156149e9579392505050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470935050505090565b600060008051602061554183398151915281614a2c613689565b805190915015614a4457805160209091012092915050565b600182015480156149e9579392505050565b6000614a6183613c9a565b9091111592915050565b805115614a7b5780518082602001fd5b604051630a12f52160e11b815260040160405180910390fd5b6040518060a0016040528060008152602001614ac3604051806040016040528060008152602001600081525090565b8152602001614ae5604051806040016040528060008152602001600081525090565b8152602001614b006040518060200160405280600081525090565b8152602001600081525090565b60008060408385031215614b2057600080fd5b50508035926020909101359150565b6001600160a01b03811681146110c857600080fd5b60008083601f840112614b5657600080fd5b50813567ffffffffffffffff811115614b6e57600080fd5b602083019150836020828501011115614b8657600080fd5b9250929050565b600080600060408486031215614ba257600080fd5b8335614bad81614b2f565b9250602084013567ffffffffffffffff811115614bc957600080fd5b614bd586828701614b44565b9497909650939450505050565b600060208284031215614bf457600080fd5b5035919050565b80356001600160801b031981168114614c1357600080fd5b919050565b803561ffff81168114614c1357600080fd5b600080600080600060808688031215614c4257600080fd5b8535614c4d81614b2f565b9450614c5b60208701614bfb565b9350614c6960408701614c18565b9250606086013567ffffffffffffffff811115614c8557600080fd5b614c9188828901614b44565b969995985093965092949392505050565b80151581146110c857600080fd5b600080600080600060a08688031215614cc857600080fd5b8535945060208601359350614cdf60408701614bfb565b9250614ced60608701614c18565b91506080860135614cfd81614ca2565b809150509295509295909350565b600060208284031215614d1d57600080fd5b8135612a7c81614b2f565b634e487b7160e01b600052604160045260246000fd5b60008060408385031215614d5157600080fd5b8235614d5c81614b2f565b9150602083013567ffffffffffffffff80821115614d7957600080fd5b818501915085601f830112614d8d57600080fd5b813581811115614d9f57614d9f614d28565b604051601f8201601f19908116603f01168101908382118183101715614dc757614dc7614d28565b81604052828152886020848701011115614de057600080fd5b8260208601602083013760006020848301015280955050505050509250929050565b600080600060608486031215614e1757600080fd5b83359250602084013591506040840135614e3081614ca2565b809150509250925092565b60005b83811015614e56578181015183820152602001614e3e565b50506000910152565b60008151808452614e77816020860160208601614e3b565b601f01601f19169290920160200192915050565b60ff60f81b881681526000602060e06020840152614eac60e084018a614e5f565b8381036040850152614ebe818a614e5f565b606085018990526001600160a01b038816608086015260a0850187905284810360c08601528551808252602080880193509091019060005b81811015614f1257835183529284019291840191600101614ef6565b50909c9b505050505050505050505050565b8051825260208082015180518285015290810151604084015250604081015180516060840152602081015160808401525060608101515160a08301526080015160c090910152565b6101e08101614f7b8286614f24565b614f8860e0830185614f24565b826101c0830152949350505050565b60008060008060008060c08789031215614fb057600080fd5b8635614fbb81614b2f565b95506020870135614fcb81614b2f565b95989597505050506040840135936060810135936080820135935060a0909101359150565b6000806040838503121561500357600080fd5b61500c83614bfb565b915061501a60208401614c18565b90509250929050565b6000806000806080858703121561503957600080fd5b5050823594602084013594506040840135936060013592509050565b80516001600160a01b0316825260209081015180516001600160801b03191682840152015161ffff16604090910152565b60e081016150948286615055565b6150a16060830185615055565b8260c0830152949350505050565b602081526000612a7c6020830184614e5f565b6000602082840312156150d457600080fd5b8151612a7c81614b2f565b6000602082840312156150f157600080fd5b5051919050565b634e487b7160e01b600052601160045260246000fd5b818103818111156129f9576129f96150f8565b808201808211156129f9576129f96150f8565b600060018201615146576151466150f8565b5060010190565b60006020828403121561515f57600080fd5b8151612a7c81614ca2565b600181811c9082168061517e57607f821691505b60208210810361519e57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b600082516151cc818460208701614e3b565b9190910192915050565b601f821115612aa5576000816000526020600020601f850160051c810160208610156151ff5750805b601f850160051c820191505b8181101561521e5782815560010161520b565b505050505050565b815167ffffffffffffffff81111561524057615240614d28565b6152548161524e845461516a565b846151d6565b602080601f83116001811461528957600084156152715750858301515b600019600386901b1c1916600185901b17855561521e565b600085815260208120601f198616915b828110156152b857888601518255948401946001909101908401615299565b50858210156152d65787850151600019600388901b60f8161c191681555b5050505050600190811b0190555056fe353530307c44656c6179656453657474696e67733a2076616c756520616c726561647920696e697469616c697a6564353330327c4163636f756e743a206f6e6c792070726f636573736f722063616e2063616c6c20746869732066756e6374696f6e353334307c4163636f756e743a20736574746c656d656e7420706572696f64206973206e6f74206f766572353432307c526566756e64733a20726566756e64207265636f726420616c726561647920657869737473353531307c4f6e6554696d6550617373776f72643a20696e76616c6964204f545020636f756e746572353335307c4163636f756e743a20726573756c74696e672076657269666965642062616c616e636520657863656564732061637475616c353335317c4163636f756e743a2070726f636573736f7220616c726561647920736574353130307c436f6d6d6f6e3a20616d6f756e74206973207a65726f0000000000353431307c44656c617965645769746864726177616c733a207769746864726177616c2074696d656c6f636b206e6f742065787072696564353332307c4163636f756e743a207472616e73616374696f6e206e6f7420666f756e6476d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435500353130317c436f6d6d6f6e3a20696e73756666696369656e742066756e6473003486e95df892f06bd37fd38c44e2fee4c4efb6660a66e6eb20ed6d8a167eae0076d8021c4b979e99674b80f4e0cc10154af0e5f32df05eb51b29a464e2435501353432317c526566756e64733a20726566756e64207265636f7264206e6f7420666f756e64a16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100353430337c446562743a20616d6f756e7420697320686967686572207468616e2064656274353532317c4163636f756e744c696d6974733a207370656e64206c696d6974206578636565646564353531317c4f6e6554696d6550617373776f72643a20696e76616c6964204f5450360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc353532337c4163636f756e744c696d6974733a206e6f206f7470207370656e64206c696d6974206578636565646564353532307c4163636f756e744c696d6974733a20616d6f756e7420657863656564732073696e676c65207472616e73616374696f6e206c696d6974353331327c4163636f756e743a2063617264206f72204f5450207374617465206368616e676520697320696e2070726f6772657373353530317c44656c6179656453657474696e67733a2076616c756520616c7265616479207365744f776e657273686970416363657074616e63652875696e74323536206e6f6e636529353332317c4163636f756e743a206e657720616d6f756e7420697320657175616c206f7220686967686572207468616e20617574686f72697a6564353432337c526566756e64733a2070656e64696e6720616d6f756e74206578636565647320746f74616c353331307c4163636f756e743a207472616e73616374696f6e20494420616c72656164792075736564353333307c4163636f756e743a2076657269666965642062616c616e6365206e6f7420657175616c20746f2062616c616e6365353533317c5370656e644c696d6974733a20696e73756666696369656e74207370656e7420746f2063616e63656c353335327c4163636f756e743a20636172642077697468206f7470207374617465206e6f7420696e697469616c697a6564353535307c4f776e657273686970416363657074616e63653a20696e76616c6964207369676e6174757265353330347c4163636f756e743a20696e73756666696369656e7420626c6f636b656420616d6f756e74353532347c4163636f756e744c696d6974733a207370656e64206c696d697420706572696f64206973207a65726f353432327c526566756e64733a20696e76616c696420616d6f756e74207265636569766564353330317c4163636f756e743a206f6e6c79206f776e65722063616e2063616c6c20746869732066756e6374696f6e353337307c4163636f756e743a20756e617574686f72697a656420696d706c656d656e746174696f6ea26469706673582212201bf402f34a1057d0d739b2911423245677cc744b072d4af8e7f665bc51781a7f64736f6c63430008160033"; public static final String FUNC_UPGRADE_INTERFACE_VERSION = "UPGRADE_INTERFACE_VERSION"; @@ -59,6 +60,8 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_DEBTAMOUNT = "debtAmount"; + public static final String FUNC_EIP712DOMAIN = "eip712Domain"; + public static final String FUNC_FACTORY = "factory"; public static final String FUNC_INCREASEVERIFIEDBALANCE = "increaseVerifiedBalance"; @@ -75,7 +78,9 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_LIMITS = "limits"; - public static final String FUNC_OWNERWALLET = "ownerWallet"; + public static final String FUNC_OWNER = "owner"; + + public static final String FUNC_OWNERSHIPACCEPTANCENONCE = "ownershipAcceptanceNonce"; public static final String FUNC_PAYMENTTOKEN = "paymentToken"; @@ -103,6 +108,8 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_PROXIABLEUUID = "proxiableUUID"; + public static final String FUNC_REGISTRY = "registry"; + public static final String FUNC_SAVEPENDINGREFUND = "savePendingRefund"; public static final String FUNC_SECURITYDELAY = "securityDelay"; @@ -113,7 +120,7 @@ class TangemPaymentAccount extends Contract { public static final String FUNC_SETOTPSTATE = "setOtpState"; - public static final String FUNC_SETOWNERWALLET = "setOwnerWallet"; + public static final String FUNC_SETOWNER = "setOwner"; public static final String FUNC_SETPROCESSOR = "setProcessor"; @@ -163,6 +170,9 @@ class TangemPaymentAccount extends Contract { }, new TypeReference() { })); + public static final Event EIP712DOMAINCHANGED_EVENT = new Event("EIP712DomainChanged", + List.of()); + public static final Event INITIALIZED_EVENT = new Event("Initialized", List.of(new TypeReference() { })); @@ -189,7 +199,7 @@ class TangemPaymentAccount extends Contract { }, new TypeReference() { })); - public static final Event OWNERWALLETSET_EVENT = new Event("OwnerWalletSet", + public static final Event OWNERSET_EVENT = new Event("OwnerSet", List.of(new TypeReference

() { })); @@ -222,7 +232,6 @@ class TangemPaymentAccount extends Contract { public static final Event TRANSACTIONSETTLED_EVENT = new Event("TransactionSettled", Arrays.asList(new TypeReference(true) { - }, new TypeReference(true) { }, new TypeReference() { }, new TypeReference() { })); @@ -302,6 +311,25 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static CardSetEventResponse getCardSetEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(CARDSET_EVENT, log); + CardSetEventResponse typedResponse = new CardSetEventResponse(); + typedResponse.log = log; + typedResponse.card = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static DebtIncreasedEventResponse getDebtIncreasedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, log); + DebtIncreasedEventResponse typedResponse = new DebtIncreasedEventResponse(); + typedResponse.log = log; + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + public static List getCardSetEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(CARDSET_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -316,6 +344,35 @@ class TangemPaymentAccount extends Contract { return responses; } + public static DebtPaidEventResponse getDebtPaidEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTPAID_EVENT, log); + DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); + typedResponse.log = log; + typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static DebtWrittenOffEventResponse getDebtWrittenOffEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, log); + DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); + typedResponse.log = log; + typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static List getEIP712DomainChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(EIP712DOMAINCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + EIP712DomainChangedEventResponse typedResponse = new EIP712DomainChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + responses.add(typedResponse); + } + return responses; + } + public static List getDebtIncreasedEvents(TransactionReceipt transactionReceipt) { List valueList = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, transactionReceipt); ArrayList responses = new ArrayList(valueList.size()); @@ -329,214 +386,13 @@ class TangemPaymentAccount extends Contract { return responses; } - public static List getDebtPaidEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTPAID_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static CardSetEventResponse getCardSetEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(CARDSET_EVENT, log); - CardSetEventResponse typedResponse = new CardSetEventResponse(); + public static EIP712DomainChangedEventResponse getEIP712DomainChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(EIP712DOMAINCHANGED_EVENT, log); + EIP712DomainChangedEventResponse typedResponse = new EIP712DomainChangedEventResponse(); typedResponse.log = log; - typedResponse.card = (String) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); return typedResponse; } - public static List getDebtWrittenOffEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getInitializedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(INITIALIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - InitializedEventResponse typedResponse = new InitializedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.version = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getInsufficientFundsOnForcedAuthEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.insufficientAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtIncreasedEventResponse getDebtIncreasedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTINCREASED_EVENT, log); - DebtIncreasedEventResponse typedResponse = new DebtIncreasedEventResponse(); - typedResponse.log = log; - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getLimitsSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(LIMITSSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - LimitsSetEventResponse typedResponse = new LimitsSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.singleTransactionLimit = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.spendLimit = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.noOtpSpendLimit = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); - typedResponse.spendLimitsPeriod = (BigInteger) eventValues.getNonIndexedValues().get(3).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getNoOtpTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getOtpStateSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtPaidEventResponse getDebtPaidEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTPAID_EVENT, log); - DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); - typedResponse.log = log; - typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getOwnerWalletSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(OWNERWALLETSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - OwnerWalletSetEventResponse typedResponse = new OwnerWalletSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.ownerWallet = (String) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getPendingRefundSavedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getProcessorSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(PROCESSORSET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - ProcessorSetEventResponse typedResponse = new ProcessorSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.processor = (String) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.paymentToken = (String) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static DebtWrittenOffEventResponse getDebtWrittenOffEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, log); - DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); - typedResponse.log = log; - typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; - } - - public static List getRefundPaidEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(REFUNDPAID_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - RefundPaidEventResponse typedResponse = new RefundPaidEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getTransactionAmountChangedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONAMOUNTCHANGED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionAmountChangedEventResponse typedResponse = new TransactionAmountChangedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.newAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONAUTHORIZED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionAuthorizedEventResponse typedResponse = new TransactionAuthorizedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.otp = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); - typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static InitializedEventResponse getInitializedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(INITIALIZED_EVENT, log); InitializedEventResponse typedResponse = new InitializedEventResponse(); @@ -545,46 +401,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getTransactionSettledEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); - typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getUnsettledTransactionUnblockedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - UnsettledTransactionUnblockedEventResponse typedResponse = new UnsettledTransactionUnblockedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getUpgradedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(UPGRADED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - UpgradedEventResponse typedResponse = new UpgradedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.implementation = (String) eventValues.getIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - public static InsufficientFundsOnForcedAuthEventResponse getInsufficientFundsOnForcedAuthEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, log); InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); @@ -594,37 +410,14 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getVerifiedBalanceIncreasedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); + public static List getDebtPaidEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTPAID_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getVerifiedBalanceSetEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESET_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - VerifiedBalanceSetEventResponse typedResponse = new VerifiedBalanceSetEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getWithdrawalCanceledEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALCANCELED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalCanceledEventResponse typedResponse = new WithdrawalCanceledEventResponse(); + DebtPaidEventResponse typedResponse = new DebtPaidEventResponse(); typedResponse.log = eventValues.getLog(); + typedResponse.paid = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); responses.add(typedResponse); } return responses; @@ -641,37 +434,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static List getWithdrawalCompleteEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.to = (String) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - responses.add(typedResponse); - } - return responses; - } - - public static List getWithdrawalInitiatedEvents(TransactionReceipt transactionReceipt) { - List valueList = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, transactionReceipt); - ArrayList responses = new ArrayList(valueList.size()); - for (EventValuesWithLog eventValues : valueList) { - WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); - typedResponse.log = eventValues.getLog(); - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - responses.add(typedResponse); - } - return responses; - } - - @Deprecated - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemPaymentAccount(contractAddress, web3j, credentials, gasPrice, gasLimit); - } - public static NoOtpTransactionAuthorizedEventResponse getNoOtpTransactionAuthorizedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, log); NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); @@ -681,19 +443,6 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - @Deprecated - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { - return new TangemPaymentAccount(contractAddress, web3j, transactionManager, gasPrice, gasLimit); - } - - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { - return new TangemPaymentAccount(contractAddress, web3j, credentials, contractGasProvider); - } - - public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { - return new TangemPaymentAccount(contractAddress, web3j, transactionManager, contractGasProvider); - } - public static OtpStateSetEventResponse getOtpStateSetEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, log); OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); @@ -703,36 +452,39 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + public static List getDebtWrittenOffEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(DEBTWRITTENOFF_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + DebtWrittenOffEventResponse typedResponse = new DebtWrittenOffEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.writtenOff = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.debtLeft = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; } - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + public static List getOwnerSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(OWNERSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + OwnerSetEventResponse typedResponse = new OwnerSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.owner = (String) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); - } - - public static OwnerWalletSetEventResponse getOwnerWalletSetEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OWNERWALLETSET_EVENT, log); - OwnerWalletSetEventResponse typedResponse = new OwnerWalletSetEventResponse(); + public static OwnerSetEventResponse getOwnerSetEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(OWNERSET_EVENT, log); + OwnerSetEventResponse typedResponse = new OwnerSetEventResponse(); typedResponse.log = log; - typedResponse.ownerWallet = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getNonIndexedValues().get(0).getValue(); return typedResponse; } - @Deprecated - public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { - String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); - return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); - } - public static PendingRefundSavedEventResponse getPendingRefundSavedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, log); PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); @@ -780,12 +532,23 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getInitializedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(INITIALIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + InitializedEventResponse typedResponse = new InitializedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.version = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static TransactionSettledEventResponse getTransactionSettledEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, log); TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); typedResponse.log = log; typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); - typedResponse.settlementId = (BigInteger) eventValues.getIndexedValues().get(1).getValue(); typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); return typedResponse; @@ -808,6 +571,19 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getInsufficientFundsOnForcedAuthEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(INSUFFICIENTFUNDSONFORCEDAUTH_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + InsufficientFundsOnForcedAuthEventResponse typedResponse = new InsufficientFundsOnForcedAuthEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.insufficientAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static VerifiedBalanceIncreasedEventResponse getVerifiedBalanceIncreasedEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, log); VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); @@ -832,6 +608,21 @@ class TangemPaymentAccount extends Contract { return typedResponse; } + public static List getLimitsSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(LIMITSSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + LimitsSetEventResponse typedResponse = new LimitsSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.singleTransactionLimit = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.spendLimit = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.noOtpSpendLimit = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + typedResponse.spendLimitsPeriod = (BigInteger) eventValues.getNonIndexedValues().get(3).getValue(); + responses.add(typedResponse); + } + return responses; + } + public static WithdrawalCompleteEventResponse getWithdrawalCompleteEventFromLog(Log log) { EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, log); WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); @@ -841,13 +632,70 @@ class TangemPaymentAccount extends Contract { return typedResponse; } - public static WithdrawalInitiatedEventResponse getWithdrawalInitiatedEventFromLog(Log log) { - EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, log); - WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); - typedResponse.log = log; - typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); - typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); - return typedResponse; + @Deprecated + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccount(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccount(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static List getNoOtpTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(NOOTPTRANSACTIONAUTHORIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + NoOtpTransactionAuthorizedEventResponse typedResponse = new NoOtpTransactionAuthorizedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccount(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemPaymentAccount load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccount(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static List getOtpStateSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(OTPSTATESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + OtpStateSetEventResponse typedResponse = new OtpStateSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.otpRoot = (byte[]) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String trustedForwarder) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, trustedForwarder))); + return deployRemoteCall(TangemPaymentAccount.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); } public Flowable accountStateAfterSettlementEventFlowable(EthFilter filter) { @@ -870,6 +718,19 @@ class TangemPaymentAccount extends Contract { return cardSetEventFlowable(filter); } + public static List getPendingRefundSavedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PENDINGREFUNDSAVED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PendingRefundSavedEventResponse typedResponse = new PendingRefundSavedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtIncreasedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getDebtIncreasedEventFromLog(log)); } @@ -884,6 +745,19 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getDebtPaidEventFromLog(log)); } + public static List getProcessorSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PROCESSORSET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + ProcessorSetEventResponse typedResponse = new ProcessorSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.processor = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.paymentToken = (String) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable debtPaidEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(DEBTPAID_EVENT)); @@ -900,10 +774,46 @@ class TangemPaymentAccount extends Contract { return debtWrittenOffEventFlowable(filter); } + public static List getRefundPaidEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(REFUNDPAID_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + RefundPaidEventResponse typedResponse = new RefundPaidEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public Flowable eIP712DomainChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getEIP712DomainChangedEventFromLog(log)); + } + + public Flowable eIP712DomainChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(EIP712DOMAINCHANGED_EVENT)); + return eIP712DomainChangedEventFlowable(filter); + } + public Flowable initializedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getInitializedEventFromLog(log)); } + public static List getTransactionAmountChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONAMOUNTCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionAmountChangedEventResponse typedResponse = new TransactionAmountChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.newAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable initializedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(INITIALIZED_EVENT)); @@ -920,6 +830,21 @@ class TangemPaymentAccount extends Contract { return insufficientFundsOnForcedAuthEventFlowable(filter); } + public static List getTransactionAuthorizedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONAUTHORIZED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionAuthorizedEventResponse typedResponse = new TransactionAuthorizedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.otp = (byte[]) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.otpCounter = (BigInteger) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable limitsSetEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getLimitsSetEventFromLog(log)); } @@ -934,6 +859,20 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getNoOtpTransactionAuthorizedEventFromLog(log)); } + public static List getTransactionSettledEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(TRANSACTIONSETTLED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + TransactionSettledEventResponse typedResponse = new TransactionSettledEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.settlementAmount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.paymentAmount = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable noOtpTransactionAuthorizedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(NOOTPTRANSACTIONAUTHORIZED_EVENT)); @@ -950,20 +889,45 @@ class TangemPaymentAccount extends Contract { return otpStateSetEventFlowable(filter); } - public Flowable ownerWalletSetEventFlowable(EthFilter filter) { - return web3j.ethLogFlowable(filter).map(log -> getOwnerWalletSetEventFromLog(log)); + public static List getUnsettledTransactionUnblockedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UnsettledTransactionUnblockedEventResponse typedResponse = new UnsettledTransactionUnblockedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.transactionId = (BigInteger) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; } - public Flowable ownerWalletSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + public Flowable ownerSetEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getOwnerSetEventFromLog(log)); + } + + public Flowable ownerSetEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); - filter.addSingleTopic(EventEncoder.encode(OWNERWALLETSET_EVENT)); - return ownerWalletSetEventFlowable(filter); + filter.addSingleTopic(EventEncoder.encode(OWNERSET_EVENT)); + return ownerSetEventFlowable(filter); } public Flowable pendingRefundSavedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getPendingRefundSavedEventFromLog(log)); } + public static List getUpgradedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(UPGRADED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + UpgradedEventResponse typedResponse = new UpgradedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.implementation = (String) eventValues.getIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable pendingRefundSavedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(PENDINGREFUNDSAVED_EVENT)); @@ -980,6 +944,19 @@ class TangemPaymentAccount extends Contract { return processorSetEventFlowable(filter); } + public static List getVerifiedBalanceIncreasedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCEINCREASED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceIncreasedEventResponse typedResponse = new VerifiedBalanceIncreasedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.increase = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable refundPaidEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getRefundPaidEventFromLog(log)); } @@ -994,6 +971,18 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getTransactionAmountChangedEventFromLog(log)); } + public static List getVerifiedBalanceSetEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(VERIFIEDBALANCESET_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + VerifiedBalanceSetEventResponse typedResponse = new VerifiedBalanceSetEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.verifiedBalance = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable transactionAmountChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(TRANSACTIONAMOUNTCHANGED_EVENT)); @@ -1010,6 +999,17 @@ class TangemPaymentAccount extends Contract { return transactionAuthorizedEventFlowable(filter); } + public static List getWithdrawalCanceledEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALCANCELED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalCanceledEventResponse typedResponse = new WithdrawalCanceledEventResponse(); + typedResponse.log = eventValues.getLog(); + responses.add(typedResponse); + } + return responses; + } + public Flowable transactionSettledEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getTransactionSettledEventFromLog(log)); } @@ -1024,6 +1024,19 @@ class TangemPaymentAccount extends Contract { return web3j.ethLogFlowable(filter).map(log -> getUnsettledTransactionUnblockedEventFromLog(log)); } + public static List getWithdrawalCompleteEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALCOMPLETE_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalCompleteEventResponse typedResponse = new WithdrawalCompleteEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.to = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + responses.add(typedResponse); + } + return responses; + } + public Flowable unsettledTransactionUnblockedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); filter.addSingleTopic(EventEncoder.encode(UNSETTLEDTRANSACTIONUNBLOCKED_EVENT)); @@ -1040,6 +1053,28 @@ class TangemPaymentAccount extends Contract { return upgradedEventFlowable(filter); } + public static List getWithdrawalInitiatedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static WithdrawalInitiatedEventResponse getWithdrawalInitiatedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(WITHDRAWALINITIATED_EVENT, log); + WithdrawalInitiatedEventResponse typedResponse = new WithdrawalInitiatedEventResponse(); + typedResponse.log = log; + typedResponse.amount = (BigInteger) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.readyToWithdrawTimestamp = (BigInteger) eventValues.getNonIndexedValues().get(1).getValue(); + return typedResponse; + } + public Flowable withdrawalInitiatedEventFlowable(EthFilter filter) { return web3j.ethLogFlowable(filter).map(log -> getWithdrawalInitiatedEventFromLog(log)); } @@ -1186,6 +1221,34 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, BigInteger.class); } + public RemoteFunctionCall>> eip712Domain() { + final Function function = new Function(FUNC_EIP712DOMAIN, + List.of(), + Arrays.asList(new TypeReference() { + }, new TypeReference() { + }, new TypeReference() { + }, new TypeReference() { + }, new TypeReference
() { + }, new TypeReference() { + }, new TypeReference>() { + })); + return new RemoteFunctionCall>>(function, + new Callable>>() { + @Override + public Tuple7> call() throws Exception { + List results = executeCallMultipleValueReturn(function); + return new Tuple7>( + (byte[]) results.get(0).getValue(), + (String) results.get(1).getValue(), + (String) results.get(2).getValue(), + (BigInteger) results.get(3).getValue(), + (String) results.get(4).getValue(), + (byte[]) results.get(5).getValue(), + convertToNative((List) results.get(6).getValue())); + } + }); + } + public RemoteFunctionCall factory() { final Function function = new Function(FUNC_FACTORY, List.of(), @@ -1210,10 +1273,11 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall initialize(String ownerWallet_, BigInteger singleTransactionLimit, BigInteger spendLimit, BigInteger noOtpSpendLimit, BigInteger spendLimitsPeriod) { + public RemoteFunctionCall initialize(String owner_, String registry_, BigInteger singleTransactionLimit, BigInteger spendLimit, BigInteger noOtpSpendLimit, BigInteger spendLimitsPeriod) { final Function function = new Function( FUNC_INITIALIZE, - Arrays.asList(new Address(160, ownerWallet_), + Arrays.asList(new Address(160, owner_), + new Address(160, registry_), new Uint256(singleTransactionLimit), new Uint256(spendLimit), new Uint256(noOtpSpendLimit), @@ -1266,14 +1330,22 @@ class TangemPaymentAccount extends Contract { }); } - public RemoteFunctionCall ownerWallet() { - final Function function = new Function(FUNC_OWNERWALLET, + public RemoteFunctionCall owner() { + final Function function = new Function(FUNC_OWNER, List.of(), List.of(new TypeReference
() { })); return executeRemoteCallSingleValueReturn(function, String.class); } + public RemoteFunctionCall ownershipAcceptanceNonce() { + final Function function = new Function(FUNC_OWNERSHIPACCEPTANCENONCE, + List.of(), + List.of(new TypeReference() { + })); + return executeRemoteCallSingleValueReturn(function, BigInteger.class); + } + public RemoteFunctionCall paymentToken() { final Function function = new Function(FUNC_PAYMENTTOKEN, List.of(), @@ -1353,11 +1425,10 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall processSettlement(BigInteger transactionId, BigInteger settlementId, BigInteger amount) { + public RemoteFunctionCall processSettlement(BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_PROCESSSETTLEMENT, Arrays.asList(new Uint256(transactionId), - new Uint256(settlementId), new Uint256(amount)), Collections.emptyList()); return executeRemoteCallTransaction(function); @@ -1387,6 +1458,14 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, byte[].class); } + public RemoteFunctionCall registry() { + final Function function = new Function(FUNC_REGISTRY, + List.of(), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + public RemoteFunctionCall savePendingRefund(BigInteger transactionId, BigInteger amount) { final Function function = new Function( FUNC_SAVEPENDINGREFUND, @@ -1404,12 +1483,13 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallSingleValueReturn(function, BigInteger.class); } - public RemoteFunctionCall setCard(String card, byte[] otpRoot, BigInteger otpCounter) { + public RemoteFunctionCall setCard(String card, byte[] otpRoot, BigInteger otpCounter, byte[] ownershipAcceptanceSignature) { final Function function = new Function( FUNC_SETCARD, Arrays.asList(new Address(160, card), new Bytes16(otpRoot), - new Uint16(otpCounter)), + new Uint16(otpCounter), + new org.web3j.abi.datatypes.DynamicBytes(ownershipAcceptanceSignature)), Collections.emptyList()); return executeRemoteCallTransaction(function); } @@ -1434,10 +1514,11 @@ class TangemPaymentAccount extends Contract { return executeRemoteCallTransaction(function); } - public RemoteFunctionCall setOwnerWallet(String ownerWallet_) { + public RemoteFunctionCall setOwner(String owner_, byte[] ownershipAcceptanceSignature) { final Function function = new Function( - FUNC_SETOWNERWALLET, - List.of(new Address(160, ownerWallet_)), + FUNC_SETOWNER, + Arrays.asList(new Address(160, owner_), + new org.web3j.abi.datatypes.DynamicBytes(ownershipAcceptanceSignature)), Collections.emptyList()); return executeRemoteCallTransaction(function); } @@ -1609,7 +1690,7 @@ class TangemPaymentAccount extends Contract { super(new Uint256(singleTransactionLimit), spendLimit, noOtpSpendLimit, - spendLimitsTimer, + spendLimitsTimer, new Uint256(spendLimitsPeriod)); this._00_singleTransactionLimit = singleTransactionLimit; this._01_spendLimit = spendLimit; @@ -1664,6 +1745,9 @@ class TangemPaymentAccount extends Contract { public BigInteger debtLeft; } + public static class EIP712DomainChangedEventResponse extends BaseEventResponse { + } + public static class InitializedEventResponse extends BaseEventResponse { public BigInteger version; } @@ -1696,8 +1780,8 @@ class TangemPaymentAccount extends Contract { public BigInteger otpCounter; } - public static class OwnerWalletSetEventResponse extends BaseEventResponse { - public String ownerWallet; + public static class OwnerSetEventResponse extends BaseEventResponse { + public String owner; } public static class PendingRefundSavedEventResponse extends BaseEventResponse { @@ -1737,8 +1821,6 @@ class TangemPaymentAccount extends Contract { public static class TransactionSettledEventResponse extends BaseEventResponse { public BigInteger transactionId; - public BigInteger settlementId; - public BigInteger settlementAmount; public BigInteger paymentAmount; diff --git a/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java new file mode 100644 index 0000000000..bcf93417f6 --- /dev/null +++ b/libs/visa/src/main/java/com/tangem/lib/visa/TangemPaymentAccountRegistry.java @@ -0,0 +1,362 @@ +package com.tangem.lib.visa; + +import org.web3j.abi.EventEncoder; +import org.web3j.abi.FunctionEncoder; +import org.web3j.abi.TypeReference; +import org.web3j.abi.datatypes.*; +import org.web3j.crypto.Credentials; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameter; +import org.web3j.protocol.core.RemoteCall; +import org.web3j.protocol.core.RemoteFunctionCall; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.BaseEventResponse; +import org.web3j.protocol.core.methods.response.Log; +import org.web3j.protocol.core.methods.response.TransactionReceipt; +import org.web3j.tx.Contract; +import org.web3j.tx.TransactionManager; +import org.web3j.tx.gas.ContractGasProvider; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; + +import io.reactivex.Flowable; + +/** + *

Auto generated code. + *

Do not modify! + *

Please use the web3j command line tools, + * or the org.web3j.codegen.SolidityFunctionWrapperGenerator in the + * codegen module to update. + * + *

Generated with web3j version 1.5.2. + */ +@SuppressWarnings("rawtypes") +class TangemPaymentAccountRegistry extends Contract { + public static final String BINARY = "608060405234801561001057600080fd5b50604051610c47380380610c4783398101604081905261002f91610054565b600080546001600160a01b0319166001600160a01b0392909216919091179055610084565b60006020828403121561006657600080fd5b81516001600160a01b038116811461007d57600080fd5b9392505050565b610bb4806100936000396000f3fe608060405234801561001057600080fd5b506004361061007d5760003560e01c806368d7111a1161005b57806368d7111a146100f0578063747fc3701461010357806379da564f14610116578063c45a01551461013657600080fd5b80630cef7172146100825780633509c7c5146100c85780635cd26737146100dd575b600080fd5b6100ab6100903660046108fd565b6002602052600090815260409020546001600160a01b031681565b6040516001600160a01b0390911681526020015b60405180910390f35b6100db6100d6366004610918565b610149565b005b6100db6100eb366004610918565b610201565b6100db6100fe3660046108fd565b6103b0565b6100db610111366004610918565b610506565b6101296101243660046108fd565b6106db565b6040516100bf919061094b565b6000546100ab906001600160a01b031681565b600054604080516080810190915260428082526001600160a01b03909216331491610a576020830139906101995760405162461bcd60e51b81526004016101909190610998565b60405180910390fd5b506001600160a01b03811660009081526001602052604090206101bc9083610705565b50806001600160a01b0316826001600160a01b03167f65b303fd420aefd0541d9300bb10190bb8e3320a0e7c2ce4dec6b6bfb953027f60405160405180910390a35050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa158015610249573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061026d91906109e7565b6040518060600160405280603a8152602001610b45603a9139906102a45760405162461bcd60e51b81526004016101909190610998565b506001600160a01b038216600090815260016020526040902033906102c99082610721565b6040518060600160405280603b8152602001610ad0603b9139906103005760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03821660009081526001602052604090206103239082610705565b6040518060600160405280603b8152602001610ad0603b91399061035a5760405162461bcd60e51b81526004016101909190610998565b50604080516001600160a01b03808416825280861660208301528416918101919091527ffba993cae385d1da628b11b1b150a27262bca00bb2674980516b45ad7d331eaa906060015b60405180910390a1505050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa1580156103f8573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061041c91906109e7565b6040518060600160405280603a8152602001610b45603a9139906104535760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0381811660009081526002602090815260409182902054825160608101909352603a8084529316159290610b0b90830139906104aa5760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03811660008181526002602052604080822080546001600160a01b03191633908117909155905190929183917f02e4debb2ab36299bfcc998345f66f73428adbbda9ef5efef399d75a67a4b78a9190a35050565b6000546040516385bb392360e01b81523360048201526001600160a01b03909116906385bb392390602401602060405180830381865afa15801561054e573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061057291906109e7565b6040518060600160405280603a8152602001610b45603a9139906105a95760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0382811660009081526002602090815260409182902054825160608101909352603780845233949190911684149291610a9990830139906106055760405162461bcd60e51b81526004016101909190610998565b506001600160a01b0382811660009081526002602090815260409182902054825160608101909352603a8084529316159290610b0b908301399061065c5760405162461bcd60e51b81526004016101909190610998565b506001600160a01b03838116600081815260026020908152604080832080546001600160a01b03199081169091558786168085529382902080549091169587169586179055805194855290840192909252908201527f362e9cf018894fbf5dfe9c5cbfb25c3e3ac8c4d5beed564352f1af2e2712dfc8906060016103a3565b6001600160a01b03811660009081526001602052604090206060906106ff90610736565b92915050565b600061071a836001600160a01b038416610743565b9392505050565b600061071a836001600160a01b038416610792565b6060600061071a83610885565b600081815260018301602052604081205461078a575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556106ff565b5060006106ff565b6000818152600183016020526040812054801561087b5760006107b6600183610a09565b85549091506000906107ca90600190610a09565b905080821461082f5760008660000182815481106107ea576107ea610a2a565b906000526020600020015490508087600001848154811061080d5761080d610a2a565b6000918252602080832090910192909255918252600188019052604090208390555b855486908061084057610840610a40565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506106ff565b60009150506106ff565b6060816000018054806020026020016040519081016040528092919081815260200182805480156108d557602002820191906000526020600020905b8154815260200190600101908083116108c1575b50505050509050919050565b80356001600160a01b03811681146108f857600080fd5b919050565b60006020828403121561090f57600080fd5b61071a826108e1565b6000806040838503121561092b57600080fd5b610934836108e1565b9150610942602084016108e1565b90509250929050565b6020808252825182820181905260009190848201906040850190845b8181101561098c5783516001600160a01b031683529284019291840191600101610967565b50909695505050505050565b60006020808352835180602085015260005b818110156109c6578581018301518582016040015282016109aa565b506000604082860101526040601f19601f8301168501019250505092915050565b6000602082840312156109f957600080fd5b8151801515811461071a57600080fd5b818103818111156106ff57634e487b7160e01b600052601160045260246000fd5b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052603160045260246000fdfe353630307c52656769737472793a206f6e6c79207061796d656e74206163636f756e7420666163746f72792063616e2063616c6c20746869732066756e6374696f6e353630347c52656769737472793a2070726576696f757320636172642773207061796d656e74206163636f756e74206d69736d61746368353630327c52656769737472793a207061796d656e74206163636f756e74206265696e672072656d6f766564206e6f7420696e2074686520736574353630347c52656769737472793a207061796d656e74206163636f756e7420697320616c72656164792073657420666f72207468652063617264353630317c52656769737472793a206f6e6c79207061796d656e74206163636f756e742063616e2063616c6c20746869732066756e6374696f6ea26469706673582212203c97923f6f14a45f904087dcdced3b09f33cd32821438cd9cbf0add5824acaf064736f6c63430008160033"; + + public static final String FUNC_CHANGEPAYMENTACCOUNTCARD = "changePaymentAccountCard"; + + public static final String FUNC_CHANGEPAYMENTACCOUNTOWNER = "changePaymentAccountOwner"; + + public static final String FUNC_FACTORY = "factory"; + + public static final String FUNC_INITPAYMENTACCOUNTCARD = "initPaymentAccountCard"; + + public static final String FUNC_INITPAYMENTACCOUNTOWNER = "initPaymentAccountOwner"; + + public static final String FUNC_PAYMENTACCOUNTBYCARD = "paymentAccountByCard"; + + public static final String FUNC_PAYMENTACCOUNTSBYOWNER = "paymentAccountsByOwner"; + + public static final Event PAYMENTACCOUNTCARDCHANGED_EVENT = new Event("PaymentAccountCardChanged", + Arrays.asList(new TypeReference

() { + }, new TypeReference
() { + }, new TypeReference
() { + })); + + public static final Event PAYMENTACCOUNTCARDREGISTERED_EVENT = new Event("PaymentAccountCardRegistered", + Arrays.asList(new TypeReference
(true) { + }, new TypeReference
(true) { + })); + + public static final Event PAYMENTACCOUNTOWNERCHANGED_EVENT = new Event("PaymentAccountOwnerChanged", + Arrays.asList(new TypeReference
() { + }, new TypeReference
() { + }, new TypeReference
() { + })); + + public static final Event PAYMENTACCOUNTOWNERREGISTERED_EVENT = new Event("PaymentAccountOwnerRegistered", + Arrays.asList(new TypeReference
(true) { + }, new TypeReference
(true) { + })); + + @Deprecated + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, credentials, contractGasProvider); + } + + @Deprecated + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + super(BINARY, contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + protected TangemPaymentAccountRegistry(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + super(BINARY, contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static List getPaymentAccountCardChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountCardChangedEventResponse typedResponse = new PaymentAccountCardChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousCard = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newCard = (String) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountCardChangedEventResponse getPaymentAccountCardChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDCHANGED_EVENT, log); + PaymentAccountCardChangedEventResponse typedResponse = new PaymentAccountCardChangedEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousCard = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newCard = (String) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static List getPaymentAccountCardRegisteredEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDREGISTERED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountCardRegisteredEventResponse typedResponse = new PaymentAccountCardRegisteredEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountCardRegisteredEventResponse getPaymentAccountCardRegisteredEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTCARDREGISTERED_EVENT, log); + PaymentAccountCardRegisteredEventResponse typedResponse = new PaymentAccountCardRegisteredEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.card = (String) eventValues.getIndexedValues().get(1).getValue(); + return typedResponse; + } + + public static List getPaymentAccountOwnerChangedEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERCHANGED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountOwnerChangedEventResponse typedResponse = new PaymentAccountOwnerChangedEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousOwner = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newOwner = (String) eventValues.getNonIndexedValues().get(2).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountOwnerChangedEventResponse getPaymentAccountOwnerChangedEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERCHANGED_EVENT, log); + PaymentAccountOwnerChangedEventResponse typedResponse = new PaymentAccountOwnerChangedEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getNonIndexedValues().get(0).getValue(); + typedResponse.previousOwner = (String) eventValues.getNonIndexedValues().get(1).getValue(); + typedResponse.newOwner = (String) eventValues.getNonIndexedValues().get(2).getValue(); + return typedResponse; + } + + public static List getPaymentAccountOwnerRegisteredEvents(TransactionReceipt transactionReceipt) { + List valueList = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERREGISTERED_EVENT, transactionReceipt); + ArrayList responses = new ArrayList(valueList.size()); + for (EventValuesWithLog eventValues : valueList) { + PaymentAccountOwnerRegisteredEventResponse typedResponse = new PaymentAccountOwnerRegisteredEventResponse(); + typedResponse.log = eventValues.getLog(); + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(1).getValue(); + responses.add(typedResponse); + } + return responses; + } + + public static PaymentAccountOwnerRegisteredEventResponse getPaymentAccountOwnerRegisteredEventFromLog(Log log) { + EventValuesWithLog eventValues = staticExtractEventParametersWithLog(PAYMENTACCOUNTOWNERREGISTERED_EVENT, log); + PaymentAccountOwnerRegisteredEventResponse typedResponse = new PaymentAccountOwnerRegisteredEventResponse(); + typedResponse.log = log; + typedResponse.paymentAccount = (String) eventValues.getIndexedValues().get(0).getValue(); + typedResponse.owner = (String) eventValues.getIndexedValues().get(1).getValue(); + return typedResponse; + } + + @Deprecated + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, credentials, gasPrice, gasLimit); + } + + @Deprecated + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, transactionManager, gasPrice, gasLimit); + } + + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, credentials, contractGasProvider); + } + + public static TangemPaymentAccountRegistry load(String contractAddress, Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider) { + return new TangemPaymentAccountRegistry(contractAddress, web3j, transactionManager, contractGasProvider); + } + + public static RemoteCall deploy(Web3j web3j, Credentials credentials, ContractGasProvider contractGasProvider, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, credentials, contractGasProvider, BINARY, encodedConstructor); + } + + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, ContractGasProvider contractGasProvider, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, transactionManager, contractGasProvider, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, Credentials credentials, BigInteger gasPrice, BigInteger gasLimit, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, credentials, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + @Deprecated + public static RemoteCall deploy(Web3j web3j, TransactionManager transactionManager, BigInteger gasPrice, BigInteger gasLimit, String factory_) { + String encodedConstructor = FunctionEncoder.encodeConstructor(List.of(new Address(160, factory_))); + return deployRemoteCall(TangemPaymentAccountRegistry.class, web3j, transactionManager, gasPrice, gasLimit, BINARY, encodedConstructor); + } + + public Flowable paymentAccountCardChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountCardChangedEventFromLog(log)); + } + + public Flowable paymentAccountCardChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTCARDCHANGED_EVENT)); + return paymentAccountCardChangedEventFlowable(filter); + } + + public Flowable paymentAccountCardRegisteredEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountCardRegisteredEventFromLog(log)); + } + + public Flowable paymentAccountCardRegisteredEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTCARDREGISTERED_EVENT)); + return paymentAccountCardRegisteredEventFlowable(filter); + } + + public Flowable paymentAccountOwnerChangedEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountOwnerChangedEventFromLog(log)); + } + + public Flowable paymentAccountOwnerChangedEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTOWNERCHANGED_EVENT)); + return paymentAccountOwnerChangedEventFlowable(filter); + } + + public Flowable paymentAccountOwnerRegisteredEventFlowable(EthFilter filter) { + return web3j.ethLogFlowable(filter).map(log -> getPaymentAccountOwnerRegisteredEventFromLog(log)); + } + + public Flowable paymentAccountOwnerRegisteredEventFlowable(DefaultBlockParameter startBlock, DefaultBlockParameter endBlock) { + EthFilter filter = new EthFilter(startBlock, endBlock, getContractAddress()); + filter.addSingleTopic(EventEncoder.encode(PAYMENTACCOUNTOWNERREGISTERED_EVENT)); + return paymentAccountOwnerRegisteredEventFlowable(filter); + } + + public RemoteFunctionCall changePaymentAccountCard(String previousCard, String newCard) { + final Function function = new Function( + FUNC_CHANGEPAYMENTACCOUNTCARD, + Arrays.asList(new Address(160, previousCard), + new Address(160, newCard)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall changePaymentAccountOwner(String previousOwner, String newOwner) { + final Function function = new Function( + FUNC_CHANGEPAYMENTACCOUNTOWNER, + Arrays.asList(new Address(160, previousOwner), + new Address(160, newOwner)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall factory() { + final Function function = new Function(FUNC_FACTORY, + List.of(), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall initPaymentAccountCard(String card) { + final Function function = new Function( + FUNC_INITPAYMENTACCOUNTCARD, + List.of(new Address(160, card)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall initPaymentAccountOwner(String paymentAccount, String owner) { + final Function function = new Function( + FUNC_INITPAYMENTACCOUNTOWNER, + Arrays.asList(new Address(160, paymentAccount), + new Address(160, owner)), + Collections.emptyList()); + return executeRemoteCallTransaction(function); + } + + public RemoteFunctionCall paymentAccountByCard(String param0) { + final Function function = new Function(FUNC_PAYMENTACCOUNTBYCARD, + List.of(new Address(160, param0)), + List.of(new TypeReference
() { + })); + return executeRemoteCallSingleValueReturn(function, String.class); + } + + public RemoteFunctionCall paymentAccountsByOwner(String owner) { + final Function function = new Function(FUNC_PAYMENTACCOUNTSBYOWNER, + List.of(new Address(160, owner)), + List.of(new TypeReference>() { + })); + return new RemoteFunctionCall(function, + new Callable() { + @Override + @SuppressWarnings("unchecked") + public List call() throws Exception { + List result = (List) executeCallSingleValueReturn(function, List.class); + return convertToNative(result); + } + }); + } + + public static class PaymentAccountCardChangedEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String previousCard; + + public String newCard; + } + + public static class PaymentAccountCardRegisteredEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String card; + } + + public static class PaymentAccountOwnerChangedEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String previousOwner; + + public String newOwner; + } + + public static class PaymentAccountOwnerRegisteredEventResponse extends BaseEventResponse { + public String paymentAccount; + + public String owner; + } +} diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index c2ac96c707..21989a9ee6 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -7,7 +7,6 @@ import com.tangem.lib.visa.model.VisaBalancesAndLimits.Limits import com.tangem.lib.visa.utils.toBigDecimal import com.tangem.lib.visa.utils.toInstant import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import org.joda.time.Instant import org.web3j.protocol.Web3j import org.web3j.tx.TransactionManager @@ -18,39 +17,40 @@ internal class DefaultVisaContractInfoProvider( private val transactionManager: TransactionManager, private val gasProvider: ContractGasProvider, private val bridgeProcessorAddress: String, + private val paymentAccountRegistryAddress: String, private val dispatchers: CoroutineDispatcherProvider, ) : VisaContractInfoProvider { override suspend fun getBalancesAndLimits(walletAddress: String): VisaBalancesAndLimits { - return withContext(dispatchers.io) { - val tangemBridgeProcessor = TangemBridgeProcessor.load( - /* contractAddress = */ bridgeProcessorAddress, - /* web3j = */ web3j, - /* transactionManager = */ transactionManager, - /* contractGasProvider = */ gasProvider, - ) - - parZip( - dispatchers.io, - { loadPaymentAccount(walletAddress, tangemBridgeProcessor) }, - { loadPaymentTokenInfo(tangemBridgeProcessor) }, - { paymentAccount, paymentToken -> - fetchBalancesAndLimits(paymentAccount, paymentToken) - }, - ) - } + return parZip( + dispatchers.io, + { loadPaymentAccount(walletAddress) }, + { loadPaymentTokenInfo() }, + { paymentAccount, paymentToken -> + fetchBalancesAndLimits(paymentAccount, paymentToken) + }, + ) } - private fun loadPaymentAccount( - walletAddress: String, - tangemBridgeProcessor: TangemBridgeProcessor, - ): TangemPaymentAccount { - val paymentAccountAddress = tangemBridgeProcessor.getPaymentAccount(walletAddress).send() + private fun loadPaymentAccount(walletAddress: String): TangemPaymentAccount { + val paymentAccountRegistry = TangemPaymentAccountRegistry.load( + /* contractAddress = */ paymentAccountRegistryAddress, + /* web3j = */ web3j, + /* transactionManager = */ transactionManager, + /* contractGasProvider = */ gasProvider, + ) + val paymentAccountAddress = paymentAccountRegistry.paymentAccountByCard(walletAddress).send() return TangemPaymentAccount.load(paymentAccountAddress, web3j, transactionManager, gasProvider) } - private fun loadPaymentTokenInfo(tangemBridgeProcessor: TangemBridgeProcessor): PaymentTokenInfo { + private fun loadPaymentTokenInfo(): PaymentTokenInfo { + val tangemBridgeProcessor = TangemBridgeProcessor.load( + /* contractAddress = */ bridgeProcessorAddress, + /* web3j = */ web3j, + /* transactionManager = */ transactionManager, + /* contractGasProvider = */ gasProvider, + ) val paymentTokenContractAddress = tangemBridgeProcessor.paymentToken().send() val paymentTokenContract = ERC20.load(paymentTokenContractAddress, web3j, transactionManager, gasProvider) val paymentTokenDecimals = paymentTokenContract.decimals().send() diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 35a8a3c4ad..7829c34135 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -28,6 +28,7 @@ interface VisaContractInfoProvider { private val dispatchers: CoroutineDispatcherProvider, private val baseUrl: String = VisaConfig.BASE_RPC_URL, private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS, + private val paymentAccountRegistryAddress: String = VisaConfig.PAYMENT_ACCOUNT_REGISTRY_ADDRESS, private val chainId: Long = VisaConfig.CHAIN_ID, private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, private val decimals: Int = VisaConfig.DECIMALS, @@ -45,6 +46,7 @@ interface VisaContractInfoProvider { transactionManager = transactionManager, gasProvider = gasProvider, bridgeProcessorAddress = bridgeProcessorAddress, + paymentAccountRegistryAddress = paymentAccountRegistryAddress, dispatchers = dispatchers, ) } diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt index 7819bd734e..e3d0a9dab5 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt @@ -2,8 +2,9 @@ package com.tangem.lib.visa.utils internal object VisaConfig { - const val BASE_RPC_URL = "https://rpc-mumbai.maticvigil.com/" - const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0x62119697e78178512bfcc456ae6d1b7dee9fbaa6" + const val BASE_RPC_URL = "https://polygon-mumbai.g.alchemy.com/v2/_1qqjXgBC_IikaXChnna8KTcV2eMMIQG/" + const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0xe32ecbbc1ec17fa9c160569cd613ad568ca50279" + const val PAYMENT_ACCOUNT_REGISTRY_ADDRESS = "0x3f4ae01073d1a9d5a92315fe118e57d1cdec7c44" const val CHAIN_ID = 80_001L const val DECIMALS = 9 const val GAS_LIMIT = 500_000_000L diff --git a/settings.gradle.kts b/settings.gradle.kts index fb31682f87..8c94ba65a8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -138,6 +138,9 @@ include(":domain:transaction") include(":domain:analytics") include(":domain:visa") include(":domain:onboarding") +include(":domain:feedback") +include(":domain:qr-scanning") +include(":domain:qr-scanning:models") // endregion Domain modules // region Data modules @@ -156,4 +159,6 @@ include(":data:transaction") include(":data:visa") include(":data:promo") include(":data:onboarding") +include(":data:feedback") +include(":data:qr-scanning") // endregion Data modules \ No newline at end of file