Updated on 2026-08-14
This commit is contained in:
commit
1a13142dac
244 changed files with 9915 additions and 1592 deletions
|
|
@ -139,10 +139,10 @@ abstract class BaseTestCase : TestCase(
|
||||||
return ApplicationInjectionExecutionRule(
|
return ApplicationInjectionExecutionRule(
|
||||||
toggleStates = mapOf(
|
toggleStates = mapOf(
|
||||||
"NEW_TOKEN_RECEIVE_ENABLED" to true,
|
"NEW_TOKEN_RECEIVE_ENABLED" to true,
|
||||||
"WALLET_BALANCE_FETCHER_ENABLED" to true,
|
"SWAP_REDESIGN_ENABLED" to false,
|
||||||
"SWAP_REDESIGN_ENABLED" to true,
|
|
||||||
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
"NEW_ONRAMP_MAIN_ENABLED" to true,
|
||||||
"HOT_WALLET_ENABLED" to true
|
"HOT_WALLET_ENABLED" to true,
|
||||||
|
"YIELD_SUPPLY_FEATURE_ENABLED" to true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ object TestConstants {
|
||||||
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
|
const val ETHEREUM_RECIPIENT_ADDRESS = "0x5aa711F440Eb6d4361148bBD89d03464628ace84"
|
||||||
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
|
const val ETHEREUM_RECIPIENT_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
|
||||||
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
|
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
|
||||||
|
const val BITCOIN_RECIPIENT_ADDRESS = "bc1qt90qc0na7z05nh63kyd78tujfc8vqv6sl7e4a9"
|
||||||
const val CARDANO_ADDRESS =
|
const val CARDANO_ADDRESS =
|
||||||
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
|
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
|
||||||
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
|
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
|
||||||
|
|
@ -30,6 +31,7 @@ object TestConstants {
|
||||||
const val XRP_ACTIVATED_RECIPIENT_ADDRESS = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
|
const val XRP_ACTIVATED_RECIPIENT_ADDRESS = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
|
||||||
const val DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
|
const val DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
|
||||||
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
|
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
|
||||||
|
const val TERRA_RECIPIENT_ADDRESS = "terra148dmp5ccazcwdmrcpvqz5rprnn886kemqen3tj"
|
||||||
|
|
||||||
const val WAIT_UNTIL_TIMEOUT = 20_000L
|
const val WAIT_UNTIL_TIMEOUT = 20_000L
|
||||||
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
|
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,10 @@ fun BaseTestCase.checkNetworkFeeBlock(currentFeeAmount: String, withFeeSelector:
|
||||||
step("Assert select fee icon is displayed") {
|
step("Assert select fee icon is displayed") {
|
||||||
onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() }
|
onSendConfirmScreen { selectFeeIcon.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
step("Assert select fee icon is not displayed") {
|
||||||
|
onSendConfirmScreen { selectFeeIcon.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,4 +142,29 @@ fun BaseTestCase.checkRecentAddressItem(address: String, description: String?) {
|
||||||
recentAddressItem(recipientAddress = address, description = description).assertIsDisplayed()
|
recentAddressItem(recipientAddress = address, description = description).assertIsDisplayed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.checkCustomFeeTooltip(title: String, tooltip: String) {
|
||||||
|
step("Click on tooltip icon for '$title'") {
|
||||||
|
waitForIdle()
|
||||||
|
onSendSelectNetworkFeeBottomSheet { tooltipIcon(title).performClick() }
|
||||||
|
}
|
||||||
|
step("Check '$title' tooltip text") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { tooltipText(tooltip).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on tooltip icon again to close tooltip") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { tooltipIcon(title).performClick() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun BaseTestCase.checkChangesInInputTextField(title: String, newValue: String, addition: String = "") {
|
||||||
|
step("Click on '$title' input text field") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).performClick() }
|
||||||
|
}
|
||||||
|
step("Type '$newValue' in '$title' input text field") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).performTextReplacement(newValue) }
|
||||||
|
}
|
||||||
|
step("Assert '$title' value: '$newValue + $addition'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(title).assertTextContains(newValue + addition) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +95,8 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
hasAnyDescendant(withText(description, substring = true))
|
hasAnyDescendant(withText(description, substring = true))
|
||||||
}
|
}
|
||||||
if (isMyWallet) {
|
if (isMyWallet) {
|
||||||
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recipient_wallets_title)))
|
|
||||||
hasAnyDescendant(withText(getResourceString(CoreUiR.string.manage_tokens_network_selector_wallet)))
|
hasAnyDescendant(withText(getResourceString(CoreUiR.string.manage_tokens_network_selector_wallet)))
|
||||||
} else {
|
} else {
|
||||||
hasAnySibling(withText(getResourceString(CoreUiR.string.send_recent_transactions)))
|
|
||||||
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TRANSACTION_ICON))
|
hasAnyDescendant(withTestTag(SendAddressScreenTestTags.RECENT_ADDRESS_TRANSACTION_ICON))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,8 +117,7 @@ class SendAddressPageObject(semanticsProvider: SemanticsNodeInteractionsProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
val destinationTagTextFieldHint: KNode = child {
|
val destinationTagTextFieldHint: KNode = child {
|
||||||
hasParent(withTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD))
|
hasTestTag(SendAddressScreenTestTags.DESTINATION_TAG_TEXT_FIELD)
|
||||||
useUnmergedTree = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val destinationTagBlockText: KNode = child {
|
val destinationTagBlockText: KNode = child {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
package com.tangem.screens
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.core.ui.test.BaseBottomSheetTestTags
|
||||||
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
|
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||||
|
import io.github.kakaocup.compose.node.element.KNode
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||||
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
|
class SendSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
|
ComposeScreen<SendSelectNetworkFeeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
|
val title: KNode = child {
|
||||||
|
hasTestTag(BaseBottomSheetTestTags.TITLE)
|
||||||
|
hasText(getResourceString(R.string.common_network_fee_title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun regularFeeSelectorItem(title: String): KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_FEE_ITEM)
|
||||||
|
hasAnyDescendant(withText(title))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_ICON))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.REGULAR_ITEM_TITLE))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.TOKEN_AMOUNT))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.FIAT_AMOUNT))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.DOT_SIGN))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val customSelectorItem: KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_FEE_ITEM)
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.common_custom)))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_ICON))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_ITEM_TITLE))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun customInputItem(title: String, hasFiatAmount: Boolean = false): KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM)
|
||||||
|
hasAnyDescendant(withText(title))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON))
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD))
|
||||||
|
useUnmergedTree = true
|
||||||
|
if (hasFiatAmount) {
|
||||||
|
hasAnyDescendant(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val nonceInputItem: KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_ITEM)
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.send_nonce)))
|
||||||
|
hasAnyDescendant(withText(getResourceString(R.string.send_nonce_hint)))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tooltipIcon(title: String): KNode = child {
|
||||||
|
hasAnySibling(withText(title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tooltipText(text: String): KNode = child {
|
||||||
|
hasText(text)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun inputTextField(title: String): KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD)
|
||||||
|
hasAnySibling(withText(title))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun inputTextFieldValue(title: String): KNode = inputTextField(title).child {
|
||||||
|
hasParent(withTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val nonceInputTextField: KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val customInputItemFiatAmount: KNode = child {
|
||||||
|
hasTestTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT)
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val doneButton: KNode = child {
|
||||||
|
hasTestTag(BaseButtonTestTags.TEXT)
|
||||||
|
hasText(getResourceString(R.string.common_done))
|
||||||
|
useUnmergedTree = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun BaseTestCase.onSendSelectNetworkFeeBottomSheet(function: SendSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
|
||||||
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -2,7 +2,7 @@ package com.tangem.screens
|
||||||
|
|
||||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||||
import com.tangem.common.BaseTestCase
|
import com.tangem.common.BaseTestCase
|
||||||
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
|
import com.tangem.core.ui.test.SwapSelectNetworkFeeBottomSheetTestTags
|
||||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||||
|
|
@ -11,8 +11,8 @@ import io.github.kakaocup.compose.node.element.KNode
|
||||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
import androidx.compose.ui.test.hasText as withText
|
import androidx.compose.ui.test.hasText as withText
|
||||||
|
|
||||||
class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||||
ComposeScreen<SelectNetworkFeePageObject>(semanticsProvider = semanticsProvider) {
|
ComposeScreen<SwapSelectNetworkFeeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||||
|
|
||||||
val title: KNode = child {
|
val title: KNode = child {
|
||||||
hasTestTag(TopAppBarTestTags.TITLE)
|
hasTestTag(TopAppBarTestTags.TITLE)
|
||||||
|
|
@ -21,22 +21,22 @@ class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsPro
|
||||||
}
|
}
|
||||||
|
|
||||||
val marketSelectorItem: KNode = child {
|
val marketSelectorItem: KNode = child {
|
||||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||||
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
|
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val fastSelectorItem: KNode = child {
|
val fastSelectorItem: KNode = child {
|
||||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||||
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
|
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
|
|
||||||
val readMoreTextBlock: KNode = child {
|
val readMoreTextBlock: KNode = child {
|
||||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
|
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
|
||||||
useUnmergedTree = true
|
useUnmergedTree = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) =
|
internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
|
||||||
onComposeScreen(composeTestRule, function)
|
onComposeScreen(composeTestRule, function)
|
||||||
|
|
@ -215,19 +215,19 @@ class SwapTokenTest : BaseTestCase() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step("Assert 'Select fee' bottom sheet title is displayed") {
|
step("Assert 'Select fee' bottom sheet title is displayed") {
|
||||||
onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
onSwapSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Market' item is displayed") {
|
step("Assert 'Market' item is displayed") {
|
||||||
onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
|
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Fast' item is displayed") {
|
step("Assert 'Fast' item is displayed") {
|
||||||
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Read more' text block is displayed") {
|
step("Assert 'Read more' text block is displayed") {
|
||||||
onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
|
onSwapSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Click on 'Fast' item") {
|
step("Click on 'Fast' item") {
|
||||||
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||||
}
|
}
|
||||||
step("Assert 'Network fee' block is displayed") {
|
step("Assert 'Network fee' block is displayed") {
|
||||||
onSwapTokenScreen {
|
onSwapTokenScreen {
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,8 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() {
|
||||||
step("Assert 'Buy' button is not dimmed") {
|
step("Assert 'Buy' button is not dimmed") {
|
||||||
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
||||||
}
|
}
|
||||||
step("Assert 'Send' button is dimmed") {
|
step("Assert 'Send' button is not dimmed") {
|
||||||
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) }
|
||||||
}
|
}
|
||||||
step("Assert 'Swap' button is dimmed") {
|
step("Assert 'Swap' button is dimmed") {
|
||||||
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,438 @@
|
||||||
|
package com.tangem.tests.send.feeScreen
|
||||||
|
|
||||||
|
import com.tangem.common.BaseTestCase
|
||||||
|
import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.ETHEREUM_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
|
||||||
|
import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS
|
||||||
|
import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
|
||||||
|
import com.tangem.common.extensions.SwipeDirection
|
||||||
|
import com.tangem.common.extensions.clickWithAssertion
|
||||||
|
import com.tangem.common.extensions.swipeVertical
|
||||||
|
import com.tangem.common.utils.resetWireMockScenarioState
|
||||||
|
import com.tangem.common.utils.setWireMockScenarioState
|
||||||
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.scenarios.*
|
||||||
|
import com.tangem.screens.*
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||||
|
import io.qameta.allure.kotlin.AllureId
|
||||||
|
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
class SendFeeScreenTest : BaseTestCase() {
|
||||||
|
|
||||||
|
@AllureId("4906")
|
||||||
|
@DisplayName("Send (Fee screen): check fee block for fee in token")
|
||||||
|
@Test
|
||||||
|
fun checkFeeBlockForFeeInTokenTest() {
|
||||||
|
val tokenName = "TerraClassicUSD"
|
||||||
|
val scenarioName = "Terra"
|
||||||
|
val tokenAmount = "1"
|
||||||
|
val feeAmount = "<$0.01"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen") {
|
||||||
|
openSendScreen(tokenName, scenarioName)
|
||||||
|
}
|
||||||
|
step("Type '$tokenAmount' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(tokenAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(TERRA_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed without fee selector") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4868")
|
||||||
|
@DisplayName("Send (Fee screen): check fee block for fixed fee")
|
||||||
|
@Test
|
||||||
|
fun checkFeeBlockForFixedFeeTest() {
|
||||||
|
val tokenName = "Polkadot"
|
||||||
|
val tokenAmount = "1"
|
||||||
|
val feeAmount = "$0.05"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$tokenAmount' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(tokenAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed without fee selector") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false)
|
||||||
|
}
|
||||||
|
step("Click on 'Fee selector' block") {
|
||||||
|
onSendConfirmScreen { feeSelectorBlock.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Fee selector' bottom sheet is not displayed") {
|
||||||
|
onSwapSelectNetworkFeeBottomSheet { title.assertIsNotDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4869")
|
||||||
|
@DisplayName("Send (Fee screen): check network fee bottom sheet for EVM networks")
|
||||||
|
@Test
|
||||||
|
fun checkNetworkFeeBottomSheetForEvmTest() {
|
||||||
|
val tokenName = "Ethereum"
|
||||||
|
val tokenAmount = "0.1"
|
||||||
|
val feeAmount = "~$1.06"
|
||||||
|
val fiatFeeAmount = "$1.09"
|
||||||
|
val newFeeAmount = "~$1.09"
|
||||||
|
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
val feeUpTo = getResourceString(R.string.send_max_fee)
|
||||||
|
val feeUpToTooltip = getResourceString(R.string.send_custom_amount_fee_footer)
|
||||||
|
val feeUpToValue = "0.00042 ETH"
|
||||||
|
val newFeeUpToValue = "0.00043"
|
||||||
|
val maxFee = getResourceString(R.string.send_custom_evm_max_fee)
|
||||||
|
val maxFeeTooltip = getResourceString(R.string.send_custom_evm_max_fee_footer)
|
||||||
|
val maxFeeValue = "20 GWEI"
|
||||||
|
val newMaxFeeValue = "21"
|
||||||
|
val priorityFee = getResourceString(R.string.send_custom_evm_priority_fee)
|
||||||
|
val priorityFeeTooltip = getResourceString(R.string.send_custom_evm_priority_fee_footer)
|
||||||
|
val priorityFeeValue = "2 GWEI"
|
||||||
|
val newPriorityFeeValue = "3"
|
||||||
|
val gasLimit = getResourceString(R.string.send_gas_limit)
|
||||||
|
val gasLimitTooltip = getResourceString(R.string.send_gas_limit_footer)
|
||||||
|
val gasLimitValue = "21,000 "
|
||||||
|
val newGasLimitValue = "22"
|
||||||
|
val nonce = getResourceString(R.string.send_nonce)
|
||||||
|
val nonceTooltip = getResourceString(R.string.send_nonce_footer)
|
||||||
|
val nonceValue = "1"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen") {
|
||||||
|
openSendScreen(tokenName)
|
||||||
|
}
|
||||||
|
step("Type '$tokenAmount' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(tokenAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed with fee selector") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
|
||||||
|
}
|
||||||
|
step("Click on fee selector icon") {
|
||||||
|
onSendConfirmScreen { feeSelectorIcon.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Fee selector' bottom sheet title is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$fastSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$slowSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$marketSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Custom' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Custom' selector item") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert '$feeUpTo' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet {
|
||||||
|
customInputItem(title = feeUpTo, hasFiatAmount = true).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$maxFee' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItem(maxFee).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$priorityFee' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItem(priorityFee).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$gasLimit' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItem(gasLimit).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Swipe up") {
|
||||||
|
swipeVertical(SwipeDirection.UP)
|
||||||
|
}
|
||||||
|
step("Assert '$nonce' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { nonceInputItem.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Check '$feeUpTo' tooltip") {
|
||||||
|
checkCustomFeeTooltip(title = feeUpTo, tooltip = feeUpToTooltip)
|
||||||
|
}
|
||||||
|
step("Check '$maxFee' tooltip") {
|
||||||
|
checkCustomFeeTooltip(title = maxFee, tooltip = maxFeeTooltip)
|
||||||
|
}
|
||||||
|
step("Check '$priorityFee' tooltip") {
|
||||||
|
checkCustomFeeTooltip(title = priorityFee, tooltip = priorityFeeTooltip)
|
||||||
|
}
|
||||||
|
step("Check '$gasLimit' tooltip") {
|
||||||
|
checkCustomFeeTooltip(title = gasLimit, tooltip = gasLimitTooltip)
|
||||||
|
}
|
||||||
|
step("Check '$nonce' tooltip") {
|
||||||
|
checkCustomFeeTooltip(title = nonce, tooltip = nonceTooltip)
|
||||||
|
}
|
||||||
|
step("Assert '$feeUpTo' value: '$feeUpToValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(feeUpToValue) }
|
||||||
|
}
|
||||||
|
step("Assert '$maxFee' value: '$maxFeeValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(maxFee).assertTextContains(maxFeeValue) }
|
||||||
|
}
|
||||||
|
step("Assert '$priorityFee' value: '$priorityFeeValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(priorityFee).assertTextContains(priorityFeeValue) }
|
||||||
|
}
|
||||||
|
step("Assert '$gasLimit' value: '$gasLimitValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(gasLimit).assertTextContains(gasLimitValue) }
|
||||||
|
}
|
||||||
|
step("Check changes in '$maxFee' input text field") {
|
||||||
|
checkChangesInInputTextField(title = maxFee, newValue = newMaxFeeValue, addition = " GWEI")
|
||||||
|
}
|
||||||
|
step("Check changes in '$priorityFee' input text field") {
|
||||||
|
checkChangesInInputTextField(title = priorityFee, newValue = newPriorityFeeValue, addition = " GWEI")
|
||||||
|
}
|
||||||
|
step("Check changes in '$gasLimit' input text field") {
|
||||||
|
checkChangesInInputTextField(title = gasLimit, newValue = newGasLimitValue, addition = " ")
|
||||||
|
}
|
||||||
|
step("Type '$nonceValue' in '$nonce' text field") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet {
|
||||||
|
nonceInputTextField.performClick()
|
||||||
|
nonceInputTextField.performTextReplacement(nonceValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$nonce' value: '$nonceValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { nonceInputTextField.assertTextContains(nonceValue) }
|
||||||
|
}
|
||||||
|
step("Check changes in '$feeUpTo' input text field") {
|
||||||
|
checkChangesInInputTextField(title = feeUpTo, newValue = newFeeUpToValue, addition = " ETH")
|
||||||
|
}
|
||||||
|
step("Assert new fiat fee amount: '$fiatFeeAmount'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItemFiatAmount.assertTextContains(fiatFeeAmount) }
|
||||||
|
}
|
||||||
|
step("Click on 'Done' button") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed with new fee amount: '$newFeeAmount'") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = newFeeAmount, withFeeSelector = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4870")
|
||||||
|
@DisplayName("Send (Fee screen): check network fee bottom sheet for Bitcoin")
|
||||||
|
@Test
|
||||||
|
fun checkNetworkFeeBottomSheetForBitcoinTest() {
|
||||||
|
val tokenName = "Bitcoin"
|
||||||
|
val tokenAmount = "0.00000001"
|
||||||
|
val feeAmount = "$2.86"
|
||||||
|
val fiatFeeAmount = "$0.24"
|
||||||
|
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
val feeUpTo = getResourceString(R.string.send_max_fee)
|
||||||
|
val feeUpToValue = "0.0000264 BTC"
|
||||||
|
val newFeeUpToValue = "0.0000022 BTC"
|
||||||
|
val satoshi = getResourceString(R.string.send_satoshi_per_byte_title)
|
||||||
|
val satoshiValue = "2"
|
||||||
|
val decimalNumber = "2.11"
|
||||||
|
val newSatoshiValue = "1"
|
||||||
|
val bitcoinUtxoScenarioName = "bitcoin_utxo"
|
||||||
|
val bitcoinUtxoScenarioState = "Balance"
|
||||||
|
val feeScenarioName = "bitcoin_estimate_smart_fee"
|
||||||
|
val feeScenarioState = "Started"
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(bitcoinUtxoScenarioName)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Set WireMock scenario: '$bitcoinUtxoScenarioName' to state: '$bitcoinUtxoScenarioState'") {
|
||||||
|
setWireMockScenarioState(bitcoinUtxoScenarioName, bitcoinUtxoScenarioState)
|
||||||
|
}
|
||||||
|
step("Set WireMock scenario: '$feeScenarioName' to state: '$feeScenarioState'") {
|
||||||
|
setWireMockScenarioState(feeScenarioName, feeScenarioState)
|
||||||
|
}
|
||||||
|
step("Open 'Main' screen") {
|
||||||
|
openMainScreen()
|
||||||
|
}
|
||||||
|
step("Synchronize addresses") {
|
||||||
|
synchronizeAddresses()
|
||||||
|
}
|
||||||
|
step("Open 'Send address' screen") {
|
||||||
|
openSendAddressScreen(tokenName, tokenAmount)
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(BITCOIN_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed with fee selector") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
|
||||||
|
}
|
||||||
|
step("Click on fee selector icon") {
|
||||||
|
onSendConfirmScreen { feeSelectorIcon.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Fee selector' bottom sheet title is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$fastSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$slowSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$marketSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert 'Custom' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customSelectorItem.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on 'Custom' selector item") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customSelectorItem.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert '$feeUpTo' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet {
|
||||||
|
customInputItem(title = feeUpTo, hasFiatAmount = true).assertIsDisplayed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Assert '$feeUpTo' value: '$feeUpToValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(feeUpToValue) }
|
||||||
|
}
|
||||||
|
step("Assert '$satoshi' input item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItem(satoshi).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Click on '$satoshi' input text field") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).performClick() }
|
||||||
|
}
|
||||||
|
step("Type '$decimalNumber' in '$satoshi' input text field") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).performTextReplacement(decimalNumber) }
|
||||||
|
}
|
||||||
|
step("Assert '$satoshi' value: '$satoshiValue + '") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(satoshi).assertTextContains("$satoshiValue ") }
|
||||||
|
}
|
||||||
|
step("Check changes in '$satoshi' input text field") {
|
||||||
|
checkChangesInInputTextField(title = satoshi, newValue = newSatoshiValue, addition = " ")
|
||||||
|
}
|
||||||
|
step("Assert new fiat fee amount: '$fiatFeeAmount'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { customInputItemFiatAmount.assertTextContains(fiatFeeAmount) }
|
||||||
|
}
|
||||||
|
step("Assert '$feeUpTo' value: '$newFeeUpToValue'") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { inputTextFieldValue(feeUpTo).assertTextContains(newFeeUpToValue) }
|
||||||
|
}
|
||||||
|
step("Click on 'Done' button") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { doneButton.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed with new fee amount: '$fiatFeeAmount'") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = fiatFeeAmount, withFeeSelector = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@AllureId("4871")
|
||||||
|
@DisplayName("Send (Fee screen): check network fee bottom sheet networks with fee in token")
|
||||||
|
@Test
|
||||||
|
fun checkNetworkFeeBottomSheetForVeThorTest() {
|
||||||
|
val tokenName = "VeThor"
|
||||||
|
val mockState = "Vechain"
|
||||||
|
val tokenAmount = "0.1"
|
||||||
|
val feeAmount = "<$0.01"
|
||||||
|
val marketSelectorItem = getResourceString(R.string.common_fee_selector_option_market)
|
||||||
|
val fastSelectorItem = getResourceString(R.string.common_fee_selector_option_fast)
|
||||||
|
val slowSelectorItem = getResourceString(R.string.common_fee_selector_option_slow)
|
||||||
|
|
||||||
|
setupHooks(
|
||||||
|
additionalAfterSection = {
|
||||||
|
resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
|
||||||
|
resetWireMockScenarioState(QUOTES_API_SCENARIO)
|
||||||
|
}
|
||||||
|
).run {
|
||||||
|
step("Open 'Send' screen") {
|
||||||
|
openSendScreen(tokenName = tokenName, mockState = mockState)
|
||||||
|
}
|
||||||
|
step("Type '$tokenAmount' in input text field") {
|
||||||
|
onSendScreen {
|
||||||
|
amountInputTextField.performClick()
|
||||||
|
amountInputTextField.performTextReplacement(tokenAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendAddressScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Type recipient address") {
|
||||||
|
onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) }
|
||||||
|
}
|
||||||
|
step("Click on 'Next' button") {
|
||||||
|
onSendScreen { nextButton.clickWithAssertion() }
|
||||||
|
}
|
||||||
|
step("Assert fee block is displayed with fee selector") {
|
||||||
|
checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true)
|
||||||
|
}
|
||||||
|
step("Click on fee selector icon") {
|
||||||
|
onSendConfirmScreen { feeSelectorIcon.performClick() }
|
||||||
|
}
|
||||||
|
step("Assert 'Fee selector' bottom sheet title is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$fastSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(fastSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$slowSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(slowSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
step("Assert '$marketSelectorItem' selector item is displayed") {
|
||||||
|
onSendSelectNetworkFeeBottomSheet { regularFeeSelectorItem(marketSelectorItem).assertIsDisplayed() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -94,12 +94,12 @@ internal object StakingDomainModule {
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideFetchStakingOptionsUseCase(
|
fun provideFetchStakingOptionsUseCase(
|
||||||
stakeKitRepository: StakeKitRepository,
|
stakeKitRepository: StakeKitRepository,
|
||||||
p2pRepository: P2PEthPoolRepository,
|
p2pEthPoolRepository: P2PEthPoolRepository,
|
||||||
stakingErrorResolver: StakingErrorResolver,
|
stakingErrorResolver: StakingErrorResolver,
|
||||||
): FetchStakingOptionsUseCase {
|
): FetchStakingOptionsUseCase {
|
||||||
return FetchStakingOptionsUseCase(
|
return FetchStakingOptionsUseCase(
|
||||||
stakeKitRepository = stakeKitRepository,
|
stakeKitRepository = stakeKitRepository,
|
||||||
p2pRepository = p2pRepository,
|
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||||
stakingErrorResolver = stakingErrorResolver,
|
stakingErrorResolver = stakingErrorResolver,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,14 @@ object WalletMockContent : MockContent {
|
||||||
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey(
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
),
|
||||||
|
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey(
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
extendedPublicKey = ExtendedPublicKey(
|
extendedPublicKey = ExtendedPublicKey(
|
||||||
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
|
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
|
||||||
|
|
@ -261,6 +269,20 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey( // Terra
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
|
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ByteArrayKey(
|
ByteArrayKey(
|
||||||
|
|
@ -318,6 +340,20 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/330'/0'/0/0") to ExtendedPublicKey( // Terra
|
||||||
|
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
|
||||||
|
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
|
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
|
||||||
|
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82),
|
||||||
|
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -371,6 +407,13 @@ object WalletMockContent : MockContent {
|
||||||
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
childNumber = 0,
|
childNumber = 0,
|
||||||
),
|
),
|
||||||
|
DerivationPath("m/44'/818'/0'/0/0") to ExtendedPublicKey( // Vechain
|
||||||
|
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
|
||||||
|
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
|
||||||
|
depth = 0,
|
||||||
|
parentFingerprint = byteArrayOf(0, 0, 0, 0),
|
||||||
|
childNumber = 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ByteArrayKey(
|
ByteArrayKey(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.models.wallet.requireColdWallet
|
import com.tangem.domain.models.wallet.requireColdWallet
|
||||||
|
import com.tangem.domain.pay.repository.OnboardingRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||||
|
|
@ -54,6 +55,7 @@ internal class CardSettingsModel @Inject constructor(
|
||||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||||
private val settingsRepository: SettingsRepository,
|
private val settingsRepository: SettingsRepository,
|
||||||
|
private val onboardingRepository: OnboardingRepository,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<CardSettingsComponent.Params>()
|
private val params = paramsContainer.require<CardSettingsComponent.Params>()
|
||||||
|
|
@ -218,15 +220,19 @@ internal class CardSettingsModel @Inject constructor(
|
||||||
} else {
|
} else {
|
||||||
val card = scanResponse.card
|
val card = scanResponse.card
|
||||||
|
|
||||||
store.dispatchNavigationAction {
|
modelScope.launch {
|
||||||
push(
|
val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
|
||||||
route = AppRoute.ResetToFactory(
|
store.dispatchNavigationAction {
|
||||||
userWalletId = userWalletId,
|
push(
|
||||||
cardId = card.cardId,
|
route = AppRoute.ResetToFactory(
|
||||||
isActiveBackupStatus = card.backupStatus?.isActive == true,
|
userWalletId = userWalletId,
|
||||||
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
|
cardId = card.cardId,
|
||||||
),
|
isActiveBackupStatus = card.backupStatus?.isActive == true,
|
||||||
)
|
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
|
||||||
|
hasTangemPay = hasTangemPay,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
|
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -114,22 +115,11 @@ private fun Description(text: TextReference) {
|
||||||
@Composable
|
@Composable
|
||||||
private fun Conditions(state: ResetCardScreenState) {
|
private fun Conditions(state: ResetCardScreenState) {
|
||||||
state.warningsToShow.forEach { warning ->
|
state.warningsToShow.forEach { warning ->
|
||||||
when (warning) {
|
ConditionCheckBox(
|
||||||
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
|
checkedState = warning.isChecked,
|
||||||
ConditionCheckBox(
|
onCheckedChange = { state.onToggleWarning(warning.type) },
|
||||||
checkedState = state.isAcceptCondition1Checked,
|
description = warning.description,
|
||||||
onCheckedChange = state.onAcceptCondition1ToggleClick,
|
)
|
||||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
|
|
||||||
ConditionCheckBox(
|
|
||||||
checkedState = state.isAcceptCondition2Checked,
|
|
||||||
onCheckedChange = state.onAcceptCondition2ToggleClick,
|
|
||||||
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,14 +229,16 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
|
||||||
ResetCardScreen(
|
ResetCardScreen(
|
||||||
state = ResetCardScreenState(
|
state = ResetCardScreenState(
|
||||||
isResetButtonEnabled = true,
|
isResetButtonEnabled = true,
|
||||||
isResetPasswordButtonShown = false,
|
warningsToShow = persistentListOf(
|
||||||
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
|
ResetCardScreenState.WarningUM(
|
||||||
|
isChecked = false,
|
||||||
|
type = ResetCardScreenState.WarningType.LOST_WALLET_ACCESS,
|
||||||
|
description = TextReference.Res(id = R.string.reset_card_to_factory_condition_1),
|
||||||
|
),
|
||||||
|
),
|
||||||
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
|
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
|
||||||
isAcceptCondition1Checked = false,
|
|
||||||
isAcceptCondition2Checked = false,
|
|
||||||
onAcceptCondition1ToggleClick = {},
|
|
||||||
onAcceptCondition2ToggleClick = {},
|
|
||||||
onResetButtonClick = {},
|
onResetButtonClick = {},
|
||||||
|
onToggleWarning = {},
|
||||||
dialog = null,
|
dialog = null,
|
||||||
),
|
),
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,13 @@ package com.tangem.tap.features.details.ui.resetcard
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
internal data class ResetCardScreenState(
|
internal data class ResetCardScreenState(
|
||||||
val isResetButtonEnabled: Boolean,
|
val isResetButtonEnabled: Boolean,
|
||||||
val descriptionText: TextReference,
|
val descriptionText: TextReference,
|
||||||
val warningsToShow: List<WarningsToReset>,
|
val warningsToShow: ImmutableList<WarningUM>,
|
||||||
val isResetPasswordButtonShown: Boolean,
|
val onToggleWarning: (WarningType) -> Unit,
|
||||||
val isAcceptCondition1Checked: Boolean,
|
|
||||||
val isAcceptCondition2Checked: Boolean,
|
|
||||||
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
|
|
||||||
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
|
|
||||||
val onResetButtonClick: () -> Unit,
|
val onResetButtonClick: () -> Unit,
|
||||||
val dialog: Dialog?,
|
val dialog: Dialog?,
|
||||||
) {
|
) {
|
||||||
|
|
@ -58,7 +55,13 @@ internal data class ResetCardScreenState(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum class WarningsToReset {
|
internal data class WarningUM(
|
||||||
LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE
|
val isChecked: Boolean,
|
||||||
|
val type: WarningType,
|
||||||
|
val description: TextReference,
|
||||||
|
)
|
||||||
|
|
||||||
|
internal enum class WarningType {
|
||||||
|
LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE, LOST_TANGEM_PAY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ interface ResetCardComponent : ComposableContentComponent {
|
||||||
val cardId: String,
|
val cardId: String,
|
||||||
val isActiveBackupStatus: Boolean,
|
val isActiveBackupStatus: Boolean,
|
||||||
val backupCardsCount: Int,
|
val backupCardsCount: Int,
|
||||||
|
val hasTangemPay: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
interface Factory : ComponentFactory<Params, ResetCardComponent>
|
interface Factory : ComponentFactory<Params, ResetCardComponent>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||||
import com.tangem.core.decompose.di.ModelScoped
|
import com.tangem.core.decompose.di.ModelScoped
|
||||||
import com.tangem.core.decompose.model.Model
|
import com.tangem.core.decompose.model.Model
|
||||||
import com.tangem.core.decompose.model.ParamsContainer
|
import com.tangem.core.decompose.model.ParamsContainer
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
|
||||||
import com.tangem.domain.card.ResetCardUseCase
|
import com.tangem.domain.card.ResetCardUseCase
|
||||||
import com.tangem.domain.card.ResetCardUserCodeParams
|
import com.tangem.domain.card.ResetCardUserCodeParams
|
||||||
|
|
@ -30,6 +31,9 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
@ -82,33 +86,22 @@ internal class ResetCardModel @Inject constructor(
|
||||||
// TODO: move logic to separate domain entity
|
// TODO: move logic to separate domain entity
|
||||||
private var resetBackupCardCount = 0
|
private var resetBackupCardCount = 0
|
||||||
|
|
||||||
|
private var warningsMap = emptyMap<ResetCardScreenState.WarningType, ResetCardScreenState.WarningUM>()
|
||||||
|
|
||||||
val screenState: MutableStateFlow<ResetCardScreenState> = MutableStateFlow(
|
val screenState: MutableStateFlow<ResetCardScreenState> = MutableStateFlow(
|
||||||
value = getInitialState(),
|
value = getInitialState(),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun getInitialState(): ResetCardScreenState {
|
private fun getInitialState(): ResetCardScreenState {
|
||||||
val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
|
|
||||||
val warningsToShow = buildList {
|
|
||||||
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
|
|
||||||
|
|
||||||
if (shouldShowResetPasswordButton) {
|
|
||||||
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResetCardScreenState(
|
return ResetCardScreenState(
|
||||||
isResetButtonEnabled = false,
|
isResetButtonEnabled = false,
|
||||||
descriptionText = getResetToFactoryDescription(
|
descriptionText = getResetToFactoryDescription(
|
||||||
isActiveBackupStatus = isActiveBackupPrimaryCard,
|
isActiveBackupStatus = isActiveBackupPrimaryCard,
|
||||||
typesResolver = currentCardTypesResolver,
|
typesResolver = currentCardTypesResolver,
|
||||||
),
|
),
|
||||||
warningsToShow = warningsToShow,
|
warningsToShow = buildInitialItems(),
|
||||||
isResetPasswordButtonShown = shouldShowResetPasswordButton,
|
|
||||||
isAcceptCondition1Checked = false,
|
|
||||||
isAcceptCondition2Checked = false,
|
|
||||||
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
|
|
||||||
onAcceptCondition2ToggleClick = ::toggleSecondCondition,
|
|
||||||
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
|
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
|
||||||
|
onToggleWarning = ::toggleCondition,
|
||||||
dialog = null,
|
dialog = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -119,28 +112,46 @@ internal class ResetCardModel @Inject constructor(
|
||||||
return isTangemWallet && isActiveBackupPrimaryCard
|
return isTangemWallet && isActiveBackupPrimaryCard
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleFirstCondition(isAccepted: Boolean) {
|
private fun buildInitialItems(): ImmutableList<ResetCardScreenState.WarningUM> {
|
||||||
screenState.update { prevState ->
|
val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
|
||||||
val isResetButtonEnabled = if (prevState.isResetPasswordButtonShown) {
|
val shouldShowResetTangemPayButton = params.hasTangemPay
|
||||||
isAccepted && prevState.isAcceptCondition2Checked
|
|
||||||
} else {
|
|
||||||
isAccepted
|
|
||||||
}
|
|
||||||
|
|
||||||
prevState.copy(
|
val lostWalletUM = ResetCardScreenState.WarningUM(
|
||||||
isAcceptCondition1Checked = isAccepted,
|
isChecked = false,
|
||||||
isResetButtonEnabled = isResetButtonEnabled,
|
type = ResetCardScreenState.WarningType.LOST_WALLET_ACCESS,
|
||||||
)
|
description = TextReference.Res(id = R.string.reset_card_to_factory_condition_1),
|
||||||
|
)
|
||||||
|
val lostPasswordUM = ResetCardScreenState.WarningUM(
|
||||||
|
isChecked = false,
|
||||||
|
type = ResetCardScreenState.WarningType.LOST_PASSWORD_RESTORE,
|
||||||
|
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
|
||||||
|
)
|
||||||
|
val lostTangemPayUM = ResetCardScreenState.WarningUM(
|
||||||
|
isChecked = false,
|
||||||
|
type = ResetCardScreenState.WarningType.LOST_TANGEM_PAY,
|
||||||
|
description = TextReference.Res(R.string.reset_card_to_factory_condition_3),
|
||||||
|
)
|
||||||
|
|
||||||
|
warningsMap = buildMap {
|
||||||
|
put(ResetCardScreenState.WarningType.LOST_WALLET_ACCESS, lostWalletUM)
|
||||||
|
if (shouldShowResetPasswordButton) {
|
||||||
|
put(ResetCardScreenState.WarningType.LOST_PASSWORD_RESTORE, lostPasswordUM)
|
||||||
|
}
|
||||||
|
if (shouldShowResetTangemPayButton) {
|
||||||
|
put(ResetCardScreenState.WarningType.LOST_TANGEM_PAY, lostTangemPayUM)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return warningsMap.values.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toggleSecondCondition(isAccepted: Boolean) {
|
private fun toggleCondition(type: ResetCardScreenState.WarningType) {
|
||||||
screenState.update { prevState ->
|
screenState.update { prevState ->
|
||||||
val isResetButtonEnabled = prevState.isAcceptCondition1Checked && isAccepted
|
warningsMap[type]?.let { current ->
|
||||||
|
warningsMap = warningsMap + (type to current.copy(isChecked = !current.isChecked))
|
||||||
|
}
|
||||||
prevState.copy(
|
prevState.copy(
|
||||||
isAcceptCondition2Checked = isAccepted,
|
warningsToShow = warningsMap.values.toImmutableList(),
|
||||||
isResetButtonEnabled = isResetButtonEnabled,
|
isResetButtonEnabled = warningsMap.values.all { it.isChecked },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.tap.network.auth
|
package com.tangem.tap.network.auth
|
||||||
|
|
||||||
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
|
|
||||||
internal class DefaultP2PEthPoolAuthProvider(
|
internal class DefaultP2PEthPoolAuthProvider(
|
||||||
|
|
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
|
||||||
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
||||||
?: error("No P2P api keys provided")
|
?: error("No P2P api keys provided")
|
||||||
|
|
||||||
return if (P2PStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,9 @@ import com.tangem.features.createwalletselection.CreateWalletSelectionComponent
|
||||||
import com.tangem.features.createwalletstart.CreateWalletStartComponent
|
import com.tangem.features.createwalletstart.CreateWalletStartComponent
|
||||||
import com.tangem.features.details.component.DetailsComponent
|
import com.tangem.features.details.component.DetailsComponent
|
||||||
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
|
||||||
|
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||||
|
import com.tangem.features.feed.entry.components.FeedEntryRoute
|
||||||
|
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
|
||||||
import com.tangem.features.home.api.HomeComponent
|
import com.tangem.features.home.api.HomeComponent
|
||||||
import com.tangem.features.hotwallet.*
|
import com.tangem.features.hotwallet.*
|
||||||
import com.tangem.features.kyc.KycComponent
|
import com.tangem.features.kyc.KycComponent
|
||||||
|
|
@ -38,10 +41,7 @@ import com.tangem.features.staking.api.StakingComponent
|
||||||
import com.tangem.features.swap.SwapComponent
|
import com.tangem.features.swap.SwapComponent
|
||||||
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.ContinueOnboarding
|
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.*
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.Deeplink
|
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerOnMain
|
|
||||||
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.FromBannerInSettings
|
|
||||||
import com.tangem.features.tokendetails.TokenDetailsComponent
|
import com.tangem.features.tokendetails.TokenDetailsComponent
|
||||||
import com.tangem.features.wallet.WalletEntryComponent
|
import com.tangem.features.wallet.WalletEntryComponent
|
||||||
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
|
||||||
|
|
@ -121,6 +121,8 @@ internal class ChildFactory @Inject constructor(
|
||||||
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
|
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
|
||||||
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
|
private val yieldSupplyActiveComponentFactory: YieldSupplyActiveComponent.Factory,
|
||||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||||
|
private val feedEntryComponentFactory: FeedEntryComponent.Factory,
|
||||||
|
private val feedFeatureToggle: FeedFeatureToggle,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
|
|
@ -204,21 +206,39 @@ internal class ChildFactory @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is AppRoute.MarketsTokenDetails -> {
|
is AppRoute.MarketsTokenDetails -> {
|
||||||
createComponentChild(
|
if (feedFeatureToggle.isFeedEnabled) {
|
||||||
context = context,
|
createComponentChild(
|
||||||
params = MarketsTokenDetailsComponent.Params(
|
context = context,
|
||||||
token = route.token,
|
params = FeedEntryRoute.MarketTokenDetails(
|
||||||
appCurrency = route.appCurrency,
|
token = route.token,
|
||||||
shouldShowPortfolio = route.shouldShowPortfolio,
|
appCurrency = route.appCurrency,
|
||||||
analyticsParams = route.analyticsParams?.let { params ->
|
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||||
MarketsTokenDetailsComponent.AnalyticsParams(
|
analyticsParams = route.analyticsParams?.let { params ->
|
||||||
blockchain = params.blockchain,
|
FeedEntryRoute.MarketTokenDetails.AnalyticsParams(
|
||||||
source = params.source,
|
blockchain = params.blockchain,
|
||||||
)
|
source = params.source,
|
||||||
},
|
)
|
||||||
),
|
},
|
||||||
componentFactory = marketsTokenDetailsComponentFactory,
|
),
|
||||||
)
|
componentFactory = feedEntryComponentFactory,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
createComponentChild(
|
||||||
|
context = context,
|
||||||
|
params = MarketsTokenDetailsComponent.Params(
|
||||||
|
token = route.token,
|
||||||
|
appCurrency = route.appCurrency,
|
||||||
|
shouldShowPortfolio = route.shouldShowPortfolio,
|
||||||
|
analyticsParams = route.analyticsParams?.let { params ->
|
||||||
|
MarketsTokenDetailsComponent.AnalyticsParams(
|
||||||
|
blockchain = params.blockchain,
|
||||||
|
source = params.source,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
componentFactory = marketsTokenDetailsComponentFactory,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
is AppRoute.Onramp -> {
|
is AppRoute.Onramp -> {
|
||||||
createComponentChild(
|
createComponentChild(
|
||||||
|
|
@ -311,7 +331,7 @@ internal class ChildFactory @Inject constructor(
|
||||||
params = StakingComponent.Params(
|
params = StakingComponent.Params(
|
||||||
userWalletId = route.userWalletId,
|
userWalletId = route.userWalletId,
|
||||||
cryptoCurrency = route.cryptoCurrency,
|
cryptoCurrency = route.cryptoCurrency,
|
||||||
yieldId = route.yieldId,
|
integrationId = route.integrationId,
|
||||||
),
|
),
|
||||||
componentFactory = stakingComponentFactory,
|
componentFactory = stakingComponentFactory,
|
||||||
)
|
)
|
||||||
|
|
@ -424,6 +444,7 @@ internal class ChildFactory @Inject constructor(
|
||||||
cardId = route.cardId,
|
cardId = route.cardId,
|
||||||
isActiveBackupStatus = route.isActiveBackupStatus,
|
isActiveBackupStatus = route.isActiveBackupStatus,
|
||||||
backupCardsCount = route.backupCardsCount,
|
backupCardsCount = route.backupCardsCount,
|
||||||
|
hasTangemPay = route.hasTangemPay,
|
||||||
),
|
),
|
||||||
componentFactory = resetCardComponentFactory,
|
componentFactory = resetCardComponentFactory,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ dependencies {
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
implementation(projects.domain.tokens.models)
|
implementation(projects.domain.tokens.models)
|
||||||
implementation(projects.domain.wallets.models)
|
implementation(projects.domain.wallets.models)
|
||||||
implementation(projects.domain.staking.models)
|
implementation(projects.domain.staking)
|
||||||
implementation(projects.domain.markets.models)
|
implementation(projects.domain.markets.models)
|
||||||
implementation(projects.domain.onramp.models)
|
implementation(projects.domain.onramp.models)
|
||||||
implementation(projects.domain.appCurrency.models)
|
implementation(projects.domain.appCurrency.models)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.os.Bundle
|
||||||
import com.tangem.common.routing.bundle.RouteBundleParams
|
import com.tangem.common.routing.bundle.RouteBundleParams
|
||||||
import com.tangem.common.routing.bundle.bundle
|
import com.tangem.common.routing.bundle.bundle
|
||||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||||
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
import com.tangem.core.decompose.navigation.Route
|
import com.tangem.core.decompose.navigation.Route
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||||
|
|
@ -112,6 +113,7 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
val cardId: String,
|
val cardId: String,
|
||||||
val isActiveBackupStatus: Boolean,
|
val isActiveBackupStatus: Boolean,
|
||||||
val backupCardsCount: Int,
|
val backupCardsCount: Int,
|
||||||
|
val hasTangemPay: Boolean,
|
||||||
) : AppRoute(
|
) : AppRoute(
|
||||||
path = "/reset_to_factory" +
|
path = "/reset_to_factory" +
|
||||||
"/${userWalletId.stringValue}" +
|
"/${userWalletId.stringValue}" +
|
||||||
|
|
@ -208,8 +210,8 @@ sealed class AppRoute(val path: String) : Route {
|
||||||
data class Staking(
|
data class Staking(
|
||||||
val userWalletId: UserWalletId,
|
val userWalletId: UserWalletId,
|
||||||
val cryptoCurrency: CryptoCurrency,
|
val cryptoCurrency: CryptoCurrency,
|
||||||
val yieldId: String,
|
val integrationId: StakingIntegrationID,
|
||||||
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
|
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class PushNotification(
|
data class PushNotification(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory for creating mock P2P ETH Pool account responses for testing
|
* Factory for creating mock P2PEthPool account responses for testing
|
||||||
*/
|
*/
|
||||||
object MockP2PEthPoolAccountResponseFactory {
|
object MockP2PEthPoolAccountResponseFactory {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import com.tangem.domain.models.account.Account
|
||||||
import com.tangem.domain.models.account.AccountId
|
import com.tangem.domain.models.account.AccountId
|
||||||
import com.tangem.domain.models.quote.PriceChange
|
import com.tangem.domain.models.quote.PriceChange
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.extensions.isZero
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
class AccountCryptoPortfolioItemStateConverter(
|
class AccountCryptoPortfolioItemStateConverter(
|
||||||
|
|
@ -41,14 +40,11 @@ class AccountCryptoPortfolioItemStateConverter(
|
||||||
private fun Account.CryptoPortfolio.mapToContentState(
|
private fun Account.CryptoPortfolio.mapToContentState(
|
||||||
fiatBalance: TotalFiatBalance.Loaded,
|
fiatBalance: TotalFiatBalance.Loaded,
|
||||||
): TokenItemState.Content {
|
): TokenItemState.Content {
|
||||||
val subtitle2State = when (fiatBalance.amount.isZero()) {
|
val subtitle2State = priceChangeLce?.fold(
|
||||||
true -> null
|
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
||||||
false -> priceChangeLce?.fold(
|
ifError = { null },
|
||||||
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
||||||
ifError = { null },
|
)
|
||||||
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return TokenItemState.Content(
|
return TokenItemState.Content(
|
||||||
id = account.accountId.toItemId(),
|
id = account.accountId.toItemId(),
|
||||||
iconState = AccountIconItemStateConverter.convert(this),
|
iconState = AccountIconItemStateConverter.convert(this),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.text.TextAutoSize
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -19,7 +20,6 @@ import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.common.ui.account.AccountTitle
|
import com.tangem.common.ui.account.AccountTitle
|
||||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||||
import com.tangem.core.ui.components.ResizableText
|
|
||||||
import com.tangem.core.ui.components.SpacerH
|
import com.tangem.core.ui.components.SpacerH
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||||
|
|
@ -66,11 +66,14 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
|
||||||
state = amountState.tokenIconState,
|
state = amountState.tokenIconState,
|
||||||
iconSize = 40.dp,
|
iconSize = 40.dp,
|
||||||
)
|
)
|
||||||
ResizableText(
|
Text(
|
||||||
text = firstAmount,
|
text = firstAmount,
|
||||||
style = TangemTheme.typography.h2,
|
style = TangemTheme.typography.h2,
|
||||||
color = TangemTheme.colors.text.primary1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
|
autoSize = TextAutoSize.StepBased(
|
||||||
|
maxFontSize = TangemTheme.typography.h2.fontSize,
|
||||||
|
),
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.TextAutoSize
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -18,7 +19,6 @@ import com.tangem.common.ui.account.AccountTitle
|
||||||
import com.tangem.common.ui.account.AccountTitleUM
|
import com.tangem.common.ui.account.AccountTitleUM
|
||||||
import com.tangem.common.ui.amountScreen.models.AmountState
|
import com.tangem.common.ui.amountScreen.models.AmountState
|
||||||
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData
|
||||||
import com.tangem.core.ui.components.ResizableText
|
|
||||||
import com.tangem.core.ui.components.SpacerWMax
|
import com.tangem.core.ui.components.SpacerWMax
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
|
||||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
|
|
@ -128,21 +128,23 @@ private fun AmountBlockV2(
|
||||||
.padding(top = 8.dp)
|
.padding(top = 8.dp)
|
||||||
.weight(1f),
|
.weight(1f),
|
||||||
) {
|
) {
|
||||||
ResizableText(
|
Text(
|
||||||
text = firstAmount,
|
text = firstAmount,
|
||||||
style = TangemTheme.typography.h2,
|
style = TangemTheme.typography.h2,
|
||||||
color = TangemTheme.colors.text.primary1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.h2.fontSize),
|
||||||
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
|
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
|
||||||
)
|
)
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
ResizableText(
|
Text(
|
||||||
text = secondAmount,
|
text = secondAmount,
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
color = TangemTheme.colors.text.tertiary,
|
color = TangemTheme.colors.text.tertiary,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.body2.fontSize),
|
||||||
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
|
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
|
||||||
)
|
)
|
||||||
extraContent()
|
extraContent()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.common.ui.news
|
package com.tangem.common.ui.news
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
@ -12,6 +13,9 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -82,6 +86,7 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
|
||||||
style = TangemTheme.typography.h3,
|
style = TangemTheme.typography.h3,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
SpacerH(8.dp)
|
SpacerH(8.dp)
|
||||||
|
|
@ -100,6 +105,41 @@ private fun TrendingArticle(articleConfigUM: ArticleConfigUM) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
|
BlockCard(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(vertical = 31.dp, horizontal = 12.dp),
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48),
|
||||||
|
contentDescription = stringResourceSafe(R.string.common_show_more),
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(16.dp)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.news_all_news),
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResourceSafe(R.string.news_stay_in_the_loop),
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
|
private fun DefaultArticle(articleConfigUM: ArticleConfigUM) {
|
||||||
Column(modifier = Modifier.padding(12.dp)) {
|
Column(modifier = Modifier.padding(12.dp)) {
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
import com.tangem.domain.staking.model.StakingAvailability
|
import com.tangem.domain.staking.model.StakingAvailability
|
||||||
import com.tangem.domain.staking.model.StakingOption
|
import com.tangem.domain.staking.model.StakingOption
|
||||||
import com.tangem.domain.staking.model.stakekit.Yield
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance
|
||||||
import com.tangem.lib.crypto.BlockchainUtils
|
import com.tangem.lib.crypto.BlockchainUtils
|
||||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||||
|
|
@ -247,9 +248,9 @@ class TokenItemStateConverter(
|
||||||
stakingApyMap = stakingApyMap,
|
stakingApyMap = stakingApyMap,
|
||||||
)
|
)
|
||||||
val rewardTypeRes = when (stakingInfo.rewardType) {
|
val rewardTypeRes = when (stakingInfo.rewardType) {
|
||||||
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
|
RewardType.APR -> R.string.staking_apr_earn_badge
|
||||||
Yield.RewardType.UNKNOWN,
|
RewardType.UNKNOWN,
|
||||||
Yield.RewardType.APY,
|
RewardType.APY,
|
||||||
null,
|
null,
|
||||||
-> R.string.yield_module_earn_badge
|
-> R.string.yield_module_earn_badge
|
||||||
}
|
}
|
||||||
|
|
@ -281,10 +282,11 @@ class TokenItemStateConverter(
|
||||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||||
|
|
||||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||||
is StakingOption.P2P -> {
|
is StakingOption.P2PEthPool -> {
|
||||||
// P2P or no balance: use preferred validators
|
RewardInfo(
|
||||||
// TODO add p2p logic
|
rate = stakingOptions.apy,
|
||||||
null
|
type = RewardType.APY,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
|
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
|
||||||
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
|
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
|
||||||
|
|
@ -458,7 +460,7 @@ class TokenItemStateConverter(
|
||||||
private data class StakingLocalInfo(
|
private data class StakingLocalInfo(
|
||||||
val rate: BigDecimal?,
|
val rate: BigDecimal?,
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val rewardType: Yield.RewardType?,
|
val rewardType: RewardType?,
|
||||||
)
|
)
|
||||||
|
|
||||||
private data class EarnApyInfo(
|
private data class EarnApyInfo(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.datasource.api.common.config
|
package com.tangem.datasource.api.common.config
|
||||||
|
|
||||||
import com.tangem.datasource.BuildConfig
|
import com.tangem.datasource.BuildConfig
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
import com.tangem.utils.ProviderSuspend
|
import com.tangem.utils.ProviderSuspend
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ internal class P2PEthPool(
|
||||||
private fun getInitialEnvironment(): ApiEnvironment {
|
private fun getInitialEnvironment(): ApiEnvironment {
|
||||||
return when (BuildConfig.BUILD_TYPE) {
|
return when (BuildConfig.BUILD_TYPE) {
|
||||||
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
MOCKED_BUILD_TYPE -> ApiEnvironment.MOCK
|
||||||
else -> if (P2PStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
else -> if (P2PEthPoolStakingConfig.USE_TESTNET) ApiEnvironment.DEV else ApiEnvironment.PROD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ data class P2PEthPoolBroadcastResponse(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transaction status from P2P API
|
* Transaction status from P2PEthPool API
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class P2PEthPoolTxStatusDTO {
|
enum class P2PEthPoolTxStatusDTO {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Error response structure for P2P.org API
|
* Error response structure for P2P.org eth pooled API
|
||||||
*
|
*
|
||||||
* All P2P API endpoints return errors in this format
|
* All P2PEthPool API endpoints return errors in this format
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class P2PEthPoolErrorResponse(
|
data class P2PEthPoolErrorResponse(
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
|
||||||
/**
|
/**
|
||||||
* Unified response wrapper for all P2P.org API responses
|
* Unified response wrapper for all P2P.org API responses
|
||||||
*
|
*
|
||||||
* All P2P API endpoints return responses in this format:
|
* All P2PEthPool API endpoints return responses in this format:
|
||||||
* ```json
|
* ```json
|
||||||
* {
|
* {
|
||||||
* "error": null | { code, message, name, errors },
|
* "error": null | { code, message, name, errors },
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package com.tangem.datasource.api.ethpool.models.response
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for GET /api/v1/staking/pool/{network}/vaults
|
* Response for GET /api/v1/staking/pool/{network}/vaults
|
||||||
|
|
@ -15,7 +16,7 @@ data class P2PEthPoolVaultsResponse(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Network identifier in P2P API
|
* Network identifier in P2PEthPool API
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class P2PEthPoolNetworkDTO {
|
enum class P2PEthPoolNetworkDTO {
|
||||||
|
|
@ -33,15 +34,15 @@ data class P2PEthPoolVaultDTO(
|
||||||
@Json(name = "displayName")
|
@Json(name = "displayName")
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
@Json(name = "apy")
|
@Json(name = "apy")
|
||||||
val apy: Double,
|
val apy: BigDecimal,
|
||||||
@Json(name = "baseApy")
|
@Json(name = "baseApy")
|
||||||
val baseApy: Double,
|
val baseApy: BigDecimal,
|
||||||
@Json(name = "capacity")
|
@Json(name = "capacity")
|
||||||
val capacity: Double,
|
val capacity: BigDecimal,
|
||||||
@Json(name = "totalAssets")
|
@Json(name = "totalAssets")
|
||||||
val totalAssets: Double,
|
val totalAssets: BigDecimal,
|
||||||
@Json(name = "feePercent")
|
@Json(name = "feePercent")
|
||||||
val feePercent: Double,
|
val feePercent: BigDecimal,
|
||||||
@Json(name = "isPrivate")
|
@Json(name = "isPrivate")
|
||||||
val isPrivate: Boolean,
|
val isPrivate: Boolean,
|
||||||
@Json(name = "isGenesis")
|
@Json(name = "isGenesis")
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ internal object StakingStoreModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideP2PBalancesPersistenceStore(
|
fun provideP2PEthPoolBalancesPersistenceStore(
|
||||||
@NetworkMoshi moshi: Moshi,
|
@NetworkMoshi moshi: Moshi,
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
|
@ -91,7 +91,7 @@ internal object StakingStoreModule {
|
||||||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||||
defaultValue = emptyMap(),
|
defaultValue = emptyMap(),
|
||||||
),
|
),
|
||||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
|
||||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
||||||
* (similar to StakingYieldsStore for StakeKit yields)
|
* (similar to StakingYieldsStore for StakeKit yields)
|
||||||
*
|
*
|
||||||
* Vault is ETH-specific concept for pooled staking.
|
* Vault is ETH-specific concept for pooled staking.
|
||||||
* For other blockchains, P2P may use different structures.
|
* For other blockchains, P2PEthPool may use different structures.
|
||||||
*/
|
*/
|
||||||
interface P2PEthPoolVaultsStore {
|
interface P2PEthPoolVaultsStore {
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
|
||||||
suspend fun getSync(): List<P2PEthPoolVault>
|
suspend fun getSync(): List<P2PEthPoolVault>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store vaults from P2P API
|
* Store vaults from P2PEthPool API
|
||||||
*/
|
*/
|
||||||
suspend fun store(vaults: List<P2PEthPoolVault>)
|
suspend fun store(vaults: List<P2PEthPoolVault>)
|
||||||
}
|
}
|
||||||
|
|
@ -12,7 +12,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_
|
||||||
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY
|
||||||
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
import com.tangem.lib.auth.ExpressAuthProvider
|
import com.tangem.lib.auth.ExpressAuthProvider
|
||||||
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
import com.tangem.lib.auth.P2PEthPoolAuthProvider
|
||||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||||
|
|
@ -300,7 +300,7 @@ internal class ProdApiConfigsManagerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createP2PModel(): TestModel {
|
private fun createP2PModel(): TestModel {
|
||||||
val (environment, baseUrl) = if (P2PStakingConfig.USE_TESTNET) {
|
val (environment, baseUrl) = if (P2PEthPoolStakingConfig.USE_TESTNET) {
|
||||||
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
ApiEnvironment.DEV to "https://api-test.p2p.org/"
|
||||||
} else {
|
} else {
|
||||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||||
|
|
|
||||||
|
|
@ -1,184 +0,0 @@
|
||||||
package com.tangem.core.ui.components
|
|
||||||
|
|
||||||
import androidx.annotation.FloatRange
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
|
|
||||||
private const val COEFFICIENT = 0.8f
|
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
|
||||||
@Composable
|
|
||||||
fun ResizableText(
|
|
||||||
text: String,
|
|
||||||
fontSizeRange: FontSizeRange,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
color: Color = Color.Unspecified,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
style: TextStyle = LocalTextStyle.current,
|
|
||||||
) {
|
|
||||||
val fontSizeValue = remember { mutableFloatStateOf(fontSizeRange.max.value) }
|
|
||||||
val readyToDraw = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val textState = remember { mutableStateOf(text) }
|
|
||||||
if (textState.value != text) {
|
|
||||||
readyToDraw.value = false
|
|
||||||
fontSizeValue.floatValue = fontSizeRange.max.value
|
|
||||||
textState.value = text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
|
||||||
color = color,
|
|
||||||
fontSize = fontSizeValue.floatValue.sp,
|
|
||||||
overflow = overflow,
|
|
||||||
softWrap = false,
|
|
||||||
maxLines = maxLines,
|
|
||||||
onTextLayout = { textLayoutResult ->
|
|
||||||
if (textLayoutResult.hasVisualOverflow) {
|
|
||||||
val nextFontSizeValue = fontSizeValue.floatValue - fontSizeRange.step.value
|
|
||||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
|
||||||
fontSizeValue.floatValue = fontSizeRange.min.value
|
|
||||||
readyToDraw.value = true
|
|
||||||
} else {
|
|
||||||
fontSizeValue.floatValue = nextFontSizeValue * COEFFICIENT
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readyToDraw.value = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = style,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Composable function that displays text which can be resized based on its content's overflow.
|
|
||||||
*
|
|
||||||
* This function draws text on the screen and checks if it overflows. If the text overflows,
|
|
||||||
* its font size is reduced recursively until it either fits the available space or reaches a
|
|
||||||
* specified minimum font size.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
fun ResizableText(
|
|
||||||
text: String,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
color: Color = Color.Unspecified,
|
|
||||||
textAlign: TextAlign? = null,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
softWrap: Boolean = true,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
style: TextStyle = LocalTextStyle.current,
|
|
||||||
minFontSize: TextUnit = TextUnit.Unspecified,
|
|
||||||
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
|
|
||||||
reduceFactor: Double = 0.9,
|
|
||||||
) {
|
|
||||||
var fontSize by remember { mutableStateOf(style.fontSize) }
|
|
||||||
var isReadyToDraw by remember { mutableStateOf(value = false) }
|
|
||||||
|
|
||||||
Text(
|
|
||||||
modifier = modifier
|
|
||||||
.drawWithContent {
|
|
||||||
if (isReadyToDraw) drawContent()
|
|
||||||
}
|
|
||||||
.wrapContentHeight(),
|
|
||||||
text = text,
|
|
||||||
color = color,
|
|
||||||
fontSize = fontSize,
|
|
||||||
textAlign = textAlign,
|
|
||||||
overflow = overflow,
|
|
||||||
softWrap = softWrap,
|
|
||||||
maxLines = maxLines,
|
|
||||||
style = style,
|
|
||||||
onTextLayout = { result ->
|
|
||||||
fun reduceFontSize() {
|
|
||||||
val reducedFontSize = fontSize * reduceFactor
|
|
||||||
|
|
||||||
if (minFontSize != TextUnit.Unspecified && reducedFontSize <= minFontSize) {
|
|
||||||
fontSize = minFontSize
|
|
||||||
isReadyToDraw = true
|
|
||||||
} else {
|
|
||||||
fontSize = reducedFontSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.hasVisualOverflow) {
|
|
||||||
reduceFontSize()
|
|
||||||
} else {
|
|
||||||
isReadyToDraw = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ResizableText(
|
|
||||||
text: String,
|
|
||||||
fontSizeValue: TextUnit,
|
|
||||||
fontSizeRange: FontSizeRange,
|
|
||||||
onFontSizeChange: (Float) -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
color: Color = Color.Unspecified,
|
|
||||||
overflow: TextOverflow = TextOverflow.Clip,
|
|
||||||
maxLines: Int = Int.MAX_VALUE,
|
|
||||||
style: TextStyle = LocalTextStyle.current,
|
|
||||||
) {
|
|
||||||
val readyToDraw = remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val textState = remember { mutableStateOf(text) }
|
|
||||||
if (textState.value != text) {
|
|
||||||
readyToDraw.value = false
|
|
||||||
onFontSizeChange(fontSizeRange.max.value)
|
|
||||||
textState.value = text
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
|
|
||||||
color = color,
|
|
||||||
fontSize = fontSizeValue.value.sp,
|
|
||||||
overflow = overflow,
|
|
||||||
softWrap = false,
|
|
||||||
maxLines = maxLines,
|
|
||||||
onTextLayout = { result ->
|
|
||||||
if (result.hasVisualOverflow) {
|
|
||||||
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
|
|
||||||
if (nextFontSizeValue <= fontSizeRange.min.value) {
|
|
||||||
onFontSizeChange(fontSizeRange.min.value)
|
|
||||||
readyToDraw.value = true
|
|
||||||
} else {
|
|
||||||
val newSizeValue = nextFontSizeValue * COEFFICIENT
|
|
||||||
onFontSizeChange(newSizeValue)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
readyToDraw.value = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style = style,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class FontSizeRange(
|
|
||||||
val min: TextUnit,
|
|
||||||
val max: TextUnit,
|
|
||||||
val step: TextUnit = DEFAULT_TEXT_STEP,
|
|
||||||
) {
|
|
||||||
init {
|
|
||||||
require(min < max) { "min should be less than max, $this" }
|
|
||||||
require(step.value > 0) { "step should be greater than 0, $this" }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DEFAULT_TEXT_STEP = 1.sp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,6 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview
|
||||||
fun AppBarWithBackButtonAndIcon(
|
fun AppBarWithBackButtonAndIcon(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
backButtonEnabled: Boolean = true,
|
||||||
|
endButtonEnabled: Boolean = true,
|
||||||
text: String? = null,
|
text: String? = null,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
@DrawableRes backIconRes: Int? = null,
|
@DrawableRes backIconRes: Int? = null,
|
||||||
|
|
@ -30,11 +32,13 @@ fun AppBarWithBackButtonAndIcon(
|
||||||
startButton = TopAppBarButtonUM.Icon(
|
startButton = TopAppBarButtonUM.Icon(
|
||||||
iconRes = backIconRes ?: R.drawable.ic_back_24,
|
iconRes = backIconRes ?: R.drawable.ic_back_24,
|
||||||
onClicked = onBackClick,
|
onClicked = onBackClick,
|
||||||
|
isEnabled = backButtonEnabled,
|
||||||
),
|
),
|
||||||
endButton = if (iconRes != null && onIconClick != null) {
|
endButton = if (iconRes != null && onIconClick != null) {
|
||||||
TopAppBarButtonUM.Icon(
|
TopAppBarButtonUM.Icon(
|
||||||
iconRes = iconRes,
|
iconRes = iconRes,
|
||||||
onClicked = onIconClick,
|
onClicked = onIconClick,
|
||||||
|
isEnabled = endButtonEnabled,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -146,7 +145,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
|
||||||
sheetState = SheetState(
|
sheetState = SheetState(
|
||||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||||
initialValue = Expanded,
|
initialValue = Expanded,
|
||||||
density = LocalDensity.current,
|
positionalThreshold = { 0f },
|
||||||
|
velocityThreshold = { 0f },
|
||||||
),
|
),
|
||||||
onBack = null,
|
onBack = null,
|
||||||
bsContent = {
|
bsContent = {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.vectorResource
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -141,7 +140,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
|
||||||
sheetState = SheetState(
|
sheetState = SheetState(
|
||||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||||
initialValue = Expanded,
|
initialValue = Expanded,
|
||||||
density = LocalDensity.current,
|
positionalThreshold = { 0f },
|
||||||
|
velocityThreshold = { 0f },
|
||||||
),
|
),
|
||||||
onBack = null,
|
onBack = null,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
||||||
sheetState = SheetState(
|
sheetState = SheetState(
|
||||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||||
initialValue = Expanded,
|
initialValue = Expanded,
|
||||||
density = LocalDensity.current,
|
positionalThreshold = { 0f },
|
||||||
|
velocityThreshold = { 0f },
|
||||||
),
|
),
|
||||||
onBack = null,
|
onBack = null,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.TextAutoSize
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
|
@ -23,7 +24,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.tangem.core.ui.components.ResizableText
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||||
import com.tangem.core.ui.utils.MultipleClickPreventer
|
import com.tangem.core.ui.utils.MultipleClickPreventer
|
||||||
|
|
@ -75,7 +75,7 @@ fun TangemButton(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
ResizableText(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f, fill = false)
|
.weight(1f, fill = false)
|
||||||
.heightIn(MinButtonContentSize, maxContentSize)
|
.heightIn(MinButtonContentSize, maxContentSize)
|
||||||
|
|
@ -86,7 +86,10 @@ fun TangemButton(
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
minFontSize = 12.sp,
|
autoSize = TextAutoSize.StepBased(
|
||||||
|
minFontSize = 12.sp,
|
||||||
|
maxFontSize = textStyle.fontSize,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = { iconResId ->
|
icon = { iconResId ->
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,8 @@ import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusManager
|
import androidx.compose.ui.focus.FocusManager
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
|
@ -51,16 +50,22 @@ fun SearchBar(
|
||||||
val keyboardController = LocalSoftwareKeyboardController.current
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
var isInitialComposition by rememberSaveable { mutableStateOf(true) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isInitialComposition = false
|
||||||
|
}
|
||||||
|
|
||||||
BasicTextField(
|
BasicTextField(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(min = TangemTheme.dimens.size48)
|
.heightIn(min = TangemTheme.dimens.size48)
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
if (focusState.isFocused) {
|
if (!isInitialComposition) {
|
||||||
state.onActiveChange(true)
|
if (focusState.isFocused) {
|
||||||
} else {
|
state.onActiveChange(true)
|
||||||
state.onActiveChange(false)
|
} else {
|
||||||
|
state.onActiveChange(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.focusRequester(focusRequester)
|
.focusRequester(focusRequester)
|
||||||
|
|
@ -163,6 +168,7 @@ private fun ClearButton(
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
keyboardController?.hide()
|
keyboardController?.hide()
|
||||||
state.onActiveChange(false)
|
state.onActiveChange(false)
|
||||||
|
state.onClearClick()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,5 @@ data class SearchBarUM(
|
||||||
val onQueryChange: (String) -> Unit,
|
val onQueryChange: (String) -> Unit,
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val onActiveChange: (Boolean) -> Unit,
|
val onActiveChange: (Boolean) -> Unit,
|
||||||
|
val onClearClick: () -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -31,6 +32,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
|
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
|
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
|
||||||
|
|
@ -92,7 +94,7 @@ fun InputRowEnter(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(16.dp)
|
.size(16.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape),
|
||||||
text = description.resolveReference(),
|
text = description,
|
||||||
content = { contentModifier ->
|
content = { contentModifier ->
|
||||||
Icon(
|
Icon(
|
||||||
modifier = contentModifier.size(16.dp),
|
modifier = contentModifier.size(16.dp),
|
||||||
|
|
@ -113,7 +115,8 @@ fun InputRowEnter(
|
||||||
visualTransformation = visualTransformation,
|
visualTransformation = visualTransformation,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = TangemTheme.dimens.spacing8),
|
.padding(top = TangemTheme.dimens.spacing8)
|
||||||
|
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
iconRes?.let { iconRes ->
|
iconRes?.let { iconRes ->
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
|
|
@ -21,6 +22,7 @@ import com.tangem.core.ui.components.tooltip.TangemTooltip
|
||||||
import com.tangem.core.ui.extensions.TextReference
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.resolveReference
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -112,6 +114,7 @@ fun InputRowEnterInfoAmount(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun InputRowEnterInfoAmountV2(
|
fun InputRowEnterInfoAmountV2(
|
||||||
title: TextReference,
|
title: TextReference,
|
||||||
|
|
@ -139,20 +142,23 @@ fun InputRowEnterInfoAmountV2(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp),
|
.padding(16.dp)
|
||||||
|
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM),
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = title.resolveReference(),
|
text = title.resolveReference(),
|
||||||
style = TangemTheme.typography.subtitle2,
|
style = TangemTheme.typography.subtitle2,
|
||||||
color = titleColor,
|
color = titleColor,
|
||||||
|
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE),
|
||||||
)
|
)
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
TangemTooltip(
|
TangemTooltip(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(16.dp)
|
.size(16.dp)
|
||||||
.clip(CircleShape),
|
.clip(CircleShape)
|
||||||
text = description.resolveReference(),
|
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON),
|
||||||
|
text = description,
|
||||||
content = { contentModifier ->
|
content = { contentModifier ->
|
||||||
Icon(
|
Icon(
|
||||||
modifier = contentModifier.size(16.dp),
|
modifier = contentModifier.size(16.dp),
|
||||||
|
|
@ -183,7 +189,8 @@ fun InputRowEnterInfoAmountV2(
|
||||||
backgroundColor = Color.Transparent,
|
backgroundColor = Color.Transparent,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = TangemTheme.dimens.spacing8)
|
.padding(top = TangemTheme.dimens.spacing8)
|
||||||
.weight(1f),
|
.weight(1f)
|
||||||
|
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD),
|
||||||
)
|
)
|
||||||
info?.let { info ->
|
info?.let { info ->
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -192,7 +199,8 @@ fun InputRowEnterInfoAmountV2(
|
||||||
color = infoColor,
|
color = infoColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = TangemTheme.dimens.spacing8)
|
.padding(start = TangemTheme.dimens.spacing8)
|
||||||
.align(Alignment.Bottom),
|
.align(Alignment.Bottom)
|
||||||
|
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.extensions.resourceReference
|
import com.tangem.core.ui.extensions.resourceReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
|
import com.tangem.core.ui.test.SwapSelectNetworkFeeBottomSheetTestTags
|
||||||
import com.tangem.utils.StringsSigns
|
import com.tangem.utils.StringsSigns
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -70,7 +70,7 @@ fun SelectorRowItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
.testTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -239,19 +239,19 @@ private fun StandardBottomSheet(
|
||||||
}
|
}
|
||||||
val newTarget =
|
val newTarget =
|
||||||
when (val oldTarget = state.anchoredDraggableState.targetValue) {
|
when (val oldTarget = state.anchoredDraggableState.targetValue) {
|
||||||
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
|
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
|
||||||
PartiallyExpanded ->
|
PartiallyExpanded ->
|
||||||
when {
|
when {
|
||||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||||
else -> oldTarget
|
else -> oldTarget
|
||||||
}
|
}
|
||||||
Expanded ->
|
Expanded ->
|
||||||
when {
|
when {
|
||||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||||
else -> oldTarget
|
else -> oldTarget
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,11 +102,11 @@ class TangemSheetState(
|
||||||
|
|
||||||
/** Whether the sheet has an expanded state defined. */
|
/** Whether the sheet has an expanded state defined. */
|
||||||
val hasExpandedState: Boolean
|
val hasExpandedState: Boolean
|
||||||
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
|
get() = anchoredDraggableState.anchors.hasPositionFor(Expanded)
|
||||||
|
|
||||||
/** Whether the modal bottom sheet has a partially expanded state defined. */
|
/** Whether the modal bottom sheet has a partially expanded state defined. */
|
||||||
val hasPartiallyExpandedState: Boolean
|
val hasPartiallyExpandedState: Boolean
|
||||||
get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
|
get() = anchoredDraggableState.anchors.hasPositionFor(PartiallyExpanded)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
|
||||||
|
|
@ -274,8 +274,8 @@ internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
val toFling = available.toFloat()
|
val toFling = available.toFloat()
|
||||||
val currentOffset = sheetState.requireOffset()
|
val currentOffset = sheetState.requireOffset()
|
||||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
val minPosition = sheetState.anchoredDraggableState.anchors.minPosition()
|
||||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
return if (toFling < 0 && currentOffset > minPosition) {
|
||||||
onFling(toFling)
|
onFling(toFling)
|
||||||
// since we go to the anchor with tween settling, consume all for the best UX
|
// since we go to the anchor with tween settling, consume all for the best UX
|
||||||
available
|
available
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,39 @@
|
||||||
package com.tangem.core.ui.components.tooltip
|
package com.tangem.core.ui.components.tooltip
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.R
|
import com.tangem.core.ui.R
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
import com.tangem.core.ui.extensions.clickableSingle
|
import com.tangem.core.ui.extensions.clickableSingle
|
||||||
|
import com.tangem.core.ui.extensions.resolveAnnotatedReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.LocalWindowSize
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.core.ui.res.TangemThemePreview
|
import com.tangem.core.ui.res.TangemThemePreview
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Tangem-themed tooltip component that displays a tooltip with the provided text when the content is clicked.
|
||||||
|
*
|
||||||
|
* @param text The text to be displayed inside the tooltip.
|
||||||
|
* @param modifier The modifier to be applied to the tooltip component.
|
||||||
|
* @param enabled If false, the tooltip will not be shown when the content is clicked.
|
||||||
|
* @param content The content that triggers the tooltip when clicked.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun TangemTooltip(
|
fun TangemTooltip(
|
||||||
text: String,
|
text: TextReference,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
content: @Composable (Modifier) -> Unit,
|
content: @Composable (Modifier) -> Unit,
|
||||||
|
|
@ -33,30 +43,10 @@ fun TangemTooltip(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
tooltipContent = {
|
tooltipContent = {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
modifier = Modifier
|
||||||
text = text,
|
.background(TangemTheme.colors.icon.secondary)
|
||||||
style = TangemTheme.typography.body2,
|
.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||||
color = TangemTheme.colors.text.primary2,
|
text = text.resolveAnnotatedReference(),
|
||||||
)
|
|
||||||
},
|
|
||||||
content = content,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TangemTooltip(
|
|
||||||
text: AnnotatedString,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
content: @Composable (Modifier) -> Unit,
|
|
||||||
) {
|
|
||||||
InternalTangemTooltip(
|
|
||||||
modifier = modifier,
|
|
||||||
enabled = enabled,
|
|
||||||
tooltipContent = {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
|
||||||
text = text,
|
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
color = TangemTheme.colors.text.primary2,
|
color = TangemTheme.colors.text.primary2,
|
||||||
)
|
)
|
||||||
|
|
@ -75,16 +65,22 @@ private fun InternalTangemTooltip(
|
||||||
) {
|
) {
|
||||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val windowSize = LocalWindowSize.current.width
|
||||||
|
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||||
|
positioning = TooltipAnchorPosition.Above,
|
||||||
|
spacingBetweenTooltipAndAnchor = 8.dp,
|
||||||
|
),
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip(
|
PlainTooltip(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(end = 12.dp),
|
||||||
.fillMaxWidth()
|
shape = RoundedCornerShape(14.dp),
|
||||||
.padding(horizontal = 12.dp),
|
caretShape = TooltipDefaults.caretShape(),
|
||||||
caretSize = DpSize(width = 14.dp, height = 8.dp),
|
maxWidth = windowSize - 24.dp,
|
||||||
contentColor = TangemTheme.colors.text.primary2,
|
contentColor = TangemTheme.colors.text.primary2,
|
||||||
containerColor = TangemTheme.colors.icon.secondary,
|
containerColor = TangemTheme.colors.icon.secondary,
|
||||||
content = { tooltipContent() },
|
content = { tooltipContent() },
|
||||||
|
|
@ -103,24 +99,23 @@ private fun InternalTangemTooltip(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
// region Preview
|
||||||
@Composable
|
@Composable
|
||||||
|
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
|
||||||
|
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
private fun TangemTooltip_Preview() {
|
private fun TangemTooltip_Preview() {
|
||||||
TangemThemePreview {
|
TangemThemePreview {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(500.dp)
|
.fillMaxSize()
|
||||||
.background(TangemTheme.colors.background.secondary),
|
.background(TangemTheme.colors.background.tertiary),
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
) {
|
||||||
TangemTooltip(
|
TangemTooltip(
|
||||||
modifier = Modifier
|
modifier = Modifier,
|
||||||
.background(TangemTheme.colors.background.secondary)
|
text = stringReference("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis."),
|
||||||
.size(64.dp),
|
|
||||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.",
|
|
||||||
content = { contentModifier ->
|
content = { contentModifier ->
|
||||||
Icon(
|
Icon(
|
||||||
modifier = contentModifier.size(64.dp),
|
modifier = contentModifier,
|
||||||
painter = painterResource(R.drawable.ic_token_info_24),
|
painter = painterResource(R.drawable.ic_token_info_24),
|
||||||
tint = TangemTheme.colors.icon.informative,
|
tint = TangemTheme.colors.icon.informative,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.tangem.core.ui.decompose
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface describing the UI part of a component for a modular BottomSheet.
|
||||||
|
*
|
||||||
|
* Designed for use in Decompose components. It separates the UI into a title and content,
|
||||||
|
* providing access to the [BottomSheetState] to react to changes in the sheet's state (collapsed/expanded).
|
||||||
|
*/
|
||||||
|
@Stable
|
||||||
|
interface ComposableModularBottomSheetContentComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the title of the bottom sheet.
|
||||||
|
* @param bottomSheetState The current state of the bottom sheet. This can be used, for example,
|
||||||
|
* to change navigation buttons (e.g., hiding the "Back" button when collapsed).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Title(bottomSheetState: State<BottomSheetState>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the main content of the bottom sheet.
|
||||||
|
* @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility
|
||||||
|
* (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier)
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.tangem.core.ui.res
|
package com.tangem.core.ui.res
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import androidx.compose.foundation.ComposeFoundationFlags
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||||
import androidx.compose.foundation.text.selection.TextSelectionColors
|
import androidx.compose.foundation.text.selection.TextSelectionColors
|
||||||
|
|
@ -27,6 +29,7 @@ import com.tangem.core.ui.windowsize.rememberWindowSize
|
||||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.valentinilk.shimmer.Shimmer
|
import com.valentinilk.shimmer.Shimmer
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TangemTheme(
|
fun TangemTheme(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
|
|
@ -36,6 +39,9 @@ fun TangemTheme(
|
||||||
overrideSystemBarColors: Boolean = true,
|
overrideSystemBarColors: Boolean = true,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// TODO Research and implement in redesign [REDACTED_TASK_KEY]
|
||||||
|
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = false
|
||||||
|
|
||||||
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
|
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
|
||||||
val windowSize = rememberWindowSize(activity = activity)
|
val windowSize = rememberWindowSize(activity = activity)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
package com.tangem.core.ui.test
|
|
||||||
|
|
||||||
object SelectNetworkFeeBottomSheetTestTags {
|
|
||||||
const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT"
|
|
||||||
const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM"
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package com.tangem.core.ui.test
|
||||||
|
|
||||||
|
object SendSelectNetworkFeeBottomSheetTestTags {
|
||||||
|
const val REGULAR_FEE_ITEM = "SEND_SELECT_NETWORK_FEE_REGULAR_FEE_ITEM"
|
||||||
|
const val REGULAR_ITEM_ICON = "SEND_SELECT_NETWORK_FEE_REGULAR_ITEM_ICON"
|
||||||
|
const val REGULAR_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_REGULAR_ITEM_TITLE"
|
||||||
|
const val DOT_SIGN = "SEND_SELECT_NETWORK_FEE_DOT_SIGN"
|
||||||
|
const val TOKEN_AMOUNT = "SEND_SELECT_NETWORK_FEE_TOKEN_AMOUNT"
|
||||||
|
const val FIAT_AMOUNT = "SEND_SELECT_NETWORK_FEE_FIAT_AMOUNT"
|
||||||
|
|
||||||
|
const val CUSTOM_FEE_ITEM = "SEND_SELECT_NETWORK_FEE_CUSTOM_FEE_ITEM"
|
||||||
|
const val CUSTOM_ITEM_ICON = "SEND_SELECT_NETWORK_FEE_CUSTOM_ITEM_ICON"
|
||||||
|
const val CUSTOM_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_CUSTOM_ITEM_TITLE"
|
||||||
|
|
||||||
|
const val CUSTOM_INPUT_ITEM = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM"
|
||||||
|
const val NONCE_INPUT_ITEM = "SEND_SELECT_NETWORK_FEE_NONCE_INPUT_ITEM"
|
||||||
|
const val NONCE_INPUT_TEXT_FIELD = "SEND_SELECT_NETWORK_FEE_NONCE_INPUT_TEXT_FIELD"
|
||||||
|
const val CUSTOM_INPUT_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_TITLE"
|
||||||
|
const val CUSTOM_INPUT_ITEM_TOOLTIP_ICON = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_TOOLTIP_ICON"
|
||||||
|
const val CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD"
|
||||||
|
const val CUSTOM_INPUT_ITEM_FIAT_AMOUNT = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_FIAT_AMOUNT"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package com.tangem.core.ui.test
|
||||||
|
|
||||||
|
object SwapSelectNetworkFeeBottomSheetTestTags {
|
||||||
|
const val READ_MORE_TEXT = "SWAP_SELECT_NETWORK_FEE_READ_MORE_TEXT"
|
||||||
|
const val SELECTOR_ITEM = "SWAP_SELECT_NETWORK_FEE_SELECTOR_ITEM"
|
||||||
|
}
|
||||||
18
core/ui/src/main/res/drawable/ic_show_more_news_48.xml
Normal file
18
core/ui/src/main/res/drawable/ic_show_more_news_48.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<group>
|
||||||
|
<clip-path
|
||||||
|
android:pathData="M24,0L24,0A24,24 0,0 1,48 24L48,24A24,24 0,0 1,24 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M24,0L24,0A24,24 0,0 1,48 24L48,24A24,24 0,0 1,24 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"
|
||||||
|
android:strokeAlpha="0.1"
|
||||||
|
android:fillColor="#0099FF"
|
||||||
|
android:fillAlpha="0.1"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M15.782,24.005C15.781,23.508 16.184,23.105 16.681,23.104L29.128,23.099L23.92,17.891C23.569,17.539 23.569,16.97 23.92,16.618C24.272,16.267 24.842,16.267 25.193,16.618L31.943,23.368C32.295,23.72 32.295,24.289 31.943,24.641L25.193,31.391C24.842,31.742 24.272,31.742 23.92,31.391C23.569,31.039 23.569,30.47 23.92,30.118L29.139,24.899L16.682,24.904C16.185,24.905 15.782,24.502 15.782,24.005Z"
|
||||||
|
android:fillColor="#0099FF"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
|
@ -26,10 +26,10 @@ import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P staking repository implementation
|
* P2PEthPool staking repository implementation
|
||||||
*/
|
*/
|
||||||
internal class DefaultP2PEthPoolRepository(
|
internal class DefaultP2PEthPoolRepository(
|
||||||
private val p2pApi: P2PEthPoolApi,
|
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||||
|
|
@ -44,7 +44,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||||
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
||||||
getVaults(network).getOrElse { error ->
|
getVaults(network).getOrElse { error ->
|
||||||
Timber.e("Error fetching P2P vaults: $error")
|
Timber.e("Error fetching P2PEthPool vaults: $error")
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -56,8 +56,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
|
|
||||||
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
|
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
val response = p2pApi.getVaults(network.value)
|
when (val response = p2pEthPoolApi.getVaults(network.value)) {
|
||||||
when (response) {
|
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
ensure(data.error == null) {
|
ensure(data.error == null) {
|
||||||
|
|
@ -83,7 +82,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
vaultAddress = vaultAddress,
|
vaultAddress = vaultAddress,
|
||||||
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
|
amount = amount.toDoubleOrNull() ?: raise(StakingError.InvalidAmount("Invalid amount format: $amount")),
|
||||||
)
|
)
|
||||||
val response = p2pApi.createDepositTransaction(network.value, requestBody)
|
val response = p2pEthPoolApi.createDepositTransaction(network.value, requestBody)
|
||||||
when (response) {
|
when (response) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
@ -108,7 +107,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
stakerPublicKey = stakerPublicKey,
|
stakerPublicKey = stakerPublicKey,
|
||||||
stakeTransactionHash = stakeTransactionHash,
|
stakeTransactionHash = stakeTransactionHash,
|
||||||
)
|
)
|
||||||
val response = p2pApi.createUnstakeTransaction(network.value, requestBody)
|
val response = p2pEthPoolApi.createUnstakeTransaction(network.value, requestBody)
|
||||||
when (response) {
|
when (response) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
@ -140,7 +139,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
|
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
|
||||||
val response = p2pApi.createWithdrawTransaction(network.value, requestBody)
|
val response = p2pEthPoolApi.createWithdrawTransaction(network.value, requestBody)
|
||||||
when (response) {
|
when (response) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
@ -161,7 +160,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
|
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
|
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
|
||||||
val response = p2pApi.broadcastTransaction(network.value, requestBody)
|
val response = p2pEthPoolApi.broadcastTransaction(network.value, requestBody)
|
||||||
when (response) {
|
when (response) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
@ -182,7 +181,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
vaultAddress: String,
|
vaultAddress: String,
|
||||||
): Either<StakingError, P2PEthPoolAccount> = either {
|
): Either<StakingError, P2PEthPoolAccount> = either {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
val response = p2pApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
|
val response = p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
|
||||||
when (response) {
|
when (response) {
|
||||||
is ApiResponse.Success -> {
|
is ApiResponse.Success -> {
|
||||||
val data = response.data
|
val data = response.data
|
||||||
|
|
@ -204,7 +203,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
period: Int?,
|
period: Int?,
|
||||||
): Either<StakingError, List<P2PEthPoolReward>> = either {
|
): Either<StakingError, List<P2PEthPoolReward>> = either {
|
||||||
withContext(dispatchers.io) {
|
withContext(dispatchers.io) {
|
||||||
val response = p2pApi.getRewards(
|
val response = p2pEthPoolApi.getRewards(
|
||||||
network = network.value,
|
network = network.value,
|
||||||
delegatorAddress = delegatorAddress,
|
delegatorAddress = delegatorAddress,
|
||||||
vaultAddress = vaultAddress,
|
vaultAddress = vaultAddress,
|
||||||
|
|
@ -224,6 +223,10 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
|
||||||
|
return p2pEthPoolVaultsStore.get()
|
||||||
|
}
|
||||||
|
|
||||||
override fun getStakingAvailability(): Flow<StakingAvailability> {
|
override fun getStakingAvailability(): Flow<StakingAvailability> {
|
||||||
return getVaultsFlow()
|
return getVaultsFlow()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
@ -231,7 +234,7 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
if (vaults.isEmpty()) {
|
if (vaults.isEmpty()) {
|
||||||
return@map StakingAvailability.TemporaryUnavailable
|
return@map StakingAvailability.TemporaryUnavailable
|
||||||
} else {
|
} else {
|
||||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,15 +244,11 @@ internal class DefaultP2PEthPoolRepository(
|
||||||
return if (vaults.isEmpty()) {
|
return if (vaults.isEmpty()) {
|
||||||
StakingAvailability.TemporaryUnavailable
|
StakingAvailability.TemporaryUnavailable
|
||||||
} else {
|
} else {
|
||||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
||||||
return p2pEthPoolVaultsStore.getSync()
|
return p2pEthPoolVaultsStore.getSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
|
|
||||||
return p2pEthPoolVaultsStore.get()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -364,6 +364,7 @@ internal class DefaultStakeKitRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getStakingAvailability(
|
override fun getStakingAvailability(
|
||||||
|
integrationId: StakingIntegrationID.StakeKit,
|
||||||
rawCurrencyId: CryptoCurrency.RawID,
|
rawCurrencyId: CryptoCurrency.RawID,
|
||||||
symbol: String,
|
symbol: String,
|
||||||
): Flow<StakingAvailability> {
|
): Flow<StakingAvailability> {
|
||||||
|
|
@ -381,7 +382,7 @@ internal class DefaultStakeKitRepository(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (prefetchedYield != null) {
|
if (prefetchedYield != null) {
|
||||||
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
|
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
|
||||||
} else {
|
} else {
|
||||||
StakingAvailability.TemporaryUnavailable
|
StakingAvailability.TemporaryUnavailable
|
||||||
}
|
}
|
||||||
|
|
@ -389,6 +390,7 @@ internal class DefaultStakeKitRepository(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getStakingAvailabilitySync(
|
override suspend fun getStakingAvailabilitySync(
|
||||||
|
integrationId: StakingIntegrationID.StakeKit,
|
||||||
rawCurrencyId: CryptoCurrency.RawID,
|
rawCurrencyId: CryptoCurrency.RawID,
|
||||||
symbol: String,
|
symbol: String,
|
||||||
): StakingAvailability {
|
): StakingAvailability {
|
||||||
|
|
@ -404,7 +406,7 @@ internal class DefaultStakeKitRepository(
|
||||||
)
|
)
|
||||||
|
|
||||||
return if (prefetchedYield != null) {
|
return if (prefetchedYield != null) {
|
||||||
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
|
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
|
||||||
} else {
|
} else {
|
||||||
StakingAvailability.TemporaryUnavailable
|
StakingAvailability.TemporaryUnavailable
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,12 @@ internal class DefaultStakingErrorResolver(
|
||||||
is StakingError.DomainError -> {
|
is StakingError.DomainError -> {
|
||||||
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
|
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
|
||||||
}
|
}
|
||||||
// P2P errors
|
// P2PEthPool errors
|
||||||
is StakingError.InvalidAmount,
|
is StakingError.InvalidAmount,
|
||||||
is StakingError.DataError,
|
is StakingError.DataError,
|
||||||
is StakingError.UnknownError,
|
is StakingError.UnknownError,
|
||||||
-> {
|
-> {
|
||||||
// P2P errors - no specific analytics event yet
|
// P2PEthPool errors - no specific analytics event yet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,9 @@ internal class DefaultStakingRepository(
|
||||||
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id)
|
||||||
|
|
||||||
val availabilityFlow = when (stakingIntegration) {
|
val availabilityFlow = when (stakingIntegration) {
|
||||||
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability()
|
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
|
||||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
|
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
|
||||||
|
stakingIntegration,
|
||||||
rawCurrencyId,
|
rawCurrencyId,
|
||||||
cryptoCurrency.symbol,
|
cryptoCurrency.symbol,
|
||||||
)
|
)
|
||||||
|
|
@ -94,8 +95,9 @@ internal class DefaultStakingRepository(
|
||||||
?: return StakingAvailability.Unavailable
|
?: return StakingAvailability.Unavailable
|
||||||
|
|
||||||
return when (stakingIntegration) {
|
return when (stakingIntegration) {
|
||||||
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync()
|
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailabilitySync()
|
||||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
|
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
|
||||||
|
stakingIntegration,
|
||||||
rawCurrencyId,
|
rawCurrencyId,
|
||||||
cryptoCurrency.symbol,
|
cryptoCurrency.symbol,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
|
||||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
|
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO
|
||||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
|
import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO
|
||||||
import com.tangem.datasource.local.token.converter.YieldTokenConverter
|
import com.tangem.datasource.local.token.converter.YieldTokenConverter
|
||||||
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
import com.tangem.domain.staking.model.stakekit.AddressArgument
|
||||||
import com.tangem.domain.staking.model.stakekit.Yield
|
import com.tangem.domain.staking.model.stakekit.Yield
|
||||||
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
|
import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule
|
||||||
|
|
@ -124,7 +126,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.Validator {
|
private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): Yield.Validator {
|
||||||
val address = validatorDTO.address.asMandatory("address")
|
val address = validatorDTO.address.asMandatory("address")
|
||||||
|
|
||||||
return Yield.Validator(
|
return Yield.Validator(
|
||||||
|
|
@ -142,7 +144,7 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: Yield.RewardType): Yield.RewardInfo? {
|
private fun createRewardInfo(validatorDTO: YieldDTO.ValidatorDTO, rewardType: RewardType): RewardInfo? {
|
||||||
val aprOrApy = validatorDTO.apr
|
val aprOrApy = validatorDTO.apr
|
||||||
val commission = validatorDTO.commission
|
val commission = validatorDTO.commission
|
||||||
// gross = net / (1 - commission)
|
// gross = net / (1 - commission)
|
||||||
|
|
@ -162,17 +164,17 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
||||||
} else {
|
} else {
|
||||||
netApy
|
netApy
|
||||||
}
|
}
|
||||||
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
|
grossAprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
aprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
|
aprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType {
|
private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): RewardType {
|
||||||
return when (rewardTypeDTO) {
|
return when (rewardTypeDTO) {
|
||||||
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
|
YieldDTO.RewardTypeDTO.APY -> RewardType.APY
|
||||||
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR
|
YieldDTO.RewardTypeDTO.APR -> RewardType.APR
|
||||||
else -> Yield.RewardType.UNKNOWN
|
else -> RewardType.UNKNOWN
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
|
||||||
import org.joda.time.Instant
|
import org.joda.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Account Info Response to Domain model
|
* Converter from P2PEthPool Account Info Response to Domain model
|
||||||
*/
|
*/
|
||||||
internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {
|
internal object P2PEthPoolAccountConverter : Converter<P2PEthPoolAccountResponse, P2PEthPoolAccount> {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.tangem.utils.converter.Converter
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Broadcast Transaction Response to Domain model
|
* Converter from P2PEthPool Broadcast Transaction Response to Domain model
|
||||||
*/
|
*/
|
||||||
internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadcastResponse, P2PEthPoolBroadcastResult> {
|
internal object P2PEthPoolBroadcastResultConverter : Converter<P2PEthPoolBroadcastResponse, P2PEthPoolBroadcastResult> {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Error Response to Domain StakingError
|
* Converter from P2PEthPool Error Response to Domain StakingError
|
||||||
*/
|
*/
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {
|
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Reward Entry DTO to Domain model
|
* Converter from P2PEthPool Reward Entry DTO to Domain model
|
||||||
*/
|
*/
|
||||||
internal object P2PEthPoolRewardConverter : Converter<P2PEthPoolRewardDTO, P2PEthPoolReward> {
|
internal object P2PEthPoolRewardConverter : Converter<P2PEthPoolRewardDTO, P2PEthPoolReward> {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,18 @@ import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.staking.*
|
import com.tangem.domain.models.staking.*
|
||||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */
|
/** Converts P2PEthPool API response to [StakingBalance] */
|
||||||
internal object P2PStakingBalanceConverter {
|
internal object P2PEthPoolStakingBalanceConverter {
|
||||||
|
|
||||||
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P {
|
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance {
|
||||||
val stakingId = StakingID(
|
val stakingId = StakingID(
|
||||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
integrationId = StakingIntegrationID.P2PEthPool.value,
|
||||||
address = response.delegatorAddress,
|
address = response.delegatorAddress,
|
||||||
)
|
)
|
||||||
|
|
||||||
val account = P2PStakingAccount(
|
val account = P2PEthPoolStakingAccount(
|
||||||
delegatorAddress = response.delegatorAddress,
|
delegatorAddress = response.delegatorAddress,
|
||||||
vaultAddress = response.vaultAddress,
|
vaultAddress = response.vaultAddress,
|
||||||
stake = convertStake(response.stake),
|
stake = convertStake(response.stake),
|
||||||
|
|
@ -27,29 +28,40 @@ internal object P2PStakingBalanceConverter {
|
||||||
exitQueue = convertExitQueue(response.exitQueue),
|
exitQueue = convertExitQueue(response.exitQueue),
|
||||||
)
|
)
|
||||||
|
|
||||||
return StakingBalance.Data.P2P(
|
val hasActivePosition = account.stake.assets > BigDecimal.ZERO ||
|
||||||
stakingId = stakingId,
|
account.exitQueue.total > BigDecimal.ZERO ||
|
||||||
source = source,
|
account.availableToWithdraw > BigDecimal.ZERO
|
||||||
account = account,
|
|
||||||
)
|
return if (hasActivePosition) {
|
||||||
|
StakingBalance.Data.P2PEthPool(
|
||||||
|
stakingId = stakingId,
|
||||||
|
source = source,
|
||||||
|
account = account,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
StakingBalance.Empty(
|
||||||
|
stakingId = stakingId,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PStake {
|
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
|
||||||
return P2PStake(
|
return P2PEthPoolStake(
|
||||||
assets = dto.assets,
|
assets = dto.assets,
|
||||||
totalEarnedAssets = dto.totalEarnedAssets,
|
totalEarnedAssets = dto.totalEarnedAssets,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue {
|
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
|
||||||
return P2PExitQueue(
|
return P2PEthPoolExitQueue(
|
||||||
total = dto.total.toBigDecimal(),
|
total = dto.total.toBigDecimal(),
|
||||||
requests = dto.requests.map(::convertExitRequest),
|
requests = dto.requests.map(::convertExitRequest),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest {
|
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
|
||||||
return P2PExitRequest(
|
return P2PEthPoolExitRequest(
|
||||||
ticket = dto.ticket,
|
ticket = dto.ticket,
|
||||||
totalAssets = dto.totalAssets.toBigDecimal(),
|
totalAssets = dto.totalAssets.toBigDecimal(),
|
||||||
timestamp = Instant.fromEpochSeconds(dto.timestamp),
|
timestamp = Instant.fromEpochSeconds(dto.timestamp),
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.utils.converter.Converter
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Unsigned Transaction DTO to Domain model
|
* Converter from P2PEthPool Unsigned Transaction DTO to Domain model
|
||||||
*/
|
*/
|
||||||
internal object P2PEthPoolUnsignedTxConverter : Converter<P2PEthPoolUnsignedTxDTO, P2PEthPoolUnsignedTx> {
|
internal object P2PEthPoolUnsignedTxConverter : Converter<P2PEthPoolUnsignedTxDTO, P2PEthPoolUnsignedTx> {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,26 @@ package com.tangem.data.staking.converters.ethpool
|
||||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultDTO
|
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultDTO
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converter from P2P Vault DTO to Domain model
|
* Converter from P2PEthPool Vault DTO to Domain model
|
||||||
*/
|
*/
|
||||||
internal object P2PEthPoolVaultConverter : Converter<P2PEthPoolVaultDTO, P2PEthPoolVault> {
|
internal object P2PEthPoolVaultConverter : Converter<P2PEthPoolVaultDTO, P2PEthPoolVault> {
|
||||||
|
|
||||||
|
private val HUNDRED = BigDecimal(100)
|
||||||
|
private const val DIVIDE_SCALE = 8
|
||||||
|
|
||||||
override fun convert(value: P2PEthPoolVaultDTO): P2PEthPoolVault {
|
override fun convert(value: P2PEthPoolVaultDTO): P2PEthPoolVault {
|
||||||
return P2PEthPoolVault(
|
return P2PEthPoolVault(
|
||||||
vaultAddress = value.vaultAddress,
|
vaultAddress = value.vaultAddress,
|
||||||
displayName = value.displayName,
|
displayName = value.displayName,
|
||||||
apy = value.apy.toBigDecimal(),
|
apy = value.apy.divide(HUNDRED, DIVIDE_SCALE, RoundingMode.HALF_UP),
|
||||||
baseApy = value.baseApy.toBigDecimal(),
|
baseApy = value.baseApy.divide(HUNDRED, DIVIDE_SCALE, RoundingMode.HALF_UP),
|
||||||
capacity = value.capacity.toBigDecimal(),
|
capacity = value.capacity,
|
||||||
totalAssets = value.totalAssets.toBigDecimal(),
|
totalAssets = value.totalAssets,
|
||||||
feePercent = value.feePercent.toBigDecimal(),
|
feePercent = value.feePercent,
|
||||||
isPrivate = value.isPrivate,
|
isPrivate = value.isPrivate,
|
||||||
isGenesis = value.isGenesis,
|
isGenesis = value.isGenesis,
|
||||||
isSmoothingPool = value.isSmoothingPool,
|
isSmoothingPool = value.isSmoothingPool,
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
package com.tangem.data.staking.converters.ethpool
|
|
||||||
|
|
||||||
import com.tangem.domain.models.StatusSource
|
|
||||||
import com.tangem.domain.models.staking.*
|
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolAccount
|
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
|
||||||
import java.math.BigDecimal
|
|
||||||
|
|
||||||
/**
|
|
||||||
* tmp solution before facade implementation
|
|
||||||
*/
|
|
||||||
internal object P2PYieldBalanceConverter {
|
|
||||||
|
|
||||||
private const val ETH_DECIMALS = 18
|
|
||||||
private const val ETH_SYMBOL = "ETH"
|
|
||||||
private const val ETH_NAME = "Ethereum"
|
|
||||||
private const val ETH_COINGECKO_ID = "ethereum"
|
|
||||||
|
|
||||||
fun convert(
|
|
||||||
account: P2PEthPoolAccount,
|
|
||||||
vault: P2PEthPoolVault,
|
|
||||||
address: String,
|
|
||||||
source: StatusSource,
|
|
||||||
): YieldBalance {
|
|
||||||
val integrationId = "p2p-ethereum-pooled"
|
|
||||||
val stakingId = StakingID(
|
|
||||||
integrationId = integrationId,
|
|
||||||
address = address,
|
|
||||||
)
|
|
||||||
|
|
||||||
val balanceItems = buildBalanceItems(account, vault)
|
|
||||||
|
|
||||||
return if (balanceItems.isEmpty()) {
|
|
||||||
YieldBalance.Empty(stakingId = stakingId, source = source)
|
|
||||||
} else {
|
|
||||||
YieldBalance.Data(
|
|
||||||
stakingId = stakingId,
|
|
||||||
source = source,
|
|
||||||
balance = YieldBalanceItem(
|
|
||||||
items = balanceItems,
|
|
||||||
integrationId = integrationId,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildBalanceItems(account: P2PEthPoolAccount, vault: P2PEthPoolVault): List<BalanceItem> = buildList {
|
|
||||||
if (account.stake.assets > BigDecimal.ZERO) {
|
|
||||||
add(
|
|
||||||
createBalanceItem(
|
|
||||||
groupId = "p2p-staked",
|
|
||||||
amount = account.stake.assets,
|
|
||||||
type = BalanceType.STAKED,
|
|
||||||
validatorAddress = vault.vaultAddress,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createBalanceItem(
|
|
||||||
groupId: String,
|
|
||||||
amount: BigDecimal,
|
|
||||||
type: BalanceType,
|
|
||||||
validatorAddress: String,
|
|
||||||
): BalanceItem {
|
|
||||||
return BalanceItem(
|
|
||||||
groupId = groupId,
|
|
||||||
token = createEthToken(),
|
|
||||||
type = type,
|
|
||||||
amount = amount,
|
|
||||||
rawCurrencyId = ETH_COINGECKO_ID,
|
|
||||||
validatorAddress = validatorAddress,
|
|
||||||
date = null,
|
|
||||||
pendingActions = emptyList(),
|
|
||||||
pendingActionsConstraints = emptyList(),
|
|
||||||
isPending = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createEthToken(): YieldToken {
|
|
||||||
return YieldToken(
|
|
||||||
name = ETH_NAME,
|
|
||||||
network = NetworkType.ETHEREUM,
|
|
||||||
symbol = ETH_SYMBOL,
|
|
||||||
decimals = ETH_DECIMALS,
|
|
||||||
address = null,
|
|
||||||
coinGeckoId = ETH_COINGECKO_ID,
|
|
||||||
logoURI = null,
|
|
||||||
isPoints = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package com.tangem.data.staking.di
|
package com.tangem.data.staking.di
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import com.tangem.data.staking.store.DefaultP2PBalancesStore
|
import com.tangem.data.staking.store.DefaultP2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.DefaultStakingBalancesStore
|
import com.tangem.data.staking.store.DefaultStakingBalancesStore
|
||||||
import com.tangem.data.staking.store.P2PBalancesStore
|
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.StakingBalancesStore
|
import com.tangem.data.staking.store.StakingBalancesStore
|
||||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||||
|
|
@ -38,11 +38,11 @@ internal object StakingBalanceSupplierModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideP2PBalancesStore(
|
fun provideP2PEthPoolBalancesStore(
|
||||||
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
|
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): P2PBalancesStore {
|
): P2PEthPoolBalancesStore {
|
||||||
return DefaultP2PBalancesStore(
|
return DefaultP2PEthPoolBalancesStore(
|
||||||
runtimeStore = RuntimeSharedStore(),
|
runtimeStore = RuntimeSharedStore(),
|
||||||
persistenceStore = persistenceStore,
|
persistenceStore = persistenceStore,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
|
|
|
||||||
|
|
@ -75,13 +75,13 @@ internal object StakingDataModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideP2PEthPoolRepository(
|
fun provideP2PEthPoolRepository(
|
||||||
p2pApi: P2PEthPoolApi,
|
p2pEthPoolApi: P2PEthPoolApi,
|
||||||
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
stakingFeatureToggles: StakingFeatureToggles,
|
stakingFeatureToggles: StakingFeatureToggles,
|
||||||
): P2PEthPoolRepository {
|
): P2PEthPoolRepository {
|
||||||
return DefaultP2PEthPoolRepository(
|
return DefaultP2PEthPoolRepository(
|
||||||
p2pApi = p2pApi,
|
p2pEthPoolApi = p2pEthPoolApi,
|
||||||
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
stakingFeatureToggles = stakingFeatureToggles,
|
stakingFeatureToggles = stakingFeatureToggles,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import arrow.core.left
|
||||||
import arrow.core.right
|
import arrow.core.right
|
||||||
import arrow.core.toOption
|
import arrow.core.toOption
|
||||||
import com.tangem.data.common.api.safeApiCall
|
import com.tangem.data.common.api.safeApiCall
|
||||||
import com.tangem.data.staking.store.P2PBalancesStore
|
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.StakingBalancesStore
|
import com.tangem.data.staking.store.StakingBalancesStore
|
||||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
|
@ -24,7 +24,8 @@ import com.tangem.domain.models.wallet.UserWallet
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import com.tangem.domain.models.wallet.isMultiCurrency
|
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||||
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||||
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
import com.tangem.utils.coroutines.runSuspendCatching
|
import com.tangem.utils.coroutines.runSuspendCatching
|
||||||
|
|
@ -38,16 +39,16 @@ import javax.inject.Inject
|
||||||
/**
|
/**
|
||||||
* Default implementation of [MultiStakingBalanceFetcher]
|
* Default implementation of [MultiStakingBalanceFetcher]
|
||||||
*
|
*
|
||||||
* Supports both StakeKit and P2P staking providers.
|
* Supports both StakeKit and P2PEthPool staking providers.
|
||||||
*
|
*
|
||||||
* @property userWalletsStore user wallets store
|
* @property userWalletsStore user wallets store
|
||||||
* @property stakingYieldsStore staking yields store
|
* @property stakingYieldsStore staking yields store
|
||||||
* @property stakingBalancesStore staking balances store (StakeKit)
|
* @property stakingBalancesStore staking balances store (StakeKit)
|
||||||
* @property p2pBalancesStore P2P balances store
|
* @property p2PEthPoolBalancesStore P2PEthPool balances store
|
||||||
* @property stakeKitApi stake kit API
|
* @property stakeKitApi stake kit API
|
||||||
* @property p2pApi P2P ETH Pool API
|
* @property p2pEthPoolApi P2PEthPool API
|
||||||
* @property p2pVaultsStore P2P vaults store
|
* @property p2pEthPoolVaultsStore P2PEthPool vaults store
|
||||||
* @property dispatchers dispatchers
|
* @property dispatchers dispatchers
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
|
@ -56,10 +57,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
private val userWalletsStore: UserWalletsStore,
|
private val userWalletsStore: UserWalletsStore,
|
||||||
private val stakingYieldsStore: StakingYieldsStore,
|
private val stakingYieldsStore: StakingYieldsStore,
|
||||||
private val stakingBalancesStore: StakingBalancesStore,
|
private val stakingBalancesStore: StakingBalancesStore,
|
||||||
private val p2pBalancesStore: P2PBalancesStore,
|
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
|
||||||
private val stakeKitApi: StakeKitApi,
|
private val stakeKitApi: StakeKitApi,
|
||||||
private val p2pApi: P2PEthPoolApi,
|
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||||
private val p2pVaultsStore: P2PEthPoolVaultsStore,
|
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : MultiStakingBalanceFetcher {
|
) : MultiStakingBalanceFetcher {
|
||||||
|
|
||||||
|
|
@ -75,7 +76,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
return it.left()
|
return it.left()
|
||||||
}
|
}
|
||||||
|
|
||||||
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId ->
|
val (stakeKitIds, p2pEthPoolIds) = stakingIds.partition { stakingId ->
|
||||||
val stakingIntegrationID = StakingIntegrationID.entries.find {
|
val stakingIntegrationID = StakingIntegrationID.entries.find {
|
||||||
it.value == stakingId.integrationId
|
it.value == stakingId.integrationId
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +87,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
"""
|
"""
|
||||||
Staking IDs to fetch:
|
Staking IDs to fetch:
|
||||||
- StakeKit: ${stakeKitIds.joinToString()}
|
- StakeKit: ${stakeKitIds.joinToString()}
|
||||||
- P2P: ${p2pIds.joinToString()}
|
- P2PEthPool: ${p2pEthPoolIds.joinToString()}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -96,8 +97,8 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
|
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p2pIds.isNotEmpty()) {
|
if (p2pEthPoolIds.isNotEmpty()) {
|
||||||
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) }
|
launch { fetchP2PBalances(params.userWalletId, p2pEthPoolIds.toSet()) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,8 +111,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
stakingIds = stakeKitIds.toSet(),
|
stakingIds = stakeKitIds.toSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (p2pIds.isNotEmpty()) {
|
if (p2pEthPoolIds.isNotEmpty()) {
|
||||||
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet())
|
p2PEthPoolBalancesStore.storeError(
|
||||||
|
userWalletId = params.userWalletId,
|
||||||
|
stakingIds = p2pEthPoolIds.toSet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -128,12 +132,12 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
private suspend fun fetchP2PBalances(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||||
p2pBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
|
p2PEthPoolBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||||
|
|
||||||
val vaults = runSuspendCatching { p2pVaultsStore.getSync() }.getOrNull().orEmpty()
|
val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty()
|
||||||
if (vaults.isEmpty()) {
|
if (vaults.isEmpty()) {
|
||||||
Timber.w("No P2P vaults available for $userWalletId")
|
Timber.w("No P2PEthPool vaults available for $userWalletId, storing empty balances")
|
||||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
p2PEthPoolBalancesStore.storeEmpty(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,59 +147,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
private suspend fun fetchFromP2P(
|
private suspend fun fetchFromP2P(
|
||||||
userWalletId: UserWalletId,
|
userWalletId: UserWalletId,
|
||||||
stakingIds: Set<StakingID>,
|
stakingIds: Set<StakingID>,
|
||||||
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>,
|
vaults: List<P2PEthPoolVault>,
|
||||||
) {
|
) {
|
||||||
safeApiCall(
|
safeApiCall(
|
||||||
call = {
|
call = {
|
||||||
val addresses = stakingIds.map { it.address }.toSet()
|
val addresses = stakingIds.map { it.address }.toSet()
|
||||||
|
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
|
||||||
|
|
||||||
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
|
Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
|
||||||
|
|
||||||
for (vault in vaults) {
|
|
||||||
for (address in addresses) {
|
|
||||||
runSuspendCatching {
|
|
||||||
val response = p2pApi.getAccountInfo(
|
|
||||||
network = P2PStakingConfig.activeNetwork.value,
|
|
||||||
delegatorAddress = address,
|
|
||||||
vaultAddress = vault.vaultAddress,
|
|
||||||
)
|
|
||||||
|
|
||||||
when (response) {
|
|
||||||
is ApiResponse.Success -> {
|
|
||||||
val data = response.data
|
|
||||||
if (data.error != null) {
|
|
||||||
Timber.w(
|
|
||||||
"P2P API returned error for vault ${vault.vaultAddress}, " +
|
|
||||||
"address $address: ${data.error ?: "error"}",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val result = requireNotNull(data.result) {
|
|
||||||
"Result is null in successful response"
|
|
||||||
}
|
|
||||||
responses.add(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is ApiResponse.Error -> {
|
|
||||||
Timber.w(
|
|
||||||
response.cause,
|
|
||||||
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, " +
|
|
||||||
"address $address",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onFailure { error ->
|
|
||||||
Timber.w(
|
|
||||||
error,
|
|
||||||
"Failed to fetch P2P balance for vault ${vault.vaultAddress}, address $address",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.i("Successfully fetched ${responses.size} P2P balances for $userWalletId")
|
|
||||||
|
|
||||||
if (responses.isNotEmpty()) {
|
if (responses.isNotEmpty()) {
|
||||||
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
||||||
|
|
||||||
val missingStakingIds = stakingIds.filter { stakingId ->
|
val missingStakingIds = stakingIds.filter { stakingId ->
|
||||||
responses.none { response ->
|
responses.none { response ->
|
||||||
|
|
@ -205,23 +167,76 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
||||||
|
|
||||||
if (missingStakingIds.isNotEmpty()) {
|
if (missingStakingIds.isNotEmpty()) {
|
||||||
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
|
Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds")
|
||||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = missingStakingIds.toSet())
|
p2PEthPoolBalancesStore.storeError(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
stakingIds = missingStakingIds.toSet(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.i("No P2P responses received for $userWalletId")
|
Timber.i("No P2PEthPool responses received for $userWalletId")
|
||||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError = { throwable ->
|
onError = { throwable ->
|
||||||
Timber.e(throwable, "Unable to fetch P2P balances $userWalletId")
|
Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId")
|
||||||
|
|
||||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||||
|
|
||||||
throw throwable
|
throw throwable
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchP2PAccountResponses(
|
||||||
|
vaults: List<P2PEthPoolVault>,
|
||||||
|
addresses: Set<String>,
|
||||||
|
): Set<P2PEthPoolAccountResponse> {
|
||||||
|
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
|
||||||
|
|
||||||
|
for (vault in vaults) {
|
||||||
|
for (address in addresses) {
|
||||||
|
runSuspendCatching {
|
||||||
|
val response = p2pEthPoolApi.getAccountInfo(
|
||||||
|
network = P2PEthPoolStakingConfig.activeNetwork.value,
|
||||||
|
delegatorAddress = address,
|
||||||
|
vaultAddress = vault.vaultAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
when (response) {
|
||||||
|
is ApiResponse.Success -> {
|
||||||
|
val data = response.data
|
||||||
|
if (data.error != null) {
|
||||||
|
Timber.w(
|
||||||
|
"P2PEthPool API returned error for vault ${vault.vaultAddress}, " +
|
||||||
|
"address $address: ${data.error ?: "error"}",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val result = requireNotNull(data.result) {
|
||||||
|
"Result is null in successful response"
|
||||||
|
}
|
||||||
|
responses.add(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ApiResponse.Error -> {
|
||||||
|
Timber.w(
|
||||||
|
response.cause,
|
||||||
|
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " +
|
||||||
|
"address $address",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure { error ->
|
||||||
|
Timber.w(
|
||||||
|
error,
|
||||||
|
"Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
|
private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) {
|
||||||
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.staking.multi
|
||||||
|
|
||||||
import arrow.core.Option
|
import arrow.core.Option
|
||||||
import arrow.core.some
|
import arrow.core.some
|
||||||
import com.tangem.data.staking.store.P2PBalancesStore
|
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.StakingBalancesStore
|
import com.tangem.data.staking.store.StakingBalancesStore
|
||||||
import com.tangem.domain.models.staking.StakingBalance
|
import com.tangem.domain.models.staking.StakingBalance
|
||||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||||
|
|
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEmpty
|
||||||
/**
|
/**
|
||||||
* Default implementation of [MultiStakingBalanceProducer]
|
* Default implementation of [MultiStakingBalanceProducer]
|
||||||
*
|
*
|
||||||
* Combines staking balances from both StakeKit and P2P providers.
|
* Combines staking balances from both StakeKit and P2PEthPool providers.
|
||||||
*
|
*
|
||||||
* @property params params
|
* @property params params
|
||||||
* @property stakingBalancesStore StakeKit staking balances store
|
* @property stakingBalancesStore StakeKit staking balances store
|
||||||
* @property p2pBalancesStore P2P balances store
|
* @property p2PEthPoolBalancesStore P2PEthPool balances store
|
||||||
* @property dispatchers dispatchers
|
* @property dispatchers dispatchers
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.onEmpty
|
||||||
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
||||||
@Assisted val params: MultiStakingBalanceProducer.Params,
|
@Assisted val params: MultiStakingBalanceProducer.Params,
|
||||||
private val stakingBalancesStore: StakingBalancesStore,
|
private val stakingBalancesStore: StakingBalancesStore,
|
||||||
private val p2pBalancesStore: P2PBalancesStore,
|
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
|
||||||
private val dispatchers: CoroutineDispatcherProvider,
|
private val dispatchers: CoroutineDispatcherProvider,
|
||||||
) : MultiStakingBalanceProducer {
|
) : MultiStakingBalanceProducer {
|
||||||
|
|
||||||
|
|
@ -39,10 +39,10 @@ internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
||||||
|
|
||||||
override fun produce(): Flow<Set<StakingBalance>> {
|
override fun produce(): Flow<Set<StakingBalance>> {
|
||||||
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId)
|
val stakeKitFlow = stakingBalancesStore.get(userWalletId = params.userWalletId)
|
||||||
val p2pFlow = p2pBalancesStore.get(userWalletId = params.userWalletId)
|
val p2pEthPoolFlow = p2PEthPoolBalancesStore.get(userWalletId = params.userWalletId)
|
||||||
|
|
||||||
return combine(stakeKitFlow, p2pFlow) { stakeKitBalances, p2pBalances ->
|
return combine(stakeKitFlow, p2pEthPoolFlow) { stakeKitBalances, p2pEthPoolBalances ->
|
||||||
stakeKitBalances + p2pBalances
|
stakeKitBalances + p2pEthPoolBalances
|
||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEmpty { emit(value = hashSetOf()) }
|
.onEmpty { emit(value = hashSetOf()) }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.tangem.data.staking.store
|
package com.tangem.data.staking.store
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
|
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
|
||||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||||
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
import com.tangem.datasource.local.datastore.RuntimeSharedStore
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
|
|
@ -20,22 +20,22 @@ import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
internal typealias WalletIdWithP2PStakingBalances = Map<UserWalletId, Set<StakingBalance>>
|
internal typealias WalletIdWithP2PStakingBalances = Map<UserWalletId, Set<StakingBalance>>
|
||||||
internal typealias WalletIdWithP2PResponses = Map<String, Set<P2PEthPoolAccountResponse>>
|
internal typealias WalletIdWithP2PEthPoolResponses = Map<String, Set<P2PEthPoolAccountResponse>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of [P2PBalancesStore]
|
* Default implementation of [P2PEthPoolBalancesStore]
|
||||||
*
|
*
|
||||||
* Stores P2P ETH Pool staking balances.
|
* Stores P2PEthPool staking balances.
|
||||||
*
|
*
|
||||||
* @property runtimeStore runtime store
|
* @property runtimeStore runtime store
|
||||||
* @property persistenceStore persistence store
|
* @property persistenceStore persistence store
|
||||||
* @param dispatchers coroutine dispatchers
|
* @param dispatchers coroutine dispatchers
|
||||||
*/
|
*/
|
||||||
internal class DefaultP2PBalancesStore(
|
internal class DefaultP2PEthPoolBalancesStore(
|
||||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
|
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
|
||||||
private val persistenceStore: DataStore<WalletIdWithP2PResponses>,
|
private val persistenceStore: DataStore<WalletIdWithP2PEthPoolResponses>,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
) : P2PBalancesStore {
|
) : P2PEthPoolBalancesStore {
|
||||||
|
|
||||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ internal class DefaultP2PBalancesStore(
|
||||||
value = cachedData.map { (stringWalletId, responses) ->
|
value = cachedData.map { (stringWalletId, responses) ->
|
||||||
val key = UserWalletId(stringWalletId)
|
val key = UserWalletId(stringWalletId)
|
||||||
val value = responses.map { response ->
|
val value = responses.map { response ->
|
||||||
P2PStakingBalanceConverter.convert(
|
P2PEthPoolStakingBalanceConverter.convert(
|
||||||
response = response,
|
response = response,
|
||||||
source = StatusSource.CACHE,
|
source = StatusSource.CACHE,
|
||||||
)
|
)
|
||||||
|
|
@ -90,6 +90,15 @@ internal class DefaultP2PBalancesStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun storeEmpty(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||||
|
updateInRuntime(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
stakingIds = stakingIds,
|
||||||
|
ifNotFound = ::createEmptyStakingBalance,
|
||||||
|
update = { it.copySealed(source = StatusSource.ACTUAL) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>) {
|
||||||
updateInRuntime(
|
updateInRuntime(
|
||||||
userWalletId = userWalletId,
|
userWalletId = userWalletId,
|
||||||
|
|
@ -108,7 +117,7 @@ internal class DefaultP2PBalancesStore(
|
||||||
|
|
||||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
||||||
val newBalances = values.map { response ->
|
val newBalances = values.map { response ->
|
||||||
P2PStakingBalanceConverter.convert(
|
P2PEthPoolStakingBalanceConverter.convert(
|
||||||
response = response,
|
response = response,
|
||||||
source = StatusSource.ACTUAL,
|
source = StatusSource.ACTUAL,
|
||||||
)
|
)
|
||||||
|
|
@ -152,7 +161,7 @@ internal class DefaultP2PBalancesStore(
|
||||||
current.toMutableMap().apply {
|
current.toMutableMap().apply {
|
||||||
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
|
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
|
||||||
.filterNot { response ->
|
.filterNot { response ->
|
||||||
StakingIntegrationID.P2P.EthereumPooled.value in integrationIds
|
StakingIntegrationID.P2PEthPool.value in integrationIds
|
||||||
}
|
}
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|
@ -187,5 +196,8 @@ internal class DefaultP2PBalancesStore(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createEmptyStakingBalance(id: StakingID): StakingBalance =
|
||||||
|
StakingBalance.Empty(stakingId = id, source = StatusSource.ACTUAL)
|
||||||
|
|
||||||
private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id)
|
private fun createErrorStakingBalance(id: StakingID): StakingBalance = StakingBalance.Error(stakingId = id)
|
||||||
}
|
}
|
||||||
|
|
@ -7,9 +7,9 @@ import com.tangem.domain.models.wallet.UserWalletId
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store for P2P ETH Pool staking balances
|
* Store for P2PEthPool staking balances
|
||||||
*/
|
*/
|
||||||
interface P2PBalancesStore {
|
interface P2PEthPoolBalancesStore {
|
||||||
|
|
||||||
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>
|
fun get(userWalletId: UserWalletId): Flow<Set<StakingBalance>>
|
||||||
|
|
||||||
|
|
@ -23,6 +23,8 @@ interface P2PBalancesStore {
|
||||||
|
|
||||||
suspend fun storeActual(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>)
|
suspend fun storeActual(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>)
|
||||||
|
|
||||||
|
suspend fun storeEmpty(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||||
|
|
||||||
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||||
|
|
||||||
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
suspend fun clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.tangem.data.staking
|
package com.tangem.data.staking
|
||||||
|
|
||||||
import com.tangem.data.staking.converters.ethpool.P2PStakingBalanceConverter
|
import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingBalanceConverter
|
||||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||||
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
|
import com.tangem.datasource.local.token.converter.StakingBalanceConverter
|
||||||
|
|
@ -11,8 +11,10 @@ internal fun YieldBalanceWrapperDTO.toDomain(source: StatusSource = StatusSource
|
||||||
return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
|
return StakingBalanceConverter(isCached = source == StatusSource.CACHE).convert(this)!!
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P {
|
internal fun P2PEthPoolAccountResponse.toDomain(
|
||||||
return P2PStakingBalanceConverter.convert(
|
source: StatusSource = StatusSource.CACHE,
|
||||||
|
): StakingBalance {
|
||||||
|
return P2PEthPoolStakingBalanceConverter.convert(
|
||||||
response = this,
|
response = this,
|
||||||
source = source,
|
source = source,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import arrow.core.toOption
|
||||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||||
import com.tangem.common.test.data.staking.MockYieldDTOFactory
|
import com.tangem.common.test.data.staking.MockYieldDTOFactory
|
||||||
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
import com.tangem.common.test.domain.wallet.MockUserWalletFactory
|
||||||
import com.tangem.data.staking.store.P2PBalancesStore
|
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.StakingBalancesStore
|
import com.tangem.data.staking.store.StakingBalancesStore
|
||||||
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory
|
||||||
import com.tangem.datasource.api.common.response.ApiResponse
|
import com.tangem.datasource.api.common.response.ApiResponse
|
||||||
|
|
@ -36,19 +36,19 @@ internal class DefaultMultiStakingBalanceFetcherTest {
|
||||||
private val userWalletsStore: UserWalletsStore = mockk()
|
private val userWalletsStore: UserWalletsStore = mockk()
|
||||||
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
||||||
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true)
|
private val stakingBalancesStore: StakingBalancesStore = mockk(relaxUnitFun = true)
|
||||||
private val p2pBalancesStore: P2PBalancesStore = mockk(relaxUnitFun = true)
|
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore = mockk(relaxUnitFun = true)
|
||||||
private val stakeKitApi: StakeKitApi = mockk()
|
private val stakeKitApi: StakeKitApi = mockk()
|
||||||
private val p2pApi: P2PEthPoolApi = mockk()
|
private val p2pEthPoolApi: P2PEthPoolApi = mockk()
|
||||||
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk()
|
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk()
|
||||||
|
|
||||||
private val fetcher = DefaultMultiStakingBalanceFetcher(
|
private val fetcher = DefaultMultiStakingBalanceFetcher(
|
||||||
userWalletsStore = userWalletsStore,
|
userWalletsStore = userWalletsStore,
|
||||||
stakingYieldsStore = stakingYieldsStore,
|
stakingYieldsStore = stakingYieldsStore,
|
||||||
stakingBalancesStore = stakingBalancesStore,
|
stakingBalancesStore = stakingBalancesStore,
|
||||||
p2pBalancesStore = p2pBalancesStore,
|
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
|
||||||
stakeKitApi = stakeKitApi,
|
stakeKitApi = stakeKitApi,
|
||||||
p2pApi = p2pApi,
|
p2pEthPoolApi = p2pEthPoolApi,
|
||||||
p2pVaultsStore = p2pVaultsStore,
|
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
||||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.staking.multi
|
||||||
import com.google.common.truth.Truth
|
import com.google.common.truth.Truth
|
||||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||||
import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory
|
import com.tangem.common.test.data.staking.MockP2PEthPoolAccountResponseFactory
|
||||||
import com.tangem.data.staking.store.P2PBalancesStore
|
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||||
import com.tangem.data.staking.store.StakingBalancesStore
|
import com.tangem.data.staking.store.StakingBalancesStore
|
||||||
import com.tangem.data.staking.toDomain
|
import com.tangem.data.staking.toDomain
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
|
|
@ -28,13 +28,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
|
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
|
||||||
|
|
||||||
private val stakingBalancesStore = mockk<StakingBalancesStore>()
|
private val stakingBalancesStore = mockk<StakingBalancesStore>()
|
||||||
private val p2pBalancesStore = mockk<P2PBalancesStore>()
|
private val p2PEthPoolBalancesStore = mockk<P2PEthPoolBalancesStore>()
|
||||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||||
|
|
||||||
private val producer = DefaultMultiStakingBalanceProducer(
|
private val producer = DefaultMultiStakingBalanceProducer(
|
||||||
params = params,
|
params = params,
|
||||||
stakingBalancesStore = stakingBalancesStore,
|
stakingBalancesStore = stakingBalancesStore,
|
||||||
p2pBalancesStore = p2pBalancesStore,
|
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -48,13 +48,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
val networksStatusesFlow = flowOf(balances)
|
val networksStatusesFlow = flowOf(balances)
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
val values = getEmittedValues(flow = actual)
|
val values = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
|
|
@ -67,13 +67,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
// first emit
|
// first emit
|
||||||
val balances = setOf(
|
val balances = setOf(
|
||||||
|
|
@ -108,13 +108,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
// first emit
|
// first emit
|
||||||
val wrappers = setOf(
|
val wrappers = setOf(
|
||||||
|
|
@ -157,13 +157,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
.buffer(capacity = 5)
|
.buffer(capacity = 5)
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
every { stakingBalancesStore.get(params.userWalletId) } returns networksStatusesFlow
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(emptySet())
|
||||||
|
|
||||||
val actual = producer.produceWithFallback()
|
val actual = producer.produceWithFallback()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
val values1 = getEmittedValues(flow = actual)
|
val values1 = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
|
|
@ -181,13 +181,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test that flow is empty`() = runTest {
|
fun `test that flow is empty`() = runTest {
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
every { stakingBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns emptyFlow()
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
val values = getEmittedValues(flow = actual)
|
val values = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
|
|
@ -198,53 +198,53 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `test that StakeKit and P2P balances are combined`() = runTest {
|
fun `test that StakeKit and P2P balances are combined`() = runTest {
|
||||||
val stakeKitBalances = createStakeKitBalances()
|
val stakeKitBalances = createStakeKitBalances()
|
||||||
val p2pBalances = createP2PBalances()
|
val p2pEthPoolBalances = createP2PEthPoolBalances()
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns flowOf(p2pBalances)
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns flowOf(p2pEthPoolBalances)
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
val values = getEmittedValues(flow = actual)
|
val values = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
Truth.assertThat(values.size).isEqualTo(1)
|
Truth.assertThat(values.size).isEqualTo(1)
|
||||||
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances)
|
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
|
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
|
||||||
val stakeKitBalances = createStakeKitBalancesWithTonOnly()
|
val stakeKitBalances = createStakeKitBalancesWithTonOnly()
|
||||||
val p2pFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
val p2pEthPoolFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||||
|
|
||||||
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
every { stakingBalancesStore.get(params.userWalletId) } returns flowOf(stakeKitBalances)
|
||||||
every { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow
|
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns p2pEthPoolFlow
|
||||||
|
|
||||||
val actual = producer.produce()
|
val actual = producer.produce()
|
||||||
|
|
||||||
// check after producer.produce()
|
// check after producer.produce()
|
||||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||||
|
|
||||||
// first emit - empty P2P
|
// first emit - empty P2PEthPool
|
||||||
p2pFlow.emit(emptySet())
|
p2pEthPoolFlow.emit(emptySet())
|
||||||
|
|
||||||
val values1 = getEmittedValues(flow = actual)
|
val values1 = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
Truth.assertThat(values1.size).isEqualTo(1)
|
Truth.assertThat(values1.size).isEqualTo(1)
|
||||||
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
|
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
|
||||||
|
|
||||||
// second emit - with P2P balance
|
// second emit - with P2PEthPool balance
|
||||||
val p2pBalances = createP2PBalances()
|
val p2pEthPoolBalances = createP2PEthPoolBalances()
|
||||||
p2pFlow.emit(p2pBalances)
|
p2pEthPoolFlow.emit(p2pEthPoolBalances)
|
||||||
|
|
||||||
val values2 = getEmittedValues(flow = actual)
|
val values2 = getEmittedValues(flow = actual)
|
||||||
|
|
||||||
Truth.assertThat(values2.size).isEqualTo(2)
|
Truth.assertThat(values2.size).isEqualTo(2)
|
||||||
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances)
|
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|
@ -255,7 +255,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
address = "0x1",
|
address = "0x1",
|
||||||
)
|
)
|
||||||
val p2pEthereumId = StakingID(
|
val p2pEthereumId = StakingID(
|
||||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
integrationId = StakingIntegrationID.P2PEthPool.value,
|
||||||
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -272,7 +272,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createP2PBalances(): Set<StakingBalance> {
|
fun createP2PEthPoolBalances(): Set<StakingBalance> {
|
||||||
return setOf(
|
return setOf(
|
||||||
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
|
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
|
||||||
source = StatusSource.ACTUAL,
|
source = StatusSource.ACTUAL,
|
||||||
|
|
|
||||||
|
|
@ -4,31 +4,31 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
import kotlinx.datetime.Instant
|
import kotlinx.datetime.Instant
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
/** P2P.org staking account */
|
/** P2P.org eth pooled staking account */
|
||||||
@Serializable
|
@Serializable
|
||||||
data class P2PStakingAccount(
|
data class P2PEthPoolStakingAccount(
|
||||||
val delegatorAddress: String,
|
val delegatorAddress: String,
|
||||||
val vaultAddress: String,
|
val vaultAddress: String,
|
||||||
val stake: P2PStake,
|
val stake: P2PEthPoolStake,
|
||||||
val availableToUnstake: SerializedBigDecimal,
|
val availableToUnstake: SerializedBigDecimal,
|
||||||
val availableToWithdraw: SerializedBigDecimal,
|
val availableToWithdraw: SerializedBigDecimal,
|
||||||
val exitQueue: P2PExitQueue,
|
val exitQueue: P2PEthPoolExitQueue,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class P2PStake(
|
data class P2PEthPoolStake(
|
||||||
val assets: SerializedBigDecimal,
|
val assets: SerializedBigDecimal,
|
||||||
val totalEarnedAssets: SerializedBigDecimal,
|
val totalEarnedAssets: SerializedBigDecimal,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class P2PExitQueue(
|
data class P2PEthPoolExitQueue(
|
||||||
val total: SerializedBigDecimal,
|
val total: SerializedBigDecimal,
|
||||||
val requests: List<P2PExitRequest>,
|
val requests: List<P2PEthPoolExitRequest>,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class P2PExitRequest(
|
data class P2PEthPoolExitRequest(
|
||||||
val ticket: String,
|
val ticket: String,
|
||||||
val totalAssets: SerializedBigDecimal,
|
val totalAssets: SerializedBigDecimal,
|
||||||
val timestamp: Instant,
|
val timestamp: Instant,
|
||||||
|
|
@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Staking balance facade covering StakeKit and P2P balances
|
* Staking balance facade covering StakeKit and P2PEthPool balances
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
sealed interface StakingBalance {
|
sealed interface StakingBalance {
|
||||||
|
|
@ -50,10 +50,10 @@ sealed interface StakingBalance {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class P2P(
|
data class P2PEthPool(
|
||||||
override val stakingId: StakingID,
|
override val stakingId: StakingID,
|
||||||
override val source: StatusSource,
|
override val source: StatusSource,
|
||||||
val account: P2PStakingAccount,
|
val account: P2PEthPoolStakingAccount,
|
||||||
) : Data {
|
) : Data {
|
||||||
|
|
||||||
override val totalStaked: BigDecimal
|
override val totalStaked: BigDecimal
|
||||||
|
|
@ -93,7 +93,7 @@ sealed interface StakingBalance {
|
||||||
fun copySealed(source: StatusSource): StakingBalance {
|
fun copySealed(source: StatusSource): StakingBalance {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Data.StakeKit -> copy(source = source)
|
is Data.StakeKit -> copy(source = source)
|
||||||
is Data.P2P -> copy(source = source)
|
is Data.P2PEthPool -> copy(source = source)
|
||||||
is Empty -> copy(source = source)
|
is Empty -> copy(source = source)
|
||||||
is Error -> this
|
is Error -> this
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,17 @@ data class YieldToken(
|
||||||
val coinGeckoId: String?,
|
val coinGeckoId: String?,
|
||||||
val logoURI: String?,
|
val logoURI: String?,
|
||||||
val isPoints: Boolean?,
|
val isPoints: Boolean?,
|
||||||
)
|
) {
|
||||||
|
companion object {
|
||||||
|
val ETH = YieldToken( // TODO p2p
|
||||||
|
name = "Ethereum",
|
||||||
|
network = NetworkType.ETHEREUM,
|
||||||
|
symbol = "ETH",
|
||||||
|
decimals = 18,
|
||||||
|
address = null,
|
||||||
|
coinGeckoId = "ethereum",
|
||||||
|
logoURI = null,
|
||||||
|
isPoints = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,8 +29,6 @@ dependencies {
|
||||||
implementation(projects.domain.tokens.models)
|
implementation(projects.domain.tokens.models)
|
||||||
implementation(projects.domain.wallets.models)
|
implementation(projects.domain.wallets.models)
|
||||||
|
|
||||||
implementation(projects.features.staking.api)
|
|
||||||
|
|
||||||
implementation(tangemDeps.blockchain) {
|
implementation(tangemDeps.blockchain) {
|
||||||
exclude(module = "joda-time")
|
exclude(module = "joda-time")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.tangem.domain.staking.model
|
||||||
|
|
||||||
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||||
|
import com.tangem.domain.staking.model.stakekit.Yield
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents either a StakeKit Validator or a P2P ETH Pool Vault.
|
||||||
|
*/
|
||||||
|
sealed interface StakingTarget {
|
||||||
|
|
||||||
|
/** Unique identifier (validator address or vault address) */
|
||||||
|
val address: String
|
||||||
|
|
||||||
|
/** Display name */
|
||||||
|
val name: String
|
||||||
|
|
||||||
|
/** Reward info (rate and type) */
|
||||||
|
val rewardInfo: RewardInfo?
|
||||||
|
|
||||||
|
/** Whether this target is preferred/recommended */
|
||||||
|
val isPreferred: Boolean
|
||||||
|
|
||||||
|
/** Whether this target is active and available for staking */
|
||||||
|
val isActive: Boolean
|
||||||
|
|
||||||
|
/** Image URL for display (validator logo or vault icon) */
|
||||||
|
val image: String?
|
||||||
|
|
||||||
|
/** Whether this is a strategic partner (shows special badge in UI) */
|
||||||
|
val isStrategicPartner: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StakeKit Validator wrapper
|
||||||
|
*/
|
||||||
|
data class Validator(val delegate: Yield.Validator) : StakingTarget {
|
||||||
|
override val address: String = delegate.address
|
||||||
|
override val name: String = delegate.name
|
||||||
|
override val rewardInfo: RewardInfo? = delegate.rewardInfo
|
||||||
|
override val isPreferred: Boolean = delegate.preferred
|
||||||
|
override val isActive: Boolean = delegate.status == Yield.Validator.ValidatorStatus.ACTIVE
|
||||||
|
override val image: String? = delegate.image
|
||||||
|
override val isStrategicPartner: Boolean = delegate.isStrategicPartner
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P2P ETH Pool Vault wrapper
|
||||||
|
*/
|
||||||
|
data class Vault(val vault: P2PEthPoolVault) : StakingTarget {
|
||||||
|
override val address: String = vault.vaultAddress
|
||||||
|
override val name: String = vault.displayName
|
||||||
|
override val rewardInfo = RewardInfo(
|
||||||
|
rate = vault.apy,
|
||||||
|
type = RewardType.APY,
|
||||||
|
)
|
||||||
|
override val isPreferred: Boolean = true
|
||||||
|
override val isActive: Boolean = true
|
||||||
|
override val image: String? = null
|
||||||
|
override val isStrategicPartner: Boolean = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Yield.Validator.toStakingTarget(): StakingTarget = StakingTarget.Validator(this)
|
||||||
|
|
||||||
|
fun P2PEthPoolVault.toStakingTarget(): StakingTarget = StakingTarget.Vault(this)
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class RewardClaiming {
|
||||||
|
AUTO,
|
||||||
|
MANUAL,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class RewardInfo(
|
||||||
|
val rate: SerializedBigDecimal,
|
||||||
|
val type: RewardType,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class RewardSchedule {
|
||||||
|
BLOCK,
|
||||||
|
HOUR,
|
||||||
|
DAY,
|
||||||
|
WEEK,
|
||||||
|
MONTH,
|
||||||
|
ERA,
|
||||||
|
EPOCH,
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class RewardType {
|
||||||
|
APY, // compound rate
|
||||||
|
APR, // simple rate
|
||||||
|
UNKNOWN,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider-agnostic representation of staking action arguments.
|
||||||
|
* Contains amount requirements and constraints for enter/exit operations.
|
||||||
|
*
|
||||||
|
* Maps from:
|
||||||
|
* - StakeKit: Yield.Args.Enter
|
||||||
|
* - P2PEthPool: P2PEthPoolStaking.Metadata
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class StakingActionArgs(
|
||||||
|
val amountRequirement: StakingAmountRequirement?,
|
||||||
|
val isPartialAmountDisabled: Boolean,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.domain.staking.model.common
|
||||||
|
|
||||||
|
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider-agnostic representation of staking amount requirements.
|
||||||
|
* Contains validation constraints for stake/unstake amounts.
|
||||||
|
*
|
||||||
|
* Maps from:
|
||||||
|
* - StakeKit: AddressArgument with ArgType.AMOUNT
|
||||||
|
* - P2PEthPool: P2PEthPoolStaking.Metadata minimumStake/maximumStake
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class StakingAmountRequirement(
|
||||||
|
val isRequired: Boolean,
|
||||||
|
val minimum: SerializedBigDecimal? = null,
|
||||||
|
val maximum: SerializedBigDecimal? = null,
|
||||||
|
)
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
import org.joda.time.Instant
|
import org.joda.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P.org account staking information
|
* Account staking information
|
||||||
* Contains detailed balance and exit queue information
|
* Contains detailed balance and exit queue information
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolAccount(
|
data class P2PEthPoolAccount(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ data class P2PEthPoolAction(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types of P2P staking actions
|
* Types of P2PEthPool staking actions
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class P2PEthPoolActionType {
|
enum class P2PEthPoolActionType {
|
||||||
|
|
@ -33,7 +33,7 @@ enum class P2PEthPoolActionType {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of P2P staking action
|
* Status of P2PEthPool staking action
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class P2PEthPoolActionStatus {
|
enum class P2PEthPoolActionStatus {
|
||||||
|
|
@ -46,7 +46,7 @@ enum class P2PEthPoolActionStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transaction details for P2P staking action
|
* Transaction details for P2PEthPool staking action
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolStakingTransaction(
|
data class P2PEthPoolStakingTransaction(
|
||||||
val id: String?,
|
val id: String?,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
import org.joda.time.Instant
|
import org.joda.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P.org staking balance information (similar to StakeKit's YieldBalanceItem)
|
* Staking balance information (similar to StakeKit's YieldBalanceItem)
|
||||||
* Contains staked amounts, rewards, and pending actions
|
* Contains staked amounts, rewards, and pending actions
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolStakingBalance(
|
data class P2PEthPoolStakingBalance(
|
||||||
|
|
@ -15,7 +15,7 @@ data class P2PEthPoolStakingBalance(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual balance item for P2P staking
|
* Individual balance item for P2PEthPool staking
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolBalanceItem(
|
data class P2PEthPoolBalanceItem(
|
||||||
val type: P2PEthPoolBalanceType,
|
val type: P2PEthPoolBalanceType,
|
||||||
|
|
@ -26,7 +26,7 @@ data class P2PEthPoolBalanceItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Types of balances in P2P staking
|
* Types of balances in P2PEthPool staking
|
||||||
*/
|
*/
|
||||||
enum class P2PEthPoolBalanceType {
|
enum class P2PEthPoolBalanceType {
|
||||||
STAKED,
|
STAKED,
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ enum class P2PEthPoolNetwork(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if chain ID is supported for P2P staking
|
* Check if chain ID is supported for P2PEthPool staking
|
||||||
*
|
*
|
||||||
* @param chainId Ethereum chain ID
|
* @param chainId Ethereum chain ID
|
||||||
* @return true if supported, false otherwise
|
* @return true if supported, false otherwise
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
import org.joda.time.DateTime
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P.org rewards history entry
|
* Rewards history entry
|
||||||
* Historical reward information for account
|
* Historical reward information for account
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolReward(
|
data class P2PEthPoolReward(
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ data class P2PEthPoolStaking(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detailed vault information for P2P staking
|
* Detailed vault information for P2PEthPool staking
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolVaultDetails(
|
data class P2PEthPoolVaultDetails(
|
||||||
val vaultAddress: String,
|
val vaultAddress: String,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
package com.tangem.domain.staking.model.ethpool
|
package com.tangem.domain.staking.model.ethpool
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for P2P Ethereum staking network.
|
* Configuration for P2PEthPool Ethereum staking network.
|
||||||
*
|
*
|
||||||
* Change [USE_TESTNET] to switch between testnet and mainnet.
|
* Change [USE_TESTNET] to switch between testnet and mainnet.
|
||||||
*/
|
*/
|
||||||
object P2PStakingConfig {
|
object P2PEthPoolStakingConfig {
|
||||||
|
|
||||||
const val USE_TESTNET: Boolean = true
|
const val USE_TESTNET: Boolean = true
|
||||||
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.staking.model.ethpool
|
||||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* P2P.org unsigned transaction ready for signing
|
* Unsigned transaction ready for signing
|
||||||
* Contains all necessary data for transaction signing
|
* Contains all necessary data for transaction signing
|
||||||
*/
|
*/
|
||||||
data class P2PEthPoolUnsignedTx(
|
data class P2PEthPoolUnsignedTx(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ package com.tangem.domain.staking.model.stakekit
|
||||||
|
|
||||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||||
import com.tangem.domain.models.staking.YieldToken
|
import com.tangem.domain.models.staking.YieldToken
|
||||||
|
import com.tangem.domain.staking.model.common.RewardInfo
|
||||||
|
import com.tangem.domain.staking.model.common.RewardType
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
@ -138,18 +140,6 @@ data class Yield(
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class RewardType {
|
|
||||||
APY, // compound rate
|
|
||||||
APR, // simple rate
|
|
||||||
UNKNOWN,
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class RewardInfo(
|
|
||||||
val rate: SerializedBigDecimal,
|
|
||||||
val type: RewardType,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use case for fetching all staking options from all providers
|
* Use case for fetching all staking options from all providers
|
||||||
* Fetches both StakeKit yields and P2P vaults
|
* Fetches both StakeKit yields and P2PEthPool vaults
|
||||||
*/
|
*/
|
||||||
class FetchStakingOptionsUseCase(
|
class FetchStakingOptionsUseCase(
|
||||||
private val stakeKitRepository: StakeKitRepository,
|
private val stakeKitRepository: StakeKitRepository,
|
||||||
private val p2pRepository: P2PEthPoolRepository,
|
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||||
private val stakingErrorResolver: StakingErrorResolver,
|
private val stakingErrorResolver: StakingErrorResolver,
|
||||||
) {
|
) {
|
||||||
suspend operator fun invoke(): Either<StakingError, Unit> {
|
suspend operator fun invoke(): Either<StakingError, Unit> {
|
||||||
|
|
@ -25,7 +25,7 @@ class FetchStakingOptionsUseCase(
|
||||||
block = {
|
block = {
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch { stakeKitRepository.fetchYields() }
|
launch { stakeKitRepository.fetchYields() }
|
||||||
launch { p2pRepository.fetchVaults() }
|
launch { p2pEthPoolRepository.fetchVaults() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
catch = { stakingErrorResolver.resolve(it) },
|
catch = { stakingErrorResolver.resolve(it) },
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ class GetStakingEntryInfoUseCase(
|
||||||
symbol = symbol,
|
symbol = symbol,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is StakingOption.P2P -> {
|
is StakingOption.P2PEthPool -> {
|
||||||
StakingEntryInfo(
|
StakingEntryInfo(
|
||||||
tokenSymbol = "ETH",
|
tokenSymbol = "ETH",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.tangem.domain.staking.model
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.staking.YieldToken
|
||||||
|
import com.tangem.domain.staking.model.common.RewardClaiming
|
||||||
|
import com.tangem.domain.staking.model.common.RewardSchedule
|
||||||
|
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||||
|
import com.tangem.domain.staking.model.common.StakingAmountRequirement
|
||||||
|
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StakingIntegration implementation for P2PEthPool pooled staking.
|
||||||
|
* Converts P2PEthPoolVault data to the common StakingIntegration interface.
|
||||||
|
*/
|
||||||
|
class P2PEthPoolIntegration(
|
||||||
|
override val integrationId: StakingIntegrationID,
|
||||||
|
vaults: List<P2PEthPoolVault>,
|
||||||
|
) : StakingIntegration {
|
||||||
|
|
||||||
|
// Basic
|
||||||
|
|
||||||
|
override val token: YieldToken = YieldToken.ETH
|
||||||
|
|
||||||
|
override val tokens: List<YieldToken> = listOf(token)
|
||||||
|
|
||||||
|
// Targets (vaults)
|
||||||
|
|
||||||
|
override val targets: List<StakingTarget> = vaults.map { vault ->
|
||||||
|
vault.toStakingTarget()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val preferredTargets: List<StakingTarget> = targets
|
||||||
|
|
||||||
|
override val areAllTargetsFull: Boolean = false
|
||||||
|
|
||||||
|
// Enter/Exit Args
|
||||||
|
|
||||||
|
override val isPartialAmountDisabled: Boolean = false
|
||||||
|
|
||||||
|
override val enterMinimumAmount: BigDecimal = DEFAULT_MINIMUM_STAKE
|
||||||
|
|
||||||
|
override val exitMinimumAmount: BigDecimal? = null
|
||||||
|
|
||||||
|
override val enterArgs: StakingActionArgs = StakingActionArgs(
|
||||||
|
amountRequirement = StakingAmountRequirement(
|
||||||
|
isRequired = true,
|
||||||
|
minimum = DEFAULT_MINIMUM_STAKE,
|
||||||
|
maximum = null,
|
||||||
|
),
|
||||||
|
isPartialAmountDisabled = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val exitArgs: StakingActionArgs = StakingActionArgs(
|
||||||
|
amountRequirement = StakingAmountRequirement(
|
||||||
|
isRequired = true,
|
||||||
|
minimum = null,
|
||||||
|
maximum = null,
|
||||||
|
),
|
||||||
|
isPartialAmountDisabled = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
|
||||||
|
override val warmupPeriodDays: Int = 0
|
||||||
|
|
||||||
|
override val cooldownPeriodDays: Int = DEFAULT_COOLDOWN_DAYS
|
||||||
|
|
||||||
|
override val rewardSchedule: RewardSchedule = RewardSchedule.DAY
|
||||||
|
|
||||||
|
override val rewardClaiming: RewardClaiming = RewardClaiming.AUTO
|
||||||
|
|
||||||
|
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken = token
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_COOLDOWN_DAYS = 7
|
||||||
|
private val DEFAULT_MINIMUM_STAKE = BigDecimal("0.01")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
package com.tangem.domain.staking.model
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.staking.YieldToken
|
||||||
|
import com.tangem.domain.staking.model.common.RewardClaiming
|
||||||
|
import com.tangem.domain.staking.model.common.RewardSchedule
|
||||||
|
import com.tangem.domain.staking.model.common.StakingActionArgs
|
||||||
|
import com.tangem.domain.staking.model.common.StakingAmountRequirement
|
||||||
|
import com.tangem.domain.staking.model.stakekit.Yield
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StakingIntegration implementation for StakeKit.
|
||||||
|
* Delegates all calls to the underlying Yield object.
|
||||||
|
*/
|
||||||
|
class StakeKitIntegration(
|
||||||
|
override val integrationId: StakingIntegrationID,
|
||||||
|
private val yield: Yield,
|
||||||
|
) : StakingIntegration {
|
||||||
|
|
||||||
|
// Basic
|
||||||
|
|
||||||
|
override val token: YieldToken = yield.token
|
||||||
|
|
||||||
|
override val tokens: List<YieldToken> = yield.tokens
|
||||||
|
|
||||||
|
// Targets (validators)
|
||||||
|
|
||||||
|
override val targets: List<StakingTarget> = yield.validators.map { it.toStakingTarget() }
|
||||||
|
|
||||||
|
override val preferredTargets: List<StakingTarget> = yield.preferredValidators.map { it.toStakingTarget() }
|
||||||
|
|
||||||
|
override val areAllTargetsFull: Boolean = yield.allValidatorsFull
|
||||||
|
|
||||||
|
// Enter/Exit Args
|
||||||
|
|
||||||
|
override val isPartialAmountDisabled: Boolean = yield.args.enter.isPartialAmountDisabled
|
||||||
|
|
||||||
|
override val enterMinimumAmount: BigDecimal? =
|
||||||
|
yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum
|
||||||
|
|
||||||
|
override val exitMinimumAmount: BigDecimal? =
|
||||||
|
yield.args.exit?.args
|
||||||
|
?.get(Yield.Args.ArgType.AMOUNT)?.minimum
|
||||||
|
|
||||||
|
override val enterArgs: StakingActionArgs = yield.args.enter.toStakingActionArgs()
|
||||||
|
|
||||||
|
override val exitArgs: StakingActionArgs? = yield.args.exit?.toStakingActionArgs()
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
|
||||||
|
override val warmupPeriodDays: Int = yield.metadata.warmupPeriod.days
|
||||||
|
|
||||||
|
override val cooldownPeriodDays: Int? = yield.metadata.cooldownPeriod?.days
|
||||||
|
|
||||||
|
override val rewardSchedule: RewardSchedule = yield.metadata.rewardSchedule.toRewardSchedule()
|
||||||
|
|
||||||
|
override val rewardClaiming: RewardClaiming = yield.metadata.rewardClaiming.toRewardClaiming()
|
||||||
|
|
||||||
|
// Basic
|
||||||
|
|
||||||
|
override fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken =
|
||||||
|
tokens.firstOrNull { rawCurrencyId?.value == it.coinGeckoId } ?: token
|
||||||
|
|
||||||
|
private fun Yield.Args.Enter.toStakingActionArgs(): StakingActionArgs {
|
||||||
|
val amountArg = args[Yield.Args.ArgType.AMOUNT]
|
||||||
|
return StakingActionArgs(
|
||||||
|
amountRequirement = amountArg?.let { arg ->
|
||||||
|
StakingAmountRequirement(
|
||||||
|
isRequired = arg.required,
|
||||||
|
minimum = arg.minimum,
|
||||||
|
maximum = arg.maximum,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isPartialAmountDisabled = isPartialAmountDisabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Yield.Metadata.RewardSchedule.toRewardSchedule(): RewardSchedule {
|
||||||
|
return when (this) {
|
||||||
|
Yield.Metadata.RewardSchedule.BLOCK -> RewardSchedule.BLOCK
|
||||||
|
Yield.Metadata.RewardSchedule.HOUR -> RewardSchedule.HOUR
|
||||||
|
Yield.Metadata.RewardSchedule.DAY -> RewardSchedule.DAY
|
||||||
|
Yield.Metadata.RewardSchedule.WEEK -> RewardSchedule.WEEK
|
||||||
|
Yield.Metadata.RewardSchedule.MONTH -> RewardSchedule.MONTH
|
||||||
|
Yield.Metadata.RewardSchedule.ERA -> RewardSchedule.ERA
|
||||||
|
Yield.Metadata.RewardSchedule.EPOCH -> RewardSchedule.EPOCH
|
||||||
|
Yield.Metadata.RewardSchedule.UNKNOWN -> RewardSchedule.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Yield.Metadata.RewardClaiming.toRewardClaiming(): RewardClaiming {
|
||||||
|
return when (this) {
|
||||||
|
Yield.Metadata.RewardClaiming.AUTO -> RewardClaiming.AUTO
|
||||||
|
Yield.Metadata.RewardClaiming.MANUAL -> RewardClaiming.MANUAL
|
||||||
|
Yield.Metadata.RewardClaiming.UNKNOWN -> RewardClaiming.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue